Compare commits
2 Commits
a4a427ad5c
...
87ccebf229
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87ccebf229 | ||
|
|
72d54e02ab |
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
### Medium Priority
|
### Medium Priority
|
||||||
|
|
||||||
- [ ] feat: Promotion code on ticket
|
|
||||||
- [ ] feat: Promoter system with event creation, ticket types creation and metrics display
|
- [ ] feat: Promoter system with event creation, ticket types creation and metrics display
|
||||||
- [ ] feat: Multiple ticket types (early bird, VIP, general admission)
|
- [ ] feat: Multiple ticket types (early bird, VIP, general admission)
|
||||||
- [ ] feat: Refund management system
|
- [ ] feat: Refund management system
|
||||||
|
|||||||
82
app/controllers/promoter/promotion_codes_controller.rb
Normal file
82
app/controllers/promoter/promotion_codes_controller.rb
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
class Promoter::PromotionCodesController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :set_event
|
||||||
|
before_action :set_promotion_code, only: [ :edit, :update, :destroy ]
|
||||||
|
|
||||||
|
# GET /promoter/events/:event_id/promotion_codes
|
||||||
|
# Display all promotion codes for a specific event
|
||||||
|
def index
|
||||||
|
@promotion_codes = @event.promotion_codes.includes(:user)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# GET /promoter/events/:event_id/promotion_codes/new
|
||||||
|
# Show form to create a new promotion code
|
||||||
|
def new
|
||||||
|
@promotion_code = @event.promotion_codes.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /promoter/events/:event_id/promotion_codes/:id/edit
|
||||||
|
# Show form to edit an existing promotion code
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /promoter/events/:event_id/promotion_codes
|
||||||
|
# Create a new promotion code for the event
|
||||||
|
def create
|
||||||
|
@promotion_code = @event.promotion_codes.new(promotion_code_params_with_conversion)
|
||||||
|
@promotion_code.user = current_user
|
||||||
|
|
||||||
|
if @promotion_code.save
|
||||||
|
redirect_to promoter_event_promotion_codes_path(@event), notice: "Promotion code was successfully created."
|
||||||
|
else
|
||||||
|
render :new, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# PATCH/PUT /promoter/events/:event_id/promotion_codes/:id
|
||||||
|
# Update an existing promotion code
|
||||||
|
def update
|
||||||
|
if @promotion_code.update(promotion_code_params_with_conversion)
|
||||||
|
redirect_to promoter_event_promotion_codes_path(@event), notice: "Promotion code was successfully updated."
|
||||||
|
else
|
||||||
|
render :edit, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /promoter/events/:event_id/promotion_codes/:id
|
||||||
|
# Delete a promotion code
|
||||||
|
def destroy
|
||||||
|
@promotion_code.destroy
|
||||||
|
redirect_to promoter_event_promotion_codes_path(@event), notice: "Promotion code was successfully destroyed."
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Find the event based on the URL parameter
|
||||||
|
def set_event
|
||||||
|
@event = Event.find(params[:event_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Find the promotion code based on the URL parameter
|
||||||
|
def set_promotion_code
|
||||||
|
@promotion_code = @event.promotion_codes.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Strong parameters for promotion code form (accepts euros for display)
|
||||||
|
def promotion_code_params
|
||||||
|
params.require(:promotion_code).permit(:code, :discount_amount_euros, :expires_at, :active, :usage_limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Convert euros to cents for database storage
|
||||||
|
# The form displays euros for user convenience, but the database stores cents
|
||||||
|
def promotion_code_params_with_conversion
|
||||||
|
params = promotion_code_params
|
||||||
|
if params[:discount_amount_euros].present?
|
||||||
|
# Convert euros to cents (e.g., 20.50 -> 2050)
|
||||||
|
params[:discount_amount_cents] = (params[:discount_amount_euros].to_f * 100).to_i
|
||||||
|
params.delete(:discount_amount_euros) # Remove the temporary euro parameter
|
||||||
|
end
|
||||||
|
params
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -21,6 +21,7 @@ class Event < ApplicationRecord
|
|||||||
has_many :ticket_types
|
has_many :ticket_types
|
||||||
has_many :tickets, through: :ticket_types
|
has_many :tickets, through: :ticket_types
|
||||||
has_many :orders
|
has_many :orders
|
||||||
|
has_many :promotion_codes
|
||||||
|
|
||||||
# === Callbacks ===
|
# === Callbacks ===
|
||||||
before_validation :geocode_address, if: :should_geocode_address?
|
before_validation :geocode_address, if: :should_geocode_address?
|
||||||
|
|||||||
@@ -90,10 +90,34 @@ class Order < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Calculate total from ticket prices only (platform fee deducted from promoter payout)
|
# Calculate total from ticket prices minus promotion code discounts
|
||||||
def calculate_total!
|
def calculate_total!
|
||||||
ticket_total = tickets.sum(:price_cents)
|
ticket_total = tickets.sum(:price_cents)
|
||||||
update!(total_amount_cents: ticket_total)
|
discount_total = promotion_codes.sum(:discount_amount_cents)
|
||||||
|
|
||||||
|
# Ensure total doesn't go below zero
|
||||||
|
final_total = [ticket_total - discount_total, 0].max
|
||||||
|
update!(total_amount_cents: final_total)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Subtotal amount before discounts
|
||||||
|
def subtotal_amount_cents
|
||||||
|
tickets.sum(:price_cents)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Subtotal amount in euros
|
||||||
|
def subtotal_amount_euros
|
||||||
|
subtotal_amount_cents / 100.0
|
||||||
|
end
|
||||||
|
|
||||||
|
# Total discount amount from all promotion codes
|
||||||
|
def discount_amount_cents
|
||||||
|
promotion_codes.sum(:discount_amount_cents)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Discount amount in euros
|
||||||
|
def discount_amount_euros
|
||||||
|
discount_amount_cents / 100.0
|
||||||
end
|
end
|
||||||
|
|
||||||
# Platform fee: €0.50 fixed + 1.5% of ticket price, per ticket
|
# Platform fee: €0.50 fixed + 1.5% of ticket price, per ticket
|
||||||
|
|||||||
@@ -12,9 +12,28 @@ class PromotionCode < ApplicationRecord
|
|||||||
before_create :increment_uses_count
|
before_create :increment_uses_count
|
||||||
|
|
||||||
# Associations
|
# Associations
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :event
|
||||||
has_many :order_promotion_codes
|
has_many :order_promotion_codes
|
||||||
has_many :orders, through: :order_promotion_codes
|
has_many :orders, through: :order_promotion_codes
|
||||||
|
|
||||||
|
# Instance methods
|
||||||
|
def discount_amount_euros
|
||||||
|
discount_amount_cents / 100.0
|
||||||
|
end
|
||||||
|
|
||||||
|
def active?
|
||||||
|
active && (expires_at.nil? || expires_at > Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
def expired?
|
||||||
|
expires_at.present? && expires_at < Time.current
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_be_used?
|
||||||
|
active? && (usage_limit.nil? || uses_count < usage_limit)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def increment_uses_count
|
def increment_uses_count
|
||||||
|
|||||||
@@ -1,30 +1,12 @@
|
|||||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 py-8">
|
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 py-8">
|
||||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<nav class="inline-flex items-center gap-2 bg-white px-4 py-3 rounded-xl shadow-sm border border-gray-100 mb-8" aria-label="Breadcrumb">
|
<%= render 'components/breadcrumb', crumbs: [
|
||||||
<div class="inline-flex items-center text-sm font-medium">
|
{ name: 'Accueil', path: root_path },
|
||||||
<%= link_to root_path, class: "text-gray-700 hover:text-purple-600 transition-colors" do %>
|
{ name: 'Événements', path: events_path },
|
||||||
<i data-lucide="home" class="w-4 h-4 mr-2"></i>
|
{ name: @order.event.name, path: event_path(@order.event.slug, @order.event) },
|
||||||
Accueil
|
{ name: "Commande ##{@order.id}", path: nil }
|
||||||
<% end %>
|
] %>
|
||||||
</div>
|
|
||||||
<i data-lucide="chevron-right" class="w-4 h-4 text-gray-400"></i>
|
|
||||||
<div class="inline-flex items-center text-sm font-medium">
|
|
||||||
<%= link_to events_path, class: "text-gray-700 hover:text-purple-600 transition-colors" do %>
|
|
||||||
Événements
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<i data-lucide="chevron-right" class="w-4 h-4 text-gray-400"></i>
|
|
||||||
<div class="inline-flex items-center text-sm font-medium">
|
|
||||||
<%= link_to event_path(@order.event.slug, @order.event), class: "text-gray-700 hover:text-purple-600 transition-colors" do %>
|
|
||||||
<%= @order.event.name %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<i data-lucide="chevron-right" class="w-4 h-4 text-gray-400"></i>
|
|
||||||
<div class="text-sm font-medium text-purple-600">
|
|
||||||
Commande #<%= @order.id %>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<!-- Order Summary -->
|
<!-- Order Summary -->
|
||||||
@@ -77,8 +59,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Order Items -->
|
<!-- Order Items -->
|
||||||
<div class="space-y-4 mb-6">
|
<div class="space-y-4 mb-6 border-b border-gray-200 pb-6 mb-6">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif de votre commande</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-4 ">Récapitulatif de votre commande</h3>
|
||||||
|
|
||||||
<% @tickets.each do |ticket| %>
|
<% @tickets.each do |ticket| %>
|
||||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||||
@@ -99,12 +81,46 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Promotion Code Discount -->
|
||||||
|
<% if @order.promotion_codes.any? %>
|
||||||
|
<div class="space-y-2 mb-6 pb-6 border-b border-gray-200">
|
||||||
|
<% @order.promotion_codes.each do |promo_code| %>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm font-medium text-green-600">
|
||||||
|
<i data-lucide="tag" class="w-4 h-4 mr-1"></i>
|
||||||
|
Code: <%= promo_code.code %>
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-semibold text-green-600">-<%= promo_code.discount_amount_euros %>€</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<!-- Order Total -->
|
<!-- Order Total -->
|
||||||
<div class=" pt-12">
|
<div class=" pt-12">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center justify-between text-lg pt-2">
|
<!-- Subtotal -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm text-gray-600">Sous-total</span>
|
||||||
|
<span class="text-sm font-medium text-gray-600"><%= @order.subtotal_amount_euros %>€</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Discount -->
|
||||||
|
<% if @order.discount_amount_cents > 0 %>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm text-green-600">Réduction</span>
|
||||||
|
<span class="text-sm font-semibold text-green-600">-<%= @order.discount_amount_euros %>€</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- Total -->
|
||||||
|
<div class="flex items-center justify-between text-lg pt-2 border-t border-gray-200">
|
||||||
<span class="font-medium text-gray-900">Total</span>
|
<span class="font-medium text-gray-900">Total</span>
|
||||||
<span class="font-bold text-2xl text-purple-600"><%= @order.total_amount_euros %>€</span>
|
<% if @order.total_amount_cents == 0 %>
|
||||||
|
<span class="font-bold text-2xl text-green-600">GRATUIT</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="font-bold text-2xl text-purple-600"><%= @order.total_amount_euros %>€</span>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-2">TVA incluse</p>
|
<p class="text-xs text-gray-500 mt-2">TVA incluse</p>
|
||||||
@@ -150,7 +166,11 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<i data-lucide="credit-card" class="w-5 h-5 mr-2"></i>
|
<i data-lucide="credit-card" class="w-5 h-5 mr-2"></i>
|
||||||
Payer <%= @order.total_amount_euros %>€
|
<% if @order.total_amount_cents == 0 %>
|
||||||
|
Confirmer la commande
|
||||||
|
<% else %>
|
||||||
|
Payer <%= @order.total_amount_euros %>€
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -251,7 +271,11 @@ const stripeResult = await stripe.redirectToCheckout({
|
|||||||
button.innerHTML = `
|
button.innerHTML = `
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<i data-lucide="credit-card" class="w-5 h-5 mr-2"></i>
|
<i data-lucide="credit-card" class="w-5 h-5 mr-2"></i>
|
||||||
Payer <%= @order.total_amount_euros %>€
|
<% if @order.total_amount_cents == 0 %>
|
||||||
|
Confirmer la commande
|
||||||
|
<% else %>
|
||||||
|
Payer <%= @order.total_amount_euros %>€
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
alert('Erreur: ' + error.message);
|
alert('Erreur: ' + error.message);
|
||||||
|
|||||||
@@ -123,13 +123,58 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Total -->
|
<!-- Promotion Codes Applied -->
|
||||||
<div class="mt-6">
|
<% if @order.promotion_codes.any? %>
|
||||||
|
<div class="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||||
|
<h3 class="text-lg font-semibold text-green-900 mb-3 flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"/>
|
||||||
|
</svg>
|
||||||
|
Codes promotionnels appliqués
|
||||||
|
</h3>
|
||||||
|
<% @order.promotion_codes.each do |promo_code| %>
|
||||||
|
<div class="flex items-center justify-between py-2 px-3 bg-white rounded-lg border border-green-200 mb-2 last:mb-0">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-sm font-medium text-green-800">
|
||||||
|
<svg class="w-4 h-4 mr-1 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
<%= promo_code.code %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-semibold text-green-700">-<%= promo_code.discount_amount_euros %>€</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- Price Breakdown -->
|
||||||
|
<div class="mt-6 p-4 bg-gray-50 border border-gray-200 rounded-lg">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-3">Détail du paiement</h3>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center justify-between text-lg pt-2">
|
<!-- Subtotal -->
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-gray-600">Sous-total</span>
|
||||||
|
<span class="font-medium text-gray-700"><%= @order.subtotal_amount_euros %>€</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Discount -->
|
||||||
|
<% if @order.discount_amount_cents > 0 %>
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-green-600">Réduction</span>
|
||||||
|
<span class="font-semibold text-green-600">-<%= @order.discount_amount_euros %>€</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- Total -->
|
||||||
|
<div class="flex items-center justify-between pt-2 border-t border-gray-300">
|
||||||
<span class="font-medium text-gray-900">Total payé</span>
|
<span class="font-medium text-gray-900">Total payé</span>
|
||||||
<span class="font-bold text-2xl text-green-600">
|
<span class="font-bold text-xl text-green-600">
|
||||||
<%= @order.total_amount_euros %>€
|
<% if @order.total_amount_cents == 0 %>
|
||||||
|
GRATUIT
|
||||||
|
<% else %>
|
||||||
|
<%= @order.total_amount_euros %>€
|
||||||
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,14 +94,57 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Total -->
|
<!-- Promotion Codes Applied -->
|
||||||
|
<% if @order.promotion_codes.any? %>
|
||||||
|
<div class="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||||
|
<h3 class="text-lg font-semibold text-green-900 mb-3 flex items-center">
|
||||||
|
<i data-lucide="tag" class="w-5 h-5 mr-2"></i>
|
||||||
|
Codes promotionnels appliqués
|
||||||
|
</h3>
|
||||||
|
<% @order.promotion_codes.each do |promo_code| %>
|
||||||
|
<div class="flex items-center justify-between py-2 px-3 bg-white rounded-lg border border-green-200 mb-2 last:mb-0">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-sm font-medium text-green-800">
|
||||||
|
<i data-lucide="check-circle" class="w-4 h-4 mr-1 inline"></i>
|
||||||
|
<%= promo_code.code %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-semibold text-green-700">-<%= promo_code.discount_amount_euros %>€</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- Price Breakdown -->
|
||||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||||
<div class="flex items-center justify-between text-lg pt-2">
|
<h3 class="text-lg font-semibold text-gray-900 mb-3">Détail du paiement</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<!-- Subtotal -->
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-gray-600">Sous-total</span>
|
||||||
|
<span class="font-medium text-gray-700"><%= @order.subtotal_amount_euros %>€</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Discount -->
|
||||||
|
<% if @order.discount_amount_cents > 0 %>
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-green-600">Réduction</span>
|
||||||
|
<span class="font-semibold text-green-600">-<%= @order.discount_amount_euros %>€</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- Total -->
|
||||||
|
<div class="flex items-center justify-between pt-2 border-t border-gray-300">
|
||||||
<span class="font-medium text-gray-900">Total <%= @order.status == 'paid' || @order.status == 'completed' ? 'payé' : 'à payer' %></span>
|
<span class="font-medium text-gray-900">Total <%= @order.status == 'paid' || @order.status == 'completed' ? 'payé' : 'à payer' %></span>
|
||||||
<span class="font-bold text-2xl <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-purple-600' %>">
|
<span class="font-bold text-2xl <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-purple-600' %>">
|
||||||
<%= @order.total_amount_euros %>€
|
<% if @order.total_amount_cents == 0 %>
|
||||||
|
GRATUIT
|
||||||
|
<% else %>
|
||||||
|
<%= @order.total_amount_euros %>€
|
||||||
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- View Invoice -->
|
<!-- View Invoice -->
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
{ name: 'Tableau de bord', path: dashboard_path }
|
{ name: 'Tableau de bord', path: dashboard_path }
|
||||||
] %>
|
] %>
|
||||||
|
|
||||||
<!-- Page Header -->
|
<!-- Promoter Page Header -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900">Mon tableau de bord</h1>
|
<h1 class="text-3xl font-bold text-gray-900">Mon tableau de bord promoteur</h1>
|
||||||
<p class="text-gray-600 mt-1">Gérez vos commandes et accédez à vos billets</p>
|
<p class="text-gray-600 mt-1">Gérez vos commandes et accédez à vos billets</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -76,7 +76,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Brouillons -->
|
||||||
<div class="bg-gradient-to-br from-orange-50 to-orange-100 rounded-2xl p-6 border border-orange-200">
|
<div class="bg-gradient-to-br from-orange-50 to-orange-100 rounded-2xl p-6 border border-orange-200">
|
||||||
|
<%= link_to promoter_events_path do %>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-orange-600 text-sm font-medium">Brouillons</p>
|
<p class="text-orange-600 text-sm font-medium">Brouillons</p>
|
||||||
@@ -86,7 +88,9 @@
|
|||||||
<i data-lucide="edit-3" class="w-6 h-6 text-orange-700"></i>
|
<i data-lucide="edit-3" class="w-6 h-6 text-orange-700"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
|
</div><!-- /Brouillons -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Revenue Chart & Recent Events -->
|
<!-- Revenue Chart & Recent Events -->
|
||||||
@@ -273,6 +277,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<!-- User Page Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">Mon tableau de bord</h1>
|
||||||
|
<p class="text-gray-600 mt-1">Accédez à vos billets et évenements</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- User's Orders Section -->
|
<!-- User's Orders Section -->
|
||||||
<div class="bg-white rounded-2xl shadow-lg mb-8">
|
<div class="bg-white rounded-2xl shadow-lg mb-8">
|
||||||
<div class="border-b border-gray-100 p-4 sm:p-6">
|
<div class="border-b border-gray-100 p-4 sm:p-6">
|
||||||
|
|||||||
@@ -209,6 +209,42 @@
|
|||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
<!-- Quick actions -->
|
||||||
|
<div class="bg-white rounded-2xl border border-gray-200 p-4 sm: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 justify-center px-4 py-3 bg-purple-600 text-white font-medium text-sm 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 %>
|
||||||
|
|
||||||
|
<%= link_to promoter_event_promotion_codes_path(@event), class: "w-full inline-flex items-center justify-center px-4 py-3 bg-green-600 text-white font-medium text-sm rounded-lg hover:bg-green-700 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="tag" class="w-4 h-4 mr-2"></i>
|
||||||
|
Gérer les codes de réduction
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @event.sold_out? %>
|
||||||
|
<%= button_to mark_available_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-blue-50 text-blue-700 font-medium text-sm rounded-lg hover:bg-blue-100 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="refresh-ccw" class="w-4 h-4 mr-2"></i>
|
||||||
|
Marquer comme disponible
|
||||||
|
<% end %>
|
||||||
|
<% elsif @event.published? %>
|
||||||
|
<%= button_to mark_sold_out_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-gray-50 text-gray-700 font-medium text-sm rounded-lg hover:bg-gray-100 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="users" class="w-4 h-4 mr-2"></i>
|
||||||
|
Marquer comme complet
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<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 justify-center px-4 py-3 text-red-600 font-medium text-sm 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>
|
||||||
|
|
||||||
<!-- Event stats -->
|
<!-- Event stats -->
|
||||||
<div class="bg-white rounded-2xl border border-gray-200 p-4 sm:p-6">
|
<div class="bg-white rounded-2xl border border-gray-200 p-4 sm:p-6">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Statistiques</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Statistiques</h3>
|
||||||
@@ -269,36 +305,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick actions -->
|
|
||||||
<div class="bg-white rounded-2xl border border-gray-200 p-4 sm: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 justify-center px-4 py-3 bg-purple-600 text-white font-medium text-sm 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 %>
|
|
||||||
|
|
||||||
<% if @event.sold_out? %>
|
|
||||||
<%= button_to mark_available_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-blue-50 text-blue-700 font-medium text-sm rounded-lg hover:bg-blue-100 transition-colors duration-200" do %>
|
|
||||||
<i data-lucide="refresh-ccw" class="w-4 h-4 mr-2"></i>
|
|
||||||
Marquer comme disponible
|
|
||||||
<% end %>
|
|
||||||
<% elsif @event.published? %>
|
|
||||||
<%= button_to mark_sold_out_promoter_event_path(@event), method: :patch, class: "w-full inline-flex items-center justify-center px-4 py-3 bg-gray-50 text-gray-700 font-medium text-sm rounded-lg hover:bg-gray-100 transition-colors duration-200" do %>
|
|
||||||
<i data-lucide="users" class="w-4 h-4 mr-2"></i>
|
|
||||||
Marquer comme complet
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<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 justify-center px-4 py-3 text-red-600 font-medium text-sm 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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
109
app/views/promoter/promotion_codes/edit.html.erb
Normal file
109
app/views/promoter/promotion_codes/edit.html.erb
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<% content_for(:title, "Modifier le code de réduction - #{@event.name}") %>
|
||||||
|
|
||||||
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<%= render 'components/breadcrumb', crumbs: [
|
||||||
|
{ name: 'Accueil', path: root_path },
|
||||||
|
{ name: 'Tableau de bord', path: dashboard_path },
|
||||||
|
{ name: 'Mes événements', path: promoter_events_path },
|
||||||
|
{ name: @event.name, path: promoter_event_path(@event) },
|
||||||
|
{ name: 'Codes de réduction', path: promoter_event_promotion_codes_path(@event) },
|
||||||
|
{ name: "Modifier #{@promotion_code.code}" }
|
||||||
|
] %>
|
||||||
|
|
||||||
|
<div class="max-w-2xl mx-auto">
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center space-x-4 mb-4">
|
||||||
|
<%= link_to promoter_event_promotion_codes_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">Modifier le code de réduction</h1>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
<code class="bg-gray-100 px-2 py-1 rounded text-sm"><%= @promotion_code.code %></code> pour <%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= form_with(model: [@event, @promotion_code], url: promoter_event_promotion_code_path(@event, @promotion_code), method: :patch, local: true, class: "bg-white rounded-2xl border border-gray-200 p-6 sm:p-8") do |form| %>
|
||||||
|
<% if @promotion_code.errors.any? %>
|
||||||
|
<div class="bg-red-50 border border-red-200 rounded-2xl p-4 mb-6">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<i data-lucide="alert-circle" class="w-5 h-5 text-red-400 mr-3 mt-0.5 flex-shrink-0"></i>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-sm font-medium text-red-800 mb-2">
|
||||||
|
<%= pluralize(@promotion_code.errors.count, "erreur") %> ont empêché ce code de réduction d'être sauvegardé :
|
||||||
|
</h3>
|
||||||
|
<ul class="list-disc list-inside text-sm text-red-700">
|
||||||
|
<% @promotion_code.errors.full_messages.each do |message| %>
|
||||||
|
<li><%= message %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<%= form.label :code, "Code de réduction", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.text_field :code, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "Ex: SUMMER2024, BIENVENUE10, etc." %>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Ce code sera visible par les clients lors du paiement</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.label :discount_amount_euros, "Montant de la réduction (en euros)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.number_field :discount_amount_euros, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "10", min: 0, step: "0.01", value: number_with_precision(@promotion_code.discount_amount_cents.to_f / 100, precision: 2) %>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Entrez le montant en euros (ex: 10, 5.50, 25)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<%= form.label :expires_at, "Date d'expiration", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.datetime_local_field :expires_at, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors" %>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Laissez vide pour une durée illimitée</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.label :usage_limit, "Limite d'utilisation", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.number_field :usage_limit, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "Ex: 50", min: 1 %>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Laissez vide pour une utilisation illimitée</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<%= form.check_box :active, class: "h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded" %>
|
||||||
|
<%= form.label :active, "Code actif", class: "ml-3 block text-sm font-medium text-gray-900" %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
Les clients peuvent utiliser ce code de réduction
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<i data-lucide="info" class="w-5 h-5 text-blue-400 mr-3 mt-0.5 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium text-blue-800 mb-1">Statut actuel</h4>
|
||||||
|
<div class="text-sm text-blue-700">
|
||||||
|
<p>Utilisations: <%= @promotion_code.uses_count %><%= " / #{@promotion_code.usage_limit}" if @promotion_code.usage_limit %></p>
|
||||||
|
<p>Commandes associées: <%= @promotion_code.orders.count %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between pt-6 mt-8 border-t border-gray-200">
|
||||||
|
<%= link_to promoter_event_promotion_codes_path(@event), class: "inline-flex items-center px-6 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="x" class="w-4 h-4 mr-2"></i>
|
||||||
|
Annuler
|
||||||
|
<% end %>
|
||||||
|
<%= form.submit "Mettre à jour le code de réduction", class: "inline-flex items-center px-6 py-3 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
175
app/views/promoter/promotion_codes/index.html.erb
Normal file
175
app/views/promoter/promotion_codes/index.html.erb
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<% content_for(:title, "Codes de réduction - #{@event.name}") %>
|
||||||
|
|
||||||
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<%= render 'components/breadcrumb', crumbs: [
|
||||||
|
{ name: 'Accueil', path: root_path },
|
||||||
|
{ name: 'Tableau de bord', path: dashboard_path },
|
||||||
|
{ name: 'Mes événements', path: promoter_events_path },
|
||||||
|
{ name: @event.name, path: promoter_event_path(@event) },
|
||||||
|
{ name: 'Codes de réduction' }
|
||||||
|
] %>
|
||||||
|
|
||||||
|
<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">Codes de réduction</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_promotion_code_path(@event), class: "inline-flex items-center px-6 py-3 bg-gray-900 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 code
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event status info -->
|
||||||
|
<% if @event.draft? %>
|
||||||
|
<div class="bg-gray-50 border border-gray-200 rounded-2xl 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 codes de réduction ne seront actifs qu'une fois l'événement publié.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if @promotion_codes.any? %>
|
||||||
|
<div class="grid gap-6">
|
||||||
|
<% @promotion_codes.each do |promotion_code| %>
|
||||||
|
<div class="bg-white rounded-2xl border border-gray-200 p-6 hover:shadow-md transition-shadow duration-200">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<!-- Promotion code 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">
|
||||||
|
<%= promotion_code.code %>
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600 mb-3">Réduction de <%= number_to_currency(promotion_code.discount_amount_cents / 100.0, unit: "€") %></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status badge -->
|
||||||
|
<div class="ml-4">
|
||||||
|
<% if promotion_code.active? && (promotion_code.expires_at.nil? || promotion_code.expires_at > Time.current) %>
|
||||||
|
<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>
|
||||||
|
Actif
|
||||||
|
</span>
|
||||||
|
<% elsif promotion_code.expires_at && promotion_code.expires_at <= Time.current %>
|
||||||
|
<span class="inline-flex px-3 py-1 text-sm font-semibold rounded-full bg-red-100 text-red-800">
|
||||||
|
<i data-lucide="x-circle" class="w-4 h-4 mr-1"></i>
|
||||||
|
Expiré
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="inline-flex px-3 py-1 text-sm font-semibold rounded-full bg-gray-100 text-gray-800">
|
||||||
|
<i data-lucide="pause-circle" class="w-4 h-4 mr-1"></i>
|
||||||
|
Inactif
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Promotion code 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-green-600">
|
||||||
|
<%= number_to_currency(promotion_code.discount_amount_cents / 100.0, unit: "€") %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Réduction</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-gray-900">
|
||||||
|
<% if promotion_code.usage_limit %>
|
||||||
|
<%= promotion_code.usage_limit - promotion_code.uses_count %>
|
||||||
|
<% else %>
|
||||||
|
∞
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Restants</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-gray-900">
|
||||||
|
<%= promotion_code.uses_count %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Utilisés</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-blue-600">
|
||||||
|
<%= promotion_code.orders.count %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Commandes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional info -->
|
||||||
|
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500 mb-4">
|
||||||
|
<% if promotion_code.expires_at %>
|
||||||
|
<span class="flex items-center">
|
||||||
|
<i data-lucide="clock" class="w-4 h-4 mr-1"></i>
|
||||||
|
Expire le : <%= l(promotion_code.expires_at, format: :short) %>
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="flex items-center">
|
||||||
|
<i data-lucide="infinity" class="w-4 h-4 mr-1"></i>
|
||||||
|
Pas d'expiration
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<span class="flex items-center">
|
||||||
|
<i data-lucide="user" class="w-4 h-4 mr-1"></i>
|
||||||
|
<% if promotion_code.user.first_name && promotion_code.user.last_name %>
|
||||||
|
Créé par : <%= promotion_code.user.first_name %> <%= promotion_code.user.last_name %>
|
||||||
|
<% else %>
|
||||||
|
Créé par : <%= promotion_code.user.email %>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
</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 edit_promoter_event_promotion_code_path(@event, promotion_code), class: "text-gray-400 hover:text-blue-600 transition-colors", title: "Modifier" do %>
|
||||||
|
<i data-lucide="edit" class="w-5 h-5"></i>
|
||||||
|
<% end %>
|
||||||
|
<% if promotion_code.orders.empty? %>
|
||||||
|
<%= button_to promoter_event_promotion_code_path(@event, promotion_code), method: :delete,
|
||||||
|
data: { confirm: "Êtes-vous sûr de vouloir supprimer ce code de réduction ?" },
|
||||||
|
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éé il y a <%= time_ago_in_words(promotion_code.created_at) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="bg-white rounded-2xl 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="tag" class="w-12 h-12 text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 mb-2">Aucun code de réduction</h3>
|
||||||
|
<p class="text-gray-500 mb-6">Créez des codes de réduction pour offrir des remises spéciales à vos clients.</p>
|
||||||
|
<%= link_to new_promoter_event_promotion_code_path(@event), class: "inline-flex items-center px-6 py-3 bg-gray-900 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 code de réduction
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
96
app/views/promoter/promotion_codes/new.html.erb
Normal file
96
app/views/promoter/promotion_codes/new.html.erb
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<% content_for(:title, "Nouveau code de réduction - #{@event.name}") %>
|
||||||
|
|
||||||
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<%= render 'components/breadcrumb', crumbs: [
|
||||||
|
{ name: 'Accueil', path: root_path },
|
||||||
|
{ name: 'Tableau de bord', path: dashboard_path },
|
||||||
|
{ name: 'Mes événements', path: promoter_events_path },
|
||||||
|
{ name: @event.name, path: promoter_event_path(@event) },
|
||||||
|
{ name: 'Codes de réduction', path: promoter_event_promotion_codes_path(@event) },
|
||||||
|
{ name: 'Nouveau code' }
|
||||||
|
] %>
|
||||||
|
|
||||||
|
<div class="max-w-2xl mx-auto">
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center space-x-4 mb-4">
|
||||||
|
<%= link_to promoter_event_promotion_codes_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">Nouveau code de réduction</h1>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Pour <%= link_to @event.name, promoter_event_path(@event), class: "text-purple-600 hover:text-purple-800" %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= form_with(model: [@event, @promotion_code], url: promoter_event_promotion_codes_path(@event), local: true, class: "bg-white rounded-2xl border border-gray-200 p-6 sm:p-8") do |form| %>
|
||||||
|
<% if @promotion_code.errors.any? %>
|
||||||
|
<div class="bg-red-50 border border-red-200 rounded-2xl p-4 mb-6">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<i data-lucide="alert-circle" class="w-5 h-5 text-red-400 mr-3 mt-0.5 flex-shrink-0"></i>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-sm font-medium text-red-800 mb-2">
|
||||||
|
<%= pluralize(@promotion_code.errors.count, "erreur") %> ont empêché ce code de réduction d'être sauvegardé :
|
||||||
|
</h3>
|
||||||
|
<ul class="list-disc list-inside text-sm text-red-700">
|
||||||
|
<% @promotion_code.errors.full_messages.each do |message| %>
|
||||||
|
<li><%= message %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<%= form.label :code, "Code de réduction", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.text_field :code, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "Ex: BIENVENUE10, VIP20" %>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Ce code sera à appliquer par le client lors du paiement.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.label :discount_amount_euros, "Montant de la réduction (en euros)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.number_field :discount_amount_euros, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "10", min: 0, step: "0.01", value: number_with_precision(@promotion_code.discount_amount_cents.to_f / 100, precision: 2) %>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Entrez le montant en euros</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<%= form.label :expires_at, "Date d'expiration", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.datetime_local_field :expires_at, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors" %>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Laissez vide pour une durée illimitée</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.label :usage_limit, "Limite d'utilisation", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
|
<%= form.number_field :usage_limit, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors", placeholder: "Ex: 50", min: 1 %>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Laissez vide pour une utilisation illimitée</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<%= form.check_box :active, class: "h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded" %>
|
||||||
|
<%= form.label :active, "Code actif", class: "ml-3 block text-sm font-medium text-gray-900" %>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
Les clients peuvent utiliser ce code de réduction
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between pt-6 mt-8 border-t border-gray-200">
|
||||||
|
<%= link_to promoter_event_promotion_codes_path(@event), class: "inline-flex items-center px-6 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition-colors duration-200" do %>
|
||||||
|
<i data-lucide="x" class="w-4 h-4 mr-2"></i>
|
||||||
|
Annuler
|
||||||
|
<% end %>
|
||||||
|
<%= form.submit "Créer le code de réduction", class: "inline-flex items-center px-6 py-3 bg-gray-900 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -91,6 +91,16 @@ Rails.application.routes.draw do
|
|||||||
post :duplicate
|
post :duplicate
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Nested promotion codes routes
|
||||||
|
resources :promotion_codes, except: [ :show ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# === Promotion Codes Routes ===
|
||||||
|
resources :promotion_codes, only: [ :index ] do
|
||||||
|
member do
|
||||||
|
post :apply
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
class CreateOrders < ActiveRecord::Migration[8.0]
|
class CreateOrders < ActiveRecord::Migration[8.0]
|
||||||
def change
|
def change
|
||||||
create_table :orders do |t|
|
create_table :orders do |t|
|
||||||
t.references :user, null: false, foreign_key: false
|
|
||||||
t.references :event, null: false, foreign_key: false
|
|
||||||
t.string :status, null: false, default: "draft"
|
t.string :status, null: false, default: "draft"
|
||||||
t.integer :total_amount_cents, null: false, default: 0
|
t.integer :total_amount_cents, null: false, default: 0
|
||||||
t.integer :payment_attempts, null: false, default: 0
|
t.integer :payment_attempts, null: false, default: 0
|
||||||
t.timestamp :expires_at
|
t.timestamp :expires_at
|
||||||
t.timestamp :last_payment_attempt_at
|
t.timestamp :last_payment_attempt_at
|
||||||
|
|
||||||
|
t.references :user, null: false, foreign_key: false
|
||||||
|
t.references :event, null: false, foreign_key: false
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,12 @@ class CreatePromotionCodes < ActiveRecord::Migration[8.0]
|
|||||||
t.boolean :active, default: true, null: false
|
t.boolean :active, default: true, null: false
|
||||||
t.integer :usage_limit, default: nil
|
t.integer :usage_limit, default: nil
|
||||||
t.integer :uses_count, default: 0, null: false
|
t.integer :uses_count, default: 0, null: false
|
||||||
t.datetime :created_at, null: false
|
|
||||||
t.datetime :updated_at, null: false
|
# Reference user(promoter) who has created the promotion code
|
||||||
|
t.references :user, null: false, foreign_key: true
|
||||||
|
t.references :event, null: false, foreign_key: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
|
||||||
# Unique index for code
|
# Unique index for code
|
||||||
|
|||||||
10
db/schema.rb
generated
10
db/schema.rb
generated
@@ -44,13 +44,13 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_28_181311) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "orders", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
create_table "orders", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "user_id", null: false
|
|
||||||
t.bigint "event_id", null: false
|
|
||||||
t.string "status", default: "draft", null: false
|
t.string "status", default: "draft", null: false
|
||||||
t.integer "total_amount_cents", default: 0, null: false
|
t.integer "total_amount_cents", default: 0, null: false
|
||||||
t.integer "payment_attempts", default: 0, null: false
|
t.integer "payment_attempts", default: 0, null: false
|
||||||
t.timestamp "expires_at"
|
t.timestamp "expires_at"
|
||||||
t.timestamp "last_payment_attempt_at"
|
t.timestamp "last_payment_attempt_at"
|
||||||
|
t.bigint "user_id", null: false
|
||||||
|
t.bigint "event_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["event_id", "status"], name: "idx_orders_event_status"
|
t.index ["event_id", "status"], name: "idx_orders_event_status"
|
||||||
@@ -67,9 +67,13 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_28_181311) do
|
|||||||
t.boolean "active", default: true, null: false
|
t.boolean "active", default: true, null: false
|
||||||
t.integer "usage_limit"
|
t.integer "usage_limit"
|
||||||
t.integer "uses_count", default: 0, null: false
|
t.integer "uses_count", default: 0, null: false
|
||||||
|
t.bigint "user_id", null: false
|
||||||
|
t.bigint "event_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["code"], name: "index_promotion_codes_on_code", unique: true
|
t.index ["code"], name: "index_promotion_codes_on_code", unique: true
|
||||||
|
t.index ["event_id"], name: "index_promotion_codes_on_event_id"
|
||||||
|
t.index ["user_id"], name: "index_promotion_codes_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "ticket_types", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
create_table "ticket_types", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
||||||
@@ -128,4 +132,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_28_181311) do
|
|||||||
|
|
||||||
add_foreign_key "order_promotion_codes", "orders"
|
add_foreign_key "order_promotion_codes", "orders"
|
||||||
add_foreign_key "order_promotion_codes", "promotion_codes"
|
add_foreign_key "order_promotion_codes", "promotion_codes"
|
||||||
|
add_foreign_key "promotion_codes", "events"
|
||||||
|
add_foreign_key "promotion_codes", "users"
|
||||||
end
|
end
|
||||||
|
|||||||
23
db/seeds.rb
23
db/seeds.rb
@@ -280,3 +280,26 @@ TicketType.find_or_create_by!(event: belle_epoque_october_event, name: "Entry 10
|
|||||||
end
|
end
|
||||||
|
|
||||||
puts "Created 3 additional events from Bizouk with ticket types"
|
puts "Created 3 additional events from Bizouk with ticket types"
|
||||||
|
|
||||||
|
# Create promotion codes for events
|
||||||
|
# Promotion code for belle_epoque_event
|
||||||
|
PromotionCode.find_or_create_by!(code: "BELLE10") do |pc|
|
||||||
|
pc.discount_amount_cents = 1000 # 10€ discount
|
||||||
|
pc.expires_at = belle_epoque_event.start_time + 1.day
|
||||||
|
pc.active = true
|
||||||
|
pc.usage_limit = 20
|
||||||
|
pc.user = promoter
|
||||||
|
pc.event = belle_epoque_october_event
|
||||||
|
end
|
||||||
|
|
||||||
|
# Promotion code for belle_epoque_october_event
|
||||||
|
PromotionCode.find_or_create_by!(code: "OCTOBRE5") do |pc|
|
||||||
|
pc.discount_amount_cents = 500 # 5€ discount
|
||||||
|
pc.expires_at = belle_epoque_october_event.start_time + 1.day
|
||||||
|
pc.active = true
|
||||||
|
pc.usage_limit = 30
|
||||||
|
pc.user = promoter
|
||||||
|
pc.event = belle_epoque_october_event
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Created promotion codes for events"
|
||||||
|
|||||||
Reference in New Issue
Block a user