develop #3
104
app/controllers/promoter/ticket_types_controller.rb
Normal file
104
app/controllers/promoter/ticket_types_controller.rb
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Promoter Ticket Types Controller
|
||||||
|
#
|
||||||
|
# Handles ticket type (bundle) management for promoters
|
||||||
|
# Allows promoters to create, edit, delete and manage ticket types for their events
|
||||||
|
class Promoter::TicketTypesController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :ensure_can_manage_events!
|
||||||
|
before_action :set_event
|
||||||
|
before_action :set_ticket_type, only: [:show, :edit, :update, :destroy]
|
||||||
|
|
||||||
|
# Display all ticket types for an event
|
||||||
|
def index
|
||||||
|
@ticket_types = @event.ticket_types.order(:created_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Display a specific ticket type
|
||||||
|
def show
|
||||||
|
# Ticket type is set by set_ticket_type callback
|
||||||
|
end
|
||||||
|
|
||||||
|
# Show form to create a new ticket type
|
||||||
|
def new
|
||||||
|
@ticket_type = @event.ticket_types.build
|
||||||
|
# Set default values
|
||||||
|
@ticket_type.sale_start_at = Time.current
|
||||||
|
@ticket_type.sale_end_at = @event.start_time || 1.week.from_now
|
||||||
|
@ticket_type.requires_id = false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create a new ticket type
|
||||||
|
def create
|
||||||
|
@ticket_type = @event.ticket_types.build(ticket_type_params)
|
||||||
|
|
||||||
|
if @ticket_type.save
|
||||||
|
redirect_to promoter_event_ticket_types_path(@event), notice: 'Type de billet créé avec succès!'
|
||||||
|
else
|
||||||
|
render :new, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Show form to edit an existing ticket type
|
||||||
|
def edit
|
||||||
|
# Ticket type is set by set_ticket_type callback
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update an existing ticket type
|
||||||
|
def update
|
||||||
|
if @ticket_type.update(ticket_type_params)
|
||||||
|
redirect_to promoter_event_ticket_type_path(@event, @ticket_type), notice: 'Type de billet mis à jour avec succès!'
|
||||||
|
else
|
||||||
|
render :edit, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Delete a ticket type
|
||||||
|
def destroy
|
||||||
|
if @ticket_type.tickets.any?
|
||||||
|
redirect_to promoter_event_ticket_types_path(@event), alert: 'Impossible de supprimer ce type de billet car des billets ont déjà été vendus.'
|
||||||
|
else
|
||||||
|
@ticket_type.destroy
|
||||||
|
redirect_to promoter_event_ticket_types_path(@event), notice: 'Type de billet supprimé avec succès!'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Duplicate an existing ticket type
|
||||||
|
def duplicate
|
||||||
|
original = @event.ticket_types.find(params[:id])
|
||||||
|
@ticket_type = original.dup
|
||||||
|
@ticket_type.name = "#{original.name} (Copie)"
|
||||||
|
|
||||||
|
if @ticket_type.save
|
||||||
|
redirect_to edit_promoter_event_ticket_type_path(@event, @ticket_type), notice: 'Type de billet dupliqué avec succès!'
|
||||||
|
else
|
||||||
|
redirect_to promoter_event_ticket_types_path(@event), alert: 'Erreur lors de la duplication.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_can_manage_events!
|
||||||
|
unless current_user.can_manage_events?
|
||||||
|
redirect_to dashboard_path, alert: 'Vous n\'avez pas les permissions nécessaires pour gérer des événements.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_event
|
||||||
|
@event = current_user.events.find(params[:event_id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
redirect_to promoter_events_path, alert: 'Event non trouvé ou vous n\'avez pas accès à cet event.'
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_ticket_type
|
||||||
|
@ticket_type = @event.ticket_types.find(params[:id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
redirect_to promoter_event_ticket_types_path(@event), alert: 'Type de billet non trouvé.'
|
||||||
|
end
|
||||||
|
|
||||||
|
def ticket_type_params
|
||||||
|
params.require(:ticket_type).permit(
|
||||||
|
:name, :description, :price_euros, :quantity,
|
||||||
|
:sale_start_at, :sale_end_at, :minimum_age, :requires_id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -22,6 +22,9 @@ application.register("header", HeaderController);
|
|||||||
import EventFormController from "./event_form_controller"
|
import EventFormController from "./event_form_controller"
|
||||||
application.register("event-form", EventFormController);
|
application.register("event-form", EventFormController);
|
||||||
|
|
||||||
|
import TicketTypeFormController from "./ticket_type_form_controller"
|
||||||
|
application.register("ticket-type-form", TicketTypeFormController);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
61
app/javascript/controllers/ticket_type_form_controller.js
Normal file
61
app/javascript/controllers/ticket_type_form_controller.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Ticket Type Form Controller
|
||||||
|
// Handles dynamic pricing calculations and form interactions
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["price", "quantity", "total"]
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
console.log("Ticket type form controller connected")
|
||||||
|
this.updateTotal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total revenue calculation when price or quantity changes
|
||||||
|
updateTotal() {
|
||||||
|
const price = parseFloat(this.priceTarget.value) || 0
|
||||||
|
const quantity = parseInt(this.quantityTarget.value) || 0
|
||||||
|
const total = price * quantity
|
||||||
|
|
||||||
|
// Format as currency
|
||||||
|
const formatter = new Intl.NumberFormat('fr-FR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR',
|
||||||
|
minimumFractionDigits: 2
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.hasQuantityTarget && this.hasTotalTarget) {
|
||||||
|
// For new ticket types, calculate potential revenue
|
||||||
|
this.totalTarget.textContent = formatter.format(total)
|
||||||
|
} else if (this.hasTotalTarget) {
|
||||||
|
// For edit forms, calculate remaining potential revenue
|
||||||
|
const soldTickets = parseInt(this.element.dataset.soldTickets) || 0
|
||||||
|
const remainingQuantity = Math.max(0, quantity - soldTickets)
|
||||||
|
const remainingRevenue = price * remainingQuantity
|
||||||
|
this.totalTarget.textContent = formatter.format(remainingRevenue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate minimum quantity (for edit forms with sold tickets)
|
||||||
|
validateQuantity() {
|
||||||
|
const soldTickets = parseInt(this.element.dataset.soldTickets) || 0
|
||||||
|
const quantity = parseInt(this.quantityTarget.value) || 0
|
||||||
|
|
||||||
|
if (quantity < soldTickets) {
|
||||||
|
this.quantityTarget.value = soldTickets
|
||||||
|
this.quantityTarget.setCustomValidity(`La quantité ne peut pas être inférieure à ${soldTickets} (billets déjà vendus)`)
|
||||||
|
} else {
|
||||||
|
this.quantityTarget.setCustomValidity('')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateTotal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format price input to ensure proper decimal places
|
||||||
|
formatPrice() {
|
||||||
|
const price = parseFloat(this.priceTarget.value)
|
||||||
|
if (!isNaN(price)) {
|
||||||
|
this.priceTarget.value = price.toFixed(2)
|
||||||
|
}
|
||||||
|
this.updateTotal()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,64 @@ class TicketType < ApplicationRecord
|
|||||||
validates :sale_end_at, presence: true
|
validates :sale_end_at, presence: true
|
||||||
validates :minimum_age, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 120 }, allow_nil: true
|
validates :minimum_age, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 120 }, allow_nil: true
|
||||||
validates :event_id, presence: true
|
validates :event_id, presence: true
|
||||||
|
validates :requires_id, inclusion: { in: [true, false] }
|
||||||
|
|
||||||
|
# Custom validations
|
||||||
|
validate :sale_end_after_start
|
||||||
|
validate :sale_times_within_event_period
|
||||||
|
|
||||||
|
# Scopes
|
||||||
|
scope :available_now, -> { where("sale_start_at <= ? AND sale_end_at >= ?", Time.current, Time.current) }
|
||||||
|
scope :upcoming, -> { where("sale_start_at > ?", Time.current) }
|
||||||
|
scope :expired, -> { where("sale_end_at < ?", Time.current) }
|
||||||
|
|
||||||
|
# Helper methods
|
||||||
|
def price_euros
|
||||||
|
return 0.0 if price_cents.nil?
|
||||||
|
price_cents / 100.0
|
||||||
|
end
|
||||||
|
|
||||||
|
def price_euros=(value)
|
||||||
|
self.price_cents = (value.to_f * 100).to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def available?
|
||||||
|
return false if sale_start_at.nil? || sale_end_at.nil?
|
||||||
|
sale_start_at <= Time.current && sale_end_at >= Time.current
|
||||||
|
end
|
||||||
|
|
||||||
|
def sold_out?
|
||||||
|
return false if quantity.nil?
|
||||||
|
tickets.count >= quantity
|
||||||
|
end
|
||||||
|
|
||||||
|
def available_quantity
|
||||||
|
return 0 if quantity.nil?
|
||||||
|
[quantity - tickets.count, 0].max
|
||||||
|
end
|
||||||
|
|
||||||
|
def sales_status
|
||||||
|
return :draft if sale_start_at.nil? || sale_end_at.nil?
|
||||||
|
return :expired if sale_end_at < Time.current
|
||||||
|
return :upcoming if sale_start_at > Time.current
|
||||||
|
return :sold_out if sold_out?
|
||||||
|
return :available
|
||||||
|
end
|
||||||
|
|
||||||
|
def total_potential_revenue
|
||||||
|
return 0.0 if quantity.nil? || price_cents.nil?
|
||||||
|
quantity * price_euros
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_revenue
|
||||||
|
return 0.0 if price_cents.nil?
|
||||||
|
tickets.count * price_euros
|
||||||
|
end
|
||||||
|
|
||||||
|
def remaining_potential_revenue
|
||||||
|
return 0.0 if quantity.nil? || price_cents.nil?
|
||||||
|
available_quantity * price_euros
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
@@ -20,4 +77,9 @@ class TicketType < ApplicationRecord
|
|||||||
return unless sale_start_at && sale_end_at
|
return unless sale_start_at && sale_end_at
|
||||||
errors.add(:sale_end_at, "must be after sale start") if sale_end_at <= sale_start_at
|
errors.add(:sale_end_at, "must be after sale start") if sale_end_at <= sale_start_at
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sale_times_within_event_period
|
||||||
|
return unless event&.start_time && sale_end_at
|
||||||
|
errors.add(:sale_end_at, "cannot be after the event starts") if sale_end_at > event.start_time
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -209,9 +209,9 @@
|
|||||||
<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-4">Actions rapides</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Actions rapides</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<%= link_to "#", class: "w-full inline-flex items-center px-4 py-2 bg-gray-50 text-gray-700 font-medium rounded-lg hover:bg-gray-100 transition-colors duration-200" do %>
|
<%= link_to promoter_event_ticket_types_path(@event), class: "w-full inline-flex items-center px-4 py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
|
||||||
<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 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 px-4 py-2 bg-gray-50 text-gray-700 font-medium rounded-lg hover:bg-gray-100 transition-colors duration-200", disabled: !@event.published? do %>
|
<%= button_to mark_sold_out_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center px-4 py-2 bg-gray-50 text-gray-700 font-medium 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>
|
<i data-lucide="users" class="w-4 h-4 mr-2"></i>
|
||||||
|
|||||||
224
app/views/promoter/ticket_types/edit.html.erb
Normal file
224
app/views/promoter/ticket_types/edit.html.erb
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<% content_for(:title, "Modifier #{@ticket_type.name}") %>
|
||||||
|
|
||||||
|
<div class="container py-8">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<%= link_to promoter_event_ticket_type_path(@event, @ticket_type), class: "text-gray-400 hover:text-gray-600 transition-colors" do %>
|
||||||
|
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
||||||
|
<% end %>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 mb-2">Modifier le type de billet</h1>
|
||||||
|
<p class="text-gray-600"><%= @ticket_type.name %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= form_with model: [:promoter, @event, @ticket_type], local: true, class: "space-y-8", data: { controller: "ticket-type-form" } do |form| %>
|
||||||
|
<% if @ticket_type.errors.any? %>
|
||||||
|
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i data-lucide="alert-circle" class="w-5 h-5 text-red-400"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-800">
|
||||||
|
<%= pluralize(@ticket_type.errors.count, "erreur") %> à corriger :
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 text-sm text-red-700">
|
||||||
|
<ul class="list-disc list-inside space-y-1">
|
||||||
|
<% @ticket_type.errors.full_messages.each do |message| %>
|
||||||
|
<li><%= message %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- Warning if tickets sold -->
|
||||||
|
<% if @ticket_type.tickets.any? %>
|
||||||
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<i data-lucide="alert-triangle" class="w-5 h-5 text-yellow-400 mt-0.5 mr-3"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-yellow-900">Attention</h3>
|
||||||
|
<p class="text-sm text-yellow-800 mt-1">
|
||||||
|
<%= pluralize(@ticket_type.tickets.count, 'billet') %> de ce type ont déjà été vendus.
|
||||||
|
Modifier certains paramètres pourrait impacter les acheteurs existants.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- Basic Information -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<%= form.label :name, "Nom du type de billet", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.text_field :name, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "Ex: Early Bird, VIP, Standard" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.label :description, "Description", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.text_area :description, rows: 3, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "Décrivez ce qui est inclus dans ce type de billet..." %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pricing & Quantity -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<%= form.label :price_euros, "Prix (€)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<div class="relative">
|
||||||
|
<%= form.number_field :price_euros,
|
||||||
|
step: 0.01,
|
||||||
|
min: 0.01,
|
||||||
|
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" } %>
|
||||||
|
<div class="absolute left-3 top-2.5 text-gray-500">€</div>
|
||||||
|
</div>
|
||||||
|
<% if @ticket_type.tickets.any? %>
|
||||||
|
<p class="mt-1 text-sm text-yellow-600">
|
||||||
|
<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
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.label :quantity, "Quantité disponible", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.number_field :quantity,
|
||||||
|
min: @ticket_type.tickets.count,
|
||||||
|
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" } %>
|
||||||
|
<% if @ticket_type.tickets.any? %>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
Minimum: <%= @ticket_type.tickets.count %> (billets déjà vendus)
|
||||||
|
</p>
|
||||||
|
<% else %>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Nombre total de billets de ce type</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Revenue preview -->
|
||||||
|
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="p-4 bg-purple-50 rounded-lg border border-purple-200">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm font-medium text-purple-900">Revenus potentiels restants</span>
|
||||||
|
<span class="text-lg font-bold text-purple-600" data-ticket-type-form-target="total">
|
||||||
|
<%= number_to_currency(@ticket_type.remaining_potential_revenue, unit: "€") %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 bg-green-50 rounded-lg border border-green-200">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm font-medium text-green-900">Revenus déjà générés</span>
|
||||||
|
<span class="text-lg font-bold text-green-600">
|
||||||
|
<%= number_to_currency(@ticket_type.current_revenue, unit: "€") %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sales Period -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<%= 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,
|
||||||
|
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" %>
|
||||||
|
<% if @ticket_type.tickets.any? %>
|
||||||
|
<p class="mt-1 text-sm text-yellow-600">
|
||||||
|
<i data-lucide="alert-triangle" class="w-4 h-4 inline mr-1"></i>
|
||||||
|
Des ventes ont déjà eu lieu
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= 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,
|
||||||
|
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" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if @event.start_time %>
|
||||||
|
<div class="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<div class="flex">
|
||||||
|
<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">
|
||||||
|
<strong>É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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Access Requirements -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<%= form.label :minimum_age, "Âge minimum", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.number_field :minimum_age,
|
||||||
|
min: 0,
|
||||||
|
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",
|
||||||
|
placeholder: "Laisser vide si aucune restriction" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<%= form.check_box :requires_id, class: "h-4 w-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 mt-1" %>
|
||||||
|
<div class="ml-3">
|
||||||
|
<%= form.label :requires_id, "Vérification d'identité requise", class: "text-sm font-medium text-gray-700" %>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
|
Cochez si une pièce d'identité sera vérifiée à l'entrée.
|
||||||
|
<% if @ticket_type.tickets.any? && @ticket_type.requires_id != params.dig(:ticket_type, :requires_id) %>
|
||||||
|
<br><span class="text-yellow-600">Attention: Cette modification affectera l'expérience des acheteurs existants.</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<%= link_to promoter_event_ticket_type_path(@event, @ticket_type), class: "text-gray-500 hover:text-gray-700 transition-colors" do %>
|
||||||
|
Annuler
|
||||||
|
<% 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 class="flex items-center space-x-3">
|
||||||
|
<%= form.submit "Sauvegarder les modifications", 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>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
170
app/views/promoter/ticket_types/index.html.erb
Normal file
170
app/views/promoter/ticket_types/index.html.erb
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<% content_for(:title, "Types de billets - #{@event.name}") %>
|
||||||
|
|
||||||
|
<div class="container py-8">
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center space-x-4 mb-4">
|
||||||
|
<%= link_to promoter_event_path(@event), class: "text-gray-400 hover:text-gray-600 transition-colors" do %>
|
||||||
|
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 mb-2">Types de billets</h1>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
<%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<%= link_to new_promoter_event_ticket_type_path(@event), class: "inline-flex items-center px-6 py-3 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
|
||||||
|
Nouveau type
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event status info -->
|
||||||
|
<% if @event.draft? %>
|
||||||
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i data-lucide="info" class="w-5 h-5 text-gray-400 mr-3"></i>
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
Cet événement est en brouillon. Les types de billets ne seront visibles qu'une fois l'événement publié.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if @ticket_types.any? %>
|
||||||
|
<div class="grid gap-6">
|
||||||
|
<% @ticket_types.each do |ticket_type| %>
|
||||||
|
<div class="bg-white rounded-lg border border-gray-200 p-6 hover:shadow-md transition-shadow duration-200">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<!-- Ticket type info -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-start justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 mb-2">
|
||||||
|
<%= link_to ticket_type.name, promoter_event_ticket_type_path(@event, ticket_type), class: "hover:text-purple-600 transition-colors" %>
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600 mb-3"><%= ticket_type.description %></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status badge -->
|
||||||
|
<div class="ml-4">
|
||||||
|
<% case ticket_type.sales_status %>
|
||||||
|
<% when :available %>
|
||||||
|
<span class="inline-flex px-3 py-1 text-sm font-semibold rounded-full bg-green-100 text-green-800">
|
||||||
|
<i data-lucide="check-circle" class="w-4 h-4 mr-1"></i>
|
||||||
|
En vente
|
||||||
|
</span>
|
||||||
|
<% when :upcoming %>
|
||||||
|
<span class="inline-flex px-3 py-1 text-sm font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||||
|
<i data-lucide="clock" class="w-4 h-4 mr-1"></i>
|
||||||
|
Prochainement
|
||||||
|
</span>
|
||||||
|
<% when :sold_out %>
|
||||||
|
<span class="inline-flex px-3 py-1 text-sm font-semibold rounded-full bg-red-100 text-red-800">
|
||||||
|
<i data-lucide="users" class="w-4 h-4 mr-1"></i>
|
||||||
|
Épuisé
|
||||||
|
</span>
|
||||||
|
<% when :expired %>
|
||||||
|
<span class="inline-flex px-3 py-1 text-sm font-semibold rounded-full bg-gray-100 text-gray-800">
|
||||||
|
<i data-lucide="x-circle" class="w-4 h-4 mr-1"></i>
|
||||||
|
Expiré
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ticket details grid -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||||
|
<div class="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-purple-600">
|
||||||
|
<%= number_to_currency(ticket_type.price_euros, unit: "€") %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Prix</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-gray-900">
|
||||||
|
<%= ticket_type.available_quantity %>/<%= ticket_type.quantity %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Disponibles</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-gray-900">
|
||||||
|
<%= ticket_type.tickets.count %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Vendus</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-green-600">
|
||||||
|
<%= number_to_currency(ticket_type.current_revenue, unit: "€") %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Revenus</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional info -->
|
||||||
|
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500 mb-4">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<i data-lucide="calendar" class="w-4 h-4 mr-1"></i>
|
||||||
|
Vente: <%= ticket_type.sale_start_at.strftime("%d/%m %H:%M") %> - <%= ticket_type.sale_end_at.strftime("%d/%m %H:%M") %>
|
||||||
|
</span>
|
||||||
|
<% if ticket_type.minimum_age %>
|
||||||
|
<span class="flex items-center">
|
||||||
|
<i data-lucide="user-check" class="w-4 h-4 mr-1"></i>
|
||||||
|
Âge min: <%= ticket_type.minimum_age %> ans
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% if ticket_type.requires_id %>
|
||||||
|
<span class="flex items-center">
|
||||||
|
<i data-lucide="id-card" class="w-4 h-4 mr-1"></i>
|
||||||
|
Pièce d'identité requise
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex items-center justify-between pt-4 border-t border-gray-200">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<%= link_to promoter_event_ticket_type_path(@event, ticket_type), class: "text-gray-400 hover:text-gray-600 transition-colors", title: "Voir" do %>
|
||||||
|
<i data-lucide="eye" class="w-5 h-5"></i>
|
||||||
|
<% end %>
|
||||||
|
<%= link_to edit_promoter_event_ticket_type_path(@event, ticket_type), class: "text-gray-400 hover:text-blue-600 transition-colors", title: "Modifier" do %>
|
||||||
|
<i data-lucide="edit" class="w-5 h-5"></i>
|
||||||
|
<% end %>
|
||||||
|
<%= button_to duplicate_promoter_event_ticket_type_path(@event, ticket_type), method: :post, class: "text-gray-400 hover:text-green-600 transition-colors", title: "Dupliquer" do %>
|
||||||
|
<i data-lucide="copy" class="w-5 h-5"></i>
|
||||||
|
<% end %>
|
||||||
|
<% if ticket_type.tickets.empty? %>
|
||||||
|
<%= button_to promoter_event_ticket_type_path(@event, ticket_type), method: :delete,
|
||||||
|
data: { confirm: "Êtes-vous sûr de vouloir supprimer ce type de billet ?" },
|
||||||
|
class: "text-gray-400 hover:text-red-600 transition-colors", title: "Supprimer" do %>
|
||||||
|
<i data-lucide="trash-2" class="w-5 h-5"></i>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
Créé <%= time_ago_in_words(ticket_type.created_at) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="bg-white rounded-lg border-2 border-dashed border-gray-300 p-12 text-center">
|
||||||
|
<div class="mx-auto h-24 w-24 rounded-full bg-gray-100 flex items-center justify-center mb-6">
|
||||||
|
<i data-lucide="ticket" class="w-12 h-12 text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 mb-2">Aucun type de billet</h3>
|
||||||
|
<p class="text-gray-500 mb-6">Créez des types de billets pour permettre aux utilisateurs d'acheter des places pour votre événement.</p>
|
||||||
|
<%= link_to new_promoter_event_ticket_type_path(@event), class: "inline-flex items-center px-6 py-3 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
|
||||||
|
Créer mon premier type de billet
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
177
app/views/promoter/ticket_types/new.html.erb
Normal file
177
app/views/promoter/ticket_types/new.html.erb
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<% content_for(:title, "Nouveau type de billet - #{@event.name}") %>
|
||||||
|
|
||||||
|
<div class="container py-8">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<%= link_to promoter_event_ticket_types_path(@event), class: "text-gray-400 hover:text-gray-600 transition-colors" do %>
|
||||||
|
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
||||||
|
<% end %>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 mb-2">Nouveau type de billet</h1>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
<%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= form_with model: [:promoter, @event, @ticket_type], local: true, class: "space-y-8", data: { controller: "ticket-type-form" } do |form| %>
|
||||||
|
<% if @ticket_type.errors.any? %>
|
||||||
|
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i data-lucide="alert-circle" class="w-5 h-5 text-red-400"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-800">
|
||||||
|
<%= pluralize(@ticket_type.errors.count, "erreur") %> à corriger :
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 text-sm text-red-700">
|
||||||
|
<ul class="list-disc list-inside space-y-1">
|
||||||
|
<% @ticket_type.errors.full_messages.each do |message| %>
|
||||||
|
<li><%= message %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- Basic Information -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<%= form.label :name, "Nom du type de billet", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.text_field :name, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "Ex: Early Bird, VIP, Standard" %>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Nom affiché aux acheteurs</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.label :description, "Description", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.text_area :description, rows: 3, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "Décrivez ce qui est inclus dans ce type de billet..." %>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Description visible lors de l'achat</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pricing & Quantity -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<%= form.label :price_euros, "Prix (€)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<div class="relative">
|
||||||
|
<%= form.number_field :price_euros,
|
||||||
|
step: 0.01,
|
||||||
|
min: 0.01,
|
||||||
|
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" } %>
|
||||||
|
<div class="absolute left-3 top-2.5 text-gray-500">€</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Prix unitaire du billet</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.label :quantity, "Quantité disponible", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.number_field :quantity,
|
||||||
|
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",
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Revenue preview -->
|
||||||
|
<div class="mt-6 p-4 bg-purple-50 rounded-lg border border-purple-200">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm font-medium text-purple-900">Revenus potentiels (si tout vendu)</span>
|
||||||
|
<span class="text-lg font-bold text-purple-600" data-ticket-type-form-target="total">
|
||||||
|
<%= number_to_currency(0, unit: "€") %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sales Period -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<%= 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,
|
||||||
|
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" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= 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,
|
||||||
|
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" %>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Les ventes s'arrêtent automatiquement à cette date</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if @event.start_time %>
|
||||||
|
<div class="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<div class="flex">
|
||||||
|
<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">
|
||||||
|
<strong>É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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Access Requirements -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<%= form.label :minimum_age, "Âge minimum", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.number_field :minimum_age,
|
||||||
|
min: 0,
|
||||||
|
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",
|
||||||
|
placeholder: "Laisser vide si aucune restriction" %>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Âge minimum requis (optionnel)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<%= form.check_box :requires_id, class: "h-4 w-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 mt-1" %>
|
||||||
|
<div class="ml-3">
|
||||||
|
<%= form.label :requires_id, "Vérification d'identité requise", class: "text-sm font-medium text-gray-700" %>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
|
Cochez si une pièce d'identité sera vérifiée à l'entrée. Les noms des participants seront collectés lors de l'achat.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<%= link_to promoter_event_ticket_types_path(@event), class: "text-gray-500 hover:text-gray-700 transition-colors" do %>
|
||||||
|
Annuler
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
240
app/views/promoter/ticket_types/show.html.erb
Normal file
240
app/views/promoter/ticket_types/show.html.erb
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
<% content_for(:title, "#{@ticket_type.name} - #{@event.name}") %>
|
||||||
|
|
||||||
|
<div class="container py-8">
|
||||||
|
<!-- Header with actions -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<%= link_to promoter_event_ticket_types_path(@event), class: "text-gray-400 hover:text-gray-600 transition-colors" do %>
|
||||||
|
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
||||||
|
<% end %>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 mb-2"><%= @ticket_type.name %></h1>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
<%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<%= link_to edit_promoter_event_ticket_type_path(@event, @ticket_type), class: "inline-flex items-center px-4 py-2 bg-white border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="edit" class="w-4 h-4 mr-2"></i>
|
||||||
|
Modifier
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= button_to duplicate_promoter_event_ticket_type_path(@event, @ticket_type), method: :post, class: "inline-flex items-center px-4 py-2 bg-green-600 text-white font-medium rounded-lg hover:bg-green-700 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="copy" class="w-4 h-4 mr-2"></i>
|
||||||
|
Dupliquer
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status banner -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<% case @ticket_type.sales_status %>
|
||||||
|
<% when :available %>
|
||||||
|
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i data-lucide="check-circle" class="w-5 h-5 text-green-400 mr-3"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-green-900">Type de billet en vente</h3>
|
||||||
|
<p class="text-sm text-green-700">Ce type de billet est actuellement disponible à l'achat.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% when :upcoming %>
|
||||||
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i data-lucide="clock" class="w-5 h-5 text-blue-400 mr-3"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-blue-900">Ventes à venir</h3>
|
||||||
|
<p class="text-sm text-blue-700">Les ventes commenceront le <%= @ticket_type.sale_start_at.strftime("%d/%m/%Y à %H:%M") %>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% when :sold_out %>
|
||||||
|
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i data-lucide="users" class="w-5 h-5 text-red-400 mr-3"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-red-900">Type de billet épuisé</h3>
|
||||||
|
<p class="text-sm text-red-700">Tous les billets de ce type ont été vendus.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% when :expired %>
|
||||||
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i data-lucide="x-circle" class="w-5 h-5 text-gray-400 mr-3"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-900">Ventes terminées</h3>
|
||||||
|
<p class="text-sm text-gray-700">La période de vente pour ce type de billet est terminée.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ticket details -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
<!-- Main content -->
|
||||||
|
<div class="lg:col-span-2 space-y-8">
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Description</h3>
|
||||||
|
<p class="text-gray-700 leading-relaxed"><%= simple_format(@ticket_type.description) %></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sales Information -->
|
||||||
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Période de vente</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||||
|
<span class="text-gray-600">Début des ventes</span>
|
||||||
|
<span class="font-medium"><%= @ticket_type.sale_start_at.strftime("%d/%m/%Y à %H:%M") %></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||||
|
<span class="text-gray-600">Fin des ventes</span>
|
||||||
|
<span class="font-medium"><%= @ticket_type.sale_end_at.strftime("%d/%m/%Y à %H:%M") %></span>
|
||||||
|
</div>
|
||||||
|
<% if @ticket_type.minimum_age %>
|
||||||
|
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||||
|
<span class="text-gray-600">Âge minimum</span>
|
||||||
|
<span class="font-medium"><%= @ticket_type.minimum_age %> ans</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex items-center justify-between py-3">
|
||||||
|
<span class="text-gray-600">Vérification d'identité</span>
|
||||||
|
<span class="font-medium">
|
||||||
|
<% if @ticket_type.requires_id %>
|
||||||
|
<span class="text-green-600">Requise</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="text-gray-500">Non requise</span>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Buyers List (if any) -->
|
||||||
|
<% if @ticket_type.tickets.any? %>
|
||||||
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Acheteurs récents</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<% @ticket_type.tickets.includes(:user).order(created_at: :desc).limit(10).each do |ticket| %>
|
||||||
|
<div class="flex items-center justify-between py-2 border-b border-gray-100 last:border-b-0">
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-gray-900"><%= ticket.first_name %> <%= ticket.last_name %></p>
|
||||||
|
<p class="text-sm text-gray-500"><%= ticket.user.email %></p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-sm font-medium text-gray-900">
|
||||||
|
<%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
<%= ticket.created_at.strftime("%d/%m/%Y") %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if @ticket_type.tickets.count > 10 %>
|
||||||
|
<p class="text-sm text-gray-500 text-center pt-2">
|
||||||
|
Et <%= @ticket_type.tickets.count - 10 %> autre(s) acheteur(s)...
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Statistics -->
|
||||||
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Statistiques</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="text-center p-4 bg-purple-50 rounded-lg">
|
||||||
|
<div class="text-3xl font-bold text-purple-600">
|
||||||
|
<%= number_to_currency(@ticket_type.price_euros, unit: "€") %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Prix unitaire</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-gray-900">
|
||||||
|
<%= @ticket_type.tickets.count %>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">Vendus</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-gray-900">
|
||||||
|
<%= @ticket_type.available_quantity %>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">Restants</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-4 bg-green-50 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-green-600">
|
||||||
|
<%= number_to_currency(@ticket_type.current_revenue, unit: "€") %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Revenus générés</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-4 bg-blue-50 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-blue-600">
|
||||||
|
<%= number_to_currency(@ticket_type.total_potential_revenue, unit: "€") %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Potentiel total</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Actions rapides</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<%= link_to edit_promoter_event_ticket_type_path(@event, @ticket_type), class: "w-full inline-flex items-center px-4 py-2 bg-gray-50 text-gray-700 font-medium rounded-lg hover:bg-gray-100 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="edit" class="w-4 h-4 mr-2"></i>
|
||||||
|
Modifier les détails
|
||||||
|
<% end %>
|
||||||
|
<%= button_to duplicate_promoter_event_ticket_type_path(@event, @ticket_type), method: :post, class: "w-full inline-flex items-center px-4 py-2 bg-gray-50 text-gray-700 font-medium rounded-lg hover:bg-gray-100 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="copy" class="w-4 h-4 mr-2"></i>
|
||||||
|
Créer une copie
|
||||||
|
<% end %>
|
||||||
|
<hr class="border-gray-200">
|
||||||
|
<% if @ticket_type.tickets.empty? %>
|
||||||
|
<%= button_to promoter_event_ticket_type_path(@event, @ticket_type), method: :delete,
|
||||||
|
data: { confirm: "Êtes-vous sûr de vouloir supprimer ce type de billet ? Cette action est irréversible." },
|
||||||
|
class: "w-full inline-flex items-center px-4 py-2 text-red-600 font-medium rounded-lg hover:bg-red-50 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="trash-2" class="w-4 h-4 mr-2"></i>
|
||||||
|
Supprimer le type
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<div class="w-full inline-flex items-center px-4 py-2 text-gray-400 font-medium rounded-lg cursor-not-allowed">
|
||||||
|
<i data-lucide="trash-2" class="w-4 h-4 mr-2"></i>
|
||||||
|
Impossible de supprimer
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500">Des billets ont été vendus</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Creation info -->
|
||||||
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Informations</h3>
|
||||||
|
<div class="space-y-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-500">Créé le</span>
|
||||||
|
<p><%= @ticket_type.created_at.strftime("%d/%m/%Y à %H:%M") %></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-500">Dernière modification</span>
|
||||||
|
<p><%= @ticket_type.updated_at.strftime("%d/%m/%Y à %H:%M") %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -43,6 +43,13 @@ Rails.application.routes.draw do
|
|||||||
patch :cancel
|
patch :cancel
|
||||||
patch :mark_sold_out
|
patch :mark_sold_out
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Nested ticket types routes
|
||||||
|
resources :ticket_types do
|
||||||
|
member do
|
||||||
|
post :duplicate
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddRequiresIdToTicketTypes < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :ticket_types, :requires_id, :boolean, default: false, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
3
db/schema.rb
generated
3
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
|
ActiveRecord::Schema[8.0].define(version: 2025_08_31_184955) do
|
||||||
create_table "events", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
create_table "events", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "slug", null: false
|
t.string "slug", null: false
|
||||||
@@ -44,6 +44,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
|
|||||||
t.bigint "event_id", null: false
|
t.bigint "event_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.boolean "requires_id", default: false, null: false
|
||||||
t.index ["event_id"], name: "index_ticket_types_on_event_id"
|
t.index ["event_id"], name: "index_ticket_types_on_event_id"
|
||||||
t.index ["sale_end_at"], name: "index_ticket_types_on_sale_end_at"
|
t.index ["sale_end_at"], name: "index_ticket_types_on_sale_end_at"
|
||||||
t.index ["sale_start_at"], name: "index_ticket_types_on_sale_start_at"
|
t.index ["sale_start_at"], name: "index_ticket_types_on_sale_start_at"
|
||||||
|
|||||||
Reference in New Issue
Block a user