Location — Titre
EVJF & Evénements

Workshops & Evénements

Chaque atelier est une opportunité de grandir et de créer

Réservation Événements — French Pole Riviera

Ateliers thématiques, stages intensifs et masterclass : plongez dans un univers pensé pour vous, quelle que soit votre expérience. Que vous soyez du studio ou d'ailleurs, rejoignez une communauté passionnée pour partager, apprendre et progresser ensemble.

Déjà inscrit·e ? Retrouvez et gérez vos réservations ici. Sinon, découvrez nos événements ci-dessous pour réserver.

Chargement des événements…

Une question ?

En confirmant, vous recevrez un email récapitulatif. Votre place est garantie dès validation ✦

Une question ?

Contactez-nous à frenchpoleriviera@gmail.com

Réservation confirmée

Votre place est garantie.

Réservation confirmée

`; }let events = []; let selectedEventId = null;async function loadEvents() { const container = document.getElementById('eventsContainer'); try { // Requête simple sans index : on récupère TOUS les événements, // puis on filtre et trie côté navigateur (plus rapide que de créer un index) const snap = await getDocs(collection(db, "evenements"));// Compter le nombre de personnes en liste d'attente par événement const waitlistCounts = {}; try { const partSnap = await getDocs(query( collection(db, "evenements_participants"), where("statut", "==", "liste_attente") )); partSnap.docs.forEach(d => { const evId = d.data().evenement_id; if (evId) waitlistCounts[evId] = (waitlistCounts[evId] || 0) + 1; }); } catch (e) { console.warn('Comptage liste attente impossible:', e); }events = snap.docs .map(d => ({ id: d.id, ...d.data(), _waitlistCount: waitlistCounts[d.id] || 0 })) // Filtre : uniquement les événements actifs .filter(e => e.actif === true) // Filtre : masquer les événements passés (avec 24h de marge) .filter(e => { const endDate = e.date_fin || e.date_debut; if (!endDate) return true; const d = new Date(endDate.seconds ? endDate.seconds * 1000 : endDate); return d.getTime() + 24*60*60*1000 > Date.now(); }) // Tri : par date de début croissante .sort((a, b) => { const dA = a.date_debut?.seconds ? a.date_debut.seconds : 0; const dB = b.date_debut?.seconds ? b.date_debut.seconds : 0; return dA - dB; });if (events.length === 0) { container.innerHTML = `

Aucun événement programmé

Revenez bientôt pour découvrir nos prochaines dates ✦

`; return; }// Si 1 seul événement → pas de carrousel (affichage simple) if (events.length === 1) { container.innerHTML = `
${events.map(renderCard).join('')}
`; attachCardListeners(); } else { // Plusieurs événements → carrousel avec autoplay container.innerHTML = ` `; attachCardListeners(); initCarousel(); } } catch (err) { console.error(err); container.innerHTML = `

Erreur de chargement

Merci de rafraîchir la page ou de nous contacter.

`; } }// ⏰ Les réservations ferment 4h avant le DÉBUT de l'événement const RESERVATION_CLOSE_HOURS = 4; function isReservationClosed(evt) { if (!evt.date_debut) return false; // Date de début const debut = evt.date_debut.seconds ? new Date(evt.date_debut.seconds * 1000) : new Date(evt.date_debut); // Appliquer l'heure de début si disponible if (evt.heure_debut) { const [h, m] = evt.heure_debut.split(':').map(Number); debut.setHours(h || 0, m || 0, 0, 0); } // Limite = début - 4h const limite = debut.getTime() - RESERVATION_CLOSE_HOURS * 60 * 60 * 1000; return Date.now() >= limite; }function renderCard(evt) { const isExterne = !!evt.lien_billetterie; const placesRestantes = (evt.places_max || 0) - (evt.places_reservees || 0); const isFull = !isExterne && placesRestantes <= 0; const isLimited = !isExterne && placesRestantes > 0 && placesRestantes <= 3;// ⏰ Réservations fermées 6h avant le début de l'événement (ignoré si billetterie externe) const reservationsClosed = !isExterne && isReservationClosed(evt);// Gestion liste d'attente const maxWaitlist = (evt.max_liste_attente !== undefined && evt.max_liste_attente !== null) ? evt.max_liste_attente : 4; const waitlistCount = evt._waitlistCount || 0; const waitlistFull = isFull && (maxWaitlist === 0 || waitlistCount >= maxWaitlist); const canJoinWaitlist = isFull && !waitlistFull;const dateStr = formatDate(evt.date_debut, evt.date_fin); const horaires = (evt.heure_debut && evt.heure_fin) ? `${evt.heure_debut} - ${evt.heure_fin}` : ''; const niveauInfo = formatNiveau(evt.niveau);// Badge de statut let statusBadge; if (isExterne) { statusBadge = `🎫 Billetterie en ligne`; } else if (reservationsClosed) { statusBadge = `🔒 Réservations closes`; } else if (waitlistFull) { statusBadge = `Complet`; } else if (canJoinWaitlist) { statusBadge = `Complet`; } else if (isLimited) { statusBadge = `⚡ ${placesRestantes} place${placesRestantes > 1 ? 's' : ''} restante${placesRestantes > 1 ? 's' : ''}`; } else { statusBadge = `${placesRestantes} places disponibles`; }// Bouton selon l'état let btnHTML; if (evt.lien_billetterie) { // Billetterie externe : redirige vers la boutique (nouvel onglet) btnHTML = `Billetterie →`; } else if (reservationsClosed) { btnHTML = ``; } else if (waitlistFull) { btnHTML = ``; } else if (canJoinWaitlist) { btnHTML = ``; } else { btnHTML = ``; }const notClickable = !isExterne && (waitlistFull || reservationsClosed); const cardClass = notClickable ? 'is-full' : (canJoinWaitlist ? 'is-full is-waitlist' : '');return `
${evt.image ? `
${escapeHtml(evt.titre)}
` : ''}
${niveauInfo ? `${niveauInfo.icon} ${escapeHtml(niveauInfo.label)}` : ''} ${escapeHtml(dateStr)} ${horaires ? `${escapeHtml(horaires)}` : ''} ${statusBadge}

${escapeHtml(evt.titre)}

