## Backend Implementation

Enhanced TicketType model with helper methods and better validations

So the full context is:

## Backend Implementation
- Enhanced TicketType model with helper methods and better validations
- New Promoter::TicketTypesController with full authorization
- Sales status tracking (draft, available, upcoming, expired, sold_out)
- New Promoter::TicketTypesController with full authorization
- Safe calculation methods preventing nil value errors
- Sales status tracking (draft, available, upcoming, expired, sold_out)

## Frontend Features
- Modern responsive UI with Tailwind CSS styling
- Interactive forms with Stimulus controller for dynamic calculations
- Revenue calculators showing potential, current, and remaining revenue
- Status indicators with appropriate colors and icons
- Buyer analytics and purchase history display

## JavaScript Enhancements
- New TicketTypeFormController for dynamic pricing calculations
- Real-time total updates as users type price/quantity
- Proper French currency formatting
- Form validation for minimum quantities based on existing sales

## Bug Fixes
 Fixed nil value errors in price_euros method when price_cents is nil
 Added defensive programming for all calculation methods
 Graceful handling of incomplete ticket types during creation
 Proper default values for new ticket type instances

## Files Added/Modified
- app/controllers/promoter/ticket_types_controller.rb (new)
- app/javascript/controllers/ticket_type_form_controller.js (new)
- app/views/promoter/ticket_types/*.html.erb (4 new view files)
- app/models/ticket_type.rb (enhanced with helper methods)
- config/routes.rb (added nested ticket_types routes)
- db/migrate/*_add_requires_id_to_ticket_types.rb (new migration)

## Integration
- Seamless integration with existing event management system
- Updated promoter event show page with ticket management link
- Proper scoping ensuring promoters only manage their own tickets
- Compatible with existing ticket purchasing and checkout flow
This commit is contained in:
kbe
2025-09-01 00:03:35 +02:00
parent aa5dccb508
commit e838e91162
12 changed files with 1057 additions and 3 deletions

View File

@@ -209,9 +209,9 @@
<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 "#", 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>
Gérer les billets
Gérer les types de billets
<% 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 %>
<i data-lucide="users" class="w-4 h-4 mr-2"></i>

View 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>

View 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>

View 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>

View 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>