8 Commits

Author SHA1 Message Date
580b24bbed Merge pull request 'develop' (#3) from develop into main
All checks were successful
Ruby on Rails Test / rails-test (push) Successful in 1m43s
Reviewed-on: #3
2025-09-16 14:34:28 +00:00
a8d3bc12ae Merge pull request 'feat/free-ticket' (#2) from feat/free-ticket into develop
All checks were successful
Ruby on Rails Test / rails-test (push) Successful in 1m56s
Reviewed-on: #2
2025-09-16 14:31:43 +00:00
kbe
b228d5a174 chore: Breadcrumb on ticket edit page 2025-09-16 16:22:09 +02:00
kbe
61ad8c64d4 Fix modal overlay issue and improve modal structure
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-09-16 08:44:21 +02:00
kbe
4e06f91acb Fix modal positioning and improve Stimulus controller
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-09-16 08:43:00 +02:00
kbe
28eddb22ab Refactor duplication feature to use Stimulus controller and fix modal issues
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-09-15 21:23:29 +02:00
kbe
a34eb7aa38 Add duplication options with JavaScript modal and conditional ticket type cloning
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-09-15 21:20:22 +02:00
kbe
aa68885b84 Add event duplication feature with ticket types cloning
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-09-15 21:17:24 +02:00
10 changed files with 339 additions and 38 deletions

View File

@@ -5,7 +5,7 @@
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 ]
before_action :set_event, only: [ :show, :edit, :update, :destroy, :publish, :unpublish, :cancel, :mark_sold_out, :duplicate ]
# Display all events for the current promoter
def index
@@ -93,6 +93,18 @@ class Promoter::EventsController < ApplicationController
end
end
# Duplicate an event and all its ticket types
def duplicate
clone_ticket_types = params[:clone_ticket_types] == "true"
@new_event = @event.duplicate(clone_ticket_types: clone_ticket_types)
if @new_event
redirect_to edit_promoter_event_path(@new_event), notice: "Événement dupliqué avec succès! Vous pouvez maintenant modifier les détails de l'événement copié."
else
redirect_to promoter_event_path(@event), alert: "Erreur lors de la duplication de l'événement."
end
end
private
def ensure_can_manage_events!

View File

@@ -0,0 +1,53 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["cloneTicketTypes"]
static values = {
duplicateUrl: String
}
connect() {
// Get modal element from the document
this.modalElement = document.querySelector('[data-event-duplication-target="modal"]')
}
open() {
this.modalElement.classList.remove('hidden')
document.body.classList.add('overflow-hidden')
}
close() {
this.modalElement.classList.add('hidden')
document.body.classList.remove('overflow-hidden')
}
duplicate() {
const cloneTicketTypes = this.cloneTicketTypesTarget.checked
// Create form data
const formData = new FormData()
formData.append('clone_ticket_types', cloneTicketTypes)
formData.append('authenticity_token', document.querySelector('meta[name="csrf-token"]').getAttribute('content'))
// Send request to duplicate endpoint
fetch(this.duplicateUrlValue, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (response.redirected) {
window.location.href = response.url
} else {
return response.json()
}
})
.catch(error => {
console.error('Error:', error)
alert('Erreur lors de la duplication de l\'événement.')
this.close()
})
}
}

View File

@@ -27,3 +27,6 @@ application.register("event-form", EventFormController);
import CountdownController from "./countdown_controller";
application.register("countdown", CountdownController);
import EventDuplicationController from "./event_duplication_controller";
application.register("event-duplication", EventDuplicationController);

View File

