<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
x-data="formAppointment()"
class="w-full max-w-3xl mx-auto space-y-8"
>
<h1 class="text-3xl font-normal">Information du contact</h1>
{# Section de connexion #}
<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">
{% render "@icons-library--user-outline" with {
iconClass: "text-neutral-900"
} %}
</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">
{% render "@icons-library--chevron-right" with {
iconClass: "text-neutral-900"
} %}
</div>
</div>
{# Formulaire #}
<form @submit.prevent="validateForm" class="space-y-6">
{# Prénom #}
<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>
{# Nom #}
<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>
{# E-mail #}
<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>
{# Téléphone #}
<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>
{# Commentaire (facultatif) #}
<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>
{# Checkboxes #}
<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>
{# Bouton de soumission #}
<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>
/* No context defined. */
No notes defined.