Files
Ferme/docs/superpowers/specs/2026-05-04-bovine-info-saisie-design.md
2026-05-04 11:21:30 +02:00

7.6 KiB

Saisie information bovin (post-EDNOTIF)

Contexte

Sur la page /entry-exit, le tableau "Entrées validées" liste les receptions dont les bovins sont tous confirmés EDNOTIF (reception.validatedAt non null). Une fois cette validation acquise, l'utilisateur doit encore renseigner pour chaque bovin quatre informations métier qui ne viennent pas d'EDNOTIF :

  • poids d'arrivée
  • prix d'achat au kg
  • bâtiment
  • case

Cette spec décrit l'écran de saisie et le composant accordéon qu'il introduit.

Périmètre

  • Nouvelle page accessible uniquement via clic sur une ligne d'"Entrées validées" — pas d'entrée dans la nav globale.
  • Aucun changement d'entité Doctrine, aucune migration : les quatre champs existent déjà sur Bovine (receivedWeight, pricePerKg, buildingCase).
  • Le champ building (legacy XLSX) n'est pas écrit. Côté affichage, getEffectiveBuilding() continue de dériver le bâtiment effectif depuis buildingCase.
  • pricePerKg reste protégé par ROLE_BUREAU côté API. La page exige ROLE_ADMIN ; la hiérarchie Symfony fait que ROLE_ADMIN hérite ROLE_BUREAU, donc pas de cas particulier.
  • Pas de gestion de "session interrompue" : chaque accordéon validé individuellement est persisté immédiatement.

Routing & navigation

  • Page : frontend/pages/entry-exit/bovine-info/[id].vue[id] est le receptionId.
  • Sur frontend/pages/entry-exit/index.vue, le tableau "Entrées validées" reçoit row-clickable et @row-click="goToBovineInfo". Le handler pousse vers /entry-exit/bovine-info/{reception.id}.
  • Le bouton flèche-retour de la page renvoie vers /entry-exit.

Composant UiAccordion

Fichier : frontend/components/ui/UiAccordion.vue. Réutilisable, sans logique métier.

Props

  • modelValue: boolean — état ouvert/fermé, supporte v-model.

Slots

  • #header — contenu libre du header (badge, titre, etc., aligné à gauche).
  • default — corps de l'accordéon, rendu uniquement quand ouvert.

Comportement

  • Click sur le header → emit update:modelValue avec la valeur inversée.
  • Header : bg-slate-100, padding identique aux headers UiDataTable (px-4 py-3), texte semi-bold uppercase.
  • Chevron à droite (mdi:chevron-down), rotation 180° quand ouvert, transition CSS courte.
  • Pas d'animation de hauteur au déploiement (pour rester simple) — on rend ou pas via v-if.

Page bovine-info/[id].vue

Layout — copie du pattern entry-exit/entry/[id].vue : <div class="px-[86px]"> + bandeau titre avec flèche retour absolue, titre <h1>Saisie information bovin {{ reception.identificationNumber }}</h1>. Pas de sous-titre.