@@ -102,6 +102,35 @@ class Event < ApplicationRecord
!!allow_booking_during_event
end
# Duplicate an event with all its ticket types
def duplicate(clone_ticket_types: true)
# Duplicate the event
new_event = self.dup
new_event.name = "Copie de #{name}"
new_event.slug = "#{slug}-copy-#{Time.current.to_i}"
new_event.state = :draft
new_event.created_at = Time.current
new_event.updated_at = Time.current
Event.transaction do
if new_event.save
# Duplicate all ticket types if requested
if clone_ticket_types
ticket_types.each do |ticket_type|
new_ticket_type = ticket_type.dup
new_ticket_type.event = new_event
new_ticket_type.save!
end
end
new_event
else
nil
end
end
rescue
nil
end
private
# Determine if we should perform server-side geocoding

View File

@@ -107,22 +107,6 @@
<div class="w-5 h-5 border-2 border-purple-200 border-t-purple-600 rounded-full animate-spin"></div>
</div>
</div>
<!-- Location Actions -->
<div class="flex flex-wrap gap-2">
<button type="button" data-action="click->event-form#getCurrentLocation" data-event-form-target="getCurrentLocationBtn" class="inline-flex items-center px-3 py-2 text-xs font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
<span data-event-form-target="getCurrentLocationIcon">
<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>
</span>
<span data-event-form-target="getCurrentLocationText">Ma position</span>
</button>
<button type="button" data-action="click->event-form#previewLocation" data-event-form-target="previewLocationBtn" class="inline-flex items-center px-3 py-2 text-xs font-medium text-purple-700 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
<span data-event-form-target="previewLocationIcon">
<i data-lucide="map" class="w-3 h-3 mr-1"></i>
</span>
<span data-event-form-target="previewLocationText">Prévisualiser</span>
</button>
</div>
</div>
<p class="mt-2 text-sm text-gray-500">
@@ -144,11 +128,24 @@
<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="space-y-4">
<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>
<p class="text-sm text-gray-500">Les événements mis en avant apparaissent en premier sur la page d'accueil.</p>
<div class="flex items-start">
<%= form.check_box :allow_booking_during_event, class: "h-4 w-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 mt-1" %>
<div class="ml-2">
<%= form.label :allow_booking_during_event, "Autoriser la réservation pendant l'événement", class: "text-sm text-gray-700 font-medium" %>
<p class="text-sm text-gray-500 mt-1">
Si activé, les participants pourront acheter des billets même après le début de l'événement.
Si désactivé, la vente de billets s'arrêtera automatiquement à l'heure de début.
</p>
</div>
</div>
</div>
</div>
<!-- Actions -->
@@ -166,4 +163,3 @@
<% end %>
</div>
</div>

View File

@@ -1,5 +1,6 @@
<% content_for(:title, @event.name) %>
<div data-controller="event-duplication" data-event-duplication-duplicate-url-value="<%= duplicate_promoter_event_path(@event) %>">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
@@ -39,6 +40,11 @@
Modifier
<% end %>
<button type="button" data-action="click->event-duplication#open" class="w-full sm:w-auto inline-flex items-center justify-center px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200">
<i data-lucide="copy" class="w-4 h-4 mr-2"></i>
Dupliquer
</button>
<% if @event.draft? %>
<% if @event.ticket_types.blank? %>
<%= button_to publish_promoter_event_path(@event), method: :patch, disabled: true, class: "w-full sm:w-auto inline-flex items-center justify-center px-4 py-2 bg-gray-400 text-white font-medium rounded-lg cursor-not-allowed transition-colors duration-200", title: "Vous devez créer au moins un type de billet avant de publier" do %>
@@ -107,7 +113,7 @@
</div>
<div class="flex-shrink-0">
<%= link_to event_path(@event.slug, @event), target: "_blank", class: "text-green-600 hover:text-green-800 font-medium text-sm whitespace-nowrap" do %>
Voir publiquement <i data-lucide="external-link" class="w-4 h-4 inline ml-1"></i>
Voir la fiche publique <i data-lucide="external-link" class="w-4 h-4 inline ml-1"></i>
<% end %>
</div>
</div>
@@ -282,4 +288,52 @@
</div>
</div>
</div>
<!-- Modal -->
<div data-event-duplication-target="modal" class="hidden relative z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<!-- Background backdrop, show/hide based on modal state -->
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
<div class="fixed inset-0 z-50 overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<!-- Modal container -->
<div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<i data-lucide="copy" class="h-6 w-6 text-blue-600"></i>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg font-medium leading-6 text-gray-900" id="modal-title">
Dupliquer l'événement
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
Choisissez les options de duplication pour "<%= @event.name %>".
</p>
<div class="mt-4">
<div class="flex items-center">
<input data-event-duplication-target="cloneTicketTypes" id="cloneTicketTypes" type="checkbox" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" checked>
<label for="cloneTicketTypes" class="ml-2 block text-sm text-gray-900">
Dupliquer également les types de billets (<%= @event.ticket_types.count %> type(s))
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<button type="button" data-action="click->event-duplication#duplicate" class="inline-flex w-full justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm">
Dupliquer
</button>
<button type="button" data-action="click->event-duplication#close" class="mt-3 inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Annuler
</button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,6 +1,15 @@
<% content_for(:title, "#{@ticket_type.name} - #{@event.name}") %>
<div class="container py-8">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
<%= render 'components/breadcrumb', crumbs: [
{ name: 'Dashboard', path: dashboard_path },
{ name: 'Événements', path: promoter_events_path },
{ name: @event.name, path: promoter_event_path(@event) },
{ name: 'Types de billets', path: promoter_event_ticket_types_path(@event) },
{ name: @ticket_type.name, path: nil }
] %>
<!-- Header with actions -->
<div class="mb-8">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">

