<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>
{# datetime-picker.html.twig #}
<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>
{# Sélection de la date #}
<div class="bg-white rounded-3xl p-8">
{# Navigation du mois #}
<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>
{# Calendrier #}
<div class="grid grid-cols-7 gap-0">
{# Jours de la semaine #}
<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>
{# Grille des jours #}
<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"
>
{# Jour non disponible avec motif diagonal #}
<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>
{# Contenu du jour #}
<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>
{# Bordure de sélection #}
<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>
{# Légende #}
<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>
{# Sélection de l'heure #}
<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>
/* No context defined. */
No notes defined.