diff --git a/app/controllers/promoter/ticket_types_controller.rb b/app/controllers/promoter/ticket_types_controller.rb new file mode 100644 index 0000000..8a5b57b --- /dev/null +++ b/app/controllers/promoter/ticket_types_controller.rb @@ -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 \ No newline at end of file diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 593b714..7712b3d 100755 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -22,6 +22,9 @@ application.register("header", HeaderController); import EventFormController from "./event_form_controller" application.register("event-form", EventFormController); +import TicketTypeFormController from "./ticket_type_form_controller" +application.register("ticket-type-form", TicketTypeFormController); + diff --git a/app/javascript/controllers/ticket_type_form_controller.js b/app/javascript/controllers/ticket_type_form_controller.js new file mode 100644 index 0000000..3a7a73f --- /dev/null +++ b/app/javascript/controllers/ticket_type_form_controller.js @@ -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() + } +} \ No newline at end of file diff --git a/app/models/ticket_type.rb b/app/models/ticket_type.rb index 89e53f3..d3e8073 100755 --- a/app/models/ticket_type.rb +++ b/app/models/ticket_type.rb @@ -12,7 +12,64 @@ class TicketType < ApplicationRecord 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 :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 @@ -20,4 +77,9 @@ class TicketType < ApplicationRecord 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 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 diff --git a/app/views/promoter/events/show.html.erb b/app/views/promoter/events/show.html.erb index f43bbf5..77c3eba 100644 --- a/app/views/promoter/events/show.html.erb +++ b/app/views/promoter/events/show.html.erb @@ -209,9 +209,9 @@

Actions rapides

- <%= 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 %> - 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 %> diff --git a/app/views/promoter/ticket_types/edit.html.erb b/app/views/promoter/ticket_types/edit.html.erb new file mode 100644 index 0000000..5266b5d --- /dev/null +++ b/app/views/promoter/ticket_types/edit.html.erb @@ -0,0 +1,224 @@ +<% content_for(:title, "Modifier #{@ticket_type.name}") %> + +
+
+
+
+ <%= link_to promoter_event_ticket_type_path(@event, @ticket_type), class: "text-gray-400 hover:text-gray-600 transition-colors" do %> + + <% end %> +
+

Modifier le type de billet

+

<%= @ticket_type.name %>

+
+
+
+ + <%= 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? %> +
+
+
+ +
+
+

+ <%= pluralize(@ticket_type.errors.count, "erreur") %> à corriger : +

+
+
    + <% @ticket_type.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+
+
+
+ <% end %> + + + <% if @ticket_type.tickets.any? %> +
+
+ +
+

Attention

+

+ <%= pluralize(@ticket_type.tickets.count, 'billet') %> de ce type ont déjà été vendus. + Modifier certains paramètres pourrait impacter les acheteurs existants. +

+
+
+
+ <% end %> + + +
+

Informations générales

+ +
+
+ <%= 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" %> +
+ +
+ <%= 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..." %> +
+
+
+ + +
+

Prix et quantité

+ +
+
+ <%= form.label :price_euros, "Prix (€)", class: "block text-sm font-medium text-gray-700 mb-2" %> +
+ <%= 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" } %> +
+
+ <% if @ticket_type.tickets.any? %> +

+ + Modifier le prix n'affectera pas les billets déjà vendus +

+ <% end %> +
+ +
+ <%= 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? %> +

+ Minimum: <%= @ticket_type.tickets.count %> (billets déjà vendus) +

+ <% else %> +

Nombre total de billets de ce type

+ <% end %> +
+
+ + +
+
+
+ Revenus potentiels restants + + <%= number_to_currency(@ticket_type.remaining_potential_revenue, unit: "€") %> + +
+
+ +
+
+ Revenus déjà générés + + <%= number_to_currency(@ticket_type.current_revenue, unit: "€") %> + +
+
+
+
+ + +
+

Période de vente

