/** * Composable d'erreurs de formulaire — convention de mapping erreur→champ pour * tous les forms du projet (ERP-101). * * Le back renvoie TOUTES les violations d'une 422 d'un coup (un `propertyPath` * + `message` par champ fautif). Ce composable centralise leur affichage * inline : il tient un `Record` reactif que le template * branche directement sur la prop `:error` des composants `Malio*` (le nom du * champ cote front = le `propertyPath` cote back, donc aucun mapping manuel). * * Chaque appel cree son propre etat (refs internes a la fonction) — un form = * une instance, pas de singleton partage. * * Convention d'usage : les appels API qui veulent un retour inline doivent * passer `{ toast: false }` a `useApi` (sinon le toast natif masque le mapping * fin), puis router l'erreur via `handleApiError`. Pour les collections (1 * appel par ligne), utiliser directement `mapViolationsToRecord` par ligne. */ import { computed, reactive } from 'vue' import { extractApiErrorMessage, mapViolationsToRecord } from '~/shared/utils/api' /** * Erreur HTTP capturee par ofetch. On n'expose que les champs lus ici (status * + payload) pour eviter de typer toute la lib. */ interface ApiFetchError { response?: { status?: number _data?: unknown } } /** Options de `handleApiError`. */ interface HandleApiErrorOptions { /** Message de toast si l'erreur n'est pas une 422 exploitable. */ fallbackMessage?: string } export function useFormErrors() { const toast = useToast() // Etat d'erreurs indexe par propertyPath. Reactif : muter une cle suffit a // rafraichir la prop `:error` du champ correspondant. const errors = reactive>({}) const hasErrors = computed(() => Object.keys(errors).length > 0) /** Pose une erreur sur un champ. */ function setError(field: string, message: string): void { errors[field] = message } /** Retire l'erreur d'un champ (no-op si absente). */ function clearError(field: string): void { delete errors[field] } /** Vide toutes les erreurs (a appeler en debut de submit). */ function clearErrors(): void { for (const key of Object.keys(errors)) { delete errors[key] } } /** * Mappe les violations 422 d'un payload sur les champs. Retourne true des * qu'au moins une violation a ete posee, false sinon (payload sans * violation exploitable). */ function setServerErrors(data: unknown): boolean { const mapped = mapViolationsToRecord(data) const keys = Object.keys(mapped) if (keys.length === 0) return false for (const key of keys) { errors[key] = mapped[key] } return true } /** * Route une erreur API : 422 avec violations exploitables → mapping inline * (pas de toast, l'erreur s'affiche sous le champ) ; sinon → toast de * fallback (message serveur extrait, ou `fallbackMessage`). * * Retourne true si l'erreur a ete mappee inline, false si fallback toast. */ function handleApiError(e: unknown, opts: HandleApiErrorOptions = {}): boolean { const status = (e as ApiFetchError)?.response?.status const data = (e as ApiFetchError)?.response?._data if (status === 422 && setServerErrors(data)) { return true } const message = extractApiErrorMessage(data) || opts.fallbackMessage || 'Une erreur est survenue.' toast.error({ title: 'Erreur', message }) return false } return { errors, hasErrors, setError, clearError, clearErrors, setServerErrors, handleApiError, } }