// ═══════════════════════════════════════════════════════════════════════════ // 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 = `
MAISON DE BEAUTÉ
Nom: ${clientInfo.lastName}
Prénom: ${clientInfo.firstName}
Email: ${clientInfo.email}
Téléphone: ${clientInfo.phone}
Praticien/ne: ${praticienne.name}
Catégorie: ${category.name}
Service: ${service.name}${optionText}
Durée: ${formatDuration(service.duration)}
Date: ${dateFormatted}
Heure: ${time}
Prix: ${formatPrice(totalPrice)}
MAISON DE BEAUTÉ
Nous avons bien reçu votre demande de rendez-vous. Nous vous confirmerons la disponibilité rapidement.
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
Maison de Beauté
Choisissez votre praticien/ne
{prat.subtitle}
)}{prat.description}
Choisissez une catégorie
{cat.services.length} service{cat.services.length > 1 ? 's' : ''}
{praticienne.name}
{service.description}
)}Choisissez une option
Minimum 24h à l'avance
Fermé le dimanche
{formatDateShort(selectedDate)}
{slots.length} créneaux disponibles
Aucun créneau disponible
Pour confirmer votre rendez-vous
{errors.lastName}
}{errors.firstName}
}{errors.email}
}{errors.phone}
}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
Merci {booking.clientInfo.firstName} ! Nous vous confirmerons rapidement votre rendez-vous avec {booking.praticienne.name}.
Confirmation envoyée à
{booking.clientInfo.email}