+ +
+
+ <%= 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? %> +

+ + Des ventes ont déjà eu lieu +

+ <% end %> +
+ +
+ <%= 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" %> +
+
+ + <% if @event.start_time %> +
+
+ +

+ Événement: <%= @event.start_time.strftime("%d/%m/%Y à %H:%M") %>
+ Les ventes doivent se terminer avant le début de l'événement. +

+
+
+ <% end %> +
+ + +
+

Conditions d'accès

+ +
+
+ <%= 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" %> +
+
+ +
+
+ <%= form.check_box :requires_id, class: "h-4 w-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 mt-1" %> +
+ <%= form.label :requires_id, "Vérification d'identité requise", class: "text-sm font-medium text-gray-700" %> +

+ 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) %> +
Attention: Cette modification affectera l'expérience des acheteurs existants. + <% end %> +

+
+
+
+
+ + +
+
+ <%= 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? %> +

+ + <%= pluralize(@ticket_type.tickets.count, 'billet') %> déjà vendu(s) +

+ <% end %> +
+ +
+ <%= 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" %> +
+
+ <% end %> +
+
\ No newline at end of file diff --git a/app/views/promoter/ticket_types/index.html.erb b/app/views/promoter/ticket_types/index.html.erb new file mode 100644 index 0000000..4f51dee --- /dev/null +++ b/app/views/promoter/ticket_types/index.html.erb @@ -0,0 +1,170 @@ +<% content_for(:title, "Types de billets - #{@event.name}") %> + +
+
+
+ <%= link_to promoter_event_path(@event), class: "text-gray-400 hover:text-gray-600 transition-colors" do %> + + <% end %> +
+

Types de billets

+

+ <%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %> +

+
+ <%= 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 %> + + Nouveau type + <% end %> +
+ + + <% if @event.draft? %> +
+
+ +

+ Cet événement est en brouillon. Les types de billets ne seront visibles qu'une fois l'événement publié. +

+
+
+ <% end %> +
+ + <% if @ticket_types.any? %> +
+ <% @ticket_types.each do |ticket_type| %> +
+
+ +
+
+
+

+ <%= link_to ticket_type.name, promoter_event_ticket_type_path(@event, ticket_type), class: "hover:text-purple-600 transition-colors" %> +

+

<%= ticket_type.description %>

+
+ + +
+ <% case ticket_type.sales_status %> + <% when :available %> + + + En vente + + <% when :upcoming %> + + + Prochainement + + <% when :sold_out %> + + + Épuisé + + <% when :expired %> + + + Expiré + + <% end %> +
+
+ + +
+
+
+ <%= number_to_currency(ticket_type.price_euros, unit: "€") %> +
+
Prix
+
+ +
+
+ <%= ticket_type.available_quantity %>/<%= ticket_type.quantity %> +
+
Disponibles
+
+ +
+
+ <%= ticket_type.tickets.count %> +
+
Vendus
+
+ +
+
+ <%= number_to_currency(ticket_type.current_revenue, unit: "€") %> +
+
Revenus
+
+
+ + +
+ + + Vente: <%= ticket_type.sale_start_at.strftime("%d/%m %H:%M") %> - <%= ticket_type.sale_end_at.strftime("%d/%m %H:%M") %> + + <% if ticket_type.minimum_age %> + + + Âge min: <%= ticket_type.minimum_age %> ans + + <% end %> + <% if ticket_type.requires_id %> + + + Pièce d'identité requise + + <% end %> +
+
+
+ + +
+
+ <%= link_to promoter_event_ticket_type_path(@event, ticket_type), class: "text-gray-400 hover:text-gray-600 transition-colors", title: "Voir" do %> + + <% 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 %> + + <% 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 %> + + <% 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 %> + + <% end %> + <% end %> +
+ +
+ Créé <%= time_ago_in_words(ticket_type.created_at) %> +
+
+
+ <% end %> +
+ <% else %> +
+
+ +
+