${btnHTML}
`; }// Formater le niveau d'un événement (icône + label) function formatNiveau(niveau) { const levels = { 'tous_niveaux': { icon: '🌟', label: 'Tous niveaux' }, 'debutant': { icon: '🌱', label: 'Débutant' }, 'intermediaire': { icon: '🔥', label: 'Intermédiaire' }, 'avance': { icon: '💎', label: 'Avancé' }, 'pro': { icon: '👑', label: 'Pro' } }; return levels[niveau] || null; }// Ajoute automatiquement le € si la valeur est un simple nombre // "30" → "30€", "30€" → "30€", "Sur demande" → "Sur demande" // Extraire un nombre depuis une string de prix ("15€" → 15, "15.50" → 15.5, "Sur demande" → 0) function parsePrix(prix) { if (typeof prix === 'number') return prix; if (!prix) return 0; const match = String(prix).replace(',', '.').match(/[\d.]+/); return match ? parseFloat(match[0]) : 0; }function formatPrix(prix) { if (!prix) return ''; const str = String(prix).trim(); // Si ça contient uniquement chiffres, virgule, point, espace → ajouter € if (/^[\d\s,.]+$/.test(str)) { return str + '€'; } return str; }function formatTarifsShort(tarifs) { if (!tarifs || !Array.isArray(tarifs) || tarifs.length === 0) return ''; if (tarifs.length === 1) return formatPrix(tarifs[0].prix); const prix = tarifs.map(t => parseFloat(String(t.prix).replace(/[^\d.,]/g, '').replace(',', '.'))).filter(n => !isNaN(n)); if (prix.length > 0) { const min = Math.min(...prix); return `À partir de ${min}€`; } return `${tarifs.length} tarifs`; }function attachCardListeners() { // Clic sur le bouton "Infos & Réservation" ou "Liste d'attente" document.querySelectorAll('.event-cta, .card-cta').forEach(btn => { if (btn.disabled) return; // Bouton "Complet" désactivé if (btn.dataset.external === '1') return; // Lien billetterie externe : comportement natif du btn.addEventListener('click', (e) => { e.stopPropagation(); openEventDetail(btn.dataset.eventId); }); }); // Clic sur la carte entière (sauf si liste d'attente pleine) document.querySelectorAll('.fpr-event-card, .fpr-card[data-id]').forEach(card => { if (card.dataset.waitlistFull === '1') return; // Carte complète non cliquable card.addEventListener('click', () => { // Si billetterie externe : ouvrir le lien au lieu du modal const lien = card.dataset.billetterie; if (lien) { window.open(lien, '_blank', 'noopener,noreferrer'); return; } openEventDetail(card.dataset.id); }); }); }// ═══════════════════════════════════════════════════════════════ // CARROUSEL — autoplay avec pause au survol, flèches, dots, swipe // ═══════════════════════════════════════════════════════════════const CAROUSEL_AUTOPLAY_MS = 6000; // 6 secondes par slidelet carouselState = { currentIndex: 0, total: 0, autoplayTimer: null, progressTimer: null, progressStart: 0, isPaused: false, track: null, dots: null, progress: null, counter: null, prevBtn: null, nextBtn: null };function initCarousel() { const carousel = document.getElementById('fprCarousel'); if (!carousel) return;carouselState.track = document.getElementById('fprTrack'); carouselState.dots = document.getElementById('fprDots'); carouselState.progress = document.getElementById('fprProgress'); carouselState.counter = document.getElementById('fprCounterCurrent'); carouselState.prevBtn = carousel.querySelector('.fpr-carousel-arrow.prev'); carouselState.nextBtn = carousel.querySelector('.fpr-carousel-arrow.next'); carouselState.total = events.length; carouselState.currentIndex = 0;// Flèches carouselState.prevBtn.addEventListener('click', (e) => { e.stopPropagation(); goToSlide(carouselState.currentIndex - 1); restartAutoplay(); }); carouselState.nextBtn.addEventListener('click', (e) => { e.stopPropagation(); goToSlide(carouselState.currentIndex + 1); restartAutoplay(); });// Dots carouselState.dots.querySelectorAll('.fpr-carousel-dot').forEach(dot => { dot.addEventListener('click', (e) => { e.stopPropagation(); const idx = parseInt(dot.dataset.index, 10); goToSlide(idx); restartAutoplay(); }); });// Pause au survol carousel.addEventListener('mouseenter', pauseAutoplay); carousel.addEventListener('mouseleave', resumeAutoplay);// Pause au focus (accessibilité clavier) carousel.addEventListener('focusin', pauseAutoplay); carousel.addEventListener('focusout', resumeAutoplay);// Support swipe tactile let touchStartX = 0; let touchEndX = 0; const trackWrap = carousel.querySelector('.fpr-carousel-track-wrap'); trackWrap.addEventListener('touchstart', (e) => { touchStartX = e.changedTouches[0].screenX; pauseAutoplay(); }, { passive: true }); trackWrap.addEventListener('touchend', (e) => { touchEndX = e.changedTouches[0].screenX; const diff = touchStartX - touchEndX; if (Math.abs(diff) > 50) { if (diff > 0) goToSlide(carouselState.currentIndex + 1); else goToSlide(carouselState.currentIndex - 1); } restartAutoplay(); }, { passive: true });// Navigation clavier document.addEventListener('keydown', (e) => { // Uniquement si la modal n'est pas ouverte const modalOpen = document.querySelector('.fpr-modal-full.open'); if (modalOpen) return; if (e.key === 'ArrowLeft') { goToSlide(carouselState.currentIndex - 1); restartAutoplay(); } else if (e.key === 'ArrowRight') { goToSlide(carouselState.currentIndex + 1); restartAutoplay(); } });// Pause si l'onglet est en arrière-plan (éco-énergie) document.addEventListener('visibilitychange', () => { if (document.hidden) pauseAutoplay(); else resumeAutoplay(); });// Démarrer autoplay startAutoplay();// Égaliser la hauteur de toutes les slides (après rendu complet) setTimeout(equalizeSlideHeights, 100); // Recalculer au resize window.addEventListener('resize', () => { clearTimeout(window._fprResizeTimer); window._fprResizeTimer = setTimeout(equalizeSlideHeights, 150); }); }// Égalise la hauteur de toutes les cartes du carrousel sur la plus haute function equalizeSlideHeights() { const cards = document.querySelectorAll('.fpr-carousel-slide .fpr-event-card'); if (cards.length === 0) return;// Reset d'abord pour recalculer les hauteurs naturelles cards.forEach(c => c.style.minHeight = 'auto');// Sur mobile : on laisse chaque carte à sa taille naturelle (pas d'égalisation) // car ça créerait des cartes trop grandes sur petit écran const isMobile = window.innerWidth <= 768; if (isMobile) return;// Desktop : trouver la plus haute let maxHeight = 0; cards.forEach(c => { const h = c.offsetHeight; if (h > maxHeight) maxHeight = h; });// Appliquer la hauteur max à toutes if (maxHeight > 0) { cards.forEach(c => c.style.minHeight = maxHeight + 'px'); } }function goToSlide(index) { const total = carouselState.total; if (total === 0) return; // Boucle infinie if (index < 0) index = total - 1; if (index >= total) index = 0; carouselState.currentIndex = index;// Déplacer le track carouselState.track.style.transform = `translateX(-${index * 100}%)`;// Mettre à jour les dots carouselState.dots.querySelectorAll('.fpr-carousel-dot').forEach((dot, i) => { dot.classList.toggle('active', i === index); });// Compteur if (carouselState.counter) carouselState.counter.textContent = index + 1; }function startAutoplay() { stopAutoplay(); if (carouselState.total <= 1) return; carouselState.progressStart = Date.now();// Animation progression visuelle carouselState.progress.style.width = '0%'; carouselState.progressTimer = setInterval(() => { if (carouselState.isPaused) return; const elapsed = Date.now() - carouselState.progressStart; const pct = Math.min((elapsed / CAROUSEL_AUTOPLAY_MS) * 100, 100); carouselState.progress.style.width = pct + '%'; }, 50);// Timer de changement de slide carouselState.autoplayTimer = setTimeout(() => { if (!carouselState.isPaused) { goToSlide(carouselState.currentIndex + 1); startAutoplay(); } }, CAROUSEL_AUTOPLAY_MS); }function stopAutoplay() { if (carouselState.autoplayTimer) { clearTimeout(carouselState.autoplayTimer); carouselState.autoplayTimer = null; } if (carouselState.progressTimer) { clearInterval(carouselState.progressTimer); carouselState.progressTimer = null; } }function pauseAutoplay() { carouselState.isPaused = true; }function resumeAutoplay() { if (!carouselState.isPaused) return; carouselState.isPaused = false; // Relancer l'autoplay à partir de maintenant (pas de saut) restartAutoplay(); }function restartAutoplay() { stopAutoplay(); if (!carouselState.isPaused) startAutoplay(); }// ═══════════════════════════════════════════════════════════════ // MODAL DÉTAIL + RÉSERVATION — nouveau flow // ═══════════════════════════════════════════════════════════════let currentStep = 1; let isWaitlistMode = false;// Ouverture de la modal en mode découverte (détail + infos) // 🎟️ MODE SPECTACLE — Gestion des compteurs de billets window.billetCounts = [];window.changeBilletCount = function(idx, delta) { if (!window.billetCounts) return; const maxResa = parseInt(document.getElementById('spectacleMaxResa')?.value, 10) || 6; const currentTotal = window.billetCounts.reduce((a, b) => a + b, 0); const newCount = (window.billetCounts[idx] || 0) + delta;// Empêcher de descendre sous 0 if (newCount < 0) return;// Empêcher de dépasser le max if (delta > 0 && currentTotal >= maxResa) { showToast(`Maximum ${maxResa} billets par réservation`); return; }window.billetCounts[idx] = newCount; document.getElementById('billetCount' + idx).textContent = newCount;// Ajouter classe "actif" si > 0 const row = document.querySelector(`.spectacle-tarif-row[data-idx="${idx}"]`); if (row) { row.classList.toggle('has-billets', newCount > 0); }updateSpectacleTotal(); };function updateSpectacleTotal() { if (!window.billetCounts) return; let totalPlaces = 0; let totalMontant = 0;document.querySelectorAll('.spectacle-tarif-row').forEach(row => { const idx = parseInt(row.dataset.idx, 10); const prix = parseFloat(row.dataset.prix) || 0; const count = window.billetCounts[idx] || 0; totalPlaces += count; totalMontant += count * prix; });const placesEl = document.getElementById('totalPlaces'); const montantEl = document.getElementById('totalMontant'); if (placesEl) placesEl.textContent = totalPlaces + (totalPlaces > 1 ? ' places' : ' place'); if (montantEl) montantEl.textContent = totalMontant + ' €';// Stocker dans hidden input pour la soumission const billetsInput = document.getElementById('spectacleBillets'); if (billetsInput) billetsInput.value = JSON.stringify(window.billetCounts); }window.openEventDetail = function(id) { selectedEventId = id; reservationProcessed = false; // Reset flag pour permettre une nouvelle résa const evt = events.find(e => e.id === id); if (!evt) return;// Événement à billetterie externe : rediriger au lieu d'ouvrir le formulaire interne if (evt.lien_billetterie) { window.open(evt.lien_billetterie, '_blank', 'noopener,noreferrer'); return; }// Sécurité : bloquer si réservations closes (< 6h avant) if (isReservationClosed(evt)) { showToast("Les réservations sont closes pour cet événement (fermeture 4h avant)."); return; }// Sécurité : bloquer si liste d'attente pleine const placesMaxCheck = parseInt(evt.places_max, 10) || 0; const placesReserveesCheck = parseInt(evt.places_reservees, 10) || 0; const isFullCheck = placesMaxCheck > 0 && (placesMaxCheck - placesReserveesCheck) <= 0; const maxWaitlistCheck = (evt.max_liste_attente !== undefined && evt.max_liste_attente !== null) ? evt.max_liste_attente : 4; const waitlistCountCheck = evt._waitlistCount || 0; if (isFullCheck && (maxWaitlistCheck === 0 || waitlistCountCheck >= maxWaitlistCheck)) { showToast("Cet événement est complet. La liste d'attente est pleine."); return; }// Déterminer si l'événement est en liste d'attente // Double sécurité : on vérifie places_max ET places_reservees sont des nombres valides const placesMax = parseInt(evt.places_max, 10) || 0; const placesReservees = parseInt(evt.places_reservees, 10) || 0; const placesRestantes = placesMax - placesReservees; isWaitlistMode = placesMax > 0 && placesRestantes <= 0;console.log('🎯 Ouverture event:', evt.titre, '| Places:', placesReservees, '/', placesMax, '| Waitlist:', isWaitlistMode);// Afficher ou masquer le bandeau liste d'attente document.getElementById('waitlistBanner').style.display = isWaitlistMode ? 'flex' : 'none';// HERO (image ou vidéo) const hero = document.getElementById('modalHero'); if (evt.video) { hero.innerHTML = ``; } else if (evt.image) { hero.innerHTML = `${escapeHtml(evt.titre)}`; } else { hero.innerHTML = ''; hero.style.display = 'none'; }// BADGES date/horaires/lieu const dateStr = formatDate(evt.date_debut, evt.date_fin); const horaires = (evt.heure_debut && evt.heure_fin) ? `${evt.heure_debut} - ${evt.heure_fin}` : ''; let placesBadge; if (isWaitlistMode) { placesBadge = `Complet`; } else if (placesRestantes <= 3) { placesBadge = `⚡ ${placesRestantes} place${placesRestantes > 1 ? 's' : ''} restante${placesRestantes > 1 ? 's' : ''}`; } else { placesBadge = `${placesRestantes} places`; }// Badge niveau (si renseigné) const niveauInfo = formatNiveau(evt.niveau); const niveauBadge = niveauInfo ? `${niveauInfo.icon} ${escapeHtml(niveauInfo.label)}` : '';document.getElementById('modalBadges').innerHTML = ` `;// TITRE document.getElementById('modalTitle').textContent = evt.titre;// NOTE IMPORTANTE (si renseignée) const noteWrap = document.getElementById('modalNoteWrap'); if (evt.note_importante && evt.note_importante.trim()) { document.getElementById('modalNoteText').textContent = evt.note_importante; noteWrap.style.display = 'flex'; } else { noteWrap.style.display = 'none'; }// DESCRIPTION (avec saut de ligne préservé grâce au CSS white-space: pre-line) const descEl = document.getElementById('modalDescription'); if (evt.description && evt.description.trim()) { descEl.textContent = evt.description; descEl.style.display = 'block'; } else { descEl.style.display = 'none'; }// TARIFS — RADIOS (Workshop) ou COMPTEURS (Spectacle) const tarifOptions = document.getElementById('tarifOptions'); const stepTitle = document.querySelector('.step-panel[data-step="2"] .step-title'); const isSpectacle = evt.type === 'spectacle';if (isSpectacle && evt.tarifs && evt.tarifs.length > 0) { // MODE SPECTACLE : compteurs multi-billets const maxParResa = parseInt(evt.max_par_resa, 10) || 6; if (stepTitle) stepTitle.textContent = `🎟️ Choisissez vos billets`;tarifOptions.innerHTML = `

