// ═══════════════════════════════════════════════════════════════════════════ // SERENYS - MAISON DE BEAUTÉ // Application de prise de rendez-vous par praticienne // ═══════════════════════════════════════════════════════════════════════════ const { useState, useEffect, useCallback } = React; // ────────────────────────────────────────────────────────────────────────── // CONFIGURATION API // ────────────────────────────────────────────────────────────────────────── const API_CONFIG = { mailsApiUrl: 'https://api.mails.swissapp.net/send', apiKey: 'ak-aa773140-a328-4a40-82ad-0160281363c9', fromEmail: 'noreply@serenys.ch', fromName: 'Serenys - Maison de Beauté', toEmail: 'info@serenys.ch' }; // ────────────────────────────────────────────────────────────────────────── // DONNÉES - Catalogue des praticiennes (chargé depuis JSON) // ────────────────────────────────────────────────────────────────────────── const CATALOGUE = { "praticiennes": [ { "id": "didier", "name": "Didier", "subtitle": "Coiffure", "description": "Coiffure homme et femme", "avatar": "✂️", "color": "#5D7B93", "categories": [ { "id": "coiffure", "name": "Coiffure", "services": [ { "id": "coupe-femme", "name": "Coupe femme", "duration": 45, "price": 0, "options": [] }, { "id": "coupe-homme", "name": "Coupe homme", "duration": 30, "price": 0, "options": [] } ] } ] }, { "id": "aresky", "name": "Aresky", "subtitle": "Beauté des Mains & Pieds", "description": "Manucure, pédicure et soins", "avatar": "💅", "color": "#E8A5B3", "categories": [ { "id": "beaute-mains", "name": "Beauté des Mains", "services": [ { "id": "manucure-simple", "name": "Manucure simple", "duration": 30, "price": 35, "options": [] }, { "id": "manucure-semi", "name": "Manucure semi-permanent", "duration": 45, "price": 55, "options": [{ "id": "couleur", "name": "Couleur", "price": 0 }, { "id": "french", "name": "French", "price": 10 }] }, { "id": "pose-gel", "name": "Pose gel", "duration": 60, "price": 75, "options": [{ "id": "couleur", "name": "Couleur", "price": 0 }, { "id": "french", "name": "French", "price": 10 }] }, { "id": "remplissage-gel", "name": "Remplissage gel", "duration": 45, "price": 60, "options": [{ "id": "couleur", "name": "Couleur", "price": 0 }, { "id": "french", "name": "French", "price": 10 }] }, { "id": "capsules-americaines", "name": "Capsules américaines", "duration": 90, "price": 95, "options": [] }, { "id": "depose-mains", "name": "Dépose", "duration": 20, "price": 15, "options": [] }, { "id": "reparation-ongle", "name": "Réparation ongle", "duration": 15, "price": 10, "options": [] } ] }, { "id": "beaute-pieds", "name": "Beauté des Pieds", "services": [ { "id": "pedicure-simple", "name": "Pédicure simple (sans vernis)", "duration": 45, "price": 45, "options": [] }, { "id": "pedicure-vernis", "name": "Pédicure avec vernis", "duration": 60, "price": 55, "options": [{ "id": "couleur", "name": "Couleur", "price": 0 }, { "id": "french", "name": "French", "price": 10 }] }, { "id": "pedicure-semi", "name": "Pédicure semi-permanent", "duration": 75, "price": 70, "options": [{ "id": "couleur", "name": "Couleur", "price": 0 }, { "id": "french", "name": "French", "price": 10 }] }, { "id": "depose-pieds", "name": "Dépose", "duration": 20, "price": 15, "options": [] } ] }, { "id": "duo-mains-pieds", "name": "Duo Mains & Pieds", "services": [ { "id": "duo-simple", "name": "Mani + Pédi vernis simple", "duration": 75, "price": 75, "options": [] }, { "id": "duo-semi", "name": "Mani + Pédi semi-permanent", "duration": 105, "price": 110, "options": [] }, { "id": "duo-depose", "name": "Mani + Pédi avec dépose", "duration": 120, "price": 130, "options": [] } ] } ] }, { "id": "virginie", "name": "Virginie", "subtitle": "Épilation", "description": "Épilation orientale et traditionnelle", "avatar": "✨", "color": "#D4A574", "categories": [ { "id": "epil-orientale", "name": "Épilation Orientale", "services": [ { "id": "orientale-visage", "name": "Visage complet", "duration": 30, "price": 35, "options": [] }, { "id": "orientale-sourcils", "name": "Sourcils", "duration": 15, "price": 18, "options": [] }, { "id": "orientale-levre", "name": "Lèvre supérieure", "duration": 10, "price": 12, "options": [] }, { "id": "orientale-aisselles", "name": "Aisselles", "duration": 15, "price": 20, "options": [] }, { "id": "orientale-bras", "name": "Bras complets", "duration": 30, "price": 35, "options": [] }, { "id": "orientale-demi-jambes", "name": "Demi-jambes", "duration": 30, "price": 40, "options": [] }, { "id": "orientale-jambes", "name": "Jambes complètes", "duration": 45, "price": 60, "options": [] }, { "id": "orientale-maillot-simple", "name": "Maillot simple", "duration": 20, "price": 25, "options": [] }, { "id": "orientale-maillot-semi", "name": "Maillot semi-intégral", "duration": 25, "price": 35, "options": [] }, { "id": "orientale-maillot-integral", "name": "Maillot intégral", "duration": 30, "price": 45, "options": [] } ] }, { "id": "epil-traditionnelle", "name": "Épilation Traditionnelle", "services": [ { "id": "trad-sourcils", "name": "Sourcils", "duration": 15, "price": 15, "options": [] }, { "id": "trad-levre", "name": "Lèvre supérieure", "duration": 10, "price": 10, "options": [] }, { "id": "trad-aisselles", "name": "Aisselles", "duration": 15, "price": 18, "options": [] }, { "id": "trad-demi-jambes", "name": "Demi-jambes", "duration": 25, "price": 35, "options": [] }, { "id": "trad-jambes", "name": "Jambes complètes", "duration": 40, "price": 55, "options": [] }, { "id": "trad-maillot-simple", "name": "Maillot simple", "duration": 15, "price": 22, "options": [] }, { "id": "trad-maillot-integral", "name": "Maillot intégral", "duration": 25, "price": 42, "options": [] } ] }, { "id": "forfaits-epil", "name": "Forfaits Épilation", "services": [ { "id": "forfait-demi-jambes-maillot", "name": "Demi-jambes + Maillot simple", "duration": 45, "price": 55, "options": [] }, { "id": "forfait-jambes-maillot-simple", "name": "Jambes complètes + Maillot simple", "duration": 60, "price": 75, "options": [] }, { "id": "forfait-jambes-maillot-integral", "name": "Jambes complètes + Maillot intégral", "duration": 70, "price": 95, "options": [] }, { "id": "forfait-jambes-aisselles-maillot", "name": "Jambes + Aisselles + Maillot simple", "duration": 75, "price": 90, "options": [] }, { "id": "forfait-complet", "name": "Forfait complet (Jambes + Aisselles + Maillot intégral)", "duration": 90, "price": 120, "options": [] } ] } ] }, { "id": "kerlys", "name": "Kerlys", "subtitle": "Regard & Maquillage permanent", "description": "Cils, sourcils, lèvres & épilation", "avatar": "👁️", "color": "#9B7BB8", "categories": [ { "id": "cils", "name": "Cils", "services": [ { "id": "pose-cils-classique", "name": "Pose cils à cils (classique)", "duration": 120, "price": 140, "options": [] }, { "id": "pose-mix-volume", "name": "Pose mix volume", "duration": 150, "price": 170, "options": [] }, { "id": "pose-volume-russe", "name": "Pose volume russe", "duration": 180, "price": 200, "options": [] }, { "id": "remplissage-3sem", "name": "Remplissage 3 semaines", "duration": 60, "price": 65, "options": [] }, { "id": "remplissage-4sem", "name": "Remplissage 4 semaines", "duration": 90, "price": 85, "options": [] }, { "id": "depose-cils", "name": "Dépose", "duration": 30, "price": 30, "options": [] } ] }, { "id": "sourcils", "name": "Sourcils", "services": [ { "id": "epil-sourcils", "name": "Épilation sourcils", "duration": 15, "price": 18, "options": [] }, { "id": "epil-henna", "name": "Épilation + Henné", "duration": 30, "price": 38, "options": [] }, { "id": "brow-lift", "name": "Brow Lift + Épilation + Teinture", "duration": 60, "price": 75, "options": [] }, { "id": "microblading", "name": "Microblading", "duration": 120, "price": 350, "description": "Maquillage permanent poil à poil", "options": [] }, { "id": "brow-shadow", "name": "Brow Shadow (ombré)", "duration": 90, "price": 320, "description": "Effet poudré naturel", "options": [] }, { "id": "magic-shading", "name": "Magic Shading", "duration": 90, "price": 300, "options": [] }, { "id": "technique-hybride", "name": "Technique Hybride", "duration": 120, "price": 380, "description": "Microblading + Ombré", "options": [] }, { "id": "retouche-sourcils", "name": "Retouche (dans les 6 semaines)", "duration": 60, "price": 80, "options": [] } ] }, { "id": "levres", "name": "Lèvres", "services": [ { "id": "full-lips", "name": "Micropigmentation Full Lips", "duration": 120, "price": 380, "description": "Lèvres entièrement colorées", "options": [] }, { "id": "aquarella-lips", "name": "Aquarella Lips", "duration": 120, "price": 350, "description": "Effet naturel dégradé", "options": [] }, { "id": "retouche-levres", "name": "Retouche (dans les 6 semaines)", "duration": 60, "price": 100, "options": [] } ] }, { "id": "epil-kerlys", "name": "Épilation", "services": [ { "id": "epil-visage", "name": "Visage complet", "duration": 30, "price": 35, "options": [] }, { "id": "epil-levre", "name": "Lèvre supérieure", "duration": 10, "price": 12, "options": [] }, { "id": "epil-aisselles", "name": "Aisselles", "duration": 15, "price": 20, "options": [] }, { "id": "forfait-3-zones", "name": "Forfait 3 zones", "duration": 30, "price": 40, "description": "Choisissez 3 zones au choix", "options": [] } ] } ] }, { "id": "diane", "name": "Diane", "subtitle": "Massages & Corps", "description": "Maderothérapie, drainage et massages", "avatar": "🧘", "color": "#7BA38F", "categories": [ { "id": "maderotherapie-corps", "name": "Maderothérapie Corps", "services": [ { "id": "madero-corps-60", "name": "Maderothérapie corps (60 min)", "duration": 60, "price": 120, "options": [] }, { "id": "madero-corps-90", "name": "Maderothérapie corps (90 min)", "duration": 90, "price": 160, "options": [] }, { "id": "madero-decouverte", "name": "Séance découverte", "duration": 45, "price": 80, "promo": true, "promoLabel": "Offre découverte", "options": [] } ] }, { "id": "maderotherapie-zone", "name": "Maderothérapie par Zone", "services": [ { "id": "madero-ventre", "name": "Ventre / Abdomen", "duration": 30, "price": 55, "options": [] }, { "id": "madero-cuisses", "name": "Cuisses", "duration": 30, "price": 55, "options": [] }, { "id": "madero-fessiers", "name": "Fessiers", "duration": 30, "price": 55, "options": [] }, { "id": "madero-bras", "name": "Bras", "duration": 20, "price": 40, "options": [] } ] }, { "id": "drainage", "name": "Drainage Lymphatique", "services": [ { "id": "drainage-60", "name": "Drainage lymphatique (60 min)", "duration": 60, "price": 110, "options": [] }, { "id": "drainage-90", "name": "Drainage lymphatique (90 min)", "duration": 90, "price": 150, "options": [] } ] }, { "id": "massages", "name": "Massages", "services": [ { "id": "massage-relaxant-60", "name": "Massage relaxant (60 min)", "duration": 60, "price": 100, "options": [] }, { "id": "massage-relaxant-90", "name": "Massage relaxant (90 min)", "duration": 90, "price": 140, "options": [] }, { "id": "massage-prenatal", "name": "Massage prénatal", "duration": 60, "price": 110, "description": "Adapté aux femmes enceintes", "options": [] } ] }, { "id": "forfaits-diane", "name": "Forfaits Multi-Séances", "services": [ { "id": "forfait-madero-4", "name": "Forfait 4 séances Maderothérapie", "duration": 60, "price": 420, "description": "4 x 60 min - Économisez 60 CHF", "isForfait": true, "sessions": 4, "options": [] }, { "id": "forfait-madero-8", "name": "Forfait 8 séances Maderothérapie", "duration": 60, "price": 780, "description": "8 x 60 min - Économisez 180 CHF", "isForfait": true, "sessions": 8, "options": [] }, { "id": "forfait-drainage-4", "name": "Forfait 4 séances Drainage", "duration": 60, "price": 380, "description": "4 x 60 min - Économisez 60 CHF", "isForfait": true, "sessions": 4, "options": [] }, { "id": "forfait-drainage-8", "name": "Forfait 8 séances Drainage", "duration": 60, "price": 700, "description": "8 x 60 min - Économisez 180 CHF", "isForfait": true, "sessions": 8, "options": [] } ] } ] } ] }; // ────────────────────────────────────────────────────────────────────────── // UTILITAIRES // ────────────────────────────────────────────────────────────────────────── const formatDuration = (minutes) => { if (minutes >= 60) { const hours = Math.floor(minutes / 60); const mins = minutes % 60; return mins > 0 ? `${hours}h${mins}` : `${hours}h`; } return `${minutes} min`; }; const formatPrice = (price) => price > 0 ? `CHF ${price}.-` : 'Sur devis'; const formatDate = (date) => { const options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }; return date.toLocaleDateString('fr-CH', options); }; const formatDateShort = (date) => { const options = { weekday: 'short', day: 'numeric', month: 'short' }; return date.toLocaleDateString('fr-CH', options); }; const getMonthDays = (year, month) => { const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const daysInMonth = lastDay.getDate(); const startingDay = firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1; const days = []; for (let i = 0; i < startingDay; i++) { days.push(null); } for (let i = 1; i <= daysInMonth; i++) { days.push(new Date(year, month, i)); } return days; }; const MONTHS = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; const DAYS = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']; const generateTimeSlots = (date) => { const dayOfWeek = date.getDay(); if (dayOfWeek === 0) return []; const slots = []; const startHour = 9; const endHour = dayOfWeek === 6 ? 17 : 19; for (let hour = startHour; hour < endHour; hour++) { slots.push(`${hour.toString().padStart(2, '0')}:00`); if (hour < endHour - 1 || (hour === endHour - 1 && dayOfWeek !== 6)) { slots.push(`${hour.toString().padStart(2, '0')}:30`); } } return slots.filter(() => Math.random() > 0.2); }; const isAtLeast24hAhead = (date) => { const now = new Date(); const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000); tomorrow.setHours(0, 0, 0, 0); return date >= tomorrow; }; // Calcul du prix total avec options const calculateTotalPrice = (service, selectedOption) => { let total = service.price; if (selectedOption) { total += selectedOption.price || 0; } return total; }; // ────────────────────────────────────────────────────────────────────────── // ENVOI EMAIL // ────────────────────────────────────────────────────────────────────────── const sendBookingEmail = async (booking) => { const { praticienne, category, service, selectedOption, date, time, clientInfo } = booking; const dateFormatted = formatDate(date); const totalPrice = calculateTotalPrice(service, selectedOption); const optionText = selectedOption ? ` (${selectedOption.name})` : ''; const htmlSalon = `