Aucun type de billet

+

Créez des types de billets pour permettre aux utilisateurs d'acheter des places pour votre événement.

+ <%= 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 %> + + Créer mon premier type de billet + <% end %> +
+ <% end %> +
\ No newline at end of file diff --git a/app/views/promoter/ticket_types/new.html.erb b/app/views/promoter/ticket_types/new.html.erb new file mode 100644 index 0000000..dd57e59 --- /dev/null +++ b/app/views/promoter/ticket_types/new.html.erb @@ -0,0 +1,177 @@ +<% content_for(:title, "Nouveau type de billet - #{@event.name}") %> + +
+
+
+
+ <%= link_to promoter_event_ticket_types_path(@event), class: "text-gray-400 hover:text-gray-600 transition-colors" do %> + + <% end %> +
+

Nouveau type de billet

+

+ <%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %> +

+
+
+
+ + <%= 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? %> +
+
+
+ +
+
+

+ <%= pluralize(@ticket_type.errors.count, "erreur") %> à corriger : +

+
+
    + <% @ticket_type.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+
+
+
+ <% end %> + + +
+

Informations générales

+ +
+
+ <%= 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" %> +

Nom affiché aux acheteurs

+
+ +
+ <%= 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..." %> +

Description visible lors de l'achat

+
+
+
+ + +
+

Prix et quantité

+ +
+
+ <%= form.label :price_euros, "Prix (€)", class: "block text-sm font-medium text-gray-700 mb-2" %> +
+ <%= 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" } %> +
+
+

Prix unitaire du billet

+
+ +
+ <%= 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" } %> +

Nombre total de billets de ce type

+
+
+ + +
+
+ Revenus potentiels (si tout vendu) + + <%= number_to_currency(0, unit: "€") %> + +
+
+
+ + +
+

Période de vente

+ +
+
+ <%= 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" %> +
+ +
+ <%= 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" %> +

Les ventes s'arrêtent automatiquement à cette date

+
+
+ + <% if @event.start_time %> +
+
+ +

+ Événement: <%= @event.start_time.strftime("%d/%m/%Y à %H:%M") %>
+ Les ventes doivent se terminer avant le début de l'événement. +

+
+
+ <% end %> +
+ + +
+

Conditions d'accès

+ +
+
+ <%= 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" %> +

Âge minimum requis (optionnel)

+
+
+ +
+
+ <%= form.check_box :requires_id, class: "h-4 w-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 mt-1" %> +
+ <%= form.label :requires_id, "Vérification d'identité requise", class: "text-sm font-medium text-gray-700" %> +

+ 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. +

+
+
+
+
+ + +
+
+ <%= link_to promoter_event_ticket_types_path(@event), class: "text-gray-500 hover:text-gray-700 transition-colors" do %> + Annuler + <% end %> +
+ +
+ <%= 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" %> +
+
+ <% end %> +
+
\ No newline at end of file diff --git a/app/views/promoter/ticket_types/show.html.erb b/app/views/promoter/ticket_types/show.html.erb new file mode 100644 index 0000000..0599c43 --- /dev/null +++ b/app/views/promoter/ticket_types/show.html.erb @@ -0,0 +1,240 @@ +<% content_for(:title, "#{@ticket_type.name} - #{@event.name}") %> + +
+ +
+
+
+ <%= link_to promoter_event_ticket_types_path(@event), class: "text-gray-400 hover:text-gray-600 transition-colors" do %> + + <% end %> +
+

<%= @ticket_type.name %>

+

+ <%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %> +

+
+
+
+ <%= 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 %> + + 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 %> + + Dupliquer + <% end %> +
+
+
+ + +
+ <% case @ticket_type.sales_status %> + <% when :available %> +
+
+ +
+

Type de billet en vente

+

Ce type de billet est actuellement disponible à l'achat.

+
+
+
+ <% when :upcoming %> +
+
+ +
+

