develop #3

Merged
kbe merged 227 commits from develop into main 2025-09-16 14:35:23 +00:00
3 changed files with 170 additions and 34 deletions
Showing only changes of commit be3d80e541 - Show all commits

View File

@@ -43,14 +43,14 @@ class EventsController < ApplicationController
# Create Stripe line item
line_items << {
price_data: {
currency: 'eur',
currency: "eur",
product_data: {
name: "#{@event.name} - #{ticket_type.name}",
description: ticket_type.description,
description: ticket_type.description
},
unit_amount: ticket_type.price_cents,
unit_amount: ticket_type.price_cents
},
quantity: quantity,
quantity: quantity
}
# Store for ticket creation
@@ -72,10 +72,10 @@ class EventsController < ApplicationController
begin
# Create Stripe Checkout Session
session = Stripe::Checkout::Session.create({
payment_method_types: ['card'],
payment_method_types: [ "card" ],
line_items: line_items,
mode: 'payment',
success_url: payment_success_url(event_id: @event.id, session_id: '{CHECKOUT_SESSION_ID}'),
mode: "payment",
success_url: payment_success_url(event_id: @event.id, session_id: "{CHECKOUT_SESSION_ID}"),
cancel_url: event_url(@event.slug, @event),
customer_email: current_user.email,
metadata: {
@@ -99,19 +99,19 @@ class EventsController < ApplicationController
begin
session = Stripe::Checkout::Session.retrieve(session_id)
if session.payment_status == 'paid'
if session.payment_status == "paid"
# Create tickets
@event = Event.find(event_id)
order_items = JSON.parse(session.metadata['order_items'])
order_items = JSON.parse(session.metadata["order_items"])
@tickets = []
order_items.each do |item|
ticket_type = TicketType.find(item['ticket_type_id'])
item['quantity'].times do
ticket_type = TicketType.find(item["ticket_type_id"])
item["quantity"].times do
ticket = Ticket.create!(
user: current_user,
ticket_type: ticket_type,
status: 'active'
status: "active"
)
@tickets << ticket
@@ -120,7 +120,7 @@ class EventsController < ApplicationController
end
end
render 'payment_success'
render "payment_success"
else
redirect_to event_path(@event.slug, @event), alert: "Le paiement n'a pas été complété avec succès"
end
@@ -140,8 +140,8 @@ class EventsController < ApplicationController
pdf = @ticket.to_pdf
send_data pdf,
filename: "ticket-#{@ticket.event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf",
type: 'application/pdf',
disposition: 'attachment'
type: "application/pdf",
disposition: "attachment"
end
end
end

View File

@@ -102,7 +102,12 @@ export default class extends Controller {
proceedToCheckout() {
if (Object.keys(this.cart).length === 0) {
alert('Veuillez sélectionner au moins un billet')
this.showNotification('Veuillez sélectionner au moins un billet', 'warning')
return
}
// Validate cart contents
if (!this.validateCartAvailability()) {
return
}
@@ -110,17 +115,13 @@ export default class extends Controller {
const isAuthenticated = document.body.dataset.userAuthenticated === "true"
if (!isAuthenticated) {
if (confirm('Vous devez être connecté pour acheter des billets. Souhaitez-vous vous connecter maintenant ?')) {
// Store cart in session storage
sessionStorage.setItem('pending_cart', JSON.stringify({
eventId: this.eventIdValue,
cart: this.cart
}))
window.location.href = '/auth/sign_in'
}
this.showLoginModal()
return
}
// Show loading state
this.setCheckoutLoading(true)
// Create form and submit to checkout
const form = document.createElement('form')
form.method = 'POST'
@@ -145,6 +146,141 @@ export default class extends Controller {
form.submit()
}
validateCartAvailability() {
// Check each ticket type availability before checkout
for (let ticketTypeId in this.cart) {
const input = this.quantityTargetFor(ticketTypeId)
if (input) {
const maxAvailable = parseInt(input.max)
const requested = this.cart[ticketTypeId].quantity
if (requested > maxAvailable) {
this.showNotification(`Seulement ${maxAvailable} billets disponibles pour ${this.cart[ticketTypeId].name}`, 'error')
// Adjust cart to maximum available
input.value = maxAvailable
this.updateCartItem(ticketTypeId, input)
return false
}
}
}
return true
}
showLoginModal() {
// Create and show modern login modal
const modal = document.createElement('div')
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'
modal.innerHTML = `
<div class="bg-white rounded-2xl p-8 max-w-md mx-4 shadow-2xl">
<div class="text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-purple-100 mb-4">
<svg class="h-6 w-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Connexion requise</h3>
<p class="text-sm text-gray-500 mb-6">Vous devez être connecté pour acheter des billets. Votre panier sera conservé.</p>
<div class="flex flex-col sm:flex-row gap-3">
<button id="login-btn" class="flex-1 bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg font-medium transition-colors">
Se connecter
</button>
<button id="cancel-login" class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-lg font-medium transition-colors">
Annuler
</button>
</div>
</div>
</div>
`
document.body.appendChild(modal)
// Handle login button
modal.querySelector('#login-btn').addEventListener('click', () => {
// Store cart in session storage
sessionStorage.setItem('pending_cart', JSON.stringify({
eventId: this.eventIdValue,
cart: this.cart
}))
window.location.href = '/auth/sign_in'
})
// Handle cancel button
modal.querySelector('#cancel-login').addEventListener('click', () => {
document.body.removeChild(modal)
})
// Handle backdrop click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal)
}
})
}
setCheckoutLoading(loading) {
const checkoutBtn = this.checkoutButtonTarget
if (loading) {
checkoutBtn.disabled = true
checkoutBtn.innerHTML = `
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Redirection...
`
} else {
checkoutBtn.disabled = false
checkoutBtn.innerHTML = `
<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="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
Procéder au paiement
`
}
}
showNotification(message, type = 'info') {
// Create toast notification
const toast = document.createElement('div')
const colors = {
success: 'bg-green-50 text-green-800 border-green-200',
error: 'bg-red-50 text-red-800 border-red-200',
warning: 'bg-yellow-50 text-yellow-800 border-yellow-200',
info: 'bg-blue-50 text-blue-800 border-blue-200'
}
toast.className = `fixed top-4 right-4 z-50 max-w-sm p-4 border rounded-lg shadow-lg ${colors[type]} transform transition-all duration-300 translate-x-full`
toast.innerHTML = `
<div class="flex items-center">
<div class="flex-1">
<p class="text-sm font-medium">${message}</p>
</div>
<button class="ml-3 text-sm font-medium opacity-70 hover:opacity-100" onclick="this.parentElement.parentElement.remove()">
×
</button>
</div>
`
document.body.appendChild(toast)
// Animate in
setTimeout(() => {
toast.classList.remove('translate-x-full')
}, 10)
// Auto remove after 5 seconds
setTimeout(() => {
if (document.body.contains(toast)) {
toast.classList.add('translate-x-full')
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast)
}
}, 300)
}
}, 5000)
}
checkForPendingCart() {
const pendingCart = sessionStorage.getItem('pending_cart')
if (pendingCart) {

View File

@@ -151,14 +151,14 @@
<div class="border-t border-gray-200 pt-6 mt-6">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-600">Total billets:</span>
<span id="cart-count" class="font-medium">0</span>
<span data-ticket-cart-target="cartCount" class="font-medium">0</span>
</div>
<div class="flex justify-between items-center mb-4">
<span class="text-gray-600">Montant total:</span>
<span id="cart-total" class="text-xl font-bold text-purple-700">€0.00</span>
<span data-ticket-cart-target="cartTotal" class="text-xl font-bold text-purple-700">€0.00</span>
</div>
<button
id="checkout-btn"
data-ticket-cart-target="checkoutButton"
class="w-full bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium py-3 px-4 rounded-xl shadow-sm transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 flex items-center justify-center"
disabled
data-action="click->ticket-cart#proceedToCheckout">