# 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` où `[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` : `
` + bandeau titre avec flèche retour absolue, titre `

Saisie information bovin {{ reception.identificationNumber }}

`. 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`) : ```ts 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 state** — `ref` 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).