Ventes à venir

+

Les ventes commenceront le <%= @ticket_type.sale_start_at.strftime("%d/%m/%Y à %H:%M") %>.

+
+
+
+ <% when :sold_out %> +
+
+ +
+

Type de billet épuisé

+

Tous les billets de ce type ont été vendus.

+
+
+
+ <% when :expired %> +
+
+ +
+

Ventes terminées

+

La période de vente pour ce type de billet est terminée.

+
+
+
+ <% end %> +
+ + +
+ +
+ +
+

Description

+

<%= simple_format(@ticket_type.description) %>

+
+ + +
+

Période de vente

+
+
+ Début des ventes + <%= @ticket_type.sale_start_at.strftime("%d/%m/%Y à %H:%M") %> +
+
+ Fin des ventes + <%= @ticket_type.sale_end_at.strftime("%d/%m/%Y à %H:%M") %> +
+ <% if @ticket_type.minimum_age %> +
+ Âge minimum + <%= @ticket_type.minimum_age %> ans +
+ <% end %> +
+ Vérification d'identité + + <% if @ticket_type.requires_id %> + Requise + <% else %> + Non requise + <% end %> + +
+
+
+ + + <% if @ticket_type.tickets.any? %> +
+

Acheteurs récents

+
+ <% @ticket_type.tickets.includes(:user).order(created_at: :desc).limit(10).each do |ticket| %> +
+
+

<%= ticket.first_name %> <%= ticket.last_name %>

+

<%= ticket.user.email %>

+
+
+

+ <%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %> +

+

+ <%= ticket.created_at.strftime("%d/%m/%Y") %> +

+
+
+ <% end %> + <% if @ticket_type.tickets.count > 10 %> +

+ Et <%= @ticket_type.tickets.count - 10 %> autre(s) acheteur(s)... +

+ <% end %> +
+
+ <% end %> +
+ + +
+ +
+

Statistiques

+
+
+
+ <%= number_to_currency(@ticket_type.price_euros, unit: "€") %> +
+
Prix unitaire
+
+ +
+
+
+ <%= @ticket_type.tickets.count %> +
+
Vendus
+
+
+
+ <%= @ticket_type.available_quantity %> +
+
Restants
+
+
+ +
+
+ <%= number_to_currency(@ticket_type.current_revenue, unit: "€") %> +
+
Revenus générés
+
+ +
+
+ <%= number_to_currency(@ticket_type.total_potential_revenue, unit: "€") %> +
+
Potentiel total
+
+
+
+ + +
+

Actions rapides

+
+ <%= 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 %> + + 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 %> + + Créer une copie + <% 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 ? 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 %> + + Supprimer le type + <% end %> + <% else %> +
+ + Impossible de supprimer +
+

Des billets ont été vendus

+ <% end %> +
+
+ + +
+

Informations

+
+
+ Créé le +

<%= @ticket_type.created_at.strftime("%d/%m/%Y à %H:%M") %>

+
+
+ Dernière modification +

<%= @ticket_type.updated_at.strftime("%d/%m/%Y à %H:%M") %>

+
+
+
+
+
+
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index fbc4b51..488f1f7 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -43,6 +43,13 @@ Rails.application.routes.draw do patch :cancel patch :mark_sold_out end + + # Nested ticket types routes + resources :ticket_types do + member do + post :duplicate + end + end end end diff --git a/db/migrate/20250831184955_add_requires_id_to_ticket_types.rb b/db/migrate/20250831184955_add_requires_id_to_ticket_types.rb new file mode 100644 index 0000000..f979708 --- /dev/null +++ b/db/migrate/20250831184955_add_requires_id_to_ticket_types.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 1b19dc7..554acd5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # 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| t.string "name", 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.datetime "created_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 ["sale_end_at"], name: "index_ticket_types_on_sale_end_at" t.index ["sale_start_at"], name: "index_ticket_types_on_sale_start_at"