View File

@@ -81,6 +81,7 @@ Rails.application.routes.draw do
patch :unpublish
patch :cancel
patch :mark_sold_out
post :duplicate
end
# Nested ticket types routes

View File

@@ -26,5 +26,103 @@ class Promoter::EventsControllerTest < ActionDispatch::IntegrationTest
assert_not_includes assigns(:events), other_event
end
test "should duplicate an event with ticket types" do
sign_in @promoter
# Create ticket types for the event
ticket_type1 = TicketType.create!(
name: "Standard Ticket",
description: "A standard ticket for the event with all the basic access",
price_cents: 2000,
quantity: 100,
sale_start_at: 1.day.ago,
sale_end_at: @event.start_time - 1.hour,
event: @event
)
ticket_type2 = TicketType.create!(
name: "VIP Ticket",
description: "A VIP ticket for the event with special access",
price_cents: 5000,
quantity: 50,
sale_start_at: 1.day.ago,
sale_end_at: @event.start_time - 1.hour,
event: @event
)
# Verify that ticket types were created successfully
assert ticket_type1.valid?
assert ticket_type2.valid?
# Duplicate the event
assert_difference("Event.count", 1) do
post duplicate_promoter_event_path(@event), params: { clone_ticket_types: "true" }
end
# Check that the new event was created
assert_redirected_to edit_promoter_event_path(Event.last)
assert_equal "Événement dupliqué avec succès! Vous pouvez maintenant modifier les détails de l'événement copié.", flash[:notice]
# Check that the new event has the correct attributes
new_event = Event.last
assert_equal "Copie de #{@event.name}", new_event.name
assert_equal "draft", new_event.state
assert_equal @event.venue_name, new_event.venue_name
assert_equal @event.venue_address, new_event.venue_address
# Check that ticket types were duplicated
assert_equal 2, new_event.ticket_types.count
assert_equal "Standard Ticket", new_event.ticket_types.find_by(name: "Standard Ticket").name
assert_equal "VIP Ticket", new_event.ticket_types.find_by(name: "VIP Ticket").name
end
test "should duplicate an event without ticket types" do
sign_in @promoter
# Create ticket types for the event
ticket_type1 = TicketType.create!(
name: "Standard Ticket",
description: "A standard ticket for the event with all the basic access",
price_cents: 2000,
quantity: 100,
sale_start_at: 1.day.ago,
sale_end_at: @event.start_time - 1.hour,
event: @event
)
ticket_type2 = TicketType.create!(
name: "VIP Ticket",
description: "A VIP ticket for the event with special access",
price_cents: 5000,
quantity: 50,
sale_start_at: 1.day.ago,
sale_end_at: @event.start_time - 1.hour,
event: @event
)
# Verify that ticket types were created successfully
assert ticket_type1.valid?
assert ticket_type2.valid?
# Duplicate the event without ticket types
assert_difference("Event.count", 1) do
post duplicate_promoter_event_path(@event), params: { clone_ticket_types: "false" }
end
# Check that the new event was created
assert_redirected_to edit_promoter_event_path(Event.last)
assert_equal "Événement dupliqué avec succès! Vous pouvez maintenant modifier les détails de l'événement copié.", flash[:notice]
# Check that the new event has the correct attributes
new_event = Event.last
assert_equal "Copie de #{@event.name}", new_event.name
assert_equal "draft", new_event.state
assert_equal @event.venue_name, new_event.venue_name
assert_equal @event.venue_address, new_event.venue_address
# Check that ticket types were NOT duplicated
assert_equal 0, new_event.ticket_types.count
end
# Add tests for new, create, etc. as needed
end

