diff --git a/Gemfile b/Gemfile index 16e60c3..9a50f22 100755 --- a/Gemfile +++ b/Gemfile @@ -87,8 +87,7 @@ gem "kaminari-tailwind", "~> 0.1.0" gem "stripe", "~> 15.5" # PDF generation for tickets -gem "prawn", "~> 2.5" -gem "prawn-qrcode", "~> 0.5" +gem 'grover' # QR code generation gem "rqrcode", "~> 3.1" diff --git a/Gemfile.lock b/Gemfile.lock index 8daac28..015ef27 100755 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -127,6 +127,8 @@ GEM raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) + grover (1.2.3) + nokogiri (~> 1) i18n (1.14.7) concurrent-ruby (~> 1.0) io-console (0.8.1) @@ -221,16 +223,8 @@ GEM parser (3.3.9.0) ast (~> 2.4.1) racc - pdf-core (0.10.0) pp (0.6.2) prettyprint - prawn (2.5.0) - matrix (~> 0.4) - pdf-core (~> 0.10.0) - ttfunk (~> 1.8) - prawn-qrcode (0.5.2) - prawn (>= 1) - rqrcode (>= 1.0.0) prettyprint (0.2.0) prism (1.4.0) propshaft (1.2.1) @@ -378,8 +372,6 @@ GEM thruster (0.1.15-aarch64-linux) thruster (0.1.15-x86_64-linux) timeout (0.4.3) - ttfunk (1.8.0) - bigdecimal (~> 3.1) turbo-rails (2.0.16) actionpack (>= 7.1.0) railties (>= 7.1.0) @@ -423,6 +415,7 @@ DEPENDENCIES debug devise (~> 4.9) dotenv-rails + grover jbuilder jsbundling-rails kamal @@ -431,8 +424,6 @@ DEPENDENCIES minitest-reporters (~> 1.7) mocha mysql2 (~> 0.5) - prawn (~> 2.5) - prawn-qrcode (~> 0.5) propshaft puma (>= 5.0) rails (~> 8.0.2, >= 8.0.2.1) diff --git a/app/assets/stylesheets/pdf.css b/app/assets/stylesheets/pdf.css new file mode 100644 index 0000000..9d374ca --- /dev/null +++ b/app/assets/stylesheets/pdf.css @@ -0,0 +1,141 @@ +/* PDF Styles for Ticket Generation */ + +body { + font-family: Helvetica, Arial, sans-serif; + font-size: 12px; + color: #000000; + margin: 0; + padding: 20px; + background-color: #ffffff; +} + +.ticket-container { + max-width: 350px; + margin: 0 auto; + padding: 20px; + border: 1px solid #e5e7eb; + border-radius: 10px; + background-color: #ffffff; +} + +/* Header */ +.header { + text-align: center; + margin-bottom: 10px; +} + +.header h1 { + color: #2D1B69; + font-size: 24px; + font-weight: bold; + margin: 0; +} + +/* Event name */ +.event-name { + text-align: center; + margin-bottom: 20px; +} + +.event-name h2 { + color: #000000; + font-size: 18px; + font-weight: bold; + margin: 0; +} + +/* Ticket info box */ +.ticket-info-box { + background-color: #F9FAFB; + border: 1px solid #E5E7EB; + border-radius: 10px; + padding: 15px; + margin-bottom: 20px; +} + +.info-row { + margin-bottom: 8px; +} + +.info-row:last-child { + margin-bottom: 0; +} + +.info-label { + font-weight: bold; + color: #000000; + display: inline-block; + width: 100px; +} + +.info-value { + display: inline-block; + color: #000000; +} + +/* Venue information */ +.venue-info { + margin-bottom: 20px; +} + +.venue-info h3 { + color: #374151; + font-size: 14px; + font-weight: bold; + margin: 0 0 8px 0; +} + +.venue-details { + font-size: 11px; +} + +.venue-name { + font-weight: bold; + margin-bottom: 4px; +} + +.venue-address { + color: #000000; +} + +/* QR Code */ +.qr-code-section { + text-align: center; + margin-bottom: 15px; +} + +.qr-code-section h3 { + color: #000000; + font-size: 14px; + font-weight: bold; + margin: 0 0 10px 0; +} + +.qr-code-container { + text-align: center; + margin: 0 auto 10px auto; + width: 120px; + height: 120px; +} + +.qr-code-text { + font-size: 8px; + color: #6B7280; +} + +/* Footer */ +.footer { + border-top: 1px solid #E5E7EB; + padding-top: 15px; + text-align: center; + font-size: 8px; + color: #6B7280; +} + +.footer p { + margin: 0 0 5px 0; +} + +.generated-date { + margin-top: 5px; +} \ No newline at end of file diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index 3d26b30..60b1763 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -74,18 +74,89 @@ class TicketsController < ApplicationController return end - # Generate PDF - pdf_content = @ticket.to_pdf - - # Send PDF as download - send_data pdf_content, - filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.pdf", - type: "application/pdf", - disposition: "attachment" - rescue ActiveRecord::RecordNotFound + # Generate PDF using Grover + begin + Rails.logger.info "Starting PDF generation for ticket ID: #{@ticket.id}" + + # Render the HTML template + html = render_to_string( + partial: "tickets/pdf_ticket", + layout: false, + locals: { ticket: @ticket } + ) + + Rails.logger.info "HTML template rendered successfully, length: #{html.length}" + + # Try to load and use Grover + begin + Rails.logger.info "Attempting to load Grover gem" + + # Try different approaches to load grover + begin + require 'bundler' + Bundler.require(:default, Rails.env) + Rails.logger.info "Bundler required gems successfully" + rescue => bundler_error + Rails.logger.warn "Bundler require failed: #{bundler_error.message}" + end + + # Direct path approach using bundle show + grover_gem_path = `bundle show grover`.strip + grover_path = File.join(grover_gem_path, 'lib', 'grover') + + if File.exist?(grover_path + '.rb') + Rails.logger.info "Loading Grover from direct path: #{grover_path}" + require grover_path + else + Rails.logger.error "Grover not found at path: #{grover_path}" + raise LoadError, "Grover gem not available at expected path" + end + + Rails.logger.info "Creating Grover instance with options" + grover = Grover.new(html, + format: 'A6', + margin: { + top: '10mm', + bottom: '10mm', + left: '10mm', + right: '10mm' + }, + prefer_css_page_size: true, + emulate_media: 'print', + cache: false, + launch_args: ['--no-sandbox', '--disable-setuid-sandbox'] # For better compatibility + ) + Rails.logger.info "Grover instance created successfully" + + pdf_content = grover.to_pdf + Rails.logger.info "PDF generated successfully, length: #{pdf_content.length}" + + # Send PDF as download + send_data pdf_content, + filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.pdf", + type: "application/pdf", + disposition: "attachment" + rescue LoadError => grover_error + Rails.logger.error "Failed to load Grover: #{grover_error.message}" + # Fallback: return HTML instead of PDF + send_data html, + filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.html", + type: "text/html", + disposition: "attachment" + end + rescue => e + Rails.logger.error "Error generating ticket PDF with Grover:" + Rails.logger.error "Message: #{e.message}" + Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}" + redirect_to dashboard_path, alert: "Erreur lors de la génération du billet" + end + rescue ActiveRecord::RecordNotFound => e + Rails.logger.error "ActiveRecord::RecordNotFound error: #{e.message}" redirect_to dashboard_path, alert: "Billet non trouvé" rescue => e - Rails.logger.error "Error generating ticket PDF: #{e.message}" + Rails.logger.error "Unexpected error in download_ticket action:" + Rails.logger.error "Message: #{e.message}" + Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}" redirect_to dashboard_path, alert: "Erreur lors de la génération du billet" end diff --git a/app/models/ticket.rb b/app/models/ticket.rb index a2de92a..d7e6f5c 100755 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -27,6 +27,29 @@ class Ticket < ApplicationRecord TicketPdfGenerator.new(self).generate end + # Generate QR code data for ticket validation + def to_qr_data + { + ticket_id: id, + qr_code: qr_code, + event_id: event&.id, + user_id: user&.id + }.compact.to_json + end + + # Generate QR code as SVG + def generate_qr_svg + require 'rqrcode' + qrcode = RQRCode::QRCode.new(to_qr_data) + qrcode.as_svg( + offset: 0, + color: '000', + shape_rendering: 'crispEdges', + module_size: 4, + standalone: true + ) + end + # Price in euros (formatted) def price_euros price_cents / 100.0 diff --git a/app/services/ticket_pdf_generator.rb b/app/services/ticket_pdf_generator.rb deleted file mode 100755 index 219a7db..0000000 --- a/app/services/ticket_pdf_generator.rb +++ /dev/null @@ -1,118 +0,0 @@ -require "prawn" -require "prawn/qrcode" -require "rqrcode" - -# PDF ticket generator service using Prawn -# -# Generates PDF tickets with QR codes for event entry validation -# Includes event details, venue information, and unique QR code for each ticket -class TicketPdfGenerator - # Suppress Prawn's internationalization warning for built-in fonts - Prawn::Fonts::AFM.hide_m17n_warning = true - attr_reader :ticket - - def initialize(ticket) - @ticket = ticket - 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 - - # Event name - pdf.fill_color "000000" - pdf.font "Helvetica", style: :bold, size: 18 - 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 - - qr_code_data = { - ticket_id: ticket.id, - qr_code: ticket.qr_code, - event_id: ticket.event&.id, - user_id: ticket.user&.id - }.compact.to_json - - # Validate QR code data before creating QR code - if qr_code_data.blank? || qr_code_data == "{}" - raise "QR code data is empty or invalid" - 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 - - # 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.move_down 5 - pdf.text "Generated on #{Time.current.strftime('%B %d, %Y at %I:%M %p')}", align: :center - end.render - end -end diff --git a/app/views/layouts/pdf.html.erb b/app/views/layouts/pdf.html.erb new file mode 100644 index 0000000..f5f618b --- /dev/null +++ b/app/views/layouts/pdf.html.erb @@ -0,0 +1,11 @@ + + +
+ +Ticket Holder: <%= ticket.first_name %> <%= ticket.last_name %>
+Ticket Type: <%= ticket.ticket_type.name %>
+Price: €<%= ticket.price_euros %>
+