Maximum ${maxParResa} billets par réservation

${evt.tarifs.map((t, i) => `
${escapeHtml(t.libelle)}
${escapeHtml(formatPrix(t.prix))}
0
`).join('')}
Total
0 place
0 €
`; // Reset les compteurs window.billetCounts = Array(evt.tarifs.length).fill(0); updateSpectacleTotal(); } else if (evt.tarifs && Array.isArray(evt.tarifs) && evt.tarifs.length > 0) { // MODE WORKSHOP : radios classiques if (stepTitle) stepTitle.textContent = `Choisissez votre tarif`; tarifOptions.innerHTML = evt.tarifs.map((t, i) => ` `).join(''); } else { if (stepTitle) stepTitle.textContent = `Choisissez votre tarif`; tarifOptions.innerHTML = ` `; }// Ouverture de la modal directement sur l'étape 1 (Infos) goToStep(1); document.getElementById('eventModal').classList.add('open'); document.body.style.overflow = 'hidden'; };// Aller à une étape précise function buildAccompagnantsFields() { const wrap = document.getElementById('accompagnantsWrap'); if (!wrap) return;const isSpectacleMode = document.getElementById('spectacleBillets') !== null; const total = (isSpectacleMode && window.billetCounts) ? window.billetCounts.reduce((a, b) => a + b, 0) : 0;// Pas de spectacle ou un seul billet → pas de champs supplémentaires if (!isSpectacleMode || total <= 1) { wrap.style.display = 'none'; wrap.innerHTML = ''; return; }// Préserver les valeurs déjà saisies si on revient sur l'étape const anciens = Array.from(wrap.querySelectorAll('.accomp-input')).map(i => i.value);let html = `

