Compare commits
7 Commits
main
...
e5ed1a34dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5ed1a34dd | ||
| 3e0a354a58 | |||
|
|
b5c1846f2c | ||
|
|
04393add14 | ||
|
|
5279ebe1a4 | ||
|
|
329ba89eaa | ||
|
|
9c56b2e1e5 |
@@ -126,6 +126,15 @@ class OrdersController < ApplicationController
|
|||||||
@total_amount = @order.total_amount_cents
|
@total_amount = @order.total_amount_cents
|
||||||
@expiring_soon = @order.expiring_soon?
|
@expiring_soon = @order.expiring_soon?
|
||||||
|
|
||||||
|
# For free orders, automatically mark as paid and redirect to success
|
||||||
|
if @order.free?
|
||||||
|
@order.mark_as_paid!
|
||||||
|
session.delete(:pending_cart)
|
||||||
|
session.delete(:ticket_names)
|
||||||
|
session.delete(:draft_order_id)
|
||||||
|
return redirect_to order_path(@order), notice: "Vos billets gratuits ont été confirmés !"
|
||||||
|
end
|
||||||
|
|
||||||
# Create Stripe checkout session if Stripe is configured
|
# Create Stripe checkout session if Stripe is configured
|
||||||
if Rails.application.config.stripe[:secret_key].present?
|
if Rails.application.config.stripe[:secret_key].present?
|
||||||
begin
|
begin
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
class Promoter::EventsController < ApplicationController
|
class Promoter::EventsController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :ensure_can_manage_events!
|
before_action :ensure_can_manage_events!
|
||||||
before_action :set_event, only: [ :show, :edit, :update, :destroy, :publish, :unpublish, :cancel, :mark_sold_out, :duplicate ]
|
before_action :set_event, only: [ :show, :edit, :update, :destroy, :publish, :unpublish, :cancel, :mark_sold_out, :mark_available, :duplicate ]
|
||||||
|
|
||||||
# Display all events for the current promoter
|
# Display all events for the current promoter
|
||||||
def index
|
def index
|
||||||
@@ -93,6 +93,16 @@ class Promoter::EventsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Mark event as available again
|
||||||
|
def mark_available
|
||||||
|
if @event.sold_out?
|
||||||
|
@event.update(state: :published)
|
||||||
|
redirect_to promoter_event_path(@event), notice: "Event marqué comme disponible!"
|
||||||
|
else
|
||||||
|
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être marqué comme disponible."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Duplicate an event and all its ticket types
|
# Duplicate an event and all its ticket types
|
||||||
def duplicate
|
def duplicate
|
||||||
clone_ticket_types = params[:clone_ticket_types] == "true"
|
clone_ticket_types = params[:clone_ticket_types] == "true"
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
module TicketsHelper
|
module TicketsHelper
|
||||||
|
def format_ticket_price(price_cents)
|
||||||
|
if price_cents == 0
|
||||||
|
"Gratuit"
|
||||||
|
else
|
||||||
|
number_to_currency(price_cents / 100.0, unit: "€")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class TicketMailer < ApplicationMailer
|
class TicketMailer < ApplicationMailer
|
||||||
|
helper :tickets
|
||||||
|
|
||||||
def purchase_confirmation_order(order)
|
def purchase_confirmation_order(order)
|
||||||
@order = order
|
@order = order
|
||||||
@user = order.user
|
@user = order.user
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ class Event < ApplicationRecord
|
|||||||
# Scope for published events ordered by start time
|
# Scope for published events ordered by start time
|
||||||
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
|
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
|
||||||
|
|
||||||
|
|
||||||
# === Instance Methods ===
|
# === Instance Methods ===
|
||||||
|
|
||||||
# Check if coordinates were successfully geocoded or are fallback coordinates
|
# Check if coordinates were successfully geocoded or are fallback coordinates
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ class Order < ApplicationRecord
|
|||||||
promoter_payout_cents / 100.0
|
promoter_payout_cents / 100.0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Check if order contains only free tickets
|
||||||
|
def free?
|
||||||
|
total_amount_cents == 0
|
||||||
|
end
|
||||||
|
|
||||||
# Create Stripe invoice for accounting records
|
# Create Stripe invoice for accounting records
|
||||||
#
|
#
|
||||||
# This method creates a post-payment invoice in Stripe for accounting purposes
|
# This method creates a post-payment invoice in Stripe for accounting purposes
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Ticket < ApplicationRecord
|
|||||||
validates :qr_code, presence: true, uniqueness: true
|
validates :qr_code, presence: true, uniqueness: true
|
||||||
validates :order_id, presence: true
|
validates :order_id, presence: true
|
||||||
validates :ticket_type_id, presence: true
|
validates :ticket_type_id, presence: true
|
||||||
validates :price_cents, presence: true, numericality: { greater_than: 0 }
|
validates :price_cents, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||||
validates :status, presence: true, inclusion: { in: %w[draft active used expired refunded] }
|
validates :status, presence: true, inclusion: { in: %w[draft active used expired refunded] }
|
||||||
validates :first_name, presence: true
|
validates :first_name, presence: true
|
||||||
validates :last_name, presence: true
|
validates :last_name, presence: true
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class TicketType < ApplicationRecord
|
|||||||
# Validations
|
# Validations
|
||||||
validates :name, presence: true, length: { minimum: 3, maximum: 50 }
|
validates :name, presence: true, length: { minimum: 3, maximum: 50 }
|
||||||
validates :description, presence: true, length: { minimum: 10, maximum: 500 }
|
validates :description, presence: true, length: { minimum: 10, maximum: 500 }
|
||||||
validates :price_cents, presence: true, numericality: { greater_than: 0 }
|
validates :price_cents, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||||
validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
||||||
validates :sale_start_at, presence: true
|
validates :sale_start_at, presence: true
|
||||||
validates :sale_end_at, presence: true
|
validates :sale_end_at, presence: true
|
||||||
@@ -48,6 +48,10 @@ class TicketType < ApplicationRecord
|
|||||||
[ quantity - tickets.count, 0 ].max
|
[ quantity - tickets.count, 0 ].max
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def free?
|
||||||
|
price_cents == 0
|
||||||
|
end
|
||||||
|
|
||||||
def sales_status
|
def sales_status
|
||||||
return :draft if sale_start_at.nil? || sale_end_at.nil?
|
return :draft if sale_start_at.nil? || sale_end_at.nil?
|
||||||
return :expired if sale_end_at < Time.current
|
return :expired if sale_end_at < Time.current
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="text-xl font-bold text-purple-700 <%= "text-gray-400" if sold_out %>">
|
<p class="text-xl font-bold text-purple-700 <%= "text-gray-400" if sold_out %>">
|
||||||
<%= number_to_currency(price_cents / 100.0, unit: "€") %>
|
<%= format_ticket_price(price_cents) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-12 px-4 sm:px-6">
|
|
||||||
<div class="max-w-3xl mx-auto">
|
|
||||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="bg-gradient-to-r from-purple-600 to-indigo-700 px-6 py-8 text-center">
|
|
||||||
<div class="flex justify-center mb-4">
|
|
||||||
<div class="w-16 h-16 rounded-full bg-white/20 flex items-center justify-center">
|
|
||||||
<svg class="w-8 h-8 text-white" 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>
|
|
||||||
<h1 class="text-3xl font-bold text-white mb-2">Paiement réussi !</h1>
|
|
||||||
<p class="text-purple-100">Félicitations pour votre achat</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<div class="p-6 sm:p-8">
|
|
||||||
<div class="text-center mb-8">
|
|
||||||
<p class="text-xl text-gray-700">
|
|
||||||
Vos billets pour <span class="font-bold text-purple-700"><%= @event.name %></span> ont été achetés avec succès.
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-500 mt-2">
|
|
||||||
Un email de confirmation avec vos billets a été envoyé à <span class="font-medium"><%= current_user.email %></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Event Details -->
|
|
||||||
<div class="bg-gray-50 rounded-xl p-6 mb-8">
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
|
||||||
<svg class="w-5 h-5 mr-2 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
||||||
</svg>
|
|
||||||
Détails de l'événement
|
|
||||||
</h2>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div class="flex items-center p-3 bg-white rounded-lg">
|
|
||||||
<svg class="w-5 h-5 text-purple-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<p class="text-xs text-gray-500">Lieu</p>
|
|
||||||
<p class="font-medium"><%= @event.venue_name %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center p-3 bg-white rounded-lg">
|
|
||||||
<svg class="w-5 h-5 text-purple-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<p class="text-xs text-gray-500">Date & Heure</p>
|
|
||||||
<p class="font-medium"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tickets -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
|
||||||
<svg class="w-5 h-5 mr-2 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"></path>
|
|
||||||
</svg>
|
|
||||||
Vos billets
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<% @tickets.each do |ticket| %>
|
|
||||||
<div class="bg-gradient-to-r from-purple-50 to-indigo-50 rounded-xl border border-purple-100 p-5">
|
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center mr-4">
|
|
||||||
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-gray-900"><%= ticket.ticket_type.name %></h3>
|
|
||||||
<p class="text-sm text-gray-600">Prix: <span class="font-medium"><%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %></span></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<%= link_to ticket_download_path(ticket.qr_code, format: :pdf),
|
|
||||||
class: "inline-flex items-center px-4 py-2 btn btn-primary rounded-lg transition-all duration-200 text-sm font-medium shadow-sm" do %>
|
|
||||||
<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="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
||||||
</svg>
|
|
||||||
Télécharger PDF
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 pt-4 border-t border-purple-100 flex items-center justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<svg class="w-4 h-4 text-gray-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="text-xs text-gray-500">Code QR: <%= ticket.qr_code[0..7] %></span>
|
|
||||||
</div>
|
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
||||||
Actif
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Important Notice -->
|
|
||||||
<div class="bg-blue-50 border border-blue-100 rounded-xl p-5 mb-8">
|
|
||||||
<div class="flex">
|
|
||||||
<svg class="w-5 h-5 text-blue-500 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-blue-800 mb-1">Important</h3>
|
|
||||||
<p class="text-sm text-blue-700">
|
|
||||||
Veuillez télécharger et sauvegarder vos billets. Présentez-les à l'entrée du lieu pour accéder à l'événement.
|
|
||||||
Un email de confirmation avec vos billets a été envoyé à votre adresse email.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
|
||||||
<%= link_to dashboard_path,
|
|
||||||
class: "inline-flex items-center justify-center px-6 py-3 btn btn-primary rounded-xl transition-all duration-200 font-medium shadow-sm" do %>
|
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
|
||||||
</svg>
|
|
||||||
Tableau de bord
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to events_path,
|
|
||||||
class: "inline-flex items-center justify-center px-6 py-3 bg-white text-gray-700 rounded-xl border border-gray-300 hover:bg-gray-50 transition-all duration-200 font-medium shadow-sm" do %>
|
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
||||||
</svg>
|
|
||||||
Voir plus d'événements
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -135,11 +135,9 @@
|
|||||||
controller: "ticket-selection",
|
controller: "ticket-selection",
|
||||||
ticket_selection_target: "form",
|
ticket_selection_target: "form",
|
||||||
ticket_selection_event_slug_value: @event.slug,
|
ticket_selection_event_slug_value: @event.slug,
|
||||||
ticket_selection_event_id_value: @event.id,
|
ticket_selection_event_id_value: @event.id,
|
||||||
ticket_selection_order_new_url_value: event_order_new_path(@event.slug, @event.id),
|
ticket_selection_order_new_url_value: event_order_new_path(@event.slug, @event.id),
|
||||||
ticket_selection_store_cart_url_value: api_v1_store_cart_path,
|
ticket_selection_store_cart_url_value: api_v1_store_cart_path
|
||||||
ticket_selection_order_new_url_value: event_order_new_path(@event.slug, @event.id),
|
|
||||||
ticket_selection_store_cart_url_value: api_v1_store_cart_path
|
|
||||||
} do |form| %>
|
} do |form| %>
|
||||||
|
|
||||||
<div class="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl border border-purple-100 p-6 shadow-sm">
|
<div class="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl border border-purple-100 p-6 shadow-sm">
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
<!-- Order Total -->
|
<!-- Order Total -->
|
||||||
<div class=" pt-12">
|
<div class=" pt-12">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center justify-between text-lg pt-2 border-t border-gray-200">
|
<div class="flex items-center justify-between text-lg pt-2">
|
||||||
<span class="font-medium text-gray-900">Total</span>
|
<span class="font-medium text-gray-900">Total</span>
|
||||||
<span class="font-bold text-2xl text-purple-600"><%= @order.total_amount_euros %>€</span>
|
<span class="font-bold text-2xl text-purple-600"><%= @order.total_amount_euros %>€</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -126,7 +126,7 @@
|
|||||||
<!-- Total -->
|
<!-- Total -->
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center justify-between text-lg pt-2 border-t border-gray-200">
|
<div class="flex items-center justify-between text-lg pt-2">
|
||||||
<span class="font-medium text-gray-900">Total payé</span>
|
<span class="font-medium text-gray-900">Total payé</span>
|
||||||
<span class="font-bold text-2xl text-green-600">
|
<span class="font-bold text-2xl text-green-600">
|
||||||
<%= @order.total_amount_euros %>€
|
<%= @order.total_amount_euros %>€
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
|
|
||||||
<!-- Total -->
|
<!-- Total -->
|
||||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||||
<div class="flex items-center justify-between text-lg pt-2 border-t border-gray-200">
|
<div class="flex items-center justify-between text-lg pt-2">
|
||||||
<span class="font-medium text-gray-900">Total <%= @order.status == 'paid' || @order.status == 'completed' ? 'payé' : 'à payer' %></span>
|
<span class="font-medium text-gray-900">Total <%= @order.status == 'paid' || @order.status == 'completed' ? 'payé' : 'à payer' %></span>
|
||||||
<span class="font-bold text-2xl <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-purple-600' %>">
|
<span class="font-bold text-2xl <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-purple-600' %>">
|
||||||
<%= @order.total_amount_euros %>€
|
<%= @order.total_amount_euros %>€
|
||||||
@@ -212,4 +212,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -132,10 +132,14 @@
|
|||||||
<div class="bg-blue-50 border border-blue-200 rounded-2xl p-4">
|
<div class="bg-blue-50 border border-blue-200 rounded-2xl p-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<i data-lucide="users" class="w-5 h-5 text-blue-400 mr-3"></i>
|
<i data-lucide="users" class="w-5 h-5 text-blue-400 mr-3"></i>
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<h3 class="text-sm font-medium text-blue-900">Événement complet</h3>
|
<h3 class="text-sm font-medium text-blue-900">Événement complet</h3>
|
||||||
<p class="text-sm text-blue-700">Tous les billets pour cet événement ont été vendus.</p>
|
<p class="text-sm text-blue-700">Tous les billets pour cet événement ont été vendus.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<%= button_to mark_available_promoter_event_path(@event), method: :patch, class: "ml-4 inline-flex items-center px-3 py-1 bg-white border border-blue-300 text-blue-700 text-sm font-medium rounded-lg hover:bg-blue-50 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="refresh-ccw" class="w-4 h-4 mr-1"></i>
|
||||||
|
Marquer comme disponible
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -273,10 +277,19 @@
|
|||||||
<i data-lucide="ticket" class="w-4 h-4 mr-2"></i>
|
<i data-lucide="ticket" class="w-4 h-4 mr-2"></i>
|
||||||
Gérer les types de billets
|
Gérer les types de billets
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= button_to mark_sold_out_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-gray-50 text-gray-700 font-medium text-sm rounded-lg hover:bg-gray-100 transition-colors duration-200", disabled: !@event.published? do %>
|
|
||||||
<i data-lucide="users" class="w-4 h-4 mr-2"></i>
|
<% if @event.sold_out? %>
|
||||||
Marquer comme complet
|
<%= button_to mark_available_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-blue-50 text-blue-700 font-medium text-sm rounded-lg hover:bg-blue-100 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="refresh-ccw" class="w-4 h-4 mr-2"></i>
|
||||||
|
Marquer comme disponible
|
||||||
|
<% end %>
|
||||||
|
<% elsif @event.published? %>
|
||||||
|
<%= button_to mark_sold_out_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-gray-50 text-gray-700 font-medium text-sm rounded-lg hover:bg-gray-100 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="users" class="w-4 h-4 mr-2"></i>
|
||||||
|
Marquer comme complet
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<hr class="border-gray-200">
|
<hr class="border-gray-200">
|
||||||
<%= button_to promoter_event_path(@event), method: :delete,
|
<%= button_to promoter_event_path(@event), method: :delete,
|
||||||
data: { confirm: "Êtes-vous sûr de vouloir supprimer cet événement ? Cette action est irréversible." },
|
data: { confirm: "Êtes-vous sûr de vouloir supprimer cet événement ? Cette action est irréversible." },
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<%= form.number_field :price_euros,
|
<%= form.number_field :price_euros,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
min: 0.01,
|
min: 0,
|
||||||
class: "w-full px-4 py-2 pl-8 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
class: "w-full px-4 py-2 pl-8 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
||||||
data: { "ticket-type-form-target": "price", action: "input->ticket-type-form#updateTotal" } %>
|
data: { "ticket-type-form-target": "price", action: "input->ticket-type-form#updateTotal" } %>
|
||||||
<div class="absolute left-3 top-2.5 text-gray-500">€</div>
|
<div class="absolute left-3 top-2.5 text-gray-500">€</div>
|
||||||
@@ -91,6 +91,8 @@
|
|||||||
<i data-lucide="alert-triangle" class="w-4 h-4 inline mr-1"></i>
|
<i data-lucide="alert-triangle" class="w-4 h-4 inline mr-1"></i>
|
||||||
Modifier le prix n'affectera pas les billets déjà vendus
|
Modifier le prix n'affectera pas les billets déjà vendus
|
||||||
</p>
|
</p>
|
||||||
|
<% else %>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Prix unitaire du billet (0€ pour un billet gratuit)</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -163,8 +165,10 @@
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<i data-lucide="info" class="w-5 h-5 text-blue-400 mt-0.5 mr-2"></i>
|
<i data-lucide="info" class="w-5 h-5 text-blue-400 mt-0.5 mr-2"></i>
|
||||||
<p class="text-sm text-blue-800">
|
<p class="text-sm text-blue-800">
|
||||||
<strong>Événement:</strong> <%= @event.start_time.strftime("%d/%m/%Y à %H:%M") %><br>
|
<strong>Début d'événement :</strong> <%= @event.start_time.strftime("%d/%m/%Y à %H:%M") %><br>
|
||||||
Les ventes doivent se terminer avant le début de l'événement.
|
<% unless @event.allow_booking_during_event? %>
|
||||||
|
Les ventes doivent se terminer avant le début de l'événement.
|
||||||
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,12 +212,6 @@
|
|||||||
<%= link_to promoter_event_ticket_type_path(@event, @ticket_type), class: "text-gray-500 hover:text-gray-700 transition-colors" do %>
|
<%= link_to promoter_event_ticket_type_path(@event, @ticket_type), class: "text-gray-500 hover:text-gray-700 transition-colors" do %>
|
||||||
Annuler
|
Annuler
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @ticket_type.tickets.any? %>
|
|
||||||
<p class="text-sm text-yellow-600">
|
|
||||||
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
|
||||||
<%= pluralize(@ticket_type.tickets.count, 'billet') %> déjà vendu(s)
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<!-- Basic Information -->
|
<!-- Basic Information -->
|
||||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Informations générales</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-6">Informations générales</h3>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :name, "Nom du type de billet", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
<%= form.label :name, "Nom du type de billet", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
@@ -61,25 +61,25 @@
|
|||||||
<!-- Pricing & Quantity -->
|
<!-- Pricing & Quantity -->
|
||||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Prix et quantité</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-6">Prix et quantité</h3>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :price_euros, "Prix (€)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
<%= form.label :price_euros, "Prix (€)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<%= form.number_field :price_euros,
|
<%= form.number_field :price_euros,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
min: 0.01,
|
min: 0,
|
||||||
class: "w-full px-4 py-2 pl-8 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
class: "w-full px-4 py-2 pl-8 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
||||||
data: { "ticket-type-form-target": "price", action: "input->ticket-type-form#updateTotal" } %>
|
data: { "ticket-type-form-target": "price", action: "input->ticket-type-form#updateTotal" } %>
|
||||||
<div class="absolute left-3 top-2.5 text-gray-500">€</div>
|
<div class="absolute left-3 top-2.5 text-gray-500">€</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-sm text-gray-500">Prix unitaire du billet</p>
|
<p class="mt-1 text-sm text-gray-500">Prix unitaire du billet (0€ pour un billet gratuit)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :quantity, "Quantité disponible", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
<%= form.label :quantity, "Quantité disponible", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
<%= form.number_field :quantity,
|
<%= form.number_field :quantity,
|
||||||
min: 1,
|
min: 1,
|
||||||
class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
||||||
data: { "ticket-type-form-target": "quantity", action: "input->ticket-type-form#updateTotal" } %>
|
data: { "ticket-type-form-target": "quantity", action: "input->ticket-type-form#updateTotal" } %>
|
||||||
<p class="mt-1 text-sm text-gray-500">Nombre total de billets de ce type</p>
|
<p class="mt-1 text-sm text-gray-500">Nombre total de billets de ce type</p>
|
||||||
@@ -100,18 +100,18 @@
|
|||||||
<!-- Sales Period -->
|
<!-- Sales Period -->
|
||||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Période de vente</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-6">Période de vente</h3>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :sale_start_at, "Début des ventes", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
<%= form.label :sale_start_at, "Début des ventes", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
<%= form.datetime_local_field :sale_start_at,
|
<%= form.datetime_local_field :sale_start_at,
|
||||||
value: @ticket_type.sale_start_at&.strftime("%Y-%m-%dT%H:%M"),
|
value: @ticket_type.sale_start_at&.strftime("%Y-%m-%dT%H:%M"),
|
||||||
class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" %>
|
class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :sale_end_at, "Fin des ventes", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
<%= form.label :sale_end_at, "Fin des ventes", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
<%= form.datetime_local_field :sale_end_at,
|
<%= form.datetime_local_field :sale_end_at,
|
||||||
value: @ticket_type.sale_end_at&.strftime("%Y-%m-%dT%H:%M"),
|
value: @ticket_type.sale_end_at&.strftime("%Y-%m-%dT%H:%M"),
|
||||||
class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" %>
|
class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" %>
|
||||||
<p class="mt-1 text-sm text-gray-500">Les ventes s'arrêtent automatiquement à cette date</p>
|
<p class="mt-1 text-sm text-gray-500">Les ventes s'arrêtent automatiquement à cette date</p>
|
||||||
@@ -123,8 +123,11 @@
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<i data-lucide="info" class="w-5 h-5 text-blue-400 mt-0.5 mr-2"></i>
|
<i data-lucide="info" class="w-5 h-5 text-blue-400 mt-0.5 mr-2"></i>
|
||||||
<p class="text-sm text-blue-800">
|
<p class="text-sm text-blue-800">
|
||||||
<strong>Événement:</strong> <%= @event.start_time.strftime("%d/%m/%Y à %H:%M") %><br>
|
<strong>Début d'événement :</strong> <%= @event.start_time.strftime("%d/%m/%Y à %H:%M") %><br>
|
||||||
Les ventes doivent se terminer avant le début de l'événement.
|
|
||||||
|
<% unless @event.allow_booking_during_event? %>
|
||||||
|
Les ventes doivent se terminer avant le début de l'événement.
|
||||||
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,13 +137,13 @@
|
|||||||
<!-- Access Requirements -->
|
<!-- Access Requirements -->
|
||||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Conditions d'accès</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-6">Conditions d'accès</h3>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :minimum_age, "Âge minimum", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
<%= form.label :minimum_age, "Âge minimum", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
<%= form.number_field :minimum_age,
|
<%= form.number_field :minimum_age,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 120,
|
max: 120,
|
||||||
class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent",
|
||||||
placeholder: "Laisser vide si aucune restriction" %>
|
placeholder: "Laisser vide si aucune restriction" %>
|
||||||
<p class="mt-1 text-sm text-gray-500">Âge minimum requis (optionnel)</p>
|
<p class="mt-1 text-sm text-gray-500">Âge minimum requis (optionnel)</p>
|
||||||
@@ -167,11 +170,11 @@
|
|||||||
Annuler
|
Annuler
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<%= form.submit "Créer le type de billet", class: "inline-flex items-center px-6 py-3 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200" %>
|
<%= form.submit "Créer le type de billet", class: "inline-flex items-center px-6 py-3 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="text-sm font-medium text-gray-900">
|
<p class="text-sm font-medium text-gray-900">
|
||||||
<%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %>
|
<%= format_ticket_price(ticket.price_cents) %>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-gray-500">
|
<p class="text-xs text-gray-500">
|
||||||
<%= ticket.created_at.strftime("%d/%m/%Y") %>
|
<%= ticket.created_at.strftime("%d/%m/%Y") %>
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="text-center p-4 bg-purple-50 rounded-lg">
|
<div class="text-center p-4 bg-purple-50 rounded-lg">
|
||||||
<div class="text-3xl font-bold text-purple-600">
|
<div class="text-3xl font-bold text-purple-600">
|
||||||
<%= number_to_currency(@ticket_type.price_euros, unit: "€") %>
|
<%= format_ticket_price(@ticket_type.price_cents) %>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-500">Prix unitaire</div>
|
<div class="text-sm text-gray-500">Prix unitaire</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<p style="margin: 5px 0 0;"><a href="<%= ticket_url(ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px;">📱 Voir le détail et le code QR</a></p>
|
<p style="margin: 5px 0 0;"><a href="<%= ticket_url(ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px;">📱 Voir le détail et le code QR</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
<p style="margin: 0; font-weight: bold; color: #212529;"><%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %></p>
|
<p style="margin: 0; font-weight: bold; color: #212529;"><%= format_ticket_price(ticket.price_cents) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Prix</p>
|
<p style="margin: 0; color: #6c757d; font-size: 14px;">Prix</p>
|
||||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %></p>
|
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= format_ticket_price(@ticket.price_cents) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ DÉTAILS DE VOTRE COMMANDE
|
|||||||
Événement : <%= @event.name %>
|
Événement : <%= @event.name %>
|
||||||
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||||
Nombre de billets : <%= @tickets.count %>
|
Nombre de billets : <%= @tickets.count %>
|
||||||
Total : <%= number_to_currency(@order.total_amount_euros, unit: "€") %>
|
Total : <%= @order.free? ? "Gratuit" : number_to_currency(@order.total_amount_euros, unit: "€") %>
|
||||||
|
|
||||||
BILLETS INCLUS :
|
BILLETS INCLUS :
|
||||||
<% @tickets.each_with_index do |ticket, index| %>
|
<% @tickets.each_with_index do |ticket, index| %>
|
||||||
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> - <%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %>
|
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> - <%= format_ticket_price(ticket.price_cents) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
Vos billets sont attachés à cet email en format PDF. Présentez-les à l'entrée de l'événement pour y accéder.
|
Vos billets sont attachés à cet email en format PDF. Présentez-les à l'entrée de l'événement pour y accéder.
|
||||||
@@ -32,7 +32,7 @@ DÉTAILS DE VOTRE BILLET
|
|||||||
Événement : <%= @event.name %>
|
Événement : <%= @event.name %>
|
||||||
Type de billet : <%= @ticket.ticket_type.name %>
|
Type de billet : <%= @ticket.ticket_type.name %>
|
||||||
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||||
Prix : <%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %>
|
Prix : <%= format_ticket_price(@ticket.price_cents) %>
|
||||||
|
|
||||||
Votre billet est attaché à cet email en format PDF. Présentez-le à l'entrée de l'événement pour y accéder.
|
Votre billet est attaché à cet email en format PDF. Présentez-le à l'entrée de l'événement pour y accéder.
|
||||||
|
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-12 px-4 sm:px-6">
|
|
||||||
<div class="max-w-3xl mx-auto">
|
|
||||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="bg-gradient-to-r from-purple-600 to-indigo-700 px-6 py-8 text-center">
|
|
||||||
<div class="flex justify-center mb-4">
|
|
||||||
<div class="w-16 h-16 rounded-full bg-white/20 flex items-center justify-center">
|
|
||||||
<svg class="w-8 h-8 text-white" 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>
|
|
||||||
<h1 class="text-3xl font-bold text-white mb-2">Paiement réussi !</h1>
|
|
||||||
<p class="text-purple-100">Félicitations pour votre achat</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<div class="p-6 sm:p-8">
|
|
||||||
<div class="text-center mb-8">
|
|
||||||
<p class="text-xl text-gray-700">
|
|
||||||
Vos billets pour <span class="font-bold text-purple-700"><%= @event.name %></span> ont été achetés avec succès.
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-500 mt-2">
|
|
||||||
Un email de confirmation avec vos billets a été envoyé à <span class="font-medium"><%= current_user.email %></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Event Details -->
|
|
||||||
<div class="bg-gray-50 rounded-xl p-6 mb-8">
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
|
||||||
<svg class="w-5 h-5 mr-2 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
||||||
</svg>
|
|
||||||
Détails de l'événement
|
|
||||||
</h2>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div class="flex items-center p-3 bg-white rounded-lg">
|
|
||||||
<svg class="w-5 h-5 text-purple-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<p class="text-xs text-gray-500">Lieu</p>
|
|
||||||
<p class="font-medium"><%= @event.venue_name %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center p-3 bg-white rounded-lg">
|
|
||||||
<svg class="w-5 h-5 text-purple-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<p class="text-xs text-gray-500">Date & Heure</p>
|
|
||||||
<p class="font-medium"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tickets -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
|
||||||
<svg class="w-5 h-5 mr-2 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"></path>
|
|
||||||
</svg>
|
|
||||||
Vos billets
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<% @tickets.each do |ticket| %>
|
|
||||||
<div class="bg-gradient-to-r from-purple-50 to-indigo-50 rounded-xl border border-purple-100 p-5">
|
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center mr-4">
|
|
||||||
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-gray-900"><%= ticket.ticket_type.name %></h3>
|
|
||||||
<p class="text-sm text-gray-600">Prix: <span class="font-medium"><%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %></span></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<%= link_to ticket_download_path(ticket.qr_code, format: :pdf),
|
|
||||||
class: "inline-flex items-center px-4 py-2 btn btn-primary rounded-lg transition-all duration-200 text-sm font-medium shadow-sm" do %>
|
|
||||||
<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="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
||||||
</svg>
|
|
||||||
Télécharger PDF
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 pt-4 border-t border-purple-100 flex items-center justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<svg class="w-4 h-4 text-gray-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="text-xs text-gray-500">Code QR: <%= ticket.qr_code[0..7] %></span>
|
|
||||||
</div>
|
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
||||||
Actif
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Important Notice -->
|
|
||||||
<div class="bg-blue-50 border border-blue-100 rounded-xl p-5 mb-8">
|
|
||||||
<div class="flex">
|
|
||||||
<svg class="w-5 h-5 text-blue-500 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-blue-800 mb-1">Important</h3>
|
|
||||||
<p class="text-sm text-blue-700">
|
|
||||||
Veuillez télécharger et sauvegarder vos billets. Présentez-les à l'entrée du lieu pour accéder à l'événement.
|
|
||||||
Un email de confirmation avec vos billets a été envoyé à votre adresse email.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
|
||||||
<%= link_to dashboard_path,
|
|
||||||
class: "inline-flex items-center justify-center px-6 py-3 btn btn-primary rounded-xl transition-all duration-200 font-medium shadow-sm" do %>
|
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
|
||||||
</svg>
|
|
||||||
Tableau de bord
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to events_path,
|
|
||||||
class: "inline-flex items-center justify-center px-6 py-3 bg-white text-gray-700 rounded-xl border border-gray-300 hover:bg-gray-50 transition-all duration-200 font-medium shadow-sm" do %>
|
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
||||||
</svg>
|
|
||||||
Voir plus d'événements
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-1">Prix</label>
|
<label class="block text-sm font-medium text-gray-500 mb-1">Prix</label>
|
||||||
<p class="text-xl font-bold text-gray-900">
|
<p class="text-xl font-bold text-gray-900">
|
||||||
<%= number_to_currency(@ticket.price_euros, unit: "€", separator: ",", delimiter: " ", format: "%n %u") %>
|
<%= format_ticket_price(@ticket.price_cents) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ Rails.application.routes.draw do
|
|||||||
patch :unpublish
|
patch :unpublish
|
||||||
patch :cancel
|
patch :cancel
|
||||||
patch :mark_sold_out
|
patch :mark_sold_out
|
||||||
|
patch :mark_available
|
||||||
post :duplicate
|
post :duplicate
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
77
db/seeds.rb
77
db/seeds.rb
@@ -112,3 +112,80 @@ events.each_with_index do |event, index|
|
|||||||
end
|
end
|
||||||
|
|
||||||
puts "Created #{User.count} users, #{Event.count} events, and #{TicketType.count} ticket types"
|
puts "Created #{User.count} users, #{Event.count} events, and #{TicketType.count} ticket types"
|
||||||
|
|
||||||
|
# Create regular user as promoter
|
||||||
|
promoter = User.find_or_create_by!(email: "kbataille@vivaldi.net") do |u|
|
||||||
|
u.password = "lareunion974!"
|
||||||
|
u.password_confirmation = "lareunion974!"
|
||||||
|
u.last_name = nil
|
||||||
|
u.first_name = nil
|
||||||
|
u.is_professionnal = true
|
||||||
|
end
|
||||||
|
|
||||||
|
belle_epoque_event = Event.find_or_create_by!(name: "LA BELLE ÉPOQUE PAR SISLEY ÉVENTS") do |e|
|
||||||
|
e.slug = "la-belle-epoque-par-sisley-events"
|
||||||
|
e.state = :draft
|
||||||
|
e.description = "
|
||||||
|
Sisley évents Présente :
|
||||||
|
SAM 13 SEPT
|
||||||
|
LA BELLE ÉPOQUE de 18H à 2H
|
||||||
|
sur le Rooftop LE PATIO
|
||||||
|
ÉVÈNEMENT EN PLEIN AIR
|
||||||
|
Ambiance Rétro / old school : zouk , Ragga , kompa , Dancehall , hip hop , Groove , Rnb …
|
||||||
|
Restauration disponible sur place : Accras ,Allocos , specialités asiatique , japonaise et une large carte de choix de Pizzas pour vous régaler !
|
||||||
|
TARIF D'ENTRÉE : 10€ SUR PLACE UNIQUEMENT
|
||||||
|
Réservée aux + de 30 ans
|
||||||
|
Suivez nous sur Instagram : Sisley Évents
|
||||||
|
Le patio
|
||||||
|
38 avenue Leon Gaumont , Montreuil
|
||||||
|
Parking du Décathlon disponible , rue de la république, à 100m du Patio
|
||||||
|
"
|
||||||
|
e.venue_name = "Le Patio Rooftop"
|
||||||
|
e.venue_address = "38 Av. Léon Gaumont, 93100 Montreuil"
|
||||||
|
e.latitude = 48.862336
|
||||||
|
e.longitude = 2.441218
|
||||||
|
e.start_time = 3.days.from_now
|
||||||
|
e.end_time = 3.days.from_now + 8.hours
|
||||||
|
e.featured = false
|
||||||
|
e.image = "https://data.bizouk.com/cache1/events/images/10/78/87/b801a9a43266b4cc54bdda73bf34eec8_700_800_auto_97.jpg"
|
||||||
|
e.user = promoter
|
||||||
|
e.allow_booking_during_event = true
|
||||||
|
end
|
||||||
|
|
||||||
|
belle_epoque_event.update!(start_time: 3.days.from_now, end_time: 3.days.from_now + 8.hours)
|
||||||
|
|
||||||
|
|
||||||
|
# Create ticket types for "La belle époque" event
|
||||||
|
belle_epoque_event = Event.find_by!(slug: "la-belle-epoque-par-sisley-events")
|
||||||
|
|
||||||
|
TicketType.find_or_create_by!(event: belle_epoque_event, name: "Free invitation valid before 7 p.m.") do |tt|
|
||||||
|
tt.description = "Free invitation ticket valid before 7 p.m. for La Belle Époque"
|
||||||
|
tt.price_cents = 0
|
||||||
|
tt.quantity = 50
|
||||||
|
tt.sale_start_at = Time.current
|
||||||
|
tt.sale_end_at = belle_epoque_event.start_time
|
||||||
|
tt.minimum_age = 30
|
||||||
|
tt.requires_id = true
|
||||||
|
end
|
||||||
|
|
||||||
|
TicketType.find_or_create_by!(event: belle_epoque_event, name: "ENTRY 10€ TO BE PAYED ON SITE ONLY") do |tt|
|
||||||
|
tt.description = "Entry ticket to be paid on site only (free in system)"
|
||||||
|
tt.price_cents = 0
|
||||||
|
tt.quantity = 100
|
||||||
|
tt.sale_start_at = Time.current
|
||||||
|
tt.sale_end_at = belle_epoque_event.start_time
|
||||||
|
tt.minimum_age = 30
|
||||||
|
tt.requires_id = true
|
||||||
|
end
|
||||||
|
|
||||||
|
TicketType.find_or_create_by!(event: belle_epoque_event, name: "Paid Entry 10€") do |tt|
|
||||||
|
tt.description = "Paid entry ticket for La Belle Époque at 10€"
|
||||||
|
tt.price_cents = 1000 # 10€
|
||||||
|
tt.quantity = 200
|
||||||
|
tt.sale_start_at = Time.current
|
||||||
|
tt.sale_end_at = belle_epoque_event.start_time
|
||||||
|
tt.minimum_age = 30
|
||||||
|
tt.requires_id = true
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Created 1 promoter, 1 draft event with ticket types"
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class OrdersControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_equal "draft", new_order.status
|
assert_equal "draft", new_order.status
|
||||||
assert_equal @user, new_order.user
|
assert_equal @user, new_order.user
|
||||||
assert_equal @event, new_order.event
|
assert_equal @event, new_order.event
|
||||||
assert_equal @ticket_type.price_cents + 100, new_order.total_amount_cents # includes 1€ service fee
|
assert_equal @ticket_type.price_cents, new_order.total_amount_cents # Service fee deducted from promoter payout, not added to customer
|
||||||
|
|
||||||
assert_redirected_to checkout_order_path(new_order)
|
assert_redirected_to checkout_order_path(new_order)
|
||||||
assert_equal new_order.id, session[:draft_order_id]
|
assert_equal new_order.id, session[:draft_order_id]
|
||||||
|
|||||||
@@ -603,4 +603,22 @@ class OrderTest < ActiveSupport::TestCase
|
|||||||
result = order.stripe_invoice_pdf_url
|
result = order.stripe_invoice_pdf_url
|
||||||
assert_nil result
|
assert_nil result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "free? should return true for zero amount orders" do
|
||||||
|
free_order = Order.create!(
|
||||||
|
user: @user, event: @event, total_amount_cents: 0,
|
||||||
|
status: "draft", payment_attempts: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert free_order.free?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "free? should return false for non-zero amount orders" do
|
||||||
|
paid_order = Order.create!(
|
||||||
|
user: @user, event: @event, total_amount_cents: 1000,
|
||||||
|
status: "draft", payment_attempts: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_not paid_order.free?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -244,4 +244,38 @@ class TicketTypeTest < ActiveSupport::TestCase
|
|||||||
)
|
)
|
||||||
assert_not ticket_type.save
|
assert_not ticket_type.save
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "should allow free tickets with zero price" do
|
||||||
|
user = User.create!(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123",
|
||||||
|
password_confirmation: "password123"
|
||||||
|
)
|
||||||
|
|
||||||
|
event = Event.create!(
|
||||||
|
name: "Test Event",
|
||||||
|
slug: "test-event",
|
||||||
|
description: "Valid description for the event that is long enough",
|
||||||
|
latitude: 48.8566,
|
||||||
|
longitude: 2.3522,
|
||||||
|
venue_name: "Test Venue",
|
||||||
|
venue_address: "123 Test Street",
|
||||||
|
user: user
|
||||||
|
)
|
||||||
|
|
||||||
|
ticket_type = TicketType.new(
|
||||||
|
name: "Free Ticket",
|
||||||
|
description: "Valid description for the free ticket type",
|
||||||
|
price_cents: 0,
|
||||||
|
quantity: 50,
|
||||||
|
sale_start_at: Time.current,
|
||||||
|
sale_end_at: Time.current + 1.day,
|
||||||
|
event: event
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ticket_type.save
|
||||||
|
assert ticket_type.free?
|
||||||
|
assert_equal 0, ticket_type.price_cents
|
||||||
|
assert_equal 0.0, ticket_type.price_euros
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -210,25 +210,12 @@ class StripeInvoiceServiceTest < ActiveSupport::TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expected_service_fee_line_item = {
|
|
||||||
customer: "cus_test123",
|
|
||||||
invoice: "in_test123",
|
|
||||||
amount: 100,
|
|
||||||
currency: "eur",
|
|
||||||
description: "Frais de service - Frais de traitement de la commande",
|
|
||||||
metadata: {
|
|
||||||
item_type: "service_fee",
|
|
||||||
amount_cents: 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_invoice = mock("invoice")
|
mock_invoice = mock("invoice")
|
||||||
mock_invoice.stubs(:id).returns("in_test123")
|
mock_invoice.stubs(:id).returns("in_test123")
|
||||||
mock_invoice.stubs(:finalize_invoice).returns(mock_invoice)
|
mock_invoice.stubs(:finalize_invoice).returns(mock_invoice)
|
||||||
mock_invoice.expects(:pay)
|
mock_invoice.expects(:pay)
|
||||||
Stripe::Invoice.expects(:create).returns(mock_invoice)
|
Stripe::Invoice.expects(:create).returns(mock_invoice)
|
||||||
Stripe::InvoiceItem.expects(:create).with(expected_ticket_line_item)
|
Stripe::InvoiceItem.expects(:create).with(expected_ticket_line_item) # Only for tickets, no service fee
|
||||||
Stripe::InvoiceItem.expects(:create).with(expected_service_fee_line_item)
|
|
||||||
|
|
||||||
result = @service.create_post_payment_invoice
|
result = @service.create_post_payment_invoice
|
||||||
assert_not_nil result
|
assert_not_nil result
|
||||||
|
|||||||
Reference in New Issue
Block a user