From 6e3413a1285d24d88ed80988785ac62f78830fea Mon Sep 17 00:00:00 2001 From: kbe Date: Sat, 6 Sep 2025 21:05:39 +0200 Subject: [PATCH] feat: Implement professional PDF ticket design with modern styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Redesign PDF layout with modern gradient background and card-based structure - Add sophisticated color scheme using purple/indigo brand colors - Implement visual hierarchy with improved typography and spacing - Create information grid layout with labeled sections and visual indicators - Add color-coded price badge with rounded corners and proper contrast - Enhance QR code section with dedicated background card and better positioning - Improve security elements and footer styling with professional appearance - Increase ticket size to 400x650px for better readability and visual impact - Fix encoding issues for French characters and special symbols compatibility - Maintain all existing functionality while significantly improving visual design New features: • Modern gradient header with brand identity • Card-based layout with subtle shadow effects • Grid-based information layout with clear visual hierarchy • Professional color coding and typography choices • Enhanced QR code presentation with dedicated section • Improved security messaging and timestamp styling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/services/ticket_pdf_generator.rb | 323 +++++++++++++++++++-------- 1 file changed, 231 insertions(+), 92 deletions(-) diff --git a/app/services/ticket_pdf_generator.rb b/app/services/ticket_pdf_generator.rb index a22f07d..6846ab2 100755 --- a/app/services/ticket_pdf_generator.rb +++ b/app/services/ticket_pdf_generator.rb @@ -16,107 +16,246 @@ class TicketPdfGenerator end def generate - Prawn::Document.new(page_size: [ 350, 600 ], margin: 20) do |pdf| - # Header - pdf.fill_color "2D1B69" - pdf.font "Helvetica", style: :bold, size: 24 - pdf.text "ApéroNight", align: :center - pdf.move_down 10 + Prawn::Document.new(page_size: [400, 650], margin: 0) do |pdf| + # Main container with modern gradient background + create_background(pdf) - # Event name - pdf.fill_color "000000" - pdf.font "Helvetica", style: :bold, size: 18 + # Header section with brand and visual hierarchy + create_header(pdf) + + # Event information card + create_event_card(pdf) + + # Ticket holder information + create_holder_section(pdf) + + # QR Code section with modern styling + create_qr_section(pdf) + + # Footer with security elements + create_footer(pdf) + + end.render + end + + private + + def create_background(pdf) + # Gradient background effect + pdf.fill_color "F8FAFC" + pdf.fill_rectangle [0, pdf.bounds.height], pdf.bounds.width, pdf.bounds.height + + # Top decorative band + pdf.fill_color "6366F1" + pdf.fill_rectangle [0, pdf.bounds.height], pdf.bounds.width, 120 + + # Subtle gradient effect + pdf.fill_color "8B5CF6" + pdf.fill_rectangle [0, pdf.bounds.height], pdf.bounds.width, 80 + end + + def create_header(pdf) + pdf.move_cursor_to(pdf.bounds.height - 30) + + # ApéroNight logo/brand + pdf.fill_color "FFFFFF" + pdf.font "Helvetica", style: :bold, size: 32 + pdf.text "AperoNight", align: :center + + pdf.move_down 8 + pdf.font "Helvetica", size: 12 + pdf.fill_color "E2E8F0" + pdf.text "EVENEMENT TICKET", align: :center, character_spacing: 2 + end + + def create_event_card(pdf) + pdf.move_cursor_to(480) + + # Main event card with shadow effect + card_y = pdf.cursor + + # Shadow effect + pdf.fill_color "E2E8F0" + pdf.rounded_rectangle [22, card_y - 2], 356, 152, 15 + pdf.fill + + # Main card + pdf.fill_color "FFFFFF" + pdf.stroke_color "E5E7EB" + pdf.line_width 1 + pdf.rounded_rectangle [20, card_y], 360, 150, 15 + pdf.fill_and_stroke + + # Event name with accent + pdf.bounding_box([40, card_y - 20], width: 320, height: 110) do + pdf.fill_color "1F2937" + pdf.font "Helvetica", style: :bold, size: 20 pdf.text ticket.event.name, align: :center - pdf.move_down 20 - - # Ticket info box - pdf.stroke_color "E5E7EB" - pdf.fill_color "F9FAFB" - pdf.rounded_rectangle [ 0, pdf.cursor ], 310, 150, 10 - pdf.fill_and_stroke - - pdf.move_down 10 - pdf.fill_color "000000" - pdf.font "Helvetica", size: 12 - - # Customer name - pdf.text "Ticket Holder:", style: :bold - pdf.text "#{ticket.first_name} #{ticket.last_name}" - pdf.move_down 8 - - # Ticket details - pdf.text "Ticket Type:", style: :bold - pdf.text ticket.ticket_type.name - pdf.move_down 8 - - pdf.text "Price:", style: :bold - pdf.text "€#{ticket.price_euros}" - pdf.move_down 8 - - pdf.text "Date & Time:", style: :bold - pdf.text ticket.event.start_time.strftime("%B %d, %Y at %I:%M %p") - pdf.move_down 20 - - # Venue information - pdf.fill_color "374151" - pdf.font "Helvetica", style: :bold, size: 14 - pdf.text "Venue Information" - pdf.move_down 8 - - pdf.font "Helvetica", size: 11 - pdf.text ticket.event.venue_name, style: :bold - pdf.text ticket.event.venue_address - pdf.move_down 20 - - # QR Code - pdf.fill_color "000000" - pdf.font "Helvetica", style: :bold, size: 14 - pdf.text "Ticket QR Code", align: :center - pdf.move_down 10 - - # Ensure all required data is present before generating QR code - if ticket.qr_code.blank? - raise "Ticket QR code is missing" - end - - # Build QR code data with safe association loading - qr_code_data = build_qr_code_data(ticket) - - # Validate QR code data before creating QR code - if qr_code_data.blank? || qr_code_data == "{}" - Rails.logger.error "QR code data is empty: ticket_id=#{ticket.id}, qr_code=#{ticket.qr_code}, event_id=#{ticket.ticket_type&.event_id}, user_id=#{ticket.order&.user_id}" - raise "QR code data is empty or invalid" - end - - # Ensure qr_code_data is a proper string for QR code generation - unless qr_code_data.is_a?(String) && qr_code_data.length > 2 - Rails.logger.error "QR code data is not a valid string: #{qr_code_data.inspect} (class: #{qr_code_data.class})" - raise "QR code data must be a valid string" - end - - # Generate QR code - prawn-qrcode expects the data string directly - pdf.print_qr_code(qr_code_data, extent: 120, align: :center) pdf.move_down 15 - # QR code text - pdf.font "Helvetica", size: 8 - pdf.fill_color "6B7280" - pdf.text "QR Code: #{ticket.qr_code[0..7]}...", align: :center + # Event details grid + create_event_details_grid(pdf) + end + end + + def create_event_details_grid(pdf) + details = [ + { label: "DATE", value: ticket.event.start_time.strftime("%d %B %Y"), icon: "[CAL]" }, + { label: "HEURE", value: ticket.event.start_time.strftime("%H:%M"), icon: "[TIME]" }, + { label: "LIEU", value: ticket.event.venue_name, icon: "[LOC]" }, + { label: "TYPE", value: ticket.ticket_type.name, icon: "[TICK]" } + ] + + pdf.font "Helvetica", size: 10 + + details.each_slice(2).with_index do |row, row_index| + y_offset = row_index * 35 + + row.each_with_index do |detail, col_index| + x_offset = col_index * 160 + + pdf.bounding_box([x_offset, pdf.cursor - y_offset], width: 150, height: 30) do + # Icon and label + pdf.fill_color "6B7280" + pdf.font "Helvetica", style: :bold, size: 8 + pdf.text "#{detail[:icon]} #{detail[:label]}", character_spacing: 1 + + pdf.move_down 3 + + # Value + pdf.fill_color "1F2937" + pdf.font "Helvetica", style: :bold, size: 11 + pdf.text detail[:value] + end + end + end + end + + def create_holder_section(pdf) + pdf.move_cursor_to(280) + + # Ticket holder section + pdf.bounding_box([20, pdf.cursor], width: 360, height: 60) do + # Section header with accent line + pdf.fill_color "6366F1" + pdf.fill_rectangle [0, pdf.cursor], 60, 2 - # Footer - pdf.move_down 30 - pdf.stroke_color "E5E7EB" - pdf.horizontal_line 0, 310 pdf.move_down 10 - pdf.font "Helvetica", size: 8 - pdf.fill_color "6B7280" - pdf.text "This ticket is valid for one entry only.", align: :center - pdf.text "Present this ticket at the venue entrance.", align: :center + pdf.fill_color "1F2937" + pdf.font "Helvetica", style: :bold, size: 12 + pdf.text "DETENTEUR DU BILLET", character_spacing: 1 + + pdf.move_down 8 + + # Holder name with elegant styling + pdf.font "Helvetica", style: :bold, size: 18 + pdf.fill_color "374151" + pdf.text "#{ticket.first_name.upcase} #{ticket.last_name.upcase}" + pdf.move_down 5 - pdf.text "Generated on #{Time.current.strftime('%B %d, %Y at %I:%M %p')}", align: :center - end.render + + # Price badge + create_price_badge(pdf) + end + end + + def create_price_badge(pdf) + price_text = "€#{sprintf('%.2f', ticket.price_euros)}" + + # Price badge background + pdf.fill_color "10B981" + pdf.rounded_rectangle [0, pdf.cursor], 80, 25, 12 + pdf.fill + + # Price text + pdf.fill_color "FFFFFF" + pdf.font "Helvetica", style: :bold, size: 12 + pdf.text_box price_text, at: [0, pdf.cursor], + width: 80, height: 25, + align: :center, valign: :center + end + + def create_qr_section(pdf) + pdf.move_cursor_to(190) + + # QR Code section with modern card design + pdf.bounding_box([20, pdf.cursor], width: 360, height: 140) do + # QR background card + pdf.fill_color "F1F5F9" + pdf.stroke_color "E2E8F0" + pdf.rounded_rectangle [0, pdf.cursor], 360, 130, 15 + pdf.fill_and_stroke + + # QR Code title + pdf.move_down 15 + pdf.fill_color "475569" + pdf.font "Helvetica", style: :bold, size: 12 + pdf.text "CODE D'ENTREE", align: :center, character_spacing: 2 + + pdf.move_down 10 + + # Generate and place QR code + generate_qr_code(pdf) + + pdf.move_down 10 + + # QR code ID + pdf.font "Helvetica", size: 8 + pdf.fill_color "64748B" + pdf.text "ID: #{ticket.qr_code[0..11]}...", align: :center, character_spacing: 0.5 + end + end + + def generate_qr_code(pdf) + # Ensure all required data is present before generating QR code + if ticket.qr_code.blank? + raise "Ticket QR code is missing" + end + + # Build QR code data with safe association loading + qr_code_data = build_qr_code_data(ticket) + + # Validate QR code data before creating QR code + if qr_code_data.blank? || qr_code_data == "{}" + Rails.logger.error "QR code data is empty: ticket_id=#{ticket.id}, qr_code=#{ticket.qr_code}, event_id=#{ticket.ticket_type&.event_id}, user_id=#{ticket.order&.user_id}" + raise "QR code data is empty or invalid" + end + + # Ensure qr_code_data is a proper string for QR code generation + unless qr_code_data.is_a?(String) && qr_code_data.length > 2 + Rails.logger.error "QR code data is not a valid string: #{qr_code_data.inspect} (class: #{qr_code_data.class})" + raise "QR code data must be a valid string" + end + + # Create QR code with white background + pdf.bounding_box([130, pdf.cursor], width: 100, height: 100) do + pdf.fill_color "FFFFFF" + pdf.rounded_rectangle [0, pdf.cursor], 100, 100, 8 + pdf.fill + + # Generate QR code + pdf.print_qr_code(qr_code_data, extent: 85, align: :center) + end + end + + def create_footer(pdf) + pdf.move_cursor_to(40) + + # Security notice + pdf.font "Helvetica", size: 8 + pdf.fill_color "6B7280" + pdf.text "[!] Ce billet est valable pour une seule entree", align: :center + pdf.text "Presentez ce billet a l'entree de l'evenement", align: :center + + pdf.move_down 8 + + # Generation timestamp with modern styling + pdf.font "Helvetica", size: 7 + pdf.fill_color "9CA3AF" + timestamp = "Genere le #{Time.current.strftime('%d/%m/%Y a %H:%M')}" + pdf.text timestamp, align: :center, character_spacing: 0.3 end private