Compare commits
1 Commits
feat/wicke
...
feat/seo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5105964b39 |
141
app/assets/stylesheets/pdf.scss
Normal file
141
app/assets/stylesheets/pdf.scss
Normal file
@@ -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;
|
||||
}
|
||||
@@ -14,4 +14,48 @@ class ApplicationController < ActionController::Base
|
||||
# - CSS nesting and :has() pseudo-class
|
||||
# allow_browser versions: :modern
|
||||
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
|
||||
|
||||
protected
|
||||
|
||||
# Generate SEO-friendly path for an event
|
||||
def seo_event_path(event)
|
||||
year = event.start_time.year
|
||||
month = format("%02d", event.start_time.month)
|
||||
event_path(year: year, month: month, slug: event.slug)
|
||||
end
|
||||
helper_method :seo_event_path
|
||||
|
||||
# Generate SEO-friendly booking URL for an event
|
||||
def seo_book_tickets_path(event)
|
||||
year = event.start_time.year
|
||||
month = format("%02d", event.start_time.month)
|
||||
book_event_tickets_path(year: year, month: month, slug: event.slug)
|
||||
end
|
||||
helper_method :seo_book_tickets_path
|
||||
|
||||
# Generate SEO-friendly checkout URL for an event
|
||||
def seo_checkout_path(event)
|
||||
year = event.start_time.year
|
||||
month = format("%02d", event.start_time.month)
|
||||
event_checkout_path(year: year, month: month, slug: event.slug)
|
||||
end
|
||||
helper_method :seo_checkout_path
|
||||
|
||||
# Generate SEO-friendly ticket URL
|
||||
def seo_ticket_path(ticket)
|
||||
ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id)
|
||||
end
|
||||
helper_method :seo_ticket_path
|
||||
|
||||
# Generate SEO-friendly ticket view URL
|
||||
def seo_ticket_view_path(ticket)
|
||||
view_ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id)
|
||||
end
|
||||
helper_method :seo_ticket_view_path
|
||||
|
||||
# Generate SEO-friendly ticket download URL
|
||||
def seo_ticket_download_path(ticket)
|
||||
download_ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id)
|
||||
end
|
||||
helper_method :seo_ticket_download_path
|
||||
end
|
||||
|
||||
92
app/controllers/booking/payments_controller.rb
Normal file
92
app/controllers/booking/payments_controller.rb
Normal file
@@ -0,0 +1,92 @@
|
||||
# Handle payment callbacks for booking workflow
|
||||
class Booking::PaymentsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
# Handle successful payment callback
|
||||
def success
|
||||
session_id = params[:session_id]
|
||||
|
||||
# Check if Stripe is properly configured
|
||||
stripe_configured = Rails.application.config.stripe[:secret_key].present?
|
||||
Rails.logger.debug "Payment success - Stripe configured: #{stripe_configured}"
|
||||
|
||||
unless stripe_configured
|
||||
redirect_to root_path, alert: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur."
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
stripe_session = Stripe::Checkout::Session.retrieve(session_id)
|
||||
|
||||
if stripe_session.payment_status == "paid"
|
||||
# Get order_id from session metadata
|
||||
order_id = stripe_session.metadata["order_id"]
|
||||
|
||||
unless order_id.present?
|
||||
redirect_to dashboard_path, alert: "Informations de commande manquantes"
|
||||
return
|
||||
end
|
||||
|
||||
# Find and update the order
|
||||
@order = current_user.orders.includes(tickets: :ticket_type).find(order_id)
|
||||
@order.mark_as_paid!
|
||||
|
||||
# Schedule Stripe invoice generation in background
|
||||
begin
|
||||
StripeInvoiceGenerationJob.perform_later(@order.id)
|
||||
Rails.logger.info "Scheduled Stripe invoice generation for order #{@order.id}"
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to schedule invoice generation for order #{@order.id}: #{e.message}"
|
||||
end
|
||||
|
||||
# Send confirmation emails
|
||||
@order.tickets.each do |ticket|
|
||||
begin
|
||||
TicketMailer.purchase_confirmation(ticket).deliver_now
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
# Clear session data
|
||||
session.delete(:pending_cart)
|
||||
session.delete(:ticket_names)
|
||||
session.delete(:draft_order_id)
|
||||
|
||||
render "payment_success"
|
||||
else
|
||||
redirect_to dashboard_path, alert: "Le paiement n'a pas été complété avec succès"
|
||||
end
|
||||
rescue Stripe::StripeError => e
|
||||
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||
redirect_to dashboard_path, alert: "Erreur lors du traitement de votre confirmation de paiement : #{error_message}"
|
||||
rescue => e
|
||||
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||
Rails.logger.error "Payment success error: #{e.class} - #{error_message}"
|
||||
redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite : #{error_message}"
|
||||
end
|
||||
end
|
||||
|
||||
# Handle payment cancellation callback
|
||||
def cancel
|
||||
order_id = params[:order_id] || session[:draft_order_id]
|
||||
|
||||
if order_id.present?
|
||||
order = current_user.orders.find_by(id: order_id, status: "draft")
|
||||
|
||||
if order&.can_retry_payment?
|
||||
# Extract year and month from event start_time for SEO URL
|
||||
year = order.event.start_time.year
|
||||
month = format("%02d", order.event.start_time.month)
|
||||
|
||||
redirect_to event_checkout_path(year: year, month: month, slug: order.event.slug),
|
||||
alert: "Le paiement a été annulé. Vous pouvez réessayer."
|
||||
else
|
||||
session.delete(:draft_order_id)
|
||||
redirect_to root_path, alert: "Le paiement a été annulé et votre commande a expiré."
|
||||
end
|
||||
else
|
||||
redirect_to root_path, alert: "Le paiement a été annulé"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -27,10 +27,33 @@ class EventsController < ApplicationController
|
||||
private
|
||||
|
||||
# Find and set the current event with eager-loaded associations
|
||||
#
|
||||
# Supports both old slug-only format and new SEO-friendly year/month/slug format
|
||||
# Loads event with ticket types to avoid N+1 queries
|
||||
# Raises ActiveRecord::RecordNotFound if event doesn't exist
|
||||
def set_event
|
||||
@event = Event.includes(:ticket_types).find(params[:id])
|
||||
if params[:year] && params[:month]
|
||||
# New SEO-friendly format: /events/2024/07/summer-party
|
||||
year = params[:year].to_i
|
||||
month = params[:month].to_i
|
||||
start_of_month = Date.new(year, month, 1).beginning_of_month
|
||||
end_of_month = start_of_month.end_of_month
|
||||
|
||||
@event = Event.includes(:ticket_types)
|
||||
.where(slug: params[:slug])
|
||||
.where(start_time: start_of_month..end_of_month)
|
||||
.first!
|
||||
else
|
||||
# Legacy format: /events/summer-party (for backward compatibility)
|
||||
@event = Event.includes(:ticket_types).find_by!(slug: params[:slug])
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to events_path, alert: "Événement non trouvé"
|
||||
end
|
||||
|
||||
# Generate SEO-friendly path for an event
|
||||
def seo_event_path(event)
|
||||
year = event.start_time.year
|
||||
month = format("%02d", event.start_time.month)
|
||||
event_path(year: year, month: month, slug: event.slug)
|
||||
end
|
||||
helper_method :seo_event_path
|
||||
end
|
||||
|
||||
17
app/controllers/legacy_redirects_controller.rb
Normal file
17
app/controllers/legacy_redirects_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
# Handle legacy URL redirects to new SEO-friendly URLs
|
||||
class LegacyRedirectsController < ApplicationController
|
||||
# Redirect old event URLs to new SEO-friendly format
|
||||
# OLD: /events/summer-party-2024
|
||||
# NEW: /events/2024/07/summer-party-2024
|
||||
def event_redirect
|
||||
event = Event.find_by(slug: params[:slug])
|
||||
|
||||
if event
|
||||
year = event.start_time.year
|
||||
month = format("%02d", event.start_time.month)
|
||||
redirect_to event_path(year: year, month: month, slug: event.slug), status: :moved_permanently
|
||||
else
|
||||
redirect_to events_path, alert: "Événement non trouvé"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,11 +1,11 @@
|
||||
# Handle order management and checkout process
|
||||
# Handle order management and checkout process with SEO-friendly URLs
|
||||
#
|
||||
# This controller manages the order lifecycle from checkout to payment completion
|
||||
# Orders group multiple tickets together for better transaction management
|
||||
class OrdersController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt ]
|
||||
before_action :set_event, only: [ :new, :create ]
|
||||
before_action :set_event_from_seo_params, only: [:new, :create, :checkout]
|
||||
before_action :set_order_from_id, only: [:show, :retry_payment, :increment_payment_attempt]
|
||||
|
||||
# Display new order form with name collection
|
||||
#
|
||||
@@ -15,7 +15,7 @@ class OrdersController < ApplicationController
|
||||
@cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||
|
||||
if @cart_data.empty?
|
||||
redirect_to event_path(@event.slug, @event), alert: "Veuillez d'abord sélectionner vos billets sur la page de l'événement"
|
||||
redirect_to seo_event_path(@event), alert: "Veuillez d'abord sélectionner vos billets sur la page de l'événement"
|
||||
return
|
||||
end
|
||||
|
||||
@@ -47,7 +47,7 @@ class OrdersController < ApplicationController
|
||||
@cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||
|
||||
if @cart_data.empty?
|
||||
redirect_to event_path(@event.slug, @event), alert: "Aucun billet sélectionné"
|
||||
redirect_to seo_event_path(@event), alert: "Aucun billet sélectionné"
|
||||
return
|
||||
end
|
||||
|
||||
@@ -87,32 +87,44 @@ class OrdersController < ApplicationController
|
||||
if success
|
||||
session[:draft_order_id] = @order.id
|
||||
session.delete(:pending_cart)
|
||||
redirect_to checkout_order_path(@order)
|
||||
year = @event.start_time.year
|
||||
month = format("%02d", @event.start_time.month)
|
||||
redirect_to event_checkout_path(year: year, month: month, slug: @event.slug)
|
||||
else
|
||||
redirect_to event_order_new_path(@event.slug, @event.id)
|
||||
year = @event.start_time.year
|
||||
month = format("%02d", @event.start_time.month)
|
||||
redirect_to book_event_tickets_path(year: year, month: month, slug: @event.slug)
|
||||
end
|
||||
rescue => e
|
||||
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||
flash[:alert] = "Une erreur est survenue: #{error_message}"
|
||||
redirect_to event_order_new_path(@event.slug, @event.id)
|
||||
year = @event.start_time.year
|
||||
month = format("%02d", @event.start_time.month)
|
||||
redirect_to book_event_tickets_path(year: year, month: month, slug: @event.slug)
|
||||
end
|
||||
|
||||
# Display order summary
|
||||
#
|
||||
#
|
||||
def show
|
||||
@tickets = @order.tickets.includes(:ticket_type)
|
||||
end
|
||||
|
||||
# Display payment page for an order
|
||||
# Display payment page for an order (SEO-friendly checkout URL)
|
||||
#
|
||||
# Display a summary of all tickets in the order and permit user
|
||||
# to proceed to payment via Stripe
|
||||
def checkout
|
||||
# Find order from session or create one
|
||||
@order = current_user.orders.find_by(id: session[:draft_order_id], event: @event, status: "draft")
|
||||
|
||||
unless @order
|
||||
redirect_to seo_event_path(@event), alert: "Aucune commande en attente trouvée"
|
||||
return
|
||||
end
|
||||
|
||||
# Handle expired orders
|
||||
if @order.expired?
|
||||
@order.expire_if_overdue!
|
||||
return redirect_to event_path(@order.event.slug, @order.event),
|
||||
return redirect_to seo_event_path(@event),
|
||||
alert: "Votre commande a expiré. Veuillez recommencer."
|
||||
end
|
||||
|
||||
@@ -141,117 +153,41 @@ class OrdersController < ApplicationController
|
||||
# Allow users to retry payment for failed/cancelled payments
|
||||
def retry_payment
|
||||
unless @order.can_retry_payment?
|
||||
redirect_to event_path(@order.event.slug, @order.event),
|
||||
redirect_to seo_event_path(@order.event),
|
||||
alert: "Cette commande ne peut plus être payée"
|
||||
return
|
||||
end
|
||||
|
||||
redirect_to checkout_order_path(@order)
|
||||
end
|
||||
|
||||
# Handle successful payment
|
||||
def payment_success
|
||||
session_id = params[:session_id]
|
||||
|
||||
# Check if Stripe is properly configured
|
||||
stripe_configured = Rails.application.config.stripe[:secret_key].present?
|
||||
Rails.logger.debug "Payment success - Stripe configured: #{stripe_configured}"
|
||||
|
||||
unless stripe_configured
|
||||
redirect_to root_path, alert: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur."
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
stripe_session = Stripe::Checkout::Session.retrieve(session_id)
|
||||
|
||||
if stripe_session.payment_status == "paid"
|
||||
# Get order_id from session metadata
|
||||
order_id = stripe_session.metadata["order_id"]
|
||||
|
||||
unless order_id.present?
|
||||
redirect_to dashboard_path, alert: "Informations de commande manquantes"
|
||||
return
|
||||
end
|
||||
|
||||
# Find and update the order
|
||||
@order = current_user.orders.includes(tickets: :ticket_type).find(order_id)
|
||||
@order.mark_as_paid!
|
||||
|
||||
# Schedule Stripe invoice generation in background
|
||||
# This creates accounting records without blocking the payment success flow
|
||||
begin
|
||||
StripeInvoiceGenerationJob.perform_later(@order.id)
|
||||
Rails.logger.info "Scheduled Stripe invoice generation for order #{@order.id}"
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to schedule invoice generation for order #{@order.id}: #{e.message}"
|
||||
# Don't fail the payment process due to job scheduling issues
|
||||
end
|
||||
|
||||
# Send confirmation emails
|
||||
@order.tickets.each do |ticket|
|
||||
begin
|
||||
TicketMailer.purchase_confirmation(ticket).deliver_now
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}"
|
||||
# Don't fail the entire payment process due to email/PDF generation issues
|
||||
end
|
||||
end
|
||||
|
||||
# Clear session data
|
||||
session.delete(:pending_cart)
|
||||
session.delete(:ticket_names)
|
||||
session.delete(:draft_order_id)
|
||||
|
||||
render "payment_success"
|
||||
else
|
||||
redirect_to dashboard_path, alert: "Le paiement n'a pas été complété avec succès"
|
||||
end
|
||||
rescue Stripe::StripeError => e
|
||||
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||
redirect_to dashboard_path, alert: "Erreur lors du traitement de votre confirmation de paiement : #{error_message}"
|
||||
rescue => e
|
||||
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||
Rails.logger.error "Payment success error: #{e.class} - #{error_message}"
|
||||
redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite : #{error_message}"
|
||||
end
|
||||
end
|
||||
|
||||
# Handle payment failure/cancellation
|
||||
def payment_cancel
|
||||
order_id = params[:order_id] || session[:draft_order_id]
|
||||
|
||||
if order_id.present?
|
||||
order = current_user.orders.find_by(id: order_id, status: "draft")
|
||||
|
||||
if order&.can_retry_payment?
|
||||
redirect_to checkout_order_path(order),
|
||||
alert: "Le paiement a été annulé. Vous pouvez réessayer."
|
||||
else
|
||||
session.delete(:draft_order_id)
|
||||
redirect_to root_path, alert: "Le paiement a été annulé et votre commande a expiré."
|
||||
end
|
||||
else
|
||||
redirect_to root_path, alert: "Le paiement a été annulé"
|
||||
end
|
||||
year = @order.event.start_time.year
|
||||
month = format("%02d", @order.event.start_time.month)
|
||||
redirect_to event_checkout_path(year: year, month: month, slug: @order.event.slug)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_order
|
||||
@order = current_user.orders.includes(:tickets, :event).find(params[:id])
|
||||
def set_event_from_seo_params
|
||||
year = params[:year].to_i
|
||||
month = params[:month].to_i
|
||||
start_of_month = Date.new(year, month, 1).beginning_of_month
|
||||
end_of_month = start_of_month.end_of_month
|
||||
|
||||
@event = Event.includes(:ticket_types)
|
||||
.where(slug: params[:slug])
|
||||
.where(start_time: start_of_month..end_of_month)
|
||||
.first
|
||||
|
||||
return redirect_to events_path, alert: "Événement non trouvé" unless @event
|
||||
end
|
||||
|
||||
def set_order_from_id
|
||||
@order = current_user.orders.includes(:tickets, :event).find(params[:order_id])
|
||||
@event = @order.event
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to root_path, alert: "Commande non trouvée"
|
||||
end
|
||||
|
||||
def set_event
|
||||
@event = Event.includes(:ticket_types).find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to events_path, alert: "Événement non trouvé"
|
||||
end
|
||||
|
||||
def order_params
|
||||
params.permit(tickets_attributes: [ :ticket_type_id, :first_name, :last_name ])
|
||||
params.permit(tickets_attributes: [:ticket_type_id, :first_name, :last_name])
|
||||
end
|
||||
|
||||
def create_stripe_session
|
||||
@@ -270,15 +206,23 @@ class OrdersController < ApplicationController
|
||||
end
|
||||
|
||||
Stripe::Checkout::Session.create(
|
||||
payment_method_types: [ "card" ],
|
||||
payment_method_types: ["card"],
|
||||
line_items: line_items,
|
||||
mode: "payment",
|
||||
success_url: order_payment_success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url: order_payment_cancel_url,
|
||||
success_url: booking_payment_success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url: booking_payment_cancelled_url + "?order_id=#{@order.id}",
|
||||
metadata: {
|
||||
order_id: @order.id,
|
||||
user_id: current_user.id
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
# Generate SEO-friendly path for an event
|
||||
def seo_event_path(event)
|
||||
year = event.start_time.year
|
||||
month = format("%02d", event.start_time.month)
|
||||
event_path(year: year, month: month, slug: event.slug)
|
||||
end
|
||||
helper_method :seo_event_path
|
||||
end
|
||||
@@ -1,95 +1,24 @@
|
||||
# Legacy tickets controller - redirects to new order system
|
||||
# Tickets controller - handles ticket viewing and downloads with SEO-friendly URLs
|
||||
#
|
||||
# This controller now primarily handles legacy redirects and backward compatibility
|
||||
# Most ticket creation functionality has been moved to OrdersController
|
||||
# This controller manages individual ticket display and downloads
|
||||
# Uses event-slug-ticket-id format for SEO-friendly URLs
|
||||
class TicketsController < ApplicationController
|
||||
before_action :authenticate_user!, only: [ :payment_success, :payment_cancel, :show, :ticket_view, :download_ticket ]
|
||||
before_action :set_event, only: [ :checkout, :retry_payment ]
|
||||
|
||||
# Redirect to order-based checkout
|
||||
def checkout
|
||||
# Check for draft order
|
||||
if session[:draft_order_id].present?
|
||||
order = current_user.orders.find_by(id: session[:draft_order_id], status: "draft")
|
||||
if order.present?
|
||||
redirect_to order_checkout_path(order)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
# No order found
|
||||
@event = Event.includes(:ticket_types).find(params[:id])
|
||||
redirect_to event_path(@event.slug, @event), alert: "Aucun billet en attente de paiement"
|
||||
end
|
||||
|
||||
# Redirect to order-based payment success
|
||||
def payment_success
|
||||
redirect_to order_payment_success_path(session_id: params[:session_id])
|
||||
end
|
||||
|
||||
# Redirect to order-based payment cancel
|
||||
def payment_cancel
|
||||
redirect_to order_payment_cancel_path
|
||||
end
|
||||
|
||||
# Redirect retry payment to order system
|
||||
def retry_payment
|
||||
@event = Event.includes(:ticket_types).find(params[:id])
|
||||
|
||||
# Look for draft order for this event
|
||||
order = current_user.orders.find_by(event: @event, status: "draft")
|
||||
|
||||
if order&.can_retry_payment?
|
||||
redirect_to retry_payment_order_path(order)
|
||||
else
|
||||
redirect_to event_path(@event.slug, @event),
|
||||
alert: "Aucune commande disponible pour un nouveau paiement"
|
||||
end
|
||||
end
|
||||
before_action :authenticate_user!
|
||||
before_action :set_ticket_from_seo_params, only: [:show, :view, :download, :retry_payment]
|
||||
|
||||
# Display ticket details
|
||||
def show
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||
tickets: { id: params[:ticket_id] },
|
||||
orders: { user_id: current_user.id }
|
||||
)
|
||||
@event = @ticket.event
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
end
|
||||
|
||||
# Display ticket in PDF-like format
|
||||
def ticket_view
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||
tickets: { id: params[:ticket_id] },
|
||||
orders: { user_id: current_user.id }
|
||||
)
|
||||
|
||||
if @ticket.nil?
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||
return
|
||||
end
|
||||
|
||||
def view
|
||||
@event = @ticket.event
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
end
|
||||
|
||||
# Download PDF ticket - only accessible by ticket owner
|
||||
# User must be authenticated to download ticket
|
||||
# TODO: change ID to an unique identifier (UUID)
|
||||
def download_ticket
|
||||
# Find ticket and ensure it belongs to current user
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||
tickets: { id: params[:ticket_id] },
|
||||
orders: { user_id: current_user.id }
|
||||
)
|
||||
|
||||
if @ticket.nil?
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||
return
|
||||
end
|
||||
|
||||
def download
|
||||
# Generate PDF using Grover
|
||||
begin
|
||||
Rails.logger.info "Starting PDF generation for ticket ID: #{@ticket.id}"
|
||||
@@ -103,96 +32,96 @@ class TicketsController < ApplicationController
|
||||
|
||||
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",
|
||||
# Configure Grover options for PDF generation
|
||||
pdf_options = {
|
||||
format: 'A4',
|
||||
margin: {
|
||||
top: "10mm",
|
||||
bottom: "10mm",
|
||||
left: "10mm",
|
||||
right: "10mm"
|
||||
top: '0.5in',
|
||||
bottom: '0.5in',
|
||||
left: '0.5in',
|
||||
right: '0.5in'
|
||||
},
|
||||
print_background: true,
|
||||
display_header_footer: false,
|
||||
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"
|
||||
launch_args: ["--no-sandbox", "--disable-setuid-sandbox"] # For better compatibility
|
||||
}
|
||||
|
||||
pdf_content = grover.to_pdf
|
||||
Rails.logger.info "PDF generated successfully, length: #{pdf_content.length}"
|
||||
# Generate PDF
|
||||
pdf = Grover.new(html, pdf_options).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 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
|
||||
Rails.logger.info "PDF generation completed for ticket ID: #{@ticket.id}"
|
||||
|
||||
# Send PDF as download with SEO-friendly filename
|
||||
send_data pdf,
|
||||
filename: "billet-#{@ticket.event.slug}-#{@ticket.id}.pdf",
|
||||
type: 'application/pdf',
|
||||
disposition: 'attachment'
|
||||
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"
|
||||
Rails.logger.error "PDF generation failed for ticket ID: #{@ticket.id} - Error: #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
|
||||
redirect_to view_ticket_path(event_slug: @ticket.event.slug, ticket_id: @ticket.id),
|
||||
alert: "Erreur lors de la génération du PDF. Veuillez réessayer."
|
||||
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 "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
|
||||
|
||||
# Redirect retry payment to order system
|
||||
def retry_payment
|
||||
# Look for draft order for this ticket's event
|
||||
order = current_user.orders.find_by(event: @ticket.event, status: "draft")
|
||||
|
||||
if order&.can_retry_payment?
|
||||
year = order.event.start_time.year
|
||||
month = format("%02d", order.event.start_time.month)
|
||||
redirect_to event_checkout_path(year: year, month: month, slug: order.event.slug)
|
||||
else
|
||||
redirect_to seo_event_path(@ticket.event),
|
||||
alert: "Aucune commande disponible pour un nouveau paiement"
|
||||
end
|
||||
end
|
||||
|
||||
# Legacy redirects for backward compatibility
|
||||
def payment_success
|
||||
redirect_to booking_payment_success_path(session_id: params[:session_id])
|
||||
end
|
||||
|
||||
def payment_cancel
|
||||
redirect_to booking_payment_cancelled_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_event
|
||||
event_id = params[:id] || session[:event_id]
|
||||
def set_ticket_from_seo_params
|
||||
# Parse event_slug and ticket_id from the SEO-friendly format: event-slug-123
|
||||
slug_and_id = params[:event_slug_ticket_id] || "#{params[:event_slug]}-#{params[:ticket_id]}"
|
||||
|
||||
Rails.logger.debug "TicketsController#set_event - params[:id]: #{params[:id].inspect}, session[:event_id]: #{session[:event_id].inspect}"
|
||||
# Split by last dash to separate event slug from ticket ID
|
||||
parts = slug_and_id.split('-')
|
||||
ticket_id = parts.pop
|
||||
event_slug = parts.join('-')
|
||||
|
||||
unless event_id
|
||||
Rails.logger.error "TicketsController#set_event - No event ID found"
|
||||
redirect_to events_path, alert: "Aucun événement spécifié"
|
||||
return
|
||||
# Find ticket and ensure it belongs to current user
|
||||
@ticket = Ticket.joins(order: :user)
|
||||
.includes(:event, :ticket_type, order: :user)
|
||||
.joins(:event)
|
||||
.where(
|
||||
tickets: { id: ticket_id },
|
||||
orders: { user_id: current_user.id },
|
||||
events: { slug: event_slug }
|
||||
)
|
||||
.first
|
||||
|
||||
unless @ticket
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||
end
|
||||
end
|
||||
|
||||
@event = Event.includes(:ticket_types).find(event_id)
|
||||
Rails.logger.debug "TicketsController#set_event - Found event: #{@event.id} - #{@event.name}"
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
Rails.logger.error "TicketsController#set_event - Event not found with ID: #{event_id}"
|
||||
redirect_to events_path, alert: "Événement non trouvé"
|
||||
# Generate SEO-friendly path for an event
|
||||
def seo_event_path(event)
|
||||
year = event.start_time.year
|
||||
month = format("%02d", event.start_time.month)
|
||||
event_path(year: year, month: month, slug: event.slug)
|
||||
end
|
||||
helper_method :seo_event_path
|
||||
end
|
||||
17
app/helpers/pdf_helper.rb
Normal file
17
app/helpers/pdf_helper.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module PdfHelper
|
||||
require "rqrcode"
|
||||
|
||||
# Generate SVG QR code for tickets
|
||||
def qr_code_tag(data)
|
||||
qrcode = RQRCode::QRCode.new(data)
|
||||
|
||||
# Render as SVG
|
||||
raw qrcode.as_svg(
|
||||
offset: 0,
|
||||
color: "000",
|
||||
shape_rendering: "crispEdges",
|
||||
module_size: 4,
|
||||
standalone: true
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -118,7 +118,7 @@ export default class extends Controller {
|
||||
await this.storeCartInSession(cartData);
|
||||
|
||||
// Redirect to event-scoped orders/new page
|
||||
const OrderNewUrl = `/orders/new/events/${this.eventSlugValue}.${this.eventIdValue}`;
|
||||
const OrderNewUrl = `/events/${this.eventSlugValue}/orders/new`;
|
||||
window.location.href = OrderNewUrl;
|
||||
} catch (error) {
|
||||
console.error("Error storing cart:", error);
|
||||
|
||||
61
app/views/booking/payments/success.html.erb
Normal file
61
app/views/booking/payments/success.html.erb
Normal file
@@ -0,0 +1,61 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-green-50 to-green-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full bg-white rounded-2xl shadow-xl p-8">
|
||||
<!-- Success Icon -->
|
||||
<div class="flex justify-center mb-6">
|
||||
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">Paiement réussi !</h1>
|
||||
<p class="text-gray-600">Votre commande a été confirmée et vos billets ont été envoyés par email.</p>
|
||||
</div>
|
||||
|
||||
<!-- Order Details -->
|
||||
<% if @order&.present? %>
|
||||
<div class="border-t border-gray-200 pt-6 mb-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<span class="text-sm font-medium text-gray-900">Commande #<%= @order.id %></span>
|
||||
<span class="text-sm text-gray-500"><%= @order.created_at.strftime("%d/%m/%Y à %H:%M") %></span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 mb-4">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Événement:</span>
|
||||
<span class="text-sm font-medium text-gray-900"><%= @order.event.name %></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Nombre de billets:</span>
|
||||
<span class="text-sm font-medium text-gray-900"><%= @order.tickets.count %></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Total:</span>
|
||||
<span class="text-sm font-bold text-green-600"><%= @order.total_amount_euros %>€</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="space-y-3">
|
||||
<%= link_to dashboard_path, class: "w-full bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-4 rounded-lg transition-colors text-center block" do %>
|
||||
Voir mes billets
|
||||
<% end %>
|
||||
|
||||
<%= link_to events_path, class: "w-full bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-3 px-4 rounded-lg transition-colors text-center block" do %>
|
||||
Découvrir d'autres événements
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Help Text -->
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-xs text-gray-500">
|
||||
Un email de confirmation a été envoyé à votre adresse email avec vos billets en pièce jointe.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= link_to event_path(event.slug, event), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
|
||||
<%= link_to seo_event_path(event), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-16 h-16 bg-slate-200 dark:bg-slate-700 rounded-lg overflow-hidden flex-shrink-0">
|
||||
<%= image_tag event.image, alt: event.name, class: "w-full h-full object-cover" if event.image.present? %>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
>
|
||||
<% if event.image.present? %>
|
||||
<div class="h-48 overflow-hidden">
|
||||
<%= link_to event_path(event.slug, event) do %>
|
||||
<%= link_to event_path(event) do %>
|
||||
<img
|
||||
src="<%= event.image %>"
|
||||
alt="<%= event.name %>"
|
||||
@@ -130,7 +130,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to event_path(event.slug, event), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" do %>
|
||||
<%= link_to event_path(event), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" do %>
|
||||
Détails
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
||||
@@ -1,4 +1,62 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<!-- SEO Meta Tags for Event -->
|
||||
<% content_for :title, "#{@event.name} - #{@event.start_time.strftime('%d/%m/%Y')} | Aperonight" %>
|
||||
<% content_for :description, @event.description.truncate(160) %>
|
||||
<% content_for :keywords, "#{@event.name}, événement, soirée, #{@event.venue_name}, billets, réservation" %>
|
||||
<% content_for :canonical_url, seo_event_path(@event) %>
|
||||
<% content_for :og_image, @event.image if @event.image.present? %>
|
||||
|
||||
<!-- Structured Data for Event -->
|
||||
<% content_for :head do %>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Event",
|
||||
"name": "<%= @event.name %>",
|
||||
"description": "<%= strip_tags(@event.description) %>",
|
||||
"startDate": "<%= @event.start_time.iso8601 %>",
|
||||
"endDate": "<%= @event.end_time&.iso8601 || (@event.start_time + 4.hours).iso8601 %>",
|
||||
"eventStatus": "https://schema.org/EventScheduled",
|
||||
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
|
||||
"location": {
|
||||
"@type": "Place",
|
||||
"name": "<%= @event.venue_name %>",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "<%= @event.venue_address %>"
|
||||
},
|
||||
"geo": {
|
||||
"@type": "GeoCoordinates",
|
||||
"latitude": <%= @event.latitude %>,
|
||||
"longitude": <%= @event.longitude %>
|
||||
}
|
||||
},
|
||||
"organizer": {
|
||||
"@type": "Organization",
|
||||
"name": "<%= @event.user.company_name.present? ? @event.user.company_name : "#{@event.user.first_name} #{@event.user.last_name}" %>",
|
||||
"email": "<%= @event.user.email %>"
|
||||
},
|
||||
<% if @event.image.present? %>
|
||||
"image": [
|
||||
"<%= @event.image %>"
|
||||
],
|
||||
<% end %>
|
||||
"offers": [
|
||||
<% @event.ticket_types.each_with_index do |ticket_type, index| %>
|
||||
{
|
||||
"@type": "Offer",
|
||||
"name": "<%= ticket_type.name %>",
|
||||
"price": "<%= ticket_type.price_cents / 100.0 %>",
|
||||
"priceCurrency": "EUR",
|
||||
"availability": "https://schema.org/InStock",
|
||||
"url": "<%= seo_book_tickets_path(@event) %>"
|
||||
}<%= ',' if index < @event.ticket_types.count - 1 %>
|
||||
<% end %>
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<% end %>
|
||||
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-6" aria-label="Breadcrumb">
|
||||
@@ -197,7 +255,7 @@
|
||||
|
||||
<!-- Right Column: Ticket Selection -->
|
||||
<div class="lg:col-span-1">
|
||||
<%= form_with url: event_order_new_path(@event.slug, @event.id), method: :get, id: "checkout_form", local: true, data: {
|
||||
<%= form_with url: book_event_tickets_path(year: @event.start_time.year, month: format("%02d", @event.start_time.month), slug: @event.slug), method: :get, id: "checkout_form", local: true, data: {
|
||||
controller: "ticket-selection",
|
||||
ticket_selection_target: "form",
|
||||
ticket_selection_event_slug_value: @event.slug,
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title><%= content_for(:title) || "Aperonight" %></title>
|
||||
<title><%= content_for(:title) || "Aperonight - Événements et Soirées" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<meta name="description" content="<%= content_for(:description) || "Découvrez et réservez vos billets pour les meilleurs événements et soirées. Aperonight vous connecte aux événements incontournables près de chez vous." %>">
|
||||
<meta name="keywords" content="<%= content_for(:keywords) || "événements, soirées, billets, réservation, nightlife, fêtes" %>">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="<%= content_for(:canonical_url) || request.original_url %>">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="<%= request.original_url %>">
|
||||
<meta property="og:title" content="<%= content_for(:title) || "Aperonight - Événements et Soirées" %>">
|
||||
<meta property="og:description" content="<%= content_for(:description) || "Découvrez et réservez vos billets pour les meilleurs événements et soirées." %>">
|
||||
<meta property="og:image" content="<%= content_for(:og_image) || asset_url('aperonight-og-image.jpg') %>">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:url" content="<%= request.original_url %>">
|
||||
<meta property="twitter:title" content="<%= content_for(:title) || "Aperonight - Événements et Soirées" %>">
|
||||
<meta property="twitter:description" content="<%= content_for(:description) || "Découvrez et réservez vos billets pour les meilleurs événements et soirées." %>">
|
||||
<meta property="twitter:image" content="<%= content_for(:og_image) || asset_url('aperonight-og-image.jpg') %>">
|
||||
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
<%= yield :head %>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= link_to event_path(@order.event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= @order.event.name %>
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -289,7 +289,7 @@
|
||||
<!-- Order Actions -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="space-y-3">
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||
<%= link_to event_path(@order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to event_path(@event.slug, @event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= link_to event_path(@event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= @event.name %>
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -89,7 +89,7 @@
|
||||
<p class="text-gray-600 max-w-md mx-auto">Veuillez fournir les prénoms et noms des personnes qui utiliseront les billets.</p>
|
||||
</div>
|
||||
|
||||
<%= form_with url: event_order_create_path(@event.slug, @event.id), method: :post, local: true, class: "space-y-8" do |form| %>
|
||||
<%= form_with url: event_orders_path(@event), method: :post, local: true, class: "space-y-8" do |form| %>
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center mb-2">
|
||||
<div class="bg-purple-600 rounded-full p-2 mr-3">
|
||||
@@ -129,7 +129,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 pt-6">
|
||||
<%= link_to "Retour", event_path(@event.slug, @event), class: "px-6 py-3 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 text-center font-medium transition-colors duration-200" %>
|
||||
<%= link_to "Retour", event_path(@event), class: "px-6 py-3 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 text-center font-medium transition-colors duration-200" %>
|
||||
<%= form.submit "Procéder au paiement", class: "flex-1 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium py-3 px-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= link_to seo_event_path(@order.event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= @order.event.name %>
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -79,7 +79,7 @@
|
||||
<!-- Actions -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="flex space-x-4">
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 rounded-lg transition-colors" do %>
|
||||
<%= link_to seo_event_path(@order.event), class: "bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
@@ -88,7 +88,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.can_retry_payment? %>
|
||||
<%= link_to checkout_order_path(@order), class: "bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 px-4 rounded-lg transition-colors" do %>
|
||||
<%= link_to booking_summary_path(@order), class: "bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 px-4 rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="featured-events-grid" data-controller="featured-event">
|
||||
<% @featured_events.each do |event| %>
|
||||
<div class="featured-event-card" data-featured-event-target="card">
|
||||
<%= link_to event_path(event.slug, event) do %>
|
||||
<%= link_to event_path(event) do %>
|
||||
<img src="<%= event.image %>" alt="<%= event.name %>" class="featured-event-image" data-featured-event-target="animated">
|
||||
<% end %>
|
||||
<div class="featured-event-content">
|
||||
@@ -58,7 +58,7 @@
|
||||
<p class="featured-event-description"><%= event.description %></p>
|
||||
<div class="featured-event-footer">
|
||||
<span class="featured-event-price">€<%= event.ticket_types.minimum(:price_cents).to_f / 100 %></span>
|
||||
<%= link_to "Réserver une place", event_path(event.slug, event), class: "btn btn-sm btn-primary" %>
|
||||
<%= link_to "Réserver une place", event_path(event), class: "btn btn-sm btn-primary" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
<p class="text-sm text-green-700">Cet événement est visible publiquement et les utilisateurs peuvent acheter des billets.</p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<%= link_to event_path(@event.slug, @event), target: "_blank", class: "text-green-600 hover:text-green-800 font-medium text-sm" do %>
|
||||
<%= link_to event_path(@event), target: "_blank", class: "text-green-600 hover:text-green-800 font-medium text-sm" do %>
|
||||
Voir publiquement <i data-lucide="external-link" class="w-4 h-4 inline ml-1"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to event_path(@event.slug, @event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= link_to event_path(@event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= @event.name %>
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
<%= link_to event_path(@event.slug, @event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= link_to event_path(@event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= @event.name %>
|
||||
<% end %>
|
||||
<svg
|
||||
@@ -180,7 +180,7 @@
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 pt-6">
|
||||
<%= link_to "Retour",
|
||||
event_path(@event.slug, @event),
|
||||
event_path(@event),
|
||||
class:
|
||||
"px-6 py-3 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 text-center font-medium transition-colors duration-200" %>
|
||||
<%= form.submit "Procéder au paiement",
|
||||
|
||||
116
config/routes.rb
116
config/routes.rb
@@ -1,81 +1,72 @@
|
||||
Rails.application.routes.draw do
|
||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||
|
||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
||||
# Health check
|
||||
get "up" => "rails/health#show", as: :rails_health_check
|
||||
|
||||
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
|
||||
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
|
||||
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
||||
|
||||
# Defines the root path route ("/")
|
||||
# Root
|
||||
root "pages#home"
|
||||
|
||||
# === Devise ===
|
||||
# Routes for devise authentication Gem
|
||||
# Bind devise to user
|
||||
# === Authentication ===
|
||||
devise_for :users, path: "auth", path_names: {
|
||||
sign_in: "sign_in", # Route for user login
|
||||
sign_out: "sign_out", # Route for user logout
|
||||
password: "reset-password", # Route for changing password
|
||||
confirmation: "verification", # Route for account confirmation
|
||||
unlock: "unblock", # Route for account unlock
|
||||
# registration: "account", # Route for user account
|
||||
sign_up: "signup" # Route for user registration
|
||||
},
|
||||
controllers: {
|
||||
sessions: "auth/sessions", # Custom controller for sessions
|
||||
registrations: "auth/registrations", # Custom controller for registrations
|
||||
passwords: "auth/passwords", # Custom controller for passwords
|
||||
confirmation: "auth/confirmations" # Custom controller for confirmations
|
||||
sign_in: "sign_in",
|
||||
sign_out: "sign_out",
|
||||
password: "reset-password",
|
||||
confirmation: "verification",
|
||||
unlock: "unblock",
|
||||
sign_up: "signup"
|
||||
}, controllers: {
|
||||
sessions: "auth/sessions",
|
||||
registrations: "auth/registrations",
|
||||
passwords: "auth/passwords",
|
||||
confirmation: "auth/confirmations"
|
||||
}
|
||||
|
||||
# === Pages ===
|
||||
get "dashboard", to: "pages#dashboard", as: "dashboard"
|
||||
# === Main App - SEO Friendly URLs ===
|
||||
get "dashboard", to: "pages#dashboard"
|
||||
|
||||
# === Events ===
|
||||
# Events with date-based SEO structure
|
||||
get "events", to: "events#index", as: "events"
|
||||
get "events/:slug.:id", to: "events#show", as: "event"
|
||||
get "events/:year/:month/:slug", to: "events#show", as: "event",
|
||||
constraints: { year: /\d{4}/, month: /\d{2}/ }
|
||||
|
||||
# === Orders (scoped to events) ===
|
||||
get "orders/new/events/:slug.:id", to: "orders#new", as: "event_order_new"
|
||||
post "orders/create/events/:slug.:id", to: "orders#create", as: "event_order_create"
|
||||
# Booking workflow with semantic URLs
|
||||
get "events/:year/:month/:slug/book-tickets", to: "orders#new", as: "book_event_tickets",
|
||||
constraints: { year: /\d{4}/, month: /\d{2}/ }
|
||||
post "events/:year/:month/:slug/book-tickets", to: "orders#create", as: "create_booking",
|
||||
constraints: { year: /\d{4}/, month: /\d{2}/ }
|
||||
|
||||
resources :orders, only: [ :show ] do
|
||||
member do
|
||||
get :checkout
|
||||
post :retry_payment
|
||||
post :increment_payment_attempt
|
||||
end
|
||||
# Checkout process with semantic URLs
|
||||
get "events/:year/:month/:slug/checkout", to: "orders#checkout", as: "event_checkout",
|
||||
constraints: { year: /\d{4}/, month: /\d{2}/ }
|
||||
get "booking/:order_id/summary", to: "orders#show", as: "booking_summary"
|
||||
post "booking/:order_id/retry-payment", to: "orders#retry_payment", as: "retry_booking_payment"
|
||||
post "booking/:order_id/increment-attempts", to: "orders#increment_payment_attempt", as: "increment_booking_attempts"
|
||||
|
||||
# Individual tickets with descriptive URLs
|
||||
get "tickets/:event_slug-:ticket_id", to: "tickets#show", as: "ticket"
|
||||
get "tickets/:event_slug-:ticket_id/view", to: "tickets#view", as: "view_ticket"
|
||||
get "tickets/:event_slug-:ticket_id/download", to: "tickets#download", as: "download_ticket"
|
||||
post "tickets/:event_slug-:ticket_id/retry-payment", to: "tickets#retry_payment", as: "retry_ticket_payment"
|
||||
|
||||
# Payment callbacks with descriptive paths
|
||||
namespace :booking do
|
||||
get "payment-success", to: "payments#success", as: "payment_success"
|
||||
get "payment-cancelled", to: "payments#cancel", as: "payment_cancelled"
|
||||
end
|
||||
|
||||
get "orders/payments/success", to: "orders#payment_success", as: "order_payment_success"
|
||||
get "orders/payments/cancel", to: "orders#payment_cancel", as: "order_payment_cancel"
|
||||
# Legacy redirects for backward compatibility
|
||||
get "events/:slug", to: "legacy_redirects#event_redirect"
|
||||
|
||||
# legacy routes
|
||||
get "payments/success", to: "tickets#payment_success", as: "payment_success"
|
||||
get "payments/cancel", to: "tickets#payment_cancel", as: "payment_cancel"
|
||||
# Legacy payment routes
|
||||
get "payments/success", to: redirect("/booking/payment-success")
|
||||
get "payments/cancel", to: redirect("/booking/payment-cancelled")
|
||||
|
||||
# === Tickets ===
|
||||
get "tickets/checkout/events/:slug.:id", to: "tickets#checkout", as: "ticket_checkout"
|
||||
post "tickets/retry/events/:slug.:id", to: "tickets#retry_payment", as: "ticket_retry_payment"
|
||||
get "tickets/:ticket_id", to: "tickets#show", as: "ticket"
|
||||
get "tickets/:ticket_id/view", to: "tickets#ticket_view", as: "ticket_view"
|
||||
get "tickets/:ticket_id/download", to: "tickets#download_ticket", as: "download_ticket"
|
||||
|
||||
# === Promoter Routes ===
|
||||
# === Promoter Dashboard ===
|
||||
namespace :promoter do
|
||||
resources :events do
|
||||
resources :events, path: "my-events" do
|
||||
member do
|
||||
patch :publish
|
||||
patch :unpublish
|
||||
patch :cancel
|
||||
patch :mark_sold_out
|
||||
patch :publish, :unpublish, :cancel, :mark_sold_out
|
||||
end
|
||||
|
||||
# Nested ticket types routes
|
||||
resources :ticket_types do
|
||||
resources :ticket_types, path: "ticket-options" do
|
||||
member do
|
||||
post :duplicate
|
||||
end
|
||||
@@ -83,17 +74,14 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# API routes versioning
|
||||
# === API ===
|
||||
namespace :api do
|
||||
namespace :v1 do
|
||||
# RESTful routes for event management
|
||||
resources :events, only: [ :index, :show, :create, :update, :destroy ] do
|
||||
resources :events, except: [:new, :edit] do
|
||||
member do
|
||||
post :store_cart
|
||||
end
|
||||
end
|
||||
# resources :ticket_types, only: [ :index, :show, :create, :update, :destroy ]
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user