# ERP-101 — Mapping des erreurs de validation par champ (convention forms) > Statut : design validé — implémentation TDD en cours > Branche : `feat/ERP-101-form-field-validation-mapping` > Date : 2026-06-03 ## Problème Quand le back renvoie une **422** (violations API Platform), il renvoie **toutes** les violations d'un coup (un `propertyPath` + `message` par champ fautif). Aujourd'hui, seul le drawer Catégorie (`useCategoryForm`) exploite ce détail pour afficher l'erreur **sous le champ concerné** ; il le fait via un `if/else` manuel par champ, non réutilisable. Le formulaire Client (≈ 20 champs sur 5 submits, dont 3 collections) ne mappe rien : une 422 multi-champs ⇒ un seul **toast global**. On veut un retour par champ, et surtout **une convention unique réutilisée par tous les modules**. ## Décisions 1. **Primitif générique** plutôt que composable par form : `useFormErrors()` partagé. 2. **Périmètre complet** sur Client : champs scalaires **et** collections (erreur par ligne). ## Architecture — 3 briques ### 1. `mapViolationsToRecord(data)` — `frontend/shared/utils/api.ts` Util pur, fondation réutilisée partout. Transforme un payload 422 en `Record`. S'appuie sur `extractApiViolations` (déjà existant, gère les formats `violations` et `hydra:violations`). ```ts export function mapViolationsToRecord(data: unknown): Record { const out: Record = {} for (const v of extractApiViolations(data)) { if (v.propertyPath) out[v.propertyPath] = v.message } return out } ``` ### 2. `useFormErrors()` — `frontend/shared/composables/useFormErrors.ts` API que tous les forms **scalaires** consomment. ```ts const { errors, hasErrors, setServerErrors, setError, clearError, clearErrors, handleApiError } = useFormErrors() ``` - `errors` : `reactive>` indexé par `propertyPath`. - `setServerErrors(data)` : `mapViolationsToRecord` → remplit `errors`. Retourne `true` si au moins une violation a été mappée. - `setError(field, msg)` / `clearError(field)` / `clearErrors()` : manipulation fine. - `hasErrors` : `computed` booléen. - `handleApiError(e, opts?)` : dispatch standard depuis une erreur ofetch — **422** → `setServerErrors` (mapping inline, pas de toast) ; **autre** → toast générique de fallback (message extrait via `extractApiErrorMessage`). Côté template, le nom du champ **est** le `propertyPath` : ```vue ``` > L'unicité SIREN (RG-1.15) remonte en **422 `UniqueEntity` avec `propertyPath: "siren"`** > → mappée automatiquement. Pas de cas 409 spécial (contrairement à Catégorie). ### 3. Collections — erreurs par ligne Chaque ligne (contact / adresse / RIB) est persistée par **son propre appel API**, donc le back renvoie un 422 **relatif à la sous-entité** (`propertyPath: "email"`, `"iban"`…). - Le parent tient, par collection, un tableau d'erreurs **aligné sur l'index de ligne** : `const contactErrors = ref[]>([])`. - Au submit de la ligne `i` : `catch` → `contactErrors.value[i] = mapViolationsToRecord(data)`. - On `clearErrors` la collection au début de chaque passe de submit. - Les blocs reçoivent une prop `:errors` (`Record`) et bindent `:error="errors?.email"` sur chaque champ Malio. ## Fichiers touchés | Fichier | Action | |---|---| | `shared/utils/api.ts` | + `mapViolationsToRecord` | | `shared/composables/useFormErrors.ts` | **nouveau** composable | | `modules/commercial/pages/clients/new.vue` | scalaires (Main/Info/Compta) + erreurs par ligne | | `modules/commercial/pages/clients/[id]/edit.vue` | idem | | `modules/commercial/components/ClientContactBlock.vue` | + prop `:errors`, bind `:error` | | `modules/commercial/components/ClientAddressBlock.vue` | + prop `:errors`, bind `:error` | | RIB (inline dans new/edit) | bind `:error` par ligne | ## Tests (Vitest — règle « pas d'E2E ») - `mapViolationsToRecord` : formats `violations` / `hydra:violations`, payload vide, `propertyPath` manquant. - `useFormErrors` : `setServerErrors` mappe et retourne `true` / `false` sans violation, `clearErrors`, fallback toast sur non-422. ## Convention posée pour tous les forms À reporter dans `.claude/rules/frontend.md` une fois le pattern stabilisé : > Tout form qui veut un retour d'erreur par champ : appels API en `{ toast: false }` + > `useFormErrors` pour les champs scalaires (422 inline), `mapViolationsToRecord` par > ligne pour les collections. `useCategoryForm` migrera sur `useFormErrors`. ## Fait dans la foulée (post-ERP-101 initial) - **`useCategoryForm` migré sur `useFormErrors`** : `errors` devient le `reactive` du composable (drawer adapté : `form.errors.name` au lieu de `form.errors.value.name`, bloc `_global` retiré → erreur transverse en toast). 28 tests verts. - **Convention reportée dans `.claude/rules/frontend.md`** (section « Validation des formulaires — useFormErrors obligatoire »). ## Hors scope ERP-101 (suivi : ticket ERP-107) - Langue / présence des messages de validation côté back : le `message` affiché est celui renvoyé par le serveur. Audit des contraintes Symfony (présence d'un `message` FR, contraintes manquantes, violations sans `propertyPath`) tracké dans **ERP-107**.