<div x-data class="fixed right-0 z-40 inset-y-0 max-w-full">
<div x-cloak x-transition.opacity x-show="$store.asideBlocs.asides.find(aside => aside.name === 'storeLocatorAppointment')?.open" class="fixed inset-0 w-full h-full bg-dark-40 backdrop-blur-xl"></div>
<div x-cloak x-transition:enter="transition ease-out duration-300" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full" x-show="$store.asideBlocs.asides.find(aside => aside.name === 'storeLocatorAppointment')?.open" class="h-full relative bg-light-white overflow-hidden w-screen md:max-w-screen-sm flex flex-col" @click.outside="$store.asideBlocs.closeAside('storeLocatorAppointment')">
<div class="p-4 md:px-10 md:py-6 font-medium text-2xl flex justify-between items-center">
Livraison et retours
<button type="button" @click="$store.asideBlocs.closeAside('storeLocatorAppointment')" class="max-md:btn-size-sm btn btn-dark-ghost btn-only-icon">
<svg class=" shrink-0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.99469 7.9047C8.70179 7.6118 8.22692 7.6118 7.93403 7.9047C7.64113 8.19759 7.64113 8.67246 7.93403 8.96536L10.9392 11.9706L7.93403 14.9758C7.64114 15.2687 7.64114 15.7435 7.93403 16.0364C8.22693 16.3293 8.7018 16.3293 8.99469 16.0364L11.9999 13.0312L15.0051 16.0364C15.298 16.3293 15.7729 16.3293 16.0658 16.0364C16.3586 15.7435 16.3586 15.2687 16.0658 14.9758L13.0606 11.9706L16.0658 8.96536C16.3587 8.67246 16.3587 8.19759 16.0658 7.9047C15.7729 7.6118 15.298 7.6118 15.0051 7.9047L11.9999 10.9099L8.99469 7.9047Z" fill="currentColor" />
</svg>
</button>
</div>
<div class="px-4 md:px-10 overflow-auto flex-1 border-t border-b border-neutral-200">
<div x-data="stepInit()" x-init="init">
<template x-if="!isInitialized">
<div>Chargement...</div>
</template>
<template x-if="isInitialized">
<div class="w-full max-w-3xl mx-auto py-8">
<div x-show="!stepperData.completed">
<div class="relative">
<div class="absolute top-4 w-full h-[1px] bg-gray-200">
<div class="absolute top-0 left-0 h-full transition-all duration-500" :style="'width: ' + (((stepperData.currentStep - 1) / (stepperData.steps.length - 1)) * 100) + '%; background-color: #B8A369;'">
</div>
</div>
<div class="relative flex justify-between">
<template x-for="(step, index) in stepperData.steps" :key="step.id">
<div class="flex flex-col items-center" :class="{ 'pointer-events-none': step.id === 1 }">
<div class="w-8 h-8 rounded-full border-2 flex items-center justify-center relative bg-white" :class="{
'border-brand-400 bg-brand-400 text-white': step.completed,
'border-neutral-300 text-neutral-500': !step.completed && step.id !== stepperData.currentStep,
'border-neutral-300 text-neutral-700': !step.completed && step.id === stepperData.currentStep
}">
<template x-if="step.completed">
<svg class="text-brand-400 shrink-0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM16.5303 9.86366L11.197 15.197C10.9041 15.4899 10.4292 15.4899 10.1363 15.197L7.46967 12.5303C7.17678 12.2374 7.17678 11.7626 7.46967 11.4697C7.76256 11.1768 8.23744 11.1768 8.53033 11.4697L10.6667 13.606L15.4697 8.803C15.7626 8.51011 16.2374 8.51011 16.5303 8.803C16.8232 9.0959 16.8232 9.57077 16.5303 9.86366Z" fill="currentColor" />
</svg>
</template>
<template x-if="!step.completed">
<span x-text="step.id"></span>
</template>
</div>
<span class="mt-2 text-sm font-medium" :class="{
'text-brand-400': step.completed || step.id === stepperData.currentStep,
'text-neutral-500': !step.completed && step.id !== stepperData.currentStep
}" x-text="step.title"></span>
</div>
</template>
</div>
</div>
<div class="space-y-6">
<template x-for="(item, index) in stepperData.data" :key="index">
<div class="border-b border-gray-200 pb-4 last:border-b-0">
<p class="text-sm font-normal text-gray-500 mb-1" x-text="item.title"></p>
<div class="flex justify-between items-center">
<p class="text-base text-gray-900" x-text="item.value"></p>
<button class="text-sm text-gray-900 underline hover:no-underline focus:outline-none" @click="goToStep(index + 1)">
Modifier
</button>
</div>
</div>
</template>
<template x-if="stepperData.data.length <= 1">
<p class="text-sm text-gray-500 italic">
Complétez les étapes suivantes pour voir le récapitulatif
</p>
</template>
</div>
<div class="mt-8 p-6">
<template x-for="step in stepperData.steps" :key="step.id">
<div x-show="stepperData.currentStep === step.id" class="space-y-4">
<div x-show="step.id === 2">
<div x-data="servicesInit()" class="w-full max-w-3xl mx-auto space-y-6">
<!-- Pour chaque catégorie -->
<template x-for="(code, index) in Object.keys(categories)" :key="code">
<div class="border-b border-gray-200 last:border-b-0" x-show="services[code]">
<div class="py-4">
<h2 class="text-gray-600 text-sm font-medium mb-4" x-text="categories[code].title"></h2>
<!-- Liste des services -->
<div class="space-y-4">
<template x-for="service in services[code]" :key="service.serviceCode">
<div x-data="serviceCardInit()" class="bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow duration-200 cursor-pointer p-6" @click="selectService($event, service)">
<div class="flex items-start gap-4">
<!-- Icône du service -->
<div class="flex-shrink-0">
<div class="w-8 h-8 text-[#B8A369]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 3H17V5H19V8C19 9.9 17.7 11.4 16 11.9V19C16 20.7 14.7 22 13 22H11C9.3 22 8 20.7 8 19V11.9C6.3 11.4 5 9.9 5 8V5H7V3ZM9 3V5H15V3H9ZM7 7V8C7 9.7 8.3 11 10 11H14C15.7 11 17 9.7 17 8V7H7Z" />
</svg>
</div>
</div>
<!-- Contenu principal -->
<div class="flex-1">
<div class="flex flex-col">
<h3 class="text-lg text-neutral-900 font-medium mb-1" x-text="service.service"></h3>
<div data-expand-trigger @click.stop="isExpanded = !isExpanded">
<button class="text-neutral-500 text-sm hover:text-neutral-700 transition-colors w-fit" :class="{ 'underline': !isExpanded }">
<span x-text="isExpanded ? 'Masquer' : 'En savoir plus'"></span>
<span class="inline-block ml-1 transition-transform duration-200" :class="{ 'rotate-180': isExpanded }">↓</span>
</button>
</div>
</div>
<!-- Contenu dépliable -->
<div x-show="isExpanded" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 transform -translate-y-2" x-transition:enter-end="opacity-100 transform translate-y-0" class="mt-4 space-y-4" @click.stop>
<p class="text-sm text-neutral-600">
Durée approximative : <span x-text="service.duration + ' minutes'"></span>
</p>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
</div>
<script>
function servicesInit() {
return {
services: [],
categories: {
'AR-001': {
title: 'SANTÉ DES YEUX',
icon: 'library--vision-test'
},
'AR-002': {
title: 'LENTILLES',
icon: 'library--contact-lenses'
},
'AR-004': {
title: 'VISAGISME',
icon: 'library--glasses'
}
},
async init() {
try {
const response = await fetch(`${window.location.origin}/js/json/getStoreServices.json`);
const data = await response.json();
// Grouper les services par areaCode
this.services = data.reduce((acc, service) => {
if (!acc[service.areaCode]) {
acc[service.areaCode] = [];
}
acc[service.areaCode].push(service);
return acc;
}, {});
} catch (error) {
console.error('Erreur lors du chargement des services:', error);
this.services = {};
}
}
}
}
function serviceCardInit() {
return {
isExpanded: false,
selectService(event, currentService) {
// Ignorer si le clic est sur le bouton 'En savoir plus' ou son conteneur
if (event.target.closest('[data-expand-trigger]')) {
return;
}
// Utiliser le store locator pour mettre à jour les données
this.$store.locator.updateStepperData(
'SUJET DU RENDEZ-VOUS',
currentService.service,
this.$store.locator.stepperData.currentStep + 1
);
}
}
}
</script>
</div>
<div x-show="step.id === 3">
<div x-data="datePicker()" x-init="init().then()" class="w-full max-w-3xl mx-auto space-y-8">
<h1 class="text-3xl font-normal mb-8">Date et heure du rendez-vous</h1>
<div class="bg-white rounded-3xl p-8">
<div class="flex justify-center items-center mb-8 relative">
<button @click="handlePreviousClick().then()" class="absolute left-0 w-10 h-10 flex items-center justify-center rounded-full bg-neutral-100 hover:bg-neutral-200 transition-colors">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" class="text-neutral-600">
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<span x-text="displayRange" class="text-xl font-normal"></span>
<button @click="handleNextClick().then()" class="absolute right-0 w-10 h-10 flex items-center justify-center rounded-full bg-neutral-100 hover:bg-neutral-200 transition-colors">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" class="text-neutral-600">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
<div class="grid grid-cols-7 gap-0">
<template x-for="day in ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']">
<div class="text-center py-2 text-neutral-900 font-normal text-lg" x-text="day"></div>
</template>
<template x-for="day in days">
<div class="relative border-[0.5px] border-neutral-200">
<button @click="selectDate(day)" class="w-full aspect-square flex items-center justify-center relative" :class="{
'bg-green-50': day.isAvailable,
'cursor-pointer hover:bg-green-100': day.isAvailable,
'cursor-not-allowed bg-neutral-50': !day.isAvailable
}" :disabled="!day.isAvailable">
<div x-show="!day.isAvailable" class="absolute inset-0 bg-[linear-gradient(to_top_right,transparent_calc(50%_-_1px),#e5e7eb,transparent_calc(50%_+_1px))]">
</div>
<div class="relative flex flex-col items-center" :class="{
'text-green-700': day.isAvailable,
'text-neutral-400': !day.isAvailable
}">
<span class="text-lg" x-text="day.dayOfMonth"></span>
<div x-show="day.isAvailable" class="w-1.5 h-1.5 rounded-full bg-green-700 mt-1">
</div>
</div>
<div x-show="selectedDate && day.date.getTime() === selectedDate.getTime()" class="absolute inset-0 border-2 border-green-700 pointer-events-none">
</div>
</button>
</div>
</template>
</div>
<div class="mt-4 flex items-center gap-2 text-green-700">
<div class="w-1.5 h-1.5 rounded-full bg-green-700"></div>
<span class="text-sm">Jours avec des horaires disponibles</span>
</div>
</div>
<div x-show="selectedDate" class="space-y-4">
<h2 class="text-xl">Sélectionner l'horaire</h2>
<div class="grid grid-cols-4 gap-4">
<template x-for="slot in availableHours" :key="slot.initialHour">
<button @click="selectTime(slot.initialHour)" class="py-3 px-6 rounded-full border border-gray-200 hover:border-brand-400 transition-colors" :class="{
'bg-brand-50 border-brand-400': selectedTime === slot.initialHour,
'bg-white': selectedTime !== slot.initialHour
}">
<span x-text="slot.initialHour"></span>
</button>
</template>
</div>
</div>
</div>
<script>
function datePicker() {
return {
// États initiaux explicitement définis
startDate: new Date(), // On initialise directement avec une date par défaut
selectedDate: null,
selectedTime: null,
days: [],
availabilities: {},
availableHours: [],
isLoading: false,
displayRange: '',
initialized: false,
// Getter pour accéder aux données du stepper
get stepperData() {
return this.$store.locator.stepperData;
},
// Nouvelle méthode d'initialisation
async init() {
if (this.initialized) return;
try {
this.isLoading = true;
// Initialisation avec la date du jour
const today = new Date();
// Calcul du début de la semaine
let startOfWeek = new Date(today);
startOfWeek.setHours(0, 0, 0, 0);
// Ajustement pour commencer au lundi
const dayOfWeek = today.getDay();
const diff = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
startOfWeek.setDate(today.getDate() + diff);
// Définition explicite de la date de début
this.startDate = startOfWeek;
// Initialisation du calendrier
await this.updateDateRange(this.startDate);
this.initialized = true;
} catch (error) {
console.error('Erreur lors de l\'initialisation:', error);
} finally {
this.isLoading = false;
}
},
// Le reste des méthodes reste identique à votre code original
async fetchAvailabilities(startDate, endDate) {
try {
const url = new URL(`${window.location.origin}/js/json/getStoreServiceAvailabilities.json`);
url.searchParams.append('date_start', this.formatDateISO(startDate));
url.searchParams.append('date_end', this.formatDateISO(endDate));
const response = await fetch(url);
const data = await response.json();
this.availabilities = data.reduce((acc, item) => {
acc[item.disponibilityDay] = item.disponibilityHours;
return acc;
}, {});
} catch (error) {
console.error('Erreur lors de la récupération des disponibilités:', error);
this.availabilities = {};
}
},
// Mise à jour améliorée du calendrier
async updateDateRange(newStartDate) {
if (!newStartDate) return;
try {
this.isLoading = true;
// Création d'une copie de la date de début
const startDate = new Date(newStartDate);
startDate.setHours(0, 0, 0, 0);
// Calcul de la date de fin
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 13);
// Récupération des disponibilités
await this.fetchAvailabilities(startDate, endDate);
// Mise à jour de l'état
this.startDate = startDate;
// Mise à jour de l'affichage
this.updateCalendarDisplay();
} catch (error) {
console.error('Erreur lors de la mise à jour:', error);
} finally {
this.isLoading = false;
}
},
// Navigation améliorée
async handlePreviousClick() {
if (this.isLoading) return;
const newStartDate = new Date(this.startDate);
newStartDate.setDate(newStartDate.getDate() - 14);
await this.updateDateRange(newStartDate);
},
async handleNextClick() {
if (this.isLoading) return;
const newStartDate = new Date(this.startDate);
newStartDate.setDate(newStartDate.getDate() + 14);
await this.updateDateRange(newStartDate);
},
// Mise à jour robuste de l'affichage du calendrier
updateCalendarDisplay() {
if (!this.startDate) return;
const newDays = [];
const endDate = new Date(this.startDate);
endDate.setDate(endDate.getDate() + 13);
// Itération sur les dates
for (let currentDate = new Date(this.startDate); currentDate <= endDate; currentDate.setDate(currentDate.getDate() + 1)) {
const dateStr = this.formatDateISO(new Date(currentDate));
const isAvailable = Boolean(
this.availabilities &&
this.availabilities[dateStr] &&
this.availabilities[dateStr].length > 0
);
newDays.push({
date: new Date(currentDate),
dayOfMonth: currentDate.getDate(),
isAvailable,
dateStr
});
}
this.days = newDays;
this.updateDisplayRange();
},
// Formatage amélioré des dates
formatDateISO(date) {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
updateDisplayRange() {
if (!this.startDate) return;
const endDate = new Date(this.startDate);
endDate.setDate(endDate.getDate() + 13);
const formatOptions = {
day: 'numeric',
month: 'short',
year: 'numeric'
};
this.displayRange = `du ${this.startDate.toLocaleDateString('fr-FR', formatOptions)} au ${endDate.toLocaleDateString('fr-FR', formatOptions)}`;
},
// Sélection d'une date
selectDate(day) {
if (!day.isAvailable) return;
this.selectedDate = day.date;
this.selectedTime = null;
this.availableHours = this.availabilities[day.dateStr] || [];
},
// Formatage d'une date selon le format demandé
formatDate(date, format = 'long') {
const options = format === 'long' ?
{
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
} :
{
day: 'numeric',
month: 'short',
year: 'numeric'
};
return date.toLocaleDateString('fr-FR', options);
},
// Sélection d'un créneau horaire
selectTime(time) {
if (!this.selectedDate) return;
// Formatage de la date et l'heure pour l'affichage
const dateStr = this.selectedDate.toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
});
// Mise à jour des données du stepper et passage à l'étape suivante
this.$store.locator.updateStepperData(
'DATE ET HEURE DU RENDEZ-VOUS',
`${dateStr} à ${time}`,
this.stepperData.currentStep + 1
);
}
};
}
</script>
</div>
<div x-show="step.id === 4">
<div x-data="formAppointment()" class="w-full max-w-3xl mx-auto space-y-8">
<h1 class="text-3xl font-normal">Information du contact</h1>
<div class="bg-neutral-50 rounded-lg p-6 flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-6 h-6">
<svg class="text-neutral-900 shrink-0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 3.75C14.3472 3.75 16.25 5.65279 16.25 8C16.25 10.3472 14.3472 12.25 12 12.25C9.65279 12.25 7.75 10.3472 7.75 8C7.75 5.65279 9.65279 3.75 12 3.75ZM15.1254 12.8272C16.7051 11.8024 17.75 10.0232 17.75 8C17.75 4.82436 15.1756 2.25 12 2.25C8.82436 2.25 6.25 4.82436 6.25 8C6.25 10.0232 7.29494 11.8024 8.87458 12.8272C7.73658 13.2624 6.69098 13.9347 5.81282 14.8128C4.17187 16.4538 3.25 18.6794 3.25 21C3.25 21.4142 3.58579 21.75 4 21.75C4.41421 21.75 4.75 21.4142 4.75 21C4.75 19.0772 5.51384 17.2331 6.87348 15.8735C8.23311 14.5138 10.0772 13.75 12 13.75C13.9228 13.75 15.7669 14.5138 17.1265 15.8735C18.4862 17.2331 19.25 19.0772 19.25 21C19.25 21.4142 19.5858 21.75 20 21.75C20.4142 21.75 20.75 21.4142 20.75 21C20.75 18.6794 19.8281 16.4538 18.1872 14.8128C17.309 13.9347 16.2634 13.2624 15.1254 12.8272Z" fill="currentColor" />
</svg>
</div>
<p class="text-neutral-900">
Vous avez déjà un compte ?
<a href="#" class="text-neutral-900 underline hover:no-underline">Connectez-vous</a>
pour retrouver vos informations et gérer vos rendez-vous.
</p>
</div>
<div class="w-6 h-6">
<svg class="text-neutral-900 shrink-0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5303 11.4697C14.8232 11.7626 14.8232 12.2374 14.5303 12.5303L10.5303 16.5303C10.2374 16.8232 9.76256 16.8232 9.46967 16.5303C9.17678 16.2374 9.17678 15.7626 9.46967 15.4697L12.9393 12L9.46967 8.53033C9.17678 8.23744 9.17678 7.76256 9.46967 7.46967C9.76256 7.17678 10.2374 7.17678 10.5303 7.46967L14.5303 11.4697Z" fill="currentColor" />
</svg>
</div>
</div>
<form @submit.prevent="validateForm" class="space-y-6">
<div>
<label class="block text-neutral-900 mb-2">
Prénom <span class="text-red-500">*</span>
</label>
<input type="text" x-model="formData.firstName" class="w-full px-4 py-3 rounded-lg border border-neutral-200 focus:border-brand-400 focus:ring-1 focus:ring-brand-400" :class="{ 'border-red-500': errors.firstName }">
<p x-show="errors.firstName" x-text="errors.firstName" class="mt-1 text-red-500 text-sm"></p>
</div>
<div>
<label class="block text-neutral-900 mb-2">
Nom <span class="text-red-500">*</span>
</label>
<input type="text" x-model="formData.lastName" class="w-full px-4 py-3 rounded-lg border border-neutral-200 focus:border-brand-400 focus:ring-1 focus:ring-brand-400" :class="{ 'border-red-500': errors.lastName }">
<p x-show="errors.lastName" x-text="errors.lastName" class="mt-1 text-red-500 text-sm"></p>
</div>
<div>
<label class="block text-neutral-900 mb-2">
E-mail <span class="text-red-500">*</span>
</label>
<input type="email" x-model="formData.email" class="w-full px-4 py-3 rounded-lg border border-neutral-200 focus:border-brand-400 focus:ring-1 focus:ring-brand-400" :class="{ 'border-red-500': errors.email }">
<p x-show="errors.email" x-text="errors.email" class="mt-1 text-red-500 text-sm"></p>
</div>
<div>
<label class="block text-neutral-900 mb-2">
Téléphone <span class="text-red-500">*</span>
</label>
<input type="tel" x-model="formData.phone" class="w-full px-4 py-3 rounded-lg border border-neutral-200 focus:border-brand-400 focus:ring-1 focus:ring-brand-400" :class="{ 'border-red-500': errors.phone }">
<p x-show="errors.phone" x-text="errors.phone" class="mt-1 text-red-500 text-sm"></p>
</div>
<div>
<button type="button" @click="showComment = !showComment" class="flex items-center gap-2 text-neutral-900 hover:underline">
<span class="w-6 h-6">+</span>
Ajouter un commentaire (facultatif)
</button>
<div x-show="showComment" x-transition class="mt-4">
<textarea x-model="formData.comment" class="w-full px-4 py-3 rounded-lg border border-neutral-200 focus:border-brand-400 focus:ring-1 focus:ring-brand-400" rows="4"></textarea>
</div>
</div>
<div class="space-y-4">
<label class="flex items-start gap-3 cursor-pointer">
<input type="checkbox" x-model="formData.acceptPolicy" class="mt-1 rounded border-neutral-300 text-brand-400 focus:ring-brand-400" :class="{ 'border-red-500': errors.policy }">
<span class="text-sm">
ALAIN AFFLELOU, en tant que responsable de traitement, collecte vos données
personnelles afin de créer votre compte, vous faire parvenir vos informations de
rendez-vous et nos offres commerciales lorsque vous l'acceptez. Pour en savoir plus
sur vos droits et la gestion de vos données personnelles, consultez notre
<a href="#" class="underline hover:no-underline">Politique de confidentialité</a>.
</span>
</label>
<p x-show="errors.policy" x-text="errors.policy" class="text-red-500 text-sm"></p>
</div>
<button type="submit" class="w-full py-4 bg-neutral-500 text-white rounded-lg hover:bg-neutral-600 transition-colors">
Confirmer mon rendez-vous
</button>
</form>
</div>
<script>
function formAppointment() {
return {
formData: {
firstName: '',
lastName: '',
email: '',
phone: '',
comment: '',
acceptPolicy: false
},
showComment: false,
errors: {},
isInitialized: false,
isSubmitting: false, // Nouveau flag pour gérer l'état de soumission
get stepperData() {
return this.$store.locator.stepperData;
},
init() {
// S'il y a déjà des données à l'étape 4, charger les données immédiatement
if (this.stepperData?.currentStep === 4) {
this.loadSavedData();
}
},
// Le reste des méthodes de votre composant restent inchangées
loadSavedData() {
const contactInfo = this.stepperData.data.find(item => item.title === 'INFORMATION DU CONTACT');
if (contactInfo && contactInfo.details) {
this.formData = {
...this.formData,
...contactInfo.details
};
}
},
validateForm() {
this.errors = {};
if (!this.formData.firstName) this.errors.firstName = 'Le prénom est requis';
if (!this.formData.lastName) this.errors.lastName = 'Le nom est requis';
if (!this.formData.email) this.errors.email = 'L\'email est requis';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.formData.email)) {
this.errors.email = 'L\'email n\'est pas valide';
}
if (!this.formData.phone) this.errors.phone = 'Le téléphone est requis';
if (!this.formData.acceptPolicy) this.errors.policy = 'Vous devez accepter la politique de confidentialité';
if (Object.keys(this.errors).length === 0) {
this.submitForm();
}
},
async submitForm() {
if (!this.isInitialized || this.isSubmitting) return;
this.isSubmitting = true;
try {
const appointmentData = {
store: this.stepperData.data.find(item => item.title === 'MAGASIN')?.value,
service: this.stepperData.data.find(item => item.title === 'SUJET DU RENDEZ-VOUS')?.value,
datetime: this.stepperData.data.find(item => item.title === 'DATE ET HEURE DU RENDEZ-VOUS')?.value,
contact: {
firstName: this.formData.firstName,
lastName: this.formData.lastName,
email: this.formData.email,
phone: this.formData.phone,
comment: this.formData.comment,
},
metadata: {
code_mur: this.stepperData.code_mur,
type: this.stepperData.type
}
};
// Simulation d'un délai réseau
await new Promise(resolve => setTimeout(resolve, 1000));
// Choisir aléatoirement entre succès et erreur
const mockApiUrl = Math.random() < 0.7 ?
'/js/json/appointment-success.json' // 70% de chance de succès
:
'/js/json/appointment-error.json'; // 30% de chance d'erreur
const response = await fetch(mockApiUrl);
// Exemple d'appel api
// const response = await fetch('/api/appointments', {
// method: 'POST',
// headers: {'Content-Type': 'application/json'},
// body: JSON.stringify(appointmentData)
// });
const data = await response.json();
if (!data.success) {
throw new Error(data.error.message);
}
// Si succès, mettre à jour le stepperData
this.$store.locator.updateStepperData(
'INFORMATION DU CONTACT',
`${this.formData.firstName} ${this.formData.lastName}`,
null, {
details: {
firstName: this.formData.firstName,
lastName: this.formData.lastName,
email: this.formData.email,
phone: this.formData.phone,
comment: this.formData.comment,
acceptPolicy: this.formData.acceptPolicy,
appointmentDetails: data.data // Stocker les détails du rendez-vous
}
}
);
// Marquer comme complété
this.$store.locator.completeStepperData(data.appointmentId);
// Message de succès
this.$dispatch('show-toast', {
type: 'success',
message: data.message
});
} catch (error) {
console.error('Erreur lors de la soumission:', error);
// Message d'erreur
this.$dispatch('show-toast', {
type: 'error',
message: error.message || 'Une erreur est survenue lors de la prise de rendez-vous. Veuillez réessayer.'
});
} finally {
this.isSubmitting = false;
}
}
}
}
</script>
</div>
</div>
</template>
</div>
</div>
<div x-show="stepperData.completed">
<div x-data="appointmentConfirmation()" x-init="init()" class="w-full max-w-3xl mx-auto space-y-12">
<div class="bg-green-50 rounded-2xl p-8 space-y-8">
<div class="flex items-center gap-4">
<div class="w-8 h-8 text-green-600">
<svg class="text-green-600 w-8 h-8 shrink-0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.75 12C2.75 6.89137 6.89137 2.75 12 2.75C17.1086 2.75 21.25 6.89137 21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12ZM12 1.25C6.06294 1.25 1.25 6.06294 1.25 12C1.25 17.9371 6.06294 22.75 12 22.75C17.9371 22.75 22.75 17.9371 22.75 12C22.75 6.06294 17.9371 1.25 12 1.25ZM16.5303 9.86366C16.8232 9.57077 16.8232 9.0959 16.5303 8.803C16.2374 8.51011 15.7626 8.51011 15.4697 8.803L10.6667 13.606L8.53033 11.4697C8.23744 11.1768 7.76256 11.1768 7.46967 11.4697C7.17678 11.7626 7.17678 12.2374 7.46967 12.5303L10.1363 15.197C10.4292 15.4899 10.9041 15.4899 11.197 15.197L16.5303 9.86366Z" fill="currentColor" />
</svg>
</div>
<h1 class="text-3xl font-normal">Rendez-vous confirmé</h1>
</div>
<p class="text-neutral-700">
Vous trouverez le récapitulatif de votre rendez-vous ci-dessous. Nous vous l'envoyons également par e-mail (vérifiez vos spams) et par SMS si vous avez activé cette option.
</p>
<div class="bg-white rounded-xl p-6 space-y-4">
<template x-for="item in stepperData.data" :key="item.title">
<div>
<p class="text-neutral-600" x-text="item.title + ' : '"></p>
<p class="font-medium" x-text="item.value"></p>
<template x-if="item.details">
<div class="mt-2 text-sm text-neutral-500">
<template x-if="item.title === 'INFORMATION DU CONTACT'">
<div>
<p x-text="'Email : ' + item.details.email"></p>
<p x-text="'Téléphone : ' + item.details.phone"></p>
<template x-if="item.details.comment">
<p x-text="'Commentaire : ' + item.details.comment"></p>
</template>
</div>
</template>
</div>
</template>
</div>
</template>
</div>
<div class="space-y-4">
<p class="text-neutral-700">Ne manquez pas votre rendez-vous en l'ajoutant à votre agenda en ligne :</p>
<div class="flex gap-4 flex-wrap">
<a href="#" target="_blank" rel="noopenner noreferer" :href="getGoogleCalendarUrl()" class=" btn btn-dark-subtle ">
Ajouter à Google Agenda
</a>
<a href="#" target="_blank" rel="noopenner noreferer" :href="getOutlookCalendarUrl()" class=" btn btn-dark-subtle ">
Ajouter à l'agenda Outlook
</a>
</div>
</div>
</div>
<div class="space-y-6">
<h2 class="text-3xl font-normal">Votre pré-bilan pour gagner du temps</h2>
<p class="text-neutral-700">
Avant votre rendez-vous, remplissez ce questionnaire qui aidera votre opticien à vous recommander les solutions les plus adaptées à votre quotidien.
</p>
<a href="#" target="_blank" rel="noopenner noreferer" class=" btn btn-dark-ghost ">
Répondre au questionnaire
</a>
</div>
</div>
<script>
function appointmentConfirmation() {
return {
get stepperData() {
return this.$store.locator.stepperData;
},
init() {
this.$watch('stepperData.completed', (value) => {
if (value) {
console.log('Composant confirmation visible, initialisation...');
}
});
},
getCalendarData() {
if (!this.stepperData?.data) return null;
const location = this.stepperData.data.find(item => item.title === 'MAGASIN')?.value || '';
const subject = this.stepperData.data.find(item => item.title === 'SUJET DU RENDEZ-VOUS')?.value || '';
const datetime = this.stepperData.data.find(item => item.title === 'DATE ET HEURE DU RENDEZ-VOUS')?.value || '';
const contact = this.stepperData.data.find(item => item.title === 'INFORMATION DU CONTACT');
return {
location,
subject,
datetime,
contact: contact?.value || '',
email: contact?.details?.email || '',
phone: contact?.details?.phone || ''
};
},
getGoogleCalendarUrl() {
const data = this.getCalendarData();
if (!data) return '#';
const params = new URLSearchParams({
action: 'TEMPLATE',
text: `${data.subject} - Afflelou`,
details: `Rendez-vous chez Afflelou\n${data.contact}\nEmail: ${data.email}\nTél: ${data.phone}`,
location: data.location,
dates: this.formatDateForCalendar(data.datetime)
});
return `https://calendar.google.com/calendar/render?${params.toString()}`;
},
getOutlookCalendarUrl() {
const data = this.getCalendarData();
if (!data) return '#';
const params = new URLSearchParams({
path: '/calendar/action/compose',
rru: 'addevent',
subject: `${data.subject} - Afflelou`,
body: `Rendez-vous chez Afflelou\n${data.contact}\nEmail: ${data.email}\nTél: ${data.phone}`,
location: data.location,
startdt: this.formatDateForCalendar(data.datetime)
});
return `https://outlook.live.com/calendar/0/${params.toString()}`;
},
formatDateForCalendar(datetimeStr) {
// Exemple de format d'entrée : "Lundi 15 janvier 2024 à 14:30"
const dateRegex = /(\d{1,2})\s+(\w+)\s+(\d{4})\s+à\s+(\d{1,2}):(\d{2})/;
const matches = datetimeStr.match(dateRegex);
if (!matches) return '';
const [, day, month, year, hour, minute] = matches;
const monthMap = {
'janvier': '01',
'février': '02',
'mars': '03',
'avril': '04',
'mai': '05',
'juin': '06',
'juillet': '07',
'août': '08',
'septembre': '09',
'octobre': '10',
'novembre': '11',
'décembre': '12'
};
const monthNum = monthMap[month.toLowerCase()];
const dateObj = new Date(`${year}-${monthNum}-${day}T${hour}:${minute}:00`);
return dateObj.toISOString().replace(/[:-]/g, '').split('.')[0] + 'Z';
}
}
}
</script>
</div>
</div>
</template>
</div>
<script>
function stepInit() {
return {
isInitialized: false,
init() {
this.isInitialized = true;
},
get stepperData() {
return this.$store.locator.stepperData || this.$store.locator.defaultStepperState;
},
nextStep() {
if (!this.isInitialized) return;
if (this.stepperData.currentStep < 4) {
this.$store.locator.updateStepperSteps(this.stepperData.currentStep + 1);
} else {
this.$store.locator.stepperData.completed = true;
}
},
previousStep() {
if (!this.isInitialized) return;
if (this.stepperData.currentStep > 2) {
this.$store.locator.updateStepperSteps(this.stepperData.currentStep - 1);
}
},
goToStep(targetStep) {
if (!this.isInitialized) return;
this.$store.locator.goToStepperStep(targetStep);
},
updateStepData(title, value, targetStep) {
if (!this.isInitialized) return;
this.$store.locator.updateStepperData(title, value, targetStep);
}
}
}
</script>
</div>
</div>
</div>
{% embed "@side-panel" with panel|merge({sidePanelButton:panel.sidePanelButton|merge({label:(panel.sidePanelButton.label is defined ? panel.sidePanelButton.label|format(product_quantity|default('600')))})}) %}
{% block content %}
<div x-data="stepInit()" x-init="init">
<template x-if="!isInitialized">
<div>Chargement...</div>
</template>
<template x-if="isInitialized">
<div class="w-full max-w-3xl mx-auto py-8">
{# DATA NOT COMPLETED #}
<div x-show="!stepperData.completed">
{# STEPPER VIEW #}
<div class="relative">
{# Ligne de progression #}
<div class="absolute top-4 w-full h-[1px] bg-gray-200">
<div class="absolute top-0 left-0 h-full transition-all duration-500"
:style="'width: ' + (((stepperData.currentStep - 1) / (stepperData.steps.length - 1)) * 100) + '%; background-color: #B8A369;'">
</div>
</div>
{# Points et titres #}
<div class="relative flex justify-between">
<template x-for="(step, index) in stepperData.steps" :key="step.id">
<div class="flex flex-col items-center"
:class="{ 'pointer-events-none': step.id === 1 }">
{# Cercle de l'étape #}
<div class="w-8 h-8 rounded-full border-2 flex items-center justify-center relative bg-white"
:class="{
'border-brand-400 bg-brand-400 text-white': step.completed,
'border-neutral-300 text-neutral-500': !step.completed && step.id !== stepperData.currentStep,
'border-neutral-300 text-neutral-700': !step.completed && step.id === stepperData.currentStep
}">
<template x-if="step.completed">
{% render "@icons-library--check-circle-fill" with {
iconClass:"text-brand-400"
} %}
</template>
<template x-if="!step.completed">
<span x-text="step.id"></span>
</template>
</div>
{# Titre de l'étape #}
<span class="mt-2 text-sm font-medium"
:class="{
'text-brand-400': step.completed || step.id === stepperData.currentStep,
'text-neutral-500': !step.completed && step.id !== stepperData.currentStep
}"
x-text="step.title"></span>
</div>
</template>
</div>
</div>
{# VIEW DATA STEPPER #}
<div class="space-y-6">
<template x-for="(item, index) in stepperData.data" :key="index">
<div class="border-b border-gray-200 pb-4 last:border-b-0">
<p class="text-sm font-normal text-gray-500 mb-1" x-text="item.title"></p>
<div class="flex justify-between items-center">
<p class="text-base text-gray-900" x-text="item.value"></p>
<button
class="text-sm text-gray-900 underline hover:no-underline focus:outline-none"
@click="goToStep(index + 1)"
>
Modifier
</button>
</div>
</div>
</template>
<template x-if="stepperData.data.length <= 1">
<p class="text-sm text-gray-500 italic">
Complétez les étapes suivantes pour voir le récapitulatif
</p>
</template>
</div>
{# Contenu de l'étape #}
<div class="mt-8 p-6">
<template x-for="step in stepperData.steps" :key="step.id">
<div x-show="stepperData.currentStep === step.id" class="space-y-4">
{% for stepNumber in 1..4 %}
{% if stepNumber > 1 %}
<div x-show="step.id === {{ stepNumber }}">
{% set template = "@storelocator-step" ~ stepNumber %}
{% include template %}
</div>
{% endif %}
{% endfor %}
</div>
</template>
</div>
</div>
{# DATA COMPLETED, SHOW VALIDATION MESSAGE #}
<div x-show="stepperData.completed">
{% render "@stepcomplet" %}
</div>
</div>
</template>
</div>
<script>
function stepInit() {
return {
isInitialized: false,
init() {
this.isInitialized = true;
},
get stepperData() {
return this.$store.locator.stepperData || this.$store.locator.defaultStepperState;
},
nextStep() {
if (!this.isInitialized) return;
if (this.stepperData.currentStep < 4) {
this.$store.locator.updateStepperSteps(this.stepperData.currentStep + 1);
} else {
this.$store.locator.stepperData.completed = true;
}
},
previousStep() {
if (!this.isInitialized) return;
if (this.stepperData.currentStep > 2) {
this.$store.locator.updateStepperSteps(this.stepperData.currentStep - 1);
}
},
goToStep(targetStep) {
if (!this.isInitialized) return;
this.$store.locator.goToStepperStep(targetStep);
},
updateStepData(title, value, targetStep) {
if (!this.isInitialized) return;
this.$store.locator.updateStepperData(title, value, targetStep);
}
}
}
</script>
{% endblock %}
{% endembed %}
{
"panel": {
"title": "Livraison et retours",
"sidePanelButton": false,
"showValue": "$store.asideBlocs.asides.find(aside => aside.name === 'storeLocatorAppointment')?.open",
"closeButtonAlpineClick": "$store.asideBlocs.closeAside('storeLocatorAppointment')"
}
}
No notes defined.