Nom des participant·es (${total} places)

Merci d'indiquer le prénom et le nom de chaque personne, vous compris·e.

`;for (let i = 0; i < total; i++) { const val = anciens[i] || ''; const label = i === 0 ? 'Place 1 (vous)' : 'Place ' + (i + 1); html += `
`; } html += `
`; wrap.innerHTML = html; wrap.style.display = 'block';// La 1re place reflète automatiquement le réservant (prénom + nom du haut) syncFirstAccompagnant(); const pIn = document.getElementById('prenom'); const nIn = document.getElementById('nom'); if (pIn) pIn.oninput = syncFirstAccompagnant; if (nIn) nIn.oninput = syncFirstAccompagnant; }function syncFirstAccompagnant() { const first = document.querySelector('#accompagnantsWrap .accomp-input[data-idx="0"]'); if (!first) return; const prenom = (document.getElementById('prenom')?.value || '').trim(); const nom = (document.getElementById('nom')?.value || '').trim(); first.value = (prenom + ' ' + nom).trim(); }function goToStep(step) { currentStep = step;// Mise à jour des panels document.querySelectorAll('.step-panel').forEach(p => { p.classList.toggle('active', parseInt(p.dataset.step, 10) === step); });// Mise à jour du stepper visuel document.querySelectorAll('.stepper-item').forEach(item => { const n = parseInt(item.dataset.step, 10); item.classList.remove('active', 'done'); if (n === step) item.classList.add('active'); else if (n < step) item.classList.add('done'); });// Mise à jour des barres entre étapes document.querySelectorAll('.stepper-bar').forEach((bar, i) => { bar.style.background = (i + 1 < step) ? 'var(--gold-3)' : 'var(--border)'; });// Boutons navigation document.getElementById('btnPrev').style.display = step > 1 ? '' : 'none'; document.getElementById('btnNext').style.display = step < 4 ? '' : 'none'; document.getElementById('btnConfirm').style.display = step === 4 ? '' : 'none';// Générer les champs "noms des participants" à l'étape 3 (spectacle multi-billets) if (step === 3) buildAccompagnantsFields();// Remplir le récap à l'étape 4 if (step === 4) { fillRecap();// Adapter le banner + bouton agenda selon mode (waitlist ou non) const banner = document.getElementById('recapStatusBanner'); const agendaBtn = document.getElementById('recapAgendaBtn'); const stepTitle = document.querySelector('.step-panel[data-step="4"] .step-title');if (isWaitlistMode) { // Mode liste d'attente : pas de bouton agenda (place pas garantie) if (stepTitle) stepTitle.innerHTML = "⏳ Inscription en liste d'attente"; if (banner) banner.innerHTML = "✉️ Un email de confirmation vient de vous être envoyé. Nous vous contacterons dès qu'une place se libère."; if (agendaBtn) agendaBtn.style.display = 'none'; } else { // Mode réservation confirmée : bouton agenda visible if (stepTitle) stepTitle.innerHTML = "✓ Votre réservation est confirmée"; if (banner) banner.innerHTML = '✉️ Un email de confirmation vient de vous être envoyé avec tous les détails et un fichier .ics à ajouter à votre agenda.'; if (agendaBtn) agendaBtn.style.display = ''; } }// Adapter le texte du bouton "Suivant" selon l'étape ET le mode (liste d'attente ou non) const btnNext = document.getElementById('btnNext'); const btnConfirm = document.getElementById('btnConfirm'); if (isWaitlistMode) { if (step === 1) btnNext.textContent = "S'inscrire en liste d'attente →"; else if (step === 2) btnNext.textContent = 'Continuer →'; else if (step === 3) btnNext.textContent = 'Confirmer mon inscription ⏳'; btnConfirm.textContent = "Fermer"; } else { if (step === 1) btnNext.textContent = 'Je réserve →'; else if (step === 2) btnNext.textContent = 'Continuer →'; else if (step === 3) btnNext.textContent = 'Confirmer ma réservation ✦'; btnConfirm.textContent = "Fermer"; }// Scroll en haut de la modal document.querySelector('.fpr-modal-shell').scrollTop = 0; document.querySelector('.fpr-modal-full').scrollTop = 0; }// Passer à l'étape suivante (avec validation) window.nextStep = async function() { // Étape 1 (Infos) : pas de validation, on continue juste // Étape 2 : choix du tarif obligatoire (workshop) OU au moins 1 billet (spectacle) if (currentStep === 2) { // Mode SPECTACLE : vérifier qu'au moins 1 billet est sélectionné const isSpectacleMode = document.getElementById('spectacleBillets') !== null; if (isSpectacleMode) { const totalBillets = (window.billetCounts || []).reduce((a, b) => a + b, 0); if (totalBillets === 0) { showToast("Sélectionnez au moins 1 billet"); return; } } else { // Mode WORKSHOP : vérifier qu'un radio est coché const tarifChecked = document.querySelector('input[name="tarif_choix"]:checked'); if (!tarifChecked) { showToast("Merci de choisir un tarif"); return; } } } // Étape 3 : coordonnées obligatoires + ENREGISTREMENT + ENVOI MAIL if (currentStep === 3) { const prenom = document.getElementById('prenom').value.trim(); const nom = document.getElementById('nom').value.trim(); const email = document.getElementById('email').value.trim(); const telephone = document.getElementById('telephone').value.trim(); if (!prenom || !nom || !email || !telephone) { showToast("Merci de remplir tous les champs"); return; } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { showToast("Format d'email invalide"); return; }// 🎯 NOUVEAU : Lance le processus de réservation MAINTENANT (étape 3 → 4) const success = await processReservation(); if (!success) return; // Erreur → on reste sur l'étape 3 } if (currentStep < 4) goToStep(currentStep + 1); };// Revenir à l'étape précédente window.prevStep = function() { if (currentStep > 1) goToStep(currentStep - 1); };// Remplir le récapitulatif à l'étape 3 function fillRecap() { const evt = events.find(e => e.id === selectedEventId); if (!evt) return;const prenom = document.getElementById('prenom').value.trim(); const nom = document.getElementById('nom').value.trim(); const email = document.getElementById('email').value.trim(); const telephone = document.getElementById('telephone').value.trim();const isSpectacleMode = document.getElementById('spectacleBillets') !== null;// Construire le détail des choix (workshop = 1 ligne / spectacle = N lignes) let choixHTML = ''; let totalMontant = 0; let totalPlaces = 0;if (isSpectacleMode && window.billetCounts) { // Mode SPECTACLE : afficher chaque tarif avec sa quantité evt.tarifs.forEach((t, i) => { const count = window.billetCounts[i] || 0; if (count > 0) { const prixUnitaire = parsePrix(t.prix) || 0; const sousTotal = count * prixUnitaire; totalMontant += sousTotal; totalPlaces += count; choixHTML += `
${escapeHtml(t.libelle)} × ${count} ${sousTotal} €
`; } }); choixHTML += `
Total (${totalPlaces} place${totalPlaces > 1 ? 's' : ''}) ${totalMontant} €
`; } else { // Mode WORKSHOP : 1 seul tarif const tarifInput = document.querySelector('input[name="tarif_choix"]:checked'); const idx = tarifInput ? parseInt(tarifInput.value, 10) : 0; const tarifChoisi = (evt.tarifs && evt.tarifs[idx]) ? evt.tarifs[idx] : { libelle: 'Inscription', prix: 'Sur demande' }; choixHTML = `
Votre choix ${escapeHtml(tarifChoisi.libelle)} — ${escapeHtml(formatPrix(tarifChoisi.prix))}
`; }const horaires = (evt.heure_debut && evt.heure_fin) ? `${evt.heure_debut} - ${evt.heure_fin}` : '';document.getElementById('recapBox').innerHTML = `
Événement ${escapeHtml(evt.titre)}
Date ${escapeHtml(formatDate(evt.date_debut, evt.date_fin))}
${horaires ? `
Horaires ${escapeHtml(horaires)}
` : ''}
Lieu ${escapeHtml(evt.lieu || 'French Pole Riviera — Nice')}
${choixHTML}
Nom ${escapeHtml(prenom + ' ' + nom)}
Email ${escapeHtml(email)}
Téléphone ${escapeHtml(telephone)}
`; }// Fermer la modal window.closeEventModal = function() { document.getElementById('eventModal').classList.remove('open'); document.body.style.overflow = ''; selectedEventId = null; // Stopper toute vidéo en cours const hero = document.getElementById('modalHero'); const vid = hero.querySelector('video'); if (vid) { vid.pause(); vid.src = ''; } hero.innerHTML = ''; hero.style.display = ''; // Reset pour la prochaine ouverture setTimeout(() => { document.getElementById('reservationForm').reset(); restoreClientData(); // Re-remplit les coords si localStorage goToStep(1); }, 400); };// Clic sur le fond = fermer (mais pas le contenu) window.handleModalBgClick = function(e) { if (e.target.id === 'eventModal') closeEventModal(); };// Pour compat : anciennes fonctions aliasées window.unselectEvent = closeEventModal;// 🎯 FONCTION CENTRALE : Anti-doublon + Enregistrement Firebase + Envoi mail // Retourne true si succès, false si erreur (la modal reste sur l'étape 3) // Évite les double-appels grâce au flag global reservationProcessed let reservationProcessed = false;async function processReservation() { if (reservationProcessed) { console.log('⚠️ Réservation déjà traitée — skip'); return true; }if (!selectedEventId) { showToast("Veuillez sélectionner un événement"); return false; }const prenom = document.getElementById('prenom').value.trim(); const nom = document.getElementById('nom').value.trim(); const email = document.getElementById('email').value.trim(); const telephone = document.getElementById('telephone').value.trim();// Détection du mode (workshop ou spectacle) const isSpectacleMode = document.getElementById('spectacleBillets') !== null; let tarifChoixInput = null; let nbBilletsTotaux = 1; // workshop = 1 place / spectacle = total billetsif (isSpectacleMode) { // Mode SPECTACLE : vérifier qu'au moins 1 billet nbBilletsTotaux = (window.billetCounts || []).reduce((a, b) => a + b, 0); if (nbBilletsTotaux === 0) { showToast("Sélectionnez au moins 1 billet"); return false; } } else { // Mode WORKSHOP : vérifier qu'un radio est coché tarifChoixInput = document.querySelector('input[name="tarif_choix"]:checked'); if (!tarifChoixInput) { showToast("Merci de choisir un tarif"); return false; } }// Loader sur le bouton "Suivant" de l'étape 3 const btnNext = document.querySelector('.step-nav .fpr-cta:not(.outline)'); if (btnNext) { btnNext.classList.add('is-loading'); btnNext.disabled = true; }try { // ✅ VÉRIFICATION ANTI-DOUBLON const emailNormalized = email.toLowerCase().trim(); const dupQuery = query( collection(db, "evenements_participants"), where("evenement_id", "==", selectedEventId), where("email", "==", emailNormalized) ); const dupSnap = await getDocs(dupQuery);if (!dupSnap.empty) { const existing = dupSnap.docs[0].data(); const dateRes = existing.date_reservation?.seconds ? new Date(existing.date_reservation.seconds * 1000).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }) : 'récemment'; const statutLabel = existing.statut === 'liste_attente' ? '⏳ Liste d\'attente' : '✅ Confirmée';showDuplicateModal({ prenom: existing.prenom || prenom, statut: statutLabel, dateInscription: dateRes, email: emailNormalized }); if (btnNext) { btnNext.classList.remove('is-loading'); btnNext.disabled = false; } return false; } } catch (errCheck) { console.error('Erreur vérification doublon :', errCheck); // On continue (mieux vaut accepter qu'empêcher) }try { let evt;if (isWaitlistMode) { const eventRef = doc(db, "evenements", selectedEventId); const eventSnap = await getDoc(eventRef); if (!eventSnap.exists()) throw new Error("Événement introuvable"); evt = { id: eventSnap.id, ...eventSnap.data() };// Double-check : s'il reste finalement des places → bascule en confirmé const placesMaxFresh = parseInt(evt.places_max, 10) || 0; const placesReserveesFresh = parseInt(evt.places_reservees, 10) || 0; if (placesMaxFresh > 0 && placesReserveesFresh < placesMaxFresh) { console.log('✓ Places trouvées à la lecture — bascule en mode confirmé'); isWaitlistMode = false; evt = await runTransaction(db, async (transaction) => { const eventSnap2 = await transaction.get(eventRef); if (!eventSnap2.exists()) throw new Error("Événement introuvable"); const data = eventSnap2.data(); const restantes = (data.places_max || 0) - (data.places_reservees || 0); if (restantes < nbBilletsTotaux) { isWaitlistMode = true; return { id: eventSnap2.id, ...data }; } transaction.update(eventRef, { places_reservees: (data.places_reservees || 0) + nbBilletsTotaux }); return { id: eventSnap2.id, ...data }; }); } } else { evt = await runTransaction(db, async (transaction) => { const eventRef = doc(db, "evenements", selectedEventId); const eventSnap = await transaction.get(eventRef); if (!eventSnap.exists()) throw new Error("Événement introuvable"); const data = eventSnap.data(); const placesRestantes = (data.places_max || 0) - (data.places_reservees || 0); if (placesRestantes < nbBilletsTotaux) throw new Error("COMPLET"); transaction.update(eventRef, { places_reservees: (data.places_reservees || 0) + nbBilletsTotaux }); return { id: eventSnap.id, ...data }; }); isWaitlistMode = false; }// Construction du tarif/billets selon le mode let tarifChoisi, tarifPrixFormatte, tarifChoisiText, billetsDetail, billetsResume;if (isSpectacleMode && evt.tarifs) { // MODE SPECTACLE : construire le détail multi-billets let totalMontant = 0; const lignes = []; const billetsObj = {}; evt.tarifs.forEach((t, i) => { const count = window.billetCounts[i] || 0; if (count > 0) { const prixUnit = parsePrix(t.prix) || 0; totalMontant += count * prixUnit; lignes.push(`${t.libelle} × ${count} (${count * prixUnit}€)`); billetsObj[t.libelle] = count; } }); tarifChoisi = { libelle: `${nbBilletsTotaux} billet${nbBilletsTotaux > 1 ? 's' : ''}`, prix: String(totalMontant) }; tarifPrixFormatte = `${totalMontant}€`; tarifChoisiText = lignes.join(' + '); billetsDetail = billetsObj; billetsResume = `${nbBilletsTotaux} place${nbBilletsTotaux > 1 ? 's' : ''} — ${totalMontant}€`; } else { // MODE WORKSHOP : tarif unique const idx = parseInt(tarifChoixInput.value, 10); tarifChoisi = (evt.tarifs && evt.tarifs[idx]) ? evt.tarifs[idx] : { libelle: 'Inscription', prix: 'Sur demande' }; tarifPrixFormatte = formatPrix(tarifChoisi.prix); tarifChoisiText = `${tarifChoisi.libelle} — ${tarifPrixFormatte}`; billetsDetail = null; billetsResume = null; }// Préparer l'objet à enregistrer const participantData = { evenement_id: selectedEventId, evenement_titre: evt.titre, prenom, nom, email: email.toLowerCase().trim(), telephone, tarif_libelle: tarifChoisi.libelle, tarif_prix: tarifPrixFormatte, commentaire: "", statut: isWaitlistMode ? "liste_attente" : "confirme", paiement: "sur_place", date_reservation: Timestamp.now() };// En mode spectacle : ajouter le détail des billets if (isSpectacleMode) { participantData.type = "spectacle"; participantData.nb_places_total = nbBilletsTotaux; participantData.billets = billetsDetail; // { "Adulte": 2, "Enfant": 1 } participantData.billets_resume = billetsResume; // "3 places — 38€" // Noms de chaque participant·e (1re place = réservant) const nomsInputs = document.querySelectorAll('#accompagnantsWrap .accomp-input'); if (nomsInputs.length > 0) { participantData.participants_noms = Array.from(nomsInputs) .map(i => i.value.trim()) .filter(v => v); } }await addDoc(collection(db, "evenements_participants"), participantData);const dateStr = formatDate(evt.date_debut, evt.date_fin);// Lien Google Calendar const calDebut = evt.date_debut?.seconds ? new Date(evt.date_debut.seconds * 1000) : new Date(evt.date_debut); const calFin = evt.date_fin?.seconds ? new Date(evt.date_fin.seconds * 1000) : (evt.date_fin ? new Date(evt.date_fin) : new Date(calDebut)); const [ch1, cm1] = (evt.heure_debut || '09:00').split(':').map(Number); const [ch2, cm2] = (evt.heure_fin || '18:00').split(':').map(Number); calDebut.setHours(ch1, cm1, 0, 0); calFin.setHours(ch2, cm2, 0, 0); const toGcalFmt = (d) => d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); const gcalDates = `${toGcalFmt(calDebut)}/${toGcalFmt(calFin)}`; const gcalLocation = encodeURIComponent(evt.lieu || 'French Pole Riviera — 1 rue Oscar II, 06000 Nice'); const gcalText = encodeURIComponent(evt.titre); const gcalDetails = encodeURIComponent((evt.description || '') + '\n\nRéservation confirmée — French Pole Riviera'); const googleCalendarUrl = `https://www.google.com/calendar/render?action=TEMPLATE&text=${gcalText}&dates=${gcalDates}&location=${gcalLocation}&details=${gcalDetails}`;const params = { client_prenom: prenom, client_nom: nom, client_nom_complet: `${prenom} ${nom}`, client_email: email, client_telephone: telephone, event_titre: evt.titre, event_date: dateStr, event_horaires: (evt.heure_debut && evt.heure_fin) ? `${evt.heure_debut} - ${evt.heure_fin}` : '', event_lieu: evt.lieu || "French Pole Riviera — 1 rue Oscar II, 06000 Nice", event_tarif: tarifChoisiText, tarif_libelle: tarifChoisi.libelle, tarif_prix: tarifPrixFormatte, event_description: evt.description || "", event_note: evt.note_importante || "", google_calendar_url: googleCalendarUrl, ics_date_debut: toGcalFmt(calDebut), ics_date_fin: toGcalFmt(calFin), date_reservation: new Date().toLocaleString('fr-FR'), statut_inscription: isWaitlistMode ? "liste_attente" : "confirme", statut_titre: isWaitlistMode ? "Inscription en liste d'attente" : "Felicitations", statut_message: isWaitlistMode ? `Votre inscription en liste d'attente pour "${evt.titre}" est bien enregistree. Nous vous contacterons des qu'une place se libere !` : `Felicitations, votre reservation pour "${evt.titre}" est bien enregistree. On a hate de vous retrouver au studio !`, statut_sous_titre: isWaitlistMode ? "Vous etes sur la liste" : "Votre place est confirmee", // 🎟️ Variables SPECTACLE (utilisées seulement si mode spectacle) is_spectacle: isSpectacleMode, event_billets_detail: billetsDetail, event_billets_resume: billetsResume, nb_places: nbBilletsTotaux };// 📧 ENVOI EMAIL IMMÉDIAT console.log('📧 Envoi email client inscription...', { email, isWaitlistMode }); sendEmailViaBrevo({ ...params, to_email: email }) .then(r => { if (r.messageId) console.log('✅ Email envoyé avec succès — ID:', r.messageId); else console.error('❌ Brevo erreur:', r); }) .catch(err => console.error("❌ Envoi email KO:", err));// Sauvegarder pour le récap étape 4 + bouton "Ajouter à mon agenda" window.lastReservation = { prenom, nom, email, telephone, titre: evt.titre, date_debut: evt.date_debut, date_fin: evt.date_fin, heure_debut: evt.heure_debut, heure_fin: evt.heure_fin, lieu: evt.lieu || 'French Pole Riviera — 1 rue Oscar II, 06000 Nice', description: evt.description || '', tarif: `${tarifChoisi.libelle} : ${tarifPrixFormatte}`, isWaitlist: isWaitlistMode };// Sauvegarder les coordonnées pour pré-remplir la prochaine fois try { localStorage.setItem('fpr_client_data', JSON.stringify({ prenom, nom, email, telephone, saved_at: Date.now() })); } catch (e) { // localStorage peut être désactivé (mode privé) }// Marquer comme traité pour éviter double-envoi reservationProcessed = true;if (btnNext) { btnNext.classList.remove('is-loading'); btnNext.disabled = false; } return true;} catch (err) { console.error("🔴 Erreur réservation :", err); if (err.message === "COMPLET") { showToast("Cet événement vient de se remplir. Merci d'en choisir un autre."); loadEvents(); closeEventModal(); } else if (err.code === "permission-denied") { showToast("Erreur Firestore : permissions insuffisantes. Vérifiez les règles."); } else { showToast(`Erreur : ${err.message || 'réessayez'}`); } if (btnNext) { btnNext.classList.remove('is-loading'); btnNext.disabled = false; } return false; } }document.getElementById('reservationForm').addEventListener('submit', async (e) => { e.preventDefault();// Bloquer si on n'est pas à l'étape 4 (soumission accidentelle par Enter) if (currentStep !== 4) { nextStep(); return; }// 🎯 À l'étape 4, la réservation est DÉJÀ enregistrée et le mail DÉJÀ envoyé. // Le clic sur le bouton sert juste à fermer la modal et afficher le succès final.const reservation = window.lastReservation; if (!reservation) { showToast("Erreur : impossible de retrouver la réservation"); return; }// La résa est déjà confirmée à l'étape 4 — on ferme juste la modal closeEventModal(); loadEvents(); });function formatDate(debut, fin) { if (!debut) return ''; const opts = { day: 'numeric', month: 'long', year: 'numeric' }; const d1 = new Date(debut.seconds ? debut.seconds * 1000 : debut); const s1 = d1.toLocaleDateString('fr-FR', opts); if (!fin) return s1; const d2 = new Date(fin.seconds ? fin.seconds * 1000 : fin); const s2 = d2.toLocaleDateString('fr-FR', opts); if (s1 === s2) return s1; return `Du ${s1} au ${s2}`; }function escapeHtml(str) { if (!str) return ''; return String(str).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }// ═══════════════════════════════════════════ // AJOUT AGENDA — génération fichier .ics (iCalendar) // ═══════════════════════════════════════════ window.downloadICS = function() { const r = window.lastReservation; if (!r) { showToast("Informations manquantes"); return; }// Reconstituer les dates complètes en combinant date + heure const dateDebutObj = r.date_debut?.seconds ? new Date(r.date_debut.seconds * 1000) : new Date(r.date_debut); const dateFinObj = r.date_fin?.seconds ? new Date(r.date_fin.seconds * 1000) : (r.date_fin ? new Date(r.date_fin) : new Date(dateDebutObj));// Appliquer les heures si présentes, sinon défaut 09:00 → 18:00 const [h1, m1] = (r.heure_debut || '09:00').split(':').map(Number); const [h2, m2] = (r.heure_fin || '18:00').split(':').map(Number); dateDebutObj.setHours(h1, m1, 0, 0); dateFinObj.setHours(h2, m2, 0, 0);// Format iCalendar : YYYYMMDDTHHMMSS (UTC ou local avec TZ) // On utilise le format "local" avec TZID pour que ça soit respecté dans Paris const fmtDate = (d) => { const pad = n => String(n).padStart(2, '0'); return `${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}T${pad(d.getHours())}${pad(d.getMinutes())}00`; };const uid = `fpr-${Date.now()}@frenchpoleriviera.com`; const now = new Date(); const dtstamp = `${now.getUTCFullYear()}${String(now.getUTCMonth()+1).padStart(2,'0')}${String(now.getUTCDate()).padStart(2,'0')}T${String(now.getUTCHours()).padStart(2,'0')}${String(now.getUTCMinutes()).padStart(2,'0')}${String(now.getUTCSeconds()).padStart(2,'0')}Z`;// Échapper les caractères spéciaux iCal (virgule, point-virgule, retour ligne) const icsEscape = (txt) => String(txt || '') .replace(/\\/g, '\\\\') .replace(/\n/g, '\\n') .replace(/,/g, '\\,') .replace(/;/g, '\\;');// Construire la description const descLines = []; if (r.description) descLines.push(r.description); if (r.tarif) descLines.push(`Tarif : ${r.tarif}`); descLines.push('Réservation confirmée via French Pole Riviera'); const description = descLines.join('\\n\\n');// Contenu iCalendar (standard RFC 5545) const ics = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//French Pole Riviera//Events//FR', 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', 'BEGIN:VTIMEZONE', 'TZID:Europe/Paris', 'BEGIN:STANDARD', 'DTSTART:19701025T030000', 'TZOFFSETFROM:+0200', 'TZOFFSETTO:+0100', 'TZNAME:CET', 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10', 'END:STANDARD', 'BEGIN:DAYLIGHT', 'DTSTART:19700329T020000', 'TZOFFSETFROM:+0100', 'TZOFFSETTO:+0200', 'TZNAME:CEST', 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3', 'END:DAYLIGHT', 'END:VTIMEZONE', 'BEGIN:VEVENT', `UID:${uid}`, `DTSTAMP:${dtstamp}`, `DTSTART;TZID=Europe/Paris:${fmtDate(dateDebutObj)}`, `DTEND;TZID=Europe/Paris:${fmtDate(dateFinObj)}`, `SUMMARY:${icsEscape(r.titre)}`, `LOCATION:${icsEscape(r.lieu)}`, `DESCRIPTION:${icsEscape(description)}`, 'STATUS:CONFIRMED', 'BEGIN:VALARM', 'TRIGGER:-PT24H', 'ACTION:DISPLAY', `DESCRIPTION:Rappel : ${icsEscape(r.titre)} demain`, 'END:VALARM', 'END:VEVENT', 'END:VCALENDAR' ].join('\r\n');// Téléchargement const blob = new Blob([ics], { type: 'text/calendar;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${r.titre.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_fpr.ics`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);showToast("Événement ajouté à votre agenda ✦"); };function showToast(msg) { const t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 4000); }// 🔁 Modal "Inscription déjà existante" function showDuplicateModal(info) { // Créer le modal s'il n'existe pas encore let modal = document.getElementById('duplicateModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'duplicateModal'; modal.className = 'fpr-modal'; modal.innerHTML = `
`; modal.addEventListener('click', (e) => { if (e.target === modal) closeModal('duplicateModal'); }); document.body.appendChild(modal); } // Remplir le contenu document.getElementById('duplicateModalTitle').textContent = `Bonjour ${info.prenom} 💛`; document.getElementById('duplicateModalText').innerHTML = ` L'adresse ${info.email} est déjà inscrite à cet événement.

