[[toc]]
Composable central pour la gestion du formulaire multi-étapes de dépannage. Il gère l'état global du formulaire, la navigation entre les étapes, la validation avec Zod, et la persistance des données via sessionStorage.
Le composable useTroubleshootingForm est le cœur du système de formulaire de dépannage. Il centralise :
sessionStorageflowchart TD
A[useTroubleshootingForm] --> B[État réactif]
A --> C[Navigation]
A --> D[Persistance]
A --> E[Validation]
B --> B1[formData ref]
B --> B2[completedSteps ref]
B --> B3[apiResponse ref]
C --> C1[nextStep/prevStep]
C --> C2[goToStep]
C --> C3[isStepAccessible]
D --> D1[sessionStorage]
D --> D2[watch auto-save]
D --> D3[loadFromStorage]
E --> E1[Schémas Zod]
E --> E2[addressSchema]
E --> E3[contactSchema]
E --> E4[diagnosisSchema]
Le composable utilise ref() plutôt que useState() pour éviter les conflits d'hydration SSR. Les données sont chargées depuis sessionStorage au montage et sauvegardées automatiquement via watch.
// Clés de stockage
const STORAGE_KEY = "troubleshooting_form_data";
const COMPLETED_STEPS_KEY = "troubleshooting_completed_steps";
const API_RESPONSE_KEY = "troubleshooting_api_response";
// Chargement depuis sessionStorage
const loadFromStorage = () => {
if (import.meta.client) {
try {
const stored = sessionStorage.getItem(STORAGE_KEY);
if (stored) return JSON.parse(stored);
} catch (error) {
console.error("Error loading form data from storage:", error);
}
}
return initialState;
};
// État réactif
const formData = ref<TroubleshootingForm>(loadFromStorage());
// Sauvegarde automatique
if (import.meta.client) {
watch(
formData,
(newData) => {
try {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(newData));
} catch (error) {
console.error("Error saving form data to storage:", error);
}
},
{ deep: true }
);
}
Le composable exporte des constantes utilisables en dehors du contexte Nuxt (par exemple dans definePageMeta).
// Étapes disponibles
export const STEPS = {
POSTAL_CODE: "postal-code",
CONTRACT: "contrat",
DIAGNOSIS: "diagnostic",
AVAILABILITY: "disponibilites",
CONTACT: "contact",
CONFIRMATION: "confirmation",
} as const;
export type StepKey = (typeof STEPS)[keyof typeof STEPS];
// Ordre des étapes
export const STEP_ORDER: StepKey[] = [
STEPS.POSTAL_CODE,
STEPS.CONTRACT,
STEPS.DIAGNOSIS,
STEPS.AVAILABILITY,
STEPS.CONTACT,
];
formData: Readonly<Ref<TroubleshootingForm>>État réactif en lecture seule contenant toutes les données du formulaire.
const { formData } = useTroubleshootingForm();
console.log(formData.value.address.postalCode);
console.log(formData.value.diagnosis);
Structure :
type TroubleshootingForm = {
address: {
postalCode: string; // Code postal (5 chiffres)
city: string; // Ville
street: string; // Adresse complète
name: string; // Prénom
lastname: string; // Nom
email: string; // Email
phone: string; // Téléphone
};
hasContract: boolean; // Possède un contrat d'entretien
diagnosis: string[]; // IDs des diagnostics sélectionnés
details: string; // Description détaillée du problème
availability: string[]; // Créneaux disponibles (morning, afternoon)
availabilityDetails: string; // Précisions sur les disponibilités
}
completedSteps: Readonly<Ref<Set<StepKey>>>Set des étapes complétées par l'utilisateur.
const { completedSteps } = useTroubleshootingForm();
if (completedSteps.value.has(STEPS.POSTAL_CODE)) {
console.log("L'utilisateur a complété l'étape code postal");
}
apiResponse: Readonly<Ref<any>>Réponse de l'API après soumission du formulaire.
const { apiResponse } = useTroubleshootingForm();
if (apiResponse.value) {
console.log("ID de la demande:", apiResponse.value.id);
console.log("Statut:", apiResponse.value.status);
}
currentStep: Ref<number>Index de l'étape actuelle (0 à 5).
const { currentStep, totalSteps } = useTroubleshootingForm();
console.log(`Étape ${currentStep.value + 1} sur ${totalSteps}`);
totalSteps: numberNombre total d'étapes (6 avec confirmation).
nextStep(): voidPasse à l'étape suivante si disponible.
const { nextStep, currentStep } = useTroubleshootingForm();
nextStep();
console.log("Nouvelle étape:", currentStep.value);
prevStep(): voidRevient à l'étape précédente si disponible.
const { prevStep, currentStep } = useTroubleshootingForm();
prevStep();
console.log("Étape précédente:", currentStep.value);
goToStep(stepKey: StepKey): voidNavigation directe vers une étape spécifique.
const { goToStep } = useTroubleshootingForm();
goToStep(STEPS.DIAGNOSIS);
Paramètres :
stepKey : Clé de l'étape (depuis STEPS)isStepAccessible(stepKey: StepKey): booleanVérifie si une étape est accessible (étapes précédentes complétées).
const { isStepAccessible, markStepComplete } = useTroubleshootingForm();
// Marquer la première étape comme complétée
markStepComplete(STEPS.POSTAL_CODE);
// Vérifier l'accès à la deuxième étape
if (isStepAccessible(STEPS.CONTRACT)) {
console.log("L'étape contrat est accessible");
}
Paramètres :
stepKey : Clé de l'étape à vérifierRetourne : true si l'étape est accessible, false sinon
getNextStep(): StepKey | nullRetourne la clé de l'étape suivante ou null si c'est la dernière.
const { getNextStep } = useTroubleshootingForm();
const next = getNextStep();
if (next) {
console.log("Prochaine étape:", next);
}
getPrevStep(): StepKey | nullRetourne la clé de l'étape précédente ou null si c'est la première.
const { getPrevStep } = useTroubleshootingForm();
const prev = getPrevStep();
if (prev) {
console.log("Étape précédente:", prev);
}
updateForm(data: Partial<TroubleshootingForm>): voidMet à jour partiellement les données du formulaire.
const { updateForm, formData } = useTroubleshootingForm();
// Mise à jour de l'adresse
updateForm({
address: {
...formData.value.address,
postalCode: "33300",
city: "Bordeaux",
street: "125 Quai des Chartrons",
},
});
// Mise à jour du diagnostic
updateForm({
diagnosis: ["13", "15"],
details: "La pompe à chaleur fait un bruit anormal",
});
Paramètres :
data : Objet partiel contenant les champs à mettre à jourNote : Les données sont automatiquement sauvegardées dans sessionStorage via watch.
markStepComplete(stepKey: StepKey): voidMarque une étape comme complétée.
const { markStepComplete } = useTroubleshootingForm();
// Après validation réussie d'une étape
markStepComplete(STEPS.POSTAL_CODE);
Paramètres :
stepKey : Clé de l'étape à marquer comme complétéeresetForm(): voidRéinitialise le formulaire à son état initial et efface toutes les données de sessionStorage.
const { resetForm } = useTroubleshootingForm();
// Après soumission ou pour recommencer
resetForm();
Actions effectuées :
formData à l'état initialcompletedStepsapiResponsecurrentStep à 0sessionStoragesetApiResponse(response: any): voidStocke la réponse de l'API après soumission du formulaire.
const { setApiResponse } = useTroubleshootingForm();
try {
const response = await $fetch("/api/troubleshooting", {
method: "POST",
body: troubleshootingData,
});
setApiResponse(response);
await navigateTo(`/depannage/confirmation?id=${response.id}`);
} catch (error) {
console.error("Erreur:", error);
}
Paramètres :
response : Objet contenant la réponse de l'APIgetApiResponseById(id: string): any | nullRécupère la réponse API si son ID correspond.
const { getApiResponseById } = useTroubleshootingForm();
const response = getApiResponseById("019b22da-84ef-7f14-9396-03c67440a810");
if (response) {
console.log("Statut:", response.status);
}
Paramètres :
id : ID de la demande à vérifierRetourne : L'objet réponse si l'ID correspond, null sinon
clearApiResponse(): voidEfface la réponse API stockée.
const { clearApiResponse } = useTroubleshootingForm();
// Avant de commencer une nouvelle demande
clearApiResponse();
getStepProgress(): { completed: number; total: number; percentage: number }Retourne la progression du formulaire.
const { getStepProgress } = useTroubleshootingForm();
const progress = getStepProgress();
console.log(`${progress.completed} / ${progress.total} (${progress.percentage}%)`);
Retourne :
completed : Nombre d'étapes complétéestotal : Nombre total d'étapespercentage : Pourcentage de progressionLe composable exporte plusieurs schémas Zod pour la validation des données.
postalCodeSchemaValidation du code postal et de l'adresse.
import { postalCodeSchema } from "~/composables/useTroubleshootingForm";
const result = postalCodeSchema.safeParse({
postalCode: "33300",
city: "Bordeaux",
street: "125 Quai des Chartrons",
});
if (!result.success) {
console.error(result.error.errors);
}
Règles :
postalCode : Exactement 5 chiffrescity : Non videstreet : Non videcontactSchemaValidation des informations de contact.
import { contactSchema } from "~/composables/useTroubleshootingForm";
const result = contactSchema.safeParse({
name: "Lucas",
lastname: "Dupont",
email: "lucas@example.com",
phone: "06 75 85 85 56",
});
Règles :
name : Minimum 2 caractèreslastname : Minimum 2 caractèresemail : Format email validephone : Minimum 10 caractèrescontractSchemaValidation de l'information sur le contrat existant.
import { contractSchema } from "~/composables/useTroubleshootingForm";
const result = contractSchema.safeParse({
hasContract: false,
});
Règles :
hasContract : Booléen obligatoirediagnosisSchemaValidation du diagnostic et des détails.
import { diagnosisSchema } from "~/composables/useTroubleshootingForm";
const result = diagnosisSchema.safeParse({
diagnosis: ["13", "15"],
details: "La pompe à chaleur fait un bruit anormal depuis ce matin",
});
Règles :
diagnosis : Au moins un diagnostic sélectionnédetails : Minimum 10 caractèresavailabilitySchemaValidation des créneaux de disponibilité.
import { availabilitySchema } from "~/composables/useTroubleshootingForm";
const result = availabilitySchema.safeParse({
availability: ["morning", "afternoon"],
availabilityDetails: "Disponible toute la semaine sauf jeudi",
});
Règles :
availability : Au moins un créneau sélectionnéavailabilityDetails : OptionneltroubleshootingSchemaSchéma complet combinant tous les autres.
import { troubleshootingSchema } from "~/composables/useTroubleshootingForm";
const result = troubleshootingSchema.safeParse(formData.value);
if (!result.success) {
console.error("Validation échouée:", result.error);
}
<template>
<UCard>
<UForm :schema="postalCodeSchema" :state="form" @submit="handleSubmit">
<UFormField label="Code postal" name="postalCode">
<UInput v-model="form.postalCode" placeholder="33300" />
</UFormField>
<UFormField label="Ville" name="city">
<UInput v-model="form.city" placeholder="Bordeaux" />
</UFormField>
<UFormField label="Adresse" name="street">
<UInput v-model="form.street" placeholder="125 Quai des Chartrons" />
</UFormField>
<div class="flex gap-4 mt-6">
<UButton
v-if="getPrevStep()"
@click="handlePrev"
color="neutral"
variant="outline"
icon="i-heroicons-arrow-left"
>
Précédent
</UButton>
<UButton type="submit" icon="i-heroicons-arrow-right" trailing>
Suivant
</UButton>
</div>
</UForm>
</UCard>
</template>
<script setup lang="ts">
import { STEPS, postalCodeSchema } from "~/composables/useTroubleshootingForm";
definePageMeta({
layout: "troubleshooting",
middleware: ["auth", "troubleshooting-step"],
});
const {
formData,
updateForm,
markStepComplete,
nextStep,
prevStep,
getPrevStep,
} = useTroubleshootingForm();
const form = reactive({
postalCode: formData.value.address.postalCode,
city: formData.value.address.city,
street: formData.value.address.street,
});
const handleSubmit = () => {
// Mise à jour des données
updateForm({
address: {
...formData.value.address,
postalCode: form.postalCode,
city: form.city,
street: form.street,
},
});
// Marquer l'étape comme complétée
markStepComplete(STEPS.POSTAL_CODE);
// Passer à l'étape suivante
nextStep();
};
const handlePrev = () => {
prevStep();
};
</script>
<script setup lang="ts">
import { STEPS } from "~/composables/useTroubleshootingForm";
import { useAuth } from "~/composables/useAuth";
const {
formData,
updateForm,
markStepComplete,
setApiResponse,
} = useTroubleshootingForm();
const { getToken } = useAuth();
const toast = useToast();
const isSubmitting = ref(false);
const handleSubmit = async () => {
isSubmitting.value = true;
try {
const token = getToken();
const response = await $fetch("/api/troubleshooting", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: {
address: formData.value.address,
hasContract: formData.value.hasContract,
diagnosis: formData.value.diagnosis,
details: formData.value.details,
availability: formData.value.availability,
availabilityDetails: formData.value.availabilityDetails,
},
});
// Stocker la réponse
setApiResponse(response);
// Marquer l'étape comme complétée
markStepComplete(STEPS.CONTACT);
// Rediriger vers la confirmation
await navigateTo(`/depannage/confirmation?id=${response.id}`);
} catch (error: any) {
toast.add({
title: "Erreur",
description: error.data?.detail || "Une erreur est survenue",
color: "error",
});
} finally {
isSubmitting.value = false;
}
};
</script>
<script setup lang="ts">
const route = useRoute();
const requestId = route.query.id as string;
const { clearApiResponse, resetForm } = useTroubleshootingForm();
const { getToken } = useAuth();
// Récupération des données depuis l'API
const { data: requestData } = await useFetch(
`/api/troubleshooting/request/${requestId}`,
{
headers: {
Authorization: `Bearer ${getToken()}`,
},
}
);
// Nouvelle demande
const handleNewRequest = async () => {
clearApiResponse();
resetForm();
await navigateTo("/depannage/postal-code");
};
</script>
ref() au lieu de useState() ?useState() est SSR-friendly mais peut causer des conflits d'hydration lorsque les données sont également stockées dans sessionStorage. En utilisant ref(), on garantit que :
sessionStorage uniquement côté clientTrois clés distinctes permettent de gérer séparément :
troubleshooting_form_data : Données du formulaire en courstroubleshooting_completed_steps : Étapes validées (pour contrôle d'accès)troubleshooting_api_response : Réponse API après soumission (pour confirmation)sessionStorage garantit que :
useForm| Critère | useTroubleshootingForm | useForm |
|---|---|---|
| Usage | Formulaire de dépannage | Formulaire de contrat d'entretien |
| Étapes | 6 étapes | 5 étapes |
| Données | Adresse + Contact fusionnés | Séparés |
| Persistance | ref() + sessionStorage | useState() |
| Validation | Schémas Zod spécifiques | Schémas Zod différents |
| API Response | Stockée pour confirmation | Non stockée |
const result = schema.safeParse(form);
if (!result.success) {
// Afficher les erreurs
return;
}
updateForm(result.data);
// Après validation réussie
markStepComplete(STEPS.POSTAL_CODE);
if (isStepAccessible(STEPS.CONTRACT)) {
goToStep(STEPS.CONTRACT);
}
// Nouvelle demande
clearApiResponse();
resetForm();
await navigateTo("/depannage/postal-code");
formData.value// ❌ Mauvais
formData.value.address.postalCode = "33300";
// ✅ Bon
updateForm({
address: {
...formData.value.address,
postalCode: "33300",
},
});
ref() + sessionStorage pour éviter les conflits SSR