Compare commits
2 Commits
3414057795
...
e838e91162
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e838e91162 | ||
|
|
aa5dccb508 |
117
app/controllers/promoter/events_controller.rb
Normal file
117
app/controllers/promoter/events_controller.rb
Normal file
@@ -0,0 +1,117 @@
|
||||
# Promoter Events Controller
|
||||
#
|
||||
# Handles event management for promoters (event organizers)
|
||||
# Allows promoters to create, edit, delete and manage their events
|
||||
class Promoter::EventsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_can_manage_events!
|
||||
before_action :set_event, only: [:show, :edit, :update, :destroy, :publish, :unpublish, :cancel, :mark_sold_out]
|
||||
|
||||
# Display all events for the current promoter
|
||||
def index
|
||||
@events = current_user.events.order(created_at: :desc).page(params[:page]).per(10)
|
||||
end
|
||||
|
||||
# Display a specific event for the promoter
|
||||
def show
|
||||
# Event is set by set_event callback
|
||||
end
|
||||
|
||||
# Show form to create a new event
|
||||
def new
|
||||
@event = current_user.events.build
|
||||
end
|
||||
|
||||
# Create a new event
|
||||
def create
|
||||
@event = current_user.events.build(event_params)
|
||||
|
||||
if @event.save
|
||||
redirect_to promoter_event_path(@event), notice: 'Event créé avec succès!'
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# Show form to edit an existing event
|
||||
def edit
|
||||
# Event is set by set_event callback
|
||||
end
|
||||
|
||||
# Update an existing event
|
||||
def update
|
||||
if @event.update(event_params)
|
||||
redirect_to promoter_event_path(@event), notice: 'Event mis à jour avec succès!'
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# Delete an event
|
||||
def destroy
|
||||
@event.destroy
|
||||
redirect_to promoter_events_path, notice: 'Event supprimé avec succès!'
|
||||
end
|
||||
|
||||
# Publish an event (make it visible to public)
|
||||
def publish
|
||||
if @event.draft?
|
||||
@event.update(state: :published)
|
||||
redirect_to promoter_event_path(@event), notice: 'Event publié avec succès!'
|
||||
else
|
||||
redirect_to promoter_event_path(@event), alert: 'Cet event ne peut pas être publié.'
|
||||
end
|
||||
end
|
||||
|
||||
# Unpublish an event (make it draft)
|
||||
def unpublish
|
||||
if @event.published?
|
||||
@event.update(state: :draft)
|
||||
redirect_to promoter_event_path(@event), notice: 'Event dépublié avec succès!'
|
||||
else
|
||||
redirect_to promoter_event_path(@event), alert: 'Cet event ne peut pas être dépublié.'
|
||||
end
|
||||
end
|
||||
|
||||
# Cancel an event
|
||||
def cancel
|
||||
if @event.published?
|
||||
@event.update(state: :canceled)
|
||||
redirect_to promoter_event_path(@event), notice: 'Event annulé avec succès!'
|
||||
else
|
||||
redirect_to promoter_event_path(@event), alert: 'Cet event ne peut pas être annulé.'
|
||||
end
|
||||
end
|
||||
|
||||
# Mark event as sold out
|
||||
def mark_sold_out
|
||||
if @event.published?
|
||||
@event.update(state: :sold_out)
|
||||
redirect_to promoter_event_path(@event), notice: 'Event marqué comme complet!'
|
||||
else
|
||||
redirect_to promoter_event_path(@event), alert: 'Cet event ne peut pas être marqué comme complet.'
|
||||
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[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to promoter_events_path, alert: 'Event non trouvé ou vous n\'avez pas accès à cet event.'
|
||||
end
|
||||
|
||||
def event_params
|
||||
params.require(:event).permit(
|
||||
:name, :slug, :description, :image,
|
||||
:venue_name, :venue_address, :latitude, :longitude,
|
||||
:start_time, :end_time, :featured
|
||||
)
|
||||
end
|
||||
end
|
||||
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
|
||||
28
app/javascript/controllers/event_form_controller.js
Normal file
28
app/javascript/controllers/event_form_controller.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
// Event form controller for handling form interactions
|
||||
// Handles auto-slug generation from event names
|
||||
export default class extends Controller {
|
||||
static targets = ["name", "slug"]
|
||||
|
||||
connect() {
|
||||
console.log("Event form controller connected")
|
||||
}
|
||||
|
||||
// Auto-generate slug from name input
|
||||
generateSlug() {
|
||||
// Only auto-generate if slug field is empty
|
||||
if (this.slugTarget.value === "") {
|
||||
const slug = this.nameTarget.value
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "") // Remove accents
|
||||
.replace(/[^a-z0-9\s-]/g, "") // Remove special chars
|
||||
.replace(/\s+/g, "-") // Replace spaces with dashes
|
||||
.replace(/-+/g, "-") // Remove duplicate dashes
|
||||
.replace(/^-|-$/g, "") // Remove leading/trailing dashes
|
||||
|
||||
this.slugTarget.value = slug
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,12 @@ application.register("ticket-selection", TicketSelectionController);
|
||||
import HeaderController from "./header_controller"
|
||||
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);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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 :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
|
||||
|
||||
@@ -27,4 +27,16 @@ class User < ApplicationRecord
|
||||
validates :last_name, length: { minimum: 3, maximum: 12, allow_blank: true }
|
||||
validates :first_name, length: { minimum: 3, maximum: 12, allow_blank: true }
|
||||
validates :company_name, length: { minimum: 3, maximum: 12, allow_blank: true }
|
||||
|
||||
# Authorization methods
|
||||
def can_manage_events?
|
||||
# For now, all authenticated users can manage events
|
||||
# This can be extended later with role-based permissions
|
||||
true
|
||||
end
|
||||
|
||||
def promoter?
|
||||
# Alias for can_manage_events? to make views more semantic
|
||||
can_manage_events?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Hero section with metrics -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-6">Tableau de bord</h1>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100">Tableau de bord</h1>
|
||||
|
||||
<!-- Promoter Actions -->
|
||||
<% if current_user.promoter? %>
|
||||
<div class="flex items-center space-x-3">
|
||||
<%= link_to promoter_events_path, class: "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="calendar-plus" class="w-4 h-4 mr-2"></i>
|
||||
Mes événements
|
||||
<% end %>
|
||||
<%= link_to new_promoter_event_path, class: "inline-flex items-center px-4 py-2 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 un événement
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Mes réservations", value: @booked_events, classes: "from-green-100 to-emerald-100" } %>
|
||||
|
||||
184
app/views/promoter/events/edit.html.erb
Normal file
184
app/views/promoter/events/edit.html.erb
Normal file
@@ -0,0 +1,184 @@
|
||||
<% content_for(:title, "Modifier #{@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_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">Modifier l'événement</h1>
|
||||
<p class="text-gray-600"><%= @event.name %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= form_with model: [:promoter, @event], local: true, class: "space-y-8", data: { controller: "event-form" } do |form| %>
|
||||
<% if @event.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(@event.errors.count, "erreur") %> à corriger :
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<% @event.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="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<%= form.label :name, "Nom de l'événement", 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: Soirée d'ouverture", data: { "event-form-target": "name", action: "input->event-form#generateSlug" } %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :slug, "Slug (URL)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_field :slug, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "soiree-ouverture", data: { "event-form-target": "slug" } %>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
<% if @event.published? %>
|
||||
<i data-lucide="alert-triangle" class="w-4 h-4 inline text-yellow-500"></i>
|
||||
Attention: Modifier le slug d'un événement publié peut casser les liens existants.
|
||||
<% else %>
|
||||
Utilisé dans l'URL de l'événement
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<%= form.label :description, "Description", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_area :description, rows: 4, 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 votre événement..." %>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<%= form.label :image, "Image (URL)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.url_field :image, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "https://example.com/image.jpg" %>
|
||||
<p class="mt-1 text-sm text-gray-500">URL de l'image de couverture de l'événement</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date & Time -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Date et heure</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<%= form.label :start_time, "Date et heure de début", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.datetime_local_field :start_time,
|
||||
value: @event.start_time&.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 :end_time, "Date et heure de fin", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.datetime_local_field :end_time,
|
||||
value: @event.end_time&.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.published? && @event.tickets.any? %>
|
||||
<div class="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div class="flex">
|
||||
<i data-lucide="alert-triangle" class="w-5 h-5 text-yellow-400 mt-0.5 mr-2"></i>
|
||||
<p class="text-sm text-yellow-800">
|
||||
Des billets ont déjà été vendus pour cet événement. Modifier la date pourrait impacter les participants.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Venue Information -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Lieu de l'événement</h3>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<%= form.label :venue_name, "Nom du lieu", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_field :venue_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: Le Grand Rex" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :venue_address, "Adresse", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_field :venue_address, 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: 1 Boulevard Poissonnière, 75002 Paris" %>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<%= form.label :latitude, "Latitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.number_field :latitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "48.8566" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :longitude, "Longitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.number_field :longitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "2.3522" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500">
|
||||
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
||||
Utilisez un service comme <a href="https://www.latlong.net/" target="_blank" class="text-purple-600 hover:text-purple-800">latlong.net</a> pour obtenir les coordonnées GPS.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<% if @event.published? && @event.tickets.any? %>
|
||||
<div class="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div class="flex">
|
||||
<i data-lucide="alert-triangle" class="w-5 h-5 text-yellow-400 mt-0.5 mr-2"></i>
|
||||
<p class="text-sm text-yellow-800">
|
||||
Des billets ont déjà été vendus pour cet événement. Modifier le lieu pourrait impacter les participants.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Options</h3>
|
||||
|
||||
<div class="flex items-center">
|
||||
<%= form.check_box :featured, class: "h-4 w-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500" %>
|
||||
<%= form.label :featured, "Mettre en avant sur la page d'accueil", class: "ml-2 text-sm text-gray-700" %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500">Les événements mis en avant apparaissent en premier sur la page d'accueil.</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<%= link_to promoter_event_path(@event), class: "text-gray-500 hover:text-gray-700 transition-colors" do %>
|
||||
Annuler
|
||||
<% end %>
|
||||
<% if @event.published? && @event.tickets.any? %>
|
||||
<p class="text-sm text-yellow-600">
|
||||
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
||||
<%= @event.tickets.count %> billet(s) 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>
|
||||
136
app/views/promoter/events/index.html.erb
Normal file
136
app/views/promoter/events/index.html.erb
Normal file
@@ -0,0 +1,136 @@
|
||||
<% content_for(:title, "Mes événements") %>
|
||||
|
||||
<div class="container py-8">
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Mes événements</h1>
|
||||
<p class="text-gray-600">Gérez tous vos événements depuis cette interface</p>
|
||||
</div>
|
||||
<%= link_to new_promoter_event_path, 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 un événement
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @events.any? %>
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Événement</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Statut</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lieu</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<% @events.each do |event| %>
|
||||
<tr class="hover:bg-gray-50 transition-colors duration-150">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center">
|
||||
<div class="h-12 w-12 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center flex-shrink-0">
|
||||
<i data-lucide="calendar" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
<%= link_to event.name, promoter_event_path(event), class: "hover:text-purple-600 transition-colors" %>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 truncate max-w-xs">
|
||||
<%= event.description.truncate(60) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<% case event.state %>
|
||||
<% when "draft" %>
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-800">
|
||||
<i data-lucide="edit-3" class="w-3 h-3 mr-1"></i>
|
||||
Brouillon
|
||||
</span>
|
||||
<% when "published" %>
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
|
||||
<i data-lucide="eye" class="w-3 h-3 mr-1"></i>
|
||||
Publié
|
||||
</span>
|
||||
<% when "canceled" %>
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800">
|
||||
<i data-lucide="x-circle" class="w-3 h-3 mr-1"></i>
|
||||
Annulé
|
||||
</span>
|
||||
<% when "sold_out" %>
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||
<i data-lucide="users" class="w-3 h-3 mr-1"></i>
|
||||
Complet
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<% if event.featured? %>
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800 ml-1">
|
||||
<i data-lucide="star" class="w-3 h-3 mr-1"></i>
|
||||
À la une
|
||||
</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<% if event.start_time %>
|
||||
<div><%= event.start_time.strftime("%d/%m/%Y") %></div>
|
||||
<div class="text-xs text-gray-400"><%= event.start_time.strftime("%H:%M") %></div>
|
||||
<% else %>
|
||||
<span class="text-gray-400">Date non définie</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<div><%= event.venue_name %></div>
|
||||
<div class="text-xs text-gray-400 truncate max-w-xs"><%= event.venue_address %></div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to promoter_event_path(event), class: "text-gray-400 hover:text-gray-600 transition-colors", title: "Voir" do %>
|
||||
<i data-lucide="eye" class="w-4 h-4"></i>
|
||||
<% end %>
|
||||
<%= link_to edit_promoter_event_path(event), class: "text-gray-400 hover:text-blue-600 transition-colors", title: "Modifier" do %>
|
||||
<i data-lucide="edit" class="w-4 h-4"></i>
|
||||
<% end %>
|
||||
<% if event.draft? %>
|
||||
<%= button_to publish_promoter_event_path(event), method: :patch, class: "text-gray-400 hover:text-green-600 transition-colors", title: "Publier" do %>
|
||||
<i data-lucide="upload" class="w-4 h-4"></i>
|
||||
<% end %>
|
||||
<% elsif event.published? %>
|
||||
<%= button_to unpublish_promoter_event_path(event), method: :patch, class: "text-gray-400 hover:text-yellow-600 transition-colors", title: "Dépublier" do %>
|
||||
<i data-lucide="download" class="w-4 h-4"></i>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= button_to promoter_event_path(event), method: :delete,
|
||||
data: { confirm: "Êtes-vous sûr de vouloir supprimer cet événement ?" },
|
||||
class: "text-gray-400 hover:text-red-600 transition-colors", title: "Supprimer" do %>
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<%= paginate @events if respond_to?(:paginate) %>
|
||||
</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="calendar-plus" class="w-12 h-12 text-gray-400"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">Aucun événement</h3>
|
||||
<p class="text-gray-500 mb-6">Vous n'avez pas encore créé d'événement. Commencez dès maintenant !</p>
|
||||
<%= link_to new_promoter_event_path, 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 événement
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
146
app/views/promoter/events/new.html.erb
Normal file
146
app/views/promoter/events/new.html.erb
Normal file
@@ -0,0 +1,146 @@
|
||||
<% content_for(:title, "Créer un événement") %>
|
||||
|
||||
<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_events_path, 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">Créer un événement</h1>
|
||||
<p class="text-gray-600">Remplissez les informations de votre événement</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= form_with model: [:promoter, @event], local: true, class: "space-y-8", data: { controller: "event-form" } do |form| %>
|
||||
<% if @event.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(@event.errors.count, "erreur") %> à corriger :
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<% @event.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="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<%= form.label :name, "Nom de l'événement", 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: Soirée d'ouverture", data: { "event-form-target": "name", action: "input->event-form#generateSlug" } %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :slug, "Slug (URL)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_field :slug, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "soiree-ouverture", data: { "event-form-target": "slug" } %>
|
||||
<p class="mt-1 text-sm text-gray-500">Utilisé dans l'URL de l'événement</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<%= form.label :description, "Description", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_area :description, rows: 4, 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 votre événement..." %>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<%= form.label :image, "Image (URL)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.url_field :image, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "https://example.com/image.jpg" %>
|
||||
<p class="mt-1 text-sm text-gray-500">URL de l'image de couverture de l'événement</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date & Time -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Date et heure</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<%= form.label :start_time, "Date et heure de début", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.datetime_local_field :start_time, 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 :end_time, "Date et heure de fin", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.datetime_local_field :end_time, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Venue Information -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Lieu de l'événement</h3>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<%= form.label :venue_name, "Nom du lieu", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_field :venue_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: Le Grand Rex" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :venue_address, "Adresse", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.text_field :venue_address, 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: 1 Boulevard Poissonnière, 75002 Paris" %>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<%= form.label :latitude, "Latitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.number_field :latitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "48.8566" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :longitude, "Longitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= form.number_field :longitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "2.3522" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500">
|
||||
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
||||
Utilisez un service comme <a href="https://www.latlong.net/" target="_blank" class="text-purple-600 hover:text-purple-800">latlong.net</a> pour obtenir les coordonnées GPS.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Options</h3>
|
||||
|
||||
<div class="flex items-center">
|
||||
<%= form.check_box :featured, class: "h-4 w-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500" %>
|
||||
<%= form.label :featured, "Mettre en avant sur la page d'accueil", class: "ml-2 text-sm text-gray-700" %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500">Les événements mis en avant apparaissent en premier sur la page d'accueil.</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<%= link_to promoter_events_path, 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 en brouillon", class: "inline-flex items-center px-6 py-3 bg-gray-600 text-white font-medium rounded-lg hover:bg-gray-700 transition-colors duration-200" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
231
app/views/promoter/events/show.html.erb
Normal file
231
app/views/promoter/events/show.html.erb
Normal file
@@ -0,0 +1,231 @@
|
||||
<% content_for(:title, @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_events_path, 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"><%= @event.name %></h1>
|
||||
<div class="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<span class="flex items-center">
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-1"></i>
|
||||
<%= @event.start_time&.strftime("%d/%m/%Y à %H:%M") || "Date non définie" %>
|
||||
</span>
|
||||
<span class="flex items-center">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 mr-1"></i>
|
||||
<%= @event.venue_name %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<%= link_to edit_promoter_event_path(@event), 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 %>
|
||||
|
||||
<% if @event.draft? %>
|
||||
<%= button_to publish_promoter_event_path(@event), method: :patch, 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="upload" class="w-4 h-4 mr-2"></i>
|
||||
Publier
|
||||
<% end %>
|
||||
<% elsif @event.published? %>
|
||||
<%= button_to unpublish_promoter_event_path(@event), method: :patch, class: "inline-flex items-center px-4 py-2 bg-yellow-600 text-white font-medium rounded-lg hover:bg-yellow-700 transition-colors duration-200" do %>
|
||||
<i data-lucide="download" class="w-4 h-4 mr-2"></i>
|
||||
Dépublier
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if @event.published? %>
|
||||
<%= button_to cancel_promoter_event_path(@event), method: :patch, class: "inline-flex items-center px-4 py-2 bg-red-600 text-white font-medium rounded-lg hover:bg-red-700 transition-colors duration-200", data: { confirm: "Êtes-vous sûr de vouloir annuler cet événement ?" } do %>
|
||||
<i data-lucide="x-circle" class="w-4 h-4 mr-2"></i>
|
||||
Annuler
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status banner -->
|
||||
<div class="mb-8">
|
||||
<% case @event.state %>
|
||||
<% when "draft" %>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="edit-3" class="w-5 h-5 text-gray-400 mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-900">Événement en brouillon</h3>
|
||||
<p class="text-sm text-gray-500">Cet événement n'est pas visible publiquement. Publiez-le pour le rendre accessible aux utilisateurs.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% when "published" %>
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="eye" class="w-5 h-5 text-green-400 mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-green-900">Événement publié</h3>
|
||||
<p class="text-sm text-green-700">Cet événement est visible publiquement et les utilisateurs peuvent acheter des billets.</p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<%= link_to event_path(@event.slug, @event), target: "_blank", class: "text-green-600 hover:text-green-800 font-medium text-sm" do %>
|
||||
Voir publiquement <i data-lucide="external-link" class="w-4 h-4 inline ml-1"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% when "canceled" %>
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="x-circle" class="w-5 h-5 text-red-400 mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-red-900">Événement annulé</h3>
|
||||
<p class="text-sm text-red-700">Cet événement a été annulé et n'est plus accessible aux utilisateurs.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% when "sold_out" %>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="users" class="w-5 h-5 text-blue-400 mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-blue-900">Événement complet</h3>
|
||||
<p class="text-sm text-blue-700">Tous les billets pour cet événement ont été vendus.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @event.featured? %>
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mt-4">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="star" class="w-5 h-5 text-yellow-400 mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-yellow-900">Événement à la une</h3>
|
||||
<p class="text-sm text-yellow-700">Cet événement est mis en avant sur la page d'accueil.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Event details -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Main content -->
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
<!-- Event image -->
|
||||
<% if @event.image.present? %>
|
||||
<div class="aspect-video bg-gray-100 rounded-lg overflow-hidden">
|
||||
<img src="<%= @event.image %>" alt="<%= @event.name %>" class="w-full h-full object-cover">
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- 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>
|
||||
<div class="prose prose-gray max-w-none">
|
||||
<%= simple_format(@event.description) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location details -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Lieu</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start space-x-3">
|
||||
<i data-lucide="building" class="w-5 h-5 text-gray-400 mt-0.5"></i>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900"><%= @event.venue_name %></p>
|
||||
<p class="text-gray-500"><%= @event.venue_address %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3 text-sm text-gray-500">
|
||||
<i data-lucide="map-pin" class="w-4 h-4"></i>
|
||||
<span><%= @event.latitude %>, <%= @event.longitude %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Event stats -->
|
||||
<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="flex items-center justify-between">
|
||||
<span class="text-gray-500">Types de billets</span>
|
||||
<span class="font-medium"><%= @event.ticket_types.count %></span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-500">Billets vendus</span>
|
||||
<span class="font-medium"><%= @event.tickets.count %></span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-500">Revenus</span>
|
||||
<span class="font-medium">
|
||||
<%= number_to_currency(@event.tickets.sum(:price_cents) / 100.0, unit: "€") %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event 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-4">
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Slug</span>
|
||||
<p class="font-mono text-sm"><%= @event.slug %></p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Créé le</span>
|
||||
<p class="text-sm"><%= @event.created_at.strftime("%d/%m/%Y à %H:%M") %></p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Modifié le</span>
|
||||
<p class="text-sm"><%= @event.updated_at.strftime("%d/%m/%Y à %H:%M") %></p>
|
||||
</div>
|
||||
<% if @event.start_time %>
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Début</span>
|
||||
<p class="text-sm"><%= @event.start_time.strftime("%d/%m/%Y à %H:%M") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @event.end_time %>
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">Fin</span>
|
||||
<p class="text-sm"><%= @event.end_time.strftime("%d/%m/%Y à %H:%M") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</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 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 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>
|
||||
Marquer comme complet
|
||||
<% end %>
|
||||
<hr class="border-gray-200">
|
||||
<%= button_to promoter_event_path(@event), method: :delete,
|
||||
data: { confirm: "Êtes-vous sûr de vouloir supprimer cet événement ? Cette action est irréversible." },
|
||||
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 l'événement
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
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>
|
||||
@@ -34,6 +34,25 @@ Rails.application.routes.draw do
|
||||
# === Pages ===
|
||||
get "dashboard", to: "pages#dashboard", as: "dashboard"
|
||||
|
||||
# === Promoter Routes ===
|
||||
namespace :promoter do
|
||||
resources :events do
|
||||
member do
|
||||
patch :publish
|
||||
patch :unpublish
|
||||
patch :cancel
|
||||
patch :mark_sold_out
|
||||
end
|
||||
|
||||
# Nested ticket types routes
|
||||
resources :ticket_types do
|
||||
member do
|
||||
post :duplicate
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# === Events ===
|
||||
get "events", to: "events#index", as: "events"
|
||||
get "events/:slug.:id", to: "events#show", as: "event"
|
||||
|
||||
@@ -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.
|
||||
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user