docs : spec saisie information bovin post-EDNOTIF
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
187
docs/superpowers/specs/2026-05-04-bovine-info-saisie-design.md
Normal file
187
docs/superpowers/specs/2026-05-04-bovine-info-saisie-design.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# 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` :
|
||||
`<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>`) :
|
||||
|
||||
```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<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).
|
||||
Reference in New Issue
Block a user