SERENYS

MAISON DE BEAUTÉ

Nouvelle demande de rendez-vous

Client

Nom: ${clientInfo.lastName}

Prénom: ${clientInfo.firstName}

Email: ${clientInfo.email}

Téléphone: ${clientInfo.phone}

Rendez-vous

Praticien/ne: ${praticienne.name}

Catégorie: ${category.name}

Service: ${service.name}${optionText}

Durée: ${formatDuration(service.duration)}

Date: ${dateFormatted}

Heure: ${time}

Prix: ${formatPrice(totalPrice)}

`; const htmlClient = `

SERENYS

MAISON DE BEAUTÉ

Bonjour ${clientInfo.firstName},

Nous avons bien reçu votre demande de rendez-vous. Nous vous confirmerons la disponibilité rapidement.

Récapitulatif

Praticien/ne: ${praticienne.name}

Service: ${service.name}${optionText}

Date souhaitée: ${dateFormatted}

Heure souhaitée: ${time}

Durée: ${formatDuration(service.duration)}

Prix: ${formatPrice(totalPrice)}

À très bientôt chez Serenys !

Serenys - Maison de Beauté

Champel, Genève | info@serenys.ch

`; try { const response = await fetch(API_CONFIG.mailsApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: API_CONFIG.apiKey, from: API_CONFIG.fromEmail, from_name: API_CONFIG.fromName, to: API_CONFIG.toEmail, cc: clientInfo.email, subject: `Nouvelle demande - ${praticienne.name} - ${service.name} - ${clientInfo.firstName} ${clientInfo.lastName}`, html: htmlSalon }) }); if (!response.ok) throw new Error('Erreur envoi email'); return { success: true }; } catch (error) { console.error('Erreur email:', error); return { success: false, error: error.message }; } }; // ────────────────────────────────────────────────────────────────────────── // COMPOSANTS - Header // ────────────────────────────────────────────────────────────────────────── const Header = () => (
SERENYS

Maison de Beauté

); // ────────────────────────────────────────────────────────────────────────── // COMPOSANTS - Stepper // ────────────────────────────────────────────────────────────────────────── const Stepper = ({ currentStep, steps, onStepClick }) => (
{steps.map((step, index) => (
index < currentStep && onStepClick(index)} >
{index < currentStep ? '✓' : index + 1}
{step.label}
{index < steps.length - 1 && (
)} ))}
); // ────────────────────────────────────────────────────────────────────────── // ÉTAPE 1 - Choix de la praticienne // ────────────────────────────────────────────────────────────────────────── const StepPraticienne = ({ selectedPraticienne, onSelect }) => (

Bienvenue chez Serenys

Choisissez votre praticien/ne

{CATALOGUE.praticiennes.map((prat, index) => (
onSelect(prat)} className={`card-praticienne ${selectedPraticienne?.id === prat.id ? 'selected' : ''}`} style={{ animationDelay: `${index * 0.08}s`, '--prat-color': prat.color }} >
{prat.avatar}

{prat.name}

{prat.subtitle && (

{prat.subtitle}

)}

{prat.description}

))}
); // ────────────────────────────────────────────────────────────────────────── // ÉTAPE 2 - Choix de la catégorie // ────────────────────────────────────────────────────────────────────────── const StepCategory = ({ praticienne, selectedCategory, onSelect, onBack }) => (
{praticienne.avatar}

{praticienne.name}

Choisissez une catégorie

{praticienne.categories.map((cat, index) => (
onSelect(cat)} className={`card-category-sm ${selectedCategory?.id === cat.id ? 'selected' : ''}`} style={{ animationDelay: `${index * 0.05}s` }} >

{cat.name}

{cat.services.length} service{cat.services.length > 1 ? 's' : ''}

))}
); // ────────────────────────────────────────────────────────────────────────── // ÉTAPE 3 - Choix du service // ────────────────────────────────────────────────────────────────────────── const StepService = ({ praticienne, category, selectedService, onSelect, onBack }) => (

{category.name}

{praticienne.name}

{category.services.map((service, index) => (
onSelect(service)} className={`card-service-sm ${selectedService?.id === service.id ? 'selected' : ''}`} style={{ animationDelay: `${index * 0.04}s` }} >

{service.name}

{service.promo && ( {service.promoLabel || 'Promo'} )} {service.isForfait && ( Forfait )}
{service.description && (

{service.description}

)}
{formatDuration(service.duration)} {service.options && service.options.length > 0 && ( + options )}
{formatPrice(service.price)}
))}
); // ────────────────────────────────────────────────────────────────────────── // ÉTAPE 4 - Options (si applicable) // ────────────────────────────────────────────────────────────────────────── const StepOptions = ({ service, selectedOption, onSelect, onSkip, onBack }) => { if (!service.options || service.options.length === 0) { // Pas d'options, passer à l'étape suivante automatiquement useEffect(() => { onSkip(); }, []); return null; } return (

{service.name}

Choisissez une option

{service.options.map((option, index) => (
onSelect(option)} className={`card-option ${selectedOption?.id === option.id ? 'selected' : ''}`} style={{ animationDelay: `${index * 0.05}s` }} >
{option.name} {option.price > 0 ? `+${formatPrice(option.price)}` : 'Inclus'}
))}
); }; // ────────────────────────────────────────────────────────────────────────── // ÉTAPE 5 - Choix de la date // ────────────────────────────────────────────────────────────────────────── const StepDate = ({ selectedDate, onSelect, onBack }) => { const today = new Date(); const [currentMonth, setCurrentMonth] = useState(today.getMonth()); const [currentYear, setCurrentYear] = useState(today.getFullYear()); const days = getMonthDays(currentYear, currentMonth); const goToPrevMonth = () => { if (currentMonth === 0) { setCurrentMonth(11); setCurrentYear(currentYear - 1); } else { setCurrentMonth(currentMonth - 1); } }; const goToNextMonth = () => { if (currentMonth === 11) { setCurrentMonth(0); setCurrentYear(currentYear + 1); } else { setCurrentMonth(currentMonth + 1); } }; const isDateAvailable = (date) => { if (!date) return false; return isAtLeast24hAhead(date) && date.getDay() !== 0; }; const isSelected = (date) => { if (!date || !selectedDate) return false; return date.toDateString() === selectedDate.toDateString(); }; const canGoPrev = currentYear > today.getFullYear() || (currentYear === today.getFullYear() && currentMonth > today.getMonth()); return (

Choisissez une date

Minimum 24h à l'avance

{MONTHS[currentMonth]} {currentYear}
{DAYS.map(day => (
{day}
))}
{days.map((date, index) => (
isDateAvailable(date) && onSelect(date)} className={`calendar-day-sm ${ !date ? '' : !isDateAvailable(date) ? 'disabled' : isSelected(date) ? 'selected' : '' }`} > {date ? date.getDate() : ''}
))}

Fermé le dimanche

); }; // ────────────────────────────────────────────────────────────────────────── // ÉTAPE 6 - Choix de l'heure // ────────────────────────────────────────────────────────────────────────── const StepTime = ({ selectedDate, selectedTime, onSelect, onBack }) => { const [slots, setSlots] = useState([]); useEffect(() => { if (selectedDate) { setSlots(generateTimeSlots(selectedDate)); } }, [selectedDate]); return (

Choisissez l'heure

{formatDateShort(selectedDate)}

{slots.length > 0 ? ( <>

{slots.length} créneaux disponibles

{slots.map((slot, index) => (
onSelect(slot)} className={`time-slot-sm ${selectedTime === slot ? 'selected' : ''}`} style={{ animationDelay: `${index * 0.02}s` }} > {slot}
))}
) : (

Aucun créneau disponible

)}
); }; // ────────────────────────────────────────────────────────────────────────── // ÉTAPE 7 - Infos client // ────────────────────────────────────────────────────────────────────────── const StepInfos = ({ clientInfo, onChange, onBack, errors }) => (

Vos coordonnées

Pour confirmer votre rendez-vous

onChange('lastName', e.target.value)} placeholder="Votre nom" className={`input-field ${errors.lastName ? 'border-red-400' : ''}`} /> {errors.lastName &&

{errors.lastName}

}
onChange('firstName', e.target.value)} placeholder="Votre prénom" className={`input-field ${errors.firstName ? 'border-red-400' : ''}`} /> {errors.firstName &&

{errors.firstName}

}
onChange('email', e.target.value)} placeholder="votre@email.com" className={`input-field ${errors.email ? 'border-red-400' : ''}`} /> {errors.email &&

{errors.email}

}
onChange('phone', e.target.value)} placeholder="+41 79 123 45 67" className={`input-field ${errors.phone ? 'border-red-400' : ''}`} /> {errors.phone &&

{errors.phone}

}
); // ────────────────────────────────────────────────────────────────────────── // ÉTAPE 8 - Résumé & Confirmation // ────────────────────────────────────────────────────────────────────────── const StepSummary = ({ booking, onConfirm, onBack, isSubmitting }) => { const totalPrice = calculateTotalPrice(booking.service, booking.selectedOption); const optionText = booking.selectedOption ? ` (${booking.selectedOption.name})` : ''; return (

Récapitulatif

Client

{booking.clientInfo.firstName} {booking.clientInfo.lastName}

{booking.clientInfo.email}

{booking.clientInfo.phone}

Praticien/ne

{booking.praticienne.name}

Service

{booking.service.name}{optionText}

{booking.category.name}

Date

{formatDateShort(booking.date)}

Heure

{booking.time}

Durée

{formatDuration(booking.service.duration)}

Prix

{formatPrice(totalPrice)}

Vous recevrez un email de confirmation

); }; // ────────────────────────────────────────────────────────────────────────── // ÉCRAN DE CONFIRMATION // ────────────────────────────────────────────────────────────────────────── const ConfirmationScreen = ({ booking, onNewBooking }) => { const totalPrice = calculateTotalPrice(booking.service, booking.selectedOption); return (

Demande envoyée !

Merci {booking.clientInfo.firstName} ! Nous vous confirmerons rapidement votre rendez-vous avec {booking.praticienne.name}.

Service {booking.service.name}
Date {formatDateShort(booking.date)}
Heure {booking.time}
Total {formatPrice(totalPrice)}

Confirmation envoyée à
{booking.clientInfo.email}

); }; // ────────────────────────────────────────────────────────────────────────── // APPLICATION PRINCIPALE // ────────────────────────────────────────────────────────────────────────── const App = () => { const [step, setStep] = useState(0); const [booking, setBooking] = useState({ praticienne: null, category: null, service: null, selectedOption: null, date: null, time: null, clientInfo: { firstName: '', lastName: '', email: '', phone: '' } }); const [formErrors, setFormErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [isConfirmed, setIsConfirmed] = useState(false); // Définir les étapes dynamiquement selon si le service a des options const hasOptions = booking.service?.options?.length > 0; const steps = [ { id: 'praticienne', label: 'Praticien/ne' }, { id: 'category', label: 'Catégorie' }, { id: 'service', label: 'Service' }, ...(hasOptions ? [{ id: 'options', label: 'Options' }] : []), { id: 'date', label: 'Date' }, { id: 'time', label: 'Heure' }, { id: 'infos', label: 'Infos' }, { id: 'summary', label: 'Confirmer' } ]; // Mapping des étapes const getStepIndex = (stepName) => { if (!hasOptions && stepName === 'options') return -1; return steps.findIndex(s => s.id === stepName); }; const getCurrentStepId = () => steps[step]?.id; const handleSelectPraticienne = (prat) => { setBooking(prev => ({ ...prev, praticienne: prat, category: null, service: null, selectedOption: null })); setStep(1); }; const handleSelectCategory = (cat) => { setBooking(prev => ({ ...prev, category: cat, service: null, selectedOption: null })); setStep(2); }; const handleSelectService = (service) => { setBooking(prev => ({ ...prev, service, selectedOption: null })); // Si le service a des options, aller à l'étape options, sinon passer directement à la date if (service.options && service.options.length > 0) { setStep(3); } else { // Pas d'options, passer à la date (qui sera à l'index 3 sans options) setStep(3); } }; const handleSelectOption = (option) => { setBooking(prev => ({ ...prev, selectedOption: option })); setStep(step + 1); }; const handleSkipOptions = () => { setStep(step + 1); }; const handleSelectDate = (date) => { setBooking(prev => ({ ...prev, date, time: null })); setStep(step + 1); }; const handleSelectTime = (time) => { setBooking(prev => ({ ...prev, time })); setStep(step + 1); }; const handleClientInfoChange = (field, value) => { setBooking(prev => ({ ...prev, clientInfo: { ...prev.clientInfo, [field]: value } })); if (formErrors[field]) { setFormErrors(prev => ({ ...prev, [field]: null })); } }; const validateClientInfo = () => { const errors = {}; const { clientInfo } = booking; if (!clientInfo.lastName.trim()) errors.lastName = 'Le nom est requis'; if (!clientInfo.firstName.trim()) errors.firstName = 'Le prénom est requis'; if (!clientInfo.email.trim()) { errors.email = 'L\'email est requis'; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(clientInfo.email)) { errors.email = 'Email invalide'; } if (!clientInfo.phone.trim()) errors.phone = 'Le téléphone est requis'; setFormErrors(errors); return Object.keys(errors).length === 0; }; const handleContinue = () => { const currentId = getCurrentStepId(); if (currentId === 'infos') { if (validateClientInfo()) { setStep(step + 1); } } else { setStep(step + 1); } }; const handleConfirm = async () => { setIsSubmitting(true); const result = await sendBookingEmail(booking); setIsSubmitting(false); if (result.success) { setIsConfirmed(true); } else { alert('Une erreur est survenue. Veuillez réessayer.'); } }; const handleNewBooking = () => { setBooking({ praticienne: null, category: null, service: null, selectedOption: null, date: null, time: null, clientInfo: { firstName: '', lastName: '', email: '', phone: '' } }); setFormErrors({}); setStep(0); setIsConfirmed(false); }; const handleStepClick = (targetStep) => { if (targetStep < step) { setStep(targetStep); } }; if (isConfirmed) { return (
); } const currentId = getCurrentStepId(); // Déterminer si on peut continuer (pour le bouton flottant) const canContinue = () => { switch (currentId) { case 'service': return !!booking.service; case 'options': return !!booking.selectedOption; case 'date': return !!booking.date; case 'time': return !!booking.time; case 'infos': return true; // Validation au clic default: return false; } }; // Afficher le bouton continuer ? const showContinueButton = ['service', 'date', 'time', 'infos'].includes(currentId) || (currentId === 'options' && hasOptions); return (
{currentId === 'praticienne' && ( )} {currentId === 'category' && ( setStep(0)} /> )} {currentId === 'service' && ( setStep(1)} /> )} {currentId === 'options' && hasOptions && ( setStep(2)} /> )} {currentId === 'date' && ( setStep(hasOptions ? 3 : 2)} /> )} {currentId === 'time' && ( setStep(step - 1)} /> )} {currentId === 'infos' && ( setStep(step - 1)} errors={formErrors} /> )} {currentId === 'summary' && ( setStep(step - 1)} isSubmitting={isSubmitting} /> )}
{/* Bouton Continuer flottant */} {showContinueButton && currentId !== 'summary' && (
)}
); }; // Rendu de l'application const root = ReactDOM.createRoot(document.getElementById('root')); root.render();