View File

@@ -271,4 +271,50 @@ class EventTest < ActiveSupport::TestCase
event = Event.new
assert_not event.allow_booking_during_event?
end
test "should duplicate event with ticket types" do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Original Event", slug: "original", description: "A description that is sufficiently long", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.week.from_now, state: :published)
# Create ticket types
ticket_type1 = TicketType.create!(name: "Standard", description: "A standard ticket for the event", price_cents: 2000, quantity: 100, sale_start_at: 1.day.ago, sale_end_at: event.start_time - 1.hour, event: event)
ticket_type2 = TicketType.create!(name: "VIP", description: "A VIP ticket for the event", price_cents: 5000, quantity: 50, sale_start_at: 1.day.ago, sale_end_at: event.start_time - 1.hour, event: event)
# Duplicate the event
duplicated_event = event.duplicate
# Check that duplication was successful
assert_not_nil duplicated_event
assert_equal "Copie de #{event.name}", duplicated_event.name
assert_equal "draft", duplicated_event.state
assert_equal event.venue_name, duplicated_event.venue_name
assert_equal event.venue_address, duplicated_event.venue_address
# Check that ticket types were duplicated
assert_equal 2, duplicated_event.ticket_types.count
assert_equal "Standard", duplicated_event.ticket_types.find_by(name: "Standard").name
assert_equal "VIP", duplicated_event.ticket_types.find_by(name: "VIP").name
end
test "should duplicate event without ticket types" do
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
event = Event.create!(name: "Original Event", slug: "original", description: "A description that is sufficiently long", venue_name: "v", venue_address: "a", user: user, latitude: 48.0, longitude: 2.0, start_time: 1.week.from_now, state: :published)
# Create ticket types
ticket_type1 = TicketType.create!(name: "Standard", description: "A standard ticket for the event", price_cents: 2000, quantity: 100, sale_start_at: 1.day.ago, sale_end_at: event.start_time - 1.hour, event: event)
ticket_type2 = TicketType.create!(name: "VIP", description: "A VIP ticket for the event", price_cents: 5000, quantity: 50, sale_start_at: 1.day.ago, sale_end_at: event.start_time - 1.hour, event: event)
# Duplicate the event without ticket types
duplicated_event = event.duplicate(clone_ticket_types: false)
# Check that duplication was successful
assert_not_nil duplicated_event
assert_equal "Copie de #{event.name}", duplicated_event.name
assert_equal "draft", duplicated_event.state
assert_equal event.venue_name, duplicated_event.venue_name
assert_equal event.venue_address, duplicated_event.venue_address
# Check that ticket types were NOT duplicated
assert_equal 0, duplicated_event.ticket_types.count
end
end