Chargement (onMounted)

  1. GET receptions/{id} → alimente le titre.
  2. GET bovines?reception={id}&itemsPerPage=200 (pas de pagination — on suppose qu'une réception a au plus quelques dizaines de bovins).
  3. GET buildings — la réponse contient buildingCases imbriqués (BuildingData.buildingCases). On dérive de là à la fois la liste de bâtiments (selector "Bâtiment") et l'index des cases par bâtiment.

État local par bovin (Map<bovineId, FormState>) :

type FormState = {
  receivedWeight: number | null
  pricePerKg: number | null
  buildingId: number | null   // UI-only, drive le filtre Case
  buildingCaseId: number | null
  submitted: boolean          // pour les borders rouges au submit
  isSaving: boolean
}

Initialisé depuis l'API : receivedWeight, pricePerKg directement, buildingCaseId = bovine.buildingCase?.id, buildingId = bovine.effectiveBuilding?.id.

Source de vérité du badge : bovine.receivedWeight != null && bovine.pricePerKg != null && bovine.buildingCase != null — donc calculé sur les valeurs persistées, pas sur le FormState en cours d'édition. Vert "Saisie" si les trois sont non-null (le bâtiment est dérivé de la case), sinon jaune "Attente saisie".

Note : on garde 4 champs côté UI mais 3 conditions backend, parce que building n'est pas persisté indépendamment.

Tri — non-saisis (badge jaune) en haut puis saisis (badge vert) en bas, ordre d'API préservé à l'intérieur de chaque groupe. Le tri est calculé

  • au chargement initial,
  • après chaque PATCH OK (le bovin qui vient d'être saisi descend dans le groupe vert).

Il ne se recompute pas pendant qu'un accordéon est ouvert et en cours d'édition — sinon les bovins sauteraient de position au moindre changement de l'état "saisi/non-saisi", ce qui ne se produit ici que sur un PATCH réussi.

Open stateref<number | null> qui contient l'id du bovin actuellement ouvert. Un seul accordéon ouvert à la fois.

  • Initialisation : id du premier bovin non-saisi de la liste triée, ou null si tout est déjà saisi.
  • Click sur un header autre que celui ouvert → ferme l'ouvert, ouvre le cliqué.
  • Click sur le header ouvert → ferme (open = null).
  • Validation OK d'un accordéon → ferme l'actuel, ouvre l'id du prochain non-saisi de la liste (recalculée). Si plus de non-saisi → null.

Formulaire par accordéon

Tous les champs required, validation au submit (pattern submitted flag + .submitted :invalid du CSS global).

Champ Composant Type / format
Poids d'arrivée UiNumberInput entier kg
Prix d'achat (kg) UiNumberInput float, step 0.01
Bâtiment UiSelect options = liste building
Case UiSelect options = cases du building sélectionné
  • Watch sur buildingId : si l'utilisateur change le bâtiment et que la case actuellement sélectionnée n'appartient pas au nouveau, on remet buildingCaseId = null.
  • Bouton Valider centré, bg-primary-500, désactivé pendant isSaving.

Soumission

PATCH /bovines/{id}
{
  receivedWeight,
  pricePerKg,
  buildingCase: `/api/building_cases/${buildingCaseId}`
}
Content-Type: application/ld+json

À la réponse OK, on remplace le bovin dans la liste locale par la version retournée par l'API (qui contient buildingCase hydraté pour recomputer le badge), puis on déclenche la transition d'état (resort + open suivant).

En cas d'erreur HTTP, le toast par défaut de useApi suffit ; on garde l'accordéon ouvert et le FormState intact.

Hors périmètre

  • Pas de bulk-save (pas de "Tout valider").
  • Pas de tracking "modifié non sauvé" / warning au unload — chaque accordéon est validé explicitement, pas d'autosave.
  • Pas de tests automatisés ajoutés dans ce lot (cohérent avec le reste de la feature entry-exit).
  • Pas d'exposition de cet écran ailleurs que via le tableau "Entrées validées".

Critères d'acceptation

  • Cliquer sur une ligne du tableau "Entrées validées" ouvre la page /entry-exit/bovine-info/{id}.
  • La page liste tous les bovins de la réception, non-saisis en haut.
  • Au chargement, un seul accordéon est ouvert : le premier non-saisi (ou aucun si tout est déjà saisi).
  • Cliquer sur un autre header ferme l'ouvert et ouvre le cliqué.
  • Soumettre un accordéon avec un champ vide affiche les borders rouges (submitted flag) et bloque la requête.
  • Soumettre un accordéon valide PATCH le bovin et, après réponse OK, ferme l'accordéon, met le badge en vert et ouvre le suivant non-saisi.
  • Recharger la page après une saisie partielle réaffiche les valeurs pré-remplies et le bon badge pour chaque bovin.
  • php-cs-fixer et make test restent verts (pas de code backend modifié, donc rien à régresser).