Files
Starseed/.claude/rules/frontend.md
T
matthieu 597101262d
Auto Tag Develop / tag (push) Successful in 8s
feat(commercial) : messages de validation FR sur les contraintes back + garde-fou (ERP-107) (#59)
## Contexte

Résout **ERP-107** — pendant back du mapping d'erreur par champ front (ERP-101). Le front (`useFormErrors` / `mapViolationsToRecord`) affiche sous chaque champ le `message` renvoyé par le back. Ce ticket garantit que ces messages existent, sont en FR et rattachés au bon champ.

## Changements

- **Messages FR explicites** sur toutes les contraintes `#[Assert\*]` des entités métier : `Client`, `ClientContact`, `ClientAddress`, `ClientRib`, `Category`, `Role`, `User` (Email, NotBlank, Length, Bic, Iban, PositiveOrZero, Count…).
- **Contraintes `Assert\Length` manquantes ajoutées**, calées sur le `length` de la colonne ORM (téléphones `VARCHAR(20)`, `siren`, `nTva`, `accountNumber`, `username`…). Évite une erreur Postgres 500 non rattachée au champ → 422 propre.
- **Locale FR globale** (`symfony/translation` + `default_locale: fr`) comme filet pour les messages natifs Symfony non surchargés.
- **Garde-fou** `tests/Architecture/EntityConstraintsHaveFrenchMessageTest` : échoue si une contrainte n'a pas de message FR explicite (comparaison au défaut Symfony) ou si `Assert\Length.max` diverge du `length` ORM. Whitelist justifiée pour les formats auto-bornés (Bic/Iban/Regex CP/couleur hex).
- **Test fonctionnel** du JSON 422 réel : message FR + `propertyPath` consommable par le front.
- **Convention documentée** dans `.claude/rules/backend.md`.

## Décisions

- Stratégie retenue : message FR **explicite sur toutes** les contraintes + locale FR en filet (les deux leviers du ticket).
- Garde-fou `Length == ORM length` : **test bloquant** (anti-dérive).
- RG-1.03 (distributor/broker) : pas de `Assert\Callback` ajouté — le `ClientProcessor` gère **déjà** l'exclusivité (422 + `propertyPath`). Pas de doublon.

## Hors périmètre / à suivre

- **Alignement `nullable`(DB) / `NotBlank`(back) / `required`(front)** : les champs obligatoires existants ont été confirmés, mais aucun changement de nullabilité DB n'a été fait sans arbitrage métier. À recroiser avec les astérisques front (ERP-101 / PR #58) si divergence constatée.

## Vérifications

- `make test` : **469 tests verts** (1793 assertions), 0 échec/erreur.
- `php-cs-fixer` : 0 fichier à corriger.

---------

Co-authored-by: admin malio <malio@yuno.malio.fr>
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #59
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
2026-06-04 09:27:32 +00:00

9.2 KiB

Frontend — Regles Nuxt 4 / Vue 3 / @malio/layer-ui

Base

  • TypeScript strict
  • 4 espaces d'indentation
  • Commentaires (JSDoc, inline, bloc) en francais ; code (variables, types) en anglais
  • Chaque module front = un layer Nuxt auto-detecte (frontend/modules/*/nuxt.config.ts minimal)

Appels API

  • Toujours useApi() — jamais $fetch, ofetch, axios en direct
  • useApi() gere : cookies JWT, erreurs, toasts i18n, parsing Hydra

Stores (Pinia)

  • useAuthStore pour l'authentification
  • useUiStore pour l'etat UI global (sidebar, modales, etc.)
  • Composables avec state singleton (refs module-level) : exposer une fonction reset*() et la rappeler au logout (ex: useSidebar().resetSidebar())

Middlewares globaux

  • auth.global.ts protege les routes + charge la sidebar apres login
  • modules.global.ts redirige si la route demandee est dans disabledRoutes

i18n et sidebar

  • Labels de sidebar = cles i18n sidebar.<module>.*, jamais du texte brut
  • Le layout default.vue applique t() sur les labels retournes par /api/sidebar
  • Traductions dans frontend/i18n/locales/

Composants formulaires — @malio/layer-ui obligatoire

Tout champ de formulaire / filtre doit utiliser les composants Malio* plutot que <input> / <select> bruts :

  • MalioInputText, MalioInputNumber, MalioInputAmount, MalioInputPassword, MalioInputTextArea
  • MalioSelect, MalioSelectCheckbox, MalioCheckbox, MalioRadioButton
  • MalioInputUpload, MalioTime
  • MalioButton, MalioButtonIcon

Exceptions autorisees (commenter un // TODO pour migrer quand la lib couvrira le cas) :

  1. Type non couvert : datetime-local, date, color picker, file drag & drop
  2. Bug connu bloquant (ex: MalioSelect avec options string) — documenter le bug en commentaire

Toute autre exception requiert validation avant merge.

Validation des formulaires — useFormErrors obligatoire (erreur par champ)

Tout formulaire qui soumet a une API DOIT afficher les erreurs de validation 422 sous le champ concerne, via useFormErrors (frontend/shared/composables/useFormErrors.ts). C'est le pendant front de « le back renvoie TOUTES les violations d'une 422 d'un coup » : un seul aller-retour, chaque erreur affichee inline sous son champ (prop :error des Malio*), pas un toast fourre-tout.

Principe cle : le nom du champ cote front = le propertyPath renvoye par le back. Aucun mapping manuel champ par champ.

Pattern de reference (champs scalaires) :

const { errors, setError, clearErrors, handleApiError } = useFormErrors()

async function submit() {
    clearErrors()
    try {
        await useApi().post('/clients', payload, { toast: false }) // toast: false obligatoire
    } catch (e) {
        // 422 → mapping inline par champ (pas de toast) ; autre → toast de fallback.
        handleApiError(e, { fallbackMessage: t('foo.error') })
    }
}
<MalioInputText v-model="form.companyName" :error="errors.companyName" />
<MalioSelect    v-model="form.siren"       :error="errors.siren" />

Regles :

  • Toujours { toast: false } sur l'appel API qui veut un mapping inline (sinon le toast natif d'useApi masque le fin).
  • Cas metier specifique (ex: 409 doublon) : setError('champ', message) + toast explicite avant de deleguer le reste a handleApiError. Cf. useCategoryForm (doublon RG-1.07).
  • Collections (listes de sous-entites sauvees par un appel par ligne) : une erreur PAR LIGNE via un tableau ref<Record<string, string>[]> aligne sur l'index, peuple par mapViolationsToRecord(error.response._data) (util pur de shared/utils/api.ts). Le composant de ligne expose une prop :errors (Record<string, string>) bindee sur le :error de chaque champ. Cf. ClientContactBlock / ClientAddressBlock et les submits de clients/new.vue / clients/[id]/edit.vue.

Interdit : se contenter d'un toast global sur une 422 quand le back identifie les champs fautifs (propertyPath). Reimplementer un mapping if/else par champ a la main au lieu d'useFormErrors / mapViolationsToRecord.

Tableaux de donnees — MalioDataTable obligatoire

Tout affichage LISTE tabulaire (donnees metier paginees, CRUD admin) doit passer par MalioDataTable :

  • Pagination integree
  • Slots #header-* pour filtres, #cell-* pour rendu custom
  • Pas de <table> brut avec pagination custom

Exception : tableaux purement presentationnels non paginables (diff field/old/new, grille de comparaison, matrice RBAC d'admin, etc.) peuvent rester en <table> HTML brut.

Listes paginees (standard) — usePaginatedList obligatoire

Toute liste qui consomme une GetCollection API doit passer par usePaginatedList (frontend/shared/composables/usePaginatedList.ts). Le composable est le pendant front de la regle ABSOLUE n°13 (« toute collection est paginee cote back ») : il consomme l'envelope Hydra (member / totalItems / view) et expose un etat reactif a brancher directement sur MalioDataTable.

Pattern de reference :

const {
    items,
    totalItems,
    currentPage,
    itemsPerPage,
    itemsPerPageOptions,
    fetch: loadList,
    goToPage,
    setItemsPerPage,
} = usePaginatedList<MyEntity>({ url: '/my-resources' })

onMounted(loadList)
<MalioDataTable
    :columns="columns"
    :items="rows"
    :total-items="totalItems"
    :page="currentPage"
    :per-page="itemsPerPage"
    :per-page-options="itemsPerPageOptions"
    :empty-message="t('foo.empty')"
    @update:page="goToPage"
    @update:per-page="setItemsPerPage"
/>

Garanties offertes par le composable :

  • Force Accept: application/ld+json → API Platform 4 renvoie bien member / totalItems (sans Accept, retour tableau plat sans pagination).
  • Defaut 10 items/page, choix client 10 / 25 / 50, aligne sur le defaut serveur.
  • Mutation setFilters / setSort / setItemsPerPage → retombe systematiquement en page 1.
  • Cas limite « page hors borne apres filtre » : retombe automatiquement sur la derniere page valide (tests/usePaginatedList.test.ts).
  • Etat 100 % local (refs internes a l'instance) — jamais reflete dans l'URL, conformement a la regle « Etat des tableaux — pas de persistance URL » ci-dessous.

A NE PAS faire :

  • Charger une collection complete via ?itemsPerPage=999 pour bypasser la pagination. Le seul cas legitime de retour complet est l'alimentation d'un <select> sur un referentiel ≤ quelques dizaines d'entrees, et il passe par ?pagination=false (echappatoire prevue par pagination_client_enabled: true).
  • Reimplementer la pagination prev/next a la main au-dessus de MalioDataTable — le composant porte deja le selecteur items/page et les boutons Prev/Next.
  • Persister page/tri/filtre dans la query string — meme regle que pour <MalioDataTable> brut (cf. section suivante).

Etat des tableaux — pas de persistance URL

Interdit de persister l'etat d'un tableau (filtres, pagination, tri par colonne, selection, ligne active, scroll) dans la query string ou de le reinjecter depuis route.query au montage.

  • L'etat vit uniquement dans le composant (reactive, ref locales)
  • Seuls les deep links "de navigation metier" (ex: ouvrir un detail precis /users/42) sont dans l'URL
  • Exceptions autorisees sur demande explicite de l'utilisateur

Validation des formulaires (standard ERP-101)

Regle transverse a TOUS les formulaires front (et a rappeler a l'ecriture de chaque ticket back/front portant un formulaire). Decidee en ERP-101 (declencheur : ecran « Ajouter un client » ERP-63).

  • Champs obligatoires : prop required du composant Malio* + etoile (asterisque) rouge dans le label. Ne JAMAIS griser le bouton « Valider » sans feedback : bouton toujours actif + erreurs affichees sous les champs.
  • Couche de validation autoritaire = le back : les RG sont re-validees serveur (mode strict). Au 422, mapper violations[].propertyPath vers la prop error du champ via extractApiViolations (deja utilise par useCategoryForm). Zero duplication de RG, zero drift.
  • Feedback instantane au blur : uniquement requis / min / max / format (pas de re-implementation des RG metier cote front).
  • Regles front-only : celles sans equivalent back (ex. FK nullable cote back mais obligatoire selon un choix UI) sont validees et affichees cote front.
  • Email — PAS de masque : un email n'a pas de structure fixe. Normalisation via la prop lowercase de MalioInputEmail (trim + suppression des espaces + lowercase, coherent avec la normalisation serveur RG-1.21). Le format est valide par la prop error (violations serveur ou check au blur), jamais par un masque. Retirer tout shaping email ad hoc des ecrans.
  • Contrat back attendu : tout 422 issu d'un Processor/Validator doit porter violations[].propertyPath aligne sur les noms de champs du formulaire, pour etre consommable par extractApiViolations.
  • Dependance : le branchement des props required suppose @malio/layer-ui a jour (props required + etoile — MUI-41 / ERP-101).

Interdits

  • modules-loader.ts, .module.ts — le scan des layers est automatique
  • Hardcode de la sidebar cote front — elle vient de /api/sidebar
  • Edition manuelle de extends dans frontend/nuxt.config.ts — les layers sont scannes
  • Import d'un module front depuis un autre module — passer par frontend/shared/