Statut : ${info.statut}
Inscrite le : ${info.dateInscription}

ℹ️ Une seule inscription par personne est autorisée pour chaque événement.
Vérifiez votre boîte mail (et les spams) pour retrouver votre confirmation. `; openModal('duplicateModal'); }window.openModal = function(id) { document.getElementById(id).classList.add('open'); }; window.closeModal = function(id) { document.getElementById(id).classList.remove('open'); }; window.handleModalClick = function(e, id) { if (e.target === document.getElementById(id)) closeModal(id); };// ═══════════════════════════════════════════ // PRÉ-REMPLISSAGE — si la cliente est déjà venue // ═══════════════════════════════════════════ function restoreClientData() { try { const raw = localStorage.getItem('fpr_client_data'); if (!raw) return; const data = JSON.parse(raw);// Expiration après 90 jours (RGPD friendly) const maxAge = 90 * 24 * 60 * 60 * 1000; if (Date.now() - (data.saved_at || 0) > maxAge) { localStorage.removeItem('fpr_client_data'); return; }// Pré-remplir chaque champ if (data.prenom) document.getElementById('prenom').value = data.prenom; if (data.nom) document.getElementById('nom').value = data.nom; if (data.email) document.getElementById('email').value = data.email; if (data.telephone) document.getElementById('telephone').value = data.telephone;// Notification discrète à la cliente if (data.prenom) { setTimeout(() => { showToast(`✦ Bon retour parmi nous, ${data.prenom} !`); }, 800); } } catch (e) { // localStorage désactivé ou JSON corrompu → on ignore } } restoreClientData();// ═══════════════════════════════════════════════════ // 👤 ESPACE PERSONNEL — consulter / annuler ses réservations // ═══════════════════════════════════════════════════ const ESP = { email: '', code: '', reservations: [] };async function espEnvoiBrevo(toEmail, toNom, sujet, texte) { const html = `
${texte.replace(/\n/g,'
')}
`; const payload = { sender: { name: BREVO_CONFIG.SENDER_NAME, email: BREVO_CONFIG.SENDER_EMAIL }, to: [{ email: toEmail, name: toNom || toEmail }], bcc: [{ email: BREVO_CONFIG.ADMIN_EMAIL, name: "French Pole Riviera" }], subject: sujet, htmlContent: html, textContent: texte, replyTo: { email: BREVO_CONFIG.ADMIN_EMAIL, name: "Juliette - French Pole Riviera" }, headers: { 'X-Mailin-custom': 'transactional-espace-perso', 'X-Entity-Ref-ID': `fpr-esp-${Date.now()}` } }; return fetch('https://api.brevo.com/v3/smtp/email', { method: 'POST', headers: { 'accept': 'application/json', 'content-type': 'application/json', 'api-key': BREVO_CONFIG.API_KEY }, body: JSON.stringify(payload) }).then(r => r.json()); }function espGetModal() { let m = document.getElementById('espModal'); if (!m) { m = document.createElement('div'); m.id = 'espModal'; m.className = 'fpr-modal'; m.onclick = (e) => { if (e.target === m) closeEspacePerso(); }; document.body.appendChild(m); } return m; } window.closeEspacePerso = function() { const m = document.getElementById('espModal'); if (m) m.classList.remove('open'); };window.openEspacePerso = function() { const m = espGetModal(); m.innerHTML = `

👤 Mes réservations

Entrez l'email utilisé lors de votre réservation. Un code de vérification vous sera envoyé.

`; m.classList.add('open'); setTimeout(() => document.getElementById('espEmail')?.focus(), 100); };window.espEnvoyerCode = async function() { const email = (document.getElementById('espEmail')?.value || '').trim().toLowerCase(); const msg = document.getElementById('espMsg'); const btn = document.getElementById('espSendBtn'); if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { msg.style.color = 'var(--danger, #D48882)'; msg.textContent = "Merci d'entrer un email valide."; return; } btn.disabled = true; btn.textContent = "Envoi en cours…"; ESP.email = email; ESP.code = String(Math.floor(100000 + Math.random() * 900000)); const texte = `Bonjour,\n\nVoici votre code de vérification pour accéder à vos réservations French Pole Riviera :\n\n${ESP.code}\n\nCe code est valable pour cette session uniquement.\nSi vous n'êtes pas à l'origine de cette demande, ignorez cet email.\n\nFrench Pole Riviera`; try { await espEnvoiBrevo(email, '', `Votre code de vérification : ${ESP.code}`, texte); espEtapeCode(); } catch (e) { console.error(e); msg.style.color = 'var(--danger, #D48882)'; msg.textContent = "Erreur d'envoi. Réessayez dans un instant."; btn.disabled = false; btn.textContent = "Recevoir mon code"; } };function espEtapeCode() { const m = espGetModal(); m.innerHTML = `

Vérification

Un code à 6 chiffres a été envoyé à ${espEsc(ESP.email)}. Saisissez-le ci-dessous (pensez à vérifier vos spams).

`; setTimeout(() => document.getElementById('espCode')?.focus(), 100); }window.espVerifierCode = async function() { const saisi = (document.getElementById('espCode')?.value || '').trim(); const msg = document.getElementById('espMsg'); if (saisi !== ESP.code) { msg.style.color = 'var(--danger, #D48882)'; msg.textContent = "Code incorrect. Vérifiez et réessayez."; return; } msg.style.color = 'var(--text-3)'; msg.textContent = "Chargement…"; try { const q = query(collection(db, "evenements_participants"), where("email", "==", ESP.email)); const snap = await getDocs(q); ESP.reservations = []; snap.forEach(d => ESP.reservations.push({ id: d.id, ...d.data() })); espAfficherReservations(); } catch (e) { console.error(e); msg.style.color = 'var(--danger, #D48882)'; msg.textContent = "Erreur de chargement. Réessayez."; } };function espAfficherReservations() { const m = espGetModal(); let liste = ''; if (ESP.reservations.length === 0) { liste = `

Aucune réservation trouvée pour cet email.

Si vous venez de réserver, vérifiez l'email saisi. Pour participer à un événement, il suffit de le réserver depuis la liste — votre espace se remplira automatiquement.

`; } else { ESP.reservations.forEach(r => { const statutBadge = r.statut === 'liste_attente' ? `Liste d'attente` : `Confirmée`; const placesInfo = (r.type === 'spectacle' && r.nb_places_total) ? ` • ${r.nb_places_total} place${r.nb_places_total>1?'s':''}` : ''; liste += `
${espEsc(r.evenement_titre || 'Événement')} ${statutBadge}
🎟️ ${espEsc(r.tarif_libelle || '')}${r.tarif_prix ? ' — ' + espEsc(r.tarif_prix) : ''}${placesInfo}
`; }); } m.innerHTML = `

Mes réservations

${espEsc(ESP.email)}

${liste}

Pour toute modification, contactez-nous directement.

`; }function espEsc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }window.espAnnuler = async function(reservationId) { const r = ESP.reservations.find(x => x.id === reservationId); if (!r) return; if (!confirm(`Annuler votre réservation pour "${r.evenement_titre}" ?\n\nCette action est définitive.`)) return;const placesALiberer = (r.type === 'spectacle' && r.nb_places_total) ? r.nb_places_total : 1; const etaitConfirme = (r.statut || 'confirme') === 'confirme';try { await deleteDoc(doc(db, "evenements_participants", reservationId)); if (etaitConfirme && r.evenement_id) { const evtRef = doc(db, "evenements", r.evenement_id); await runTransaction(db, async (t) => { const snap = await t.get(evtRef); if (!snap.exists()) return; const actuel = snap.data().places_reservees || 0; t.update(evtRef, { places_reservees: Math.max(0, actuel - placesALiberer) }); }); } const texte = `Bonjour ${r.prenom || ''},\n\nVotre réservation a bien été annulée :\n\n• Événement : ${r.evenement_titre}\n• Tarif : ${r.tarif_libelle || ''}${r.tarif_prix ? ' — ' + r.tarif_prix : ''}${placesALiberer > 1 ? '\n• Places : ' + placesALiberer : ''}\n\nAu plaisir de vous revoir bientôt.\n\nFrench Pole Riviera`; espEnvoiBrevo(r.email, r.prenom, `Annulation confirmée — ${r.evenement_titre}`, texte).catch(e => console.error(e)); ESP.reservations = ESP.reservations.filter(x => x.id !== reservationId); showToast("Réservation annulée ✦"); espAfficherReservations(); loadEvents(); } catch (e) { console.error(e); showToast("Erreur lors de l'annulation", 'error'); } };loadEvents();