Files
Starseed/docs/specs/M5-tickets-pesee/spec-front.md
T
Matthieu 312c119c06 feat(logistique) : entité WeighingTicket + dette site.code (ERP-183)
Entité WeighingTicket
- Entité métier complète (#[Auditable], TimestampableBlamableTrait, relations
  ORM Client/Supplier/Site) + contrat de sérialisation à 3 maillons
  (weighing_ticket:read / :item:read + contextes par opération).
- Getters calculés displayDate et plateFreeFormat (#[SerializedName]),
  sécurité view/manage, pas de Delete/archive.
- Validation #[Assert\*] messages FR + #[Assert\Callback] RG-5.03 (->atPath()),
  libellé i18n audit.entity.logistique_weighingticket.
- Repository : interface Domain + DoctrineWeighingTicketRepository
  (recherche + tri number DESC, deletedAt IS NULL).

Dette site.code
- Site.code mappé VARCHAR(8) (groupes read/write), dérivation auto au
  PrePersist (2 premiers chiffres du CP), UniqueConstraint uq_site_code.
- Migration Version20260617160000 : ALTER COLUMN code SET NOT NULL + COMMENT.
- Fixtures (codes 86/17/82) et SiteApiTest ajustés.

Câblage
- doctrine.yaml : mapping ORM du module Logistique (absent du scaffold ERP-181).
- ColumnCommentsCatalog : site.code + table weighing_ticket.

Specs M5 versionnées (spec-back / spec-front / prompts).
2026-06-18 14:37:16 +02:00

247 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
# === IDENTITÉ ===
module: M5
nom: "Tickets de pesée"
ecran: tickets-pesee
owner_spec: Matthieu
backup_spec: Tristan
version: V0.1
date_redaction: 2026-06-17
# Historique :
# V0.1 (2026-06-17) — Restitution Markdown du docx « M5-ticket-de-pesee-V02 » (V0.2, 15/06/2026,
# validation client en attente) + maquette Figma (node 1322-16774). Précisions techniques (back)
# dans spec-back.md. Réutilise le pattern et les composants M1/M2/M3/M4.
# Maquette : les 2 blocs (vide + plein) portent « Pesée bascule » + « Pesée manuelle » ;
# contrepartie portée par le bloc « Poids à vide » ; net = plein vide (confirmé Matthieu).
# === LIENS ===
maquette_figma: "https://www.figma.com/design/jRYgT0T9c03VsEbjGhCwwS/Composants---Design-System?node-id=1322-16774&p=f&m=dev"
regles_metier: [RG-5.01, RG-5.02, RG-5.03, RG-5.04, RG-5.05, RG-5.06, RG-5.07, RG-5.08, RG-5.09, RG-5.10]
roles: [Admin, Bureau, Compta, Commerciale, Usine]
lien_spec_back: ./spec-back.md
# === VALIDATION CLIENT ===
client_validation_1:
statut: validee
version: V0.2
date_doc: 2026-06-15
date_validation: 2026-06-17
valide_par: "Matthieu (CP MALIO)"
# === LIEN LESSTIME ===
lesstime_project_id: 6
lesstime_taskgroup_id: 33 # M5 — Tickets de pesée (ERP-181 → ERP-192)
statut_global: pret_a_dev
---
# Module 5 — Tickets de pesée (V0.1 front)
> **Origine** : spec fonctionnelle `M5-ticket-de-pesee-V02` (V0.2, 15/06/2026, **validation client en attente**) + maquette Figma (node 1322-16774). Restitution Markdown pour intégration au workflow MALIO. Toute décision technique (back) vit dans [`spec-back.md`](./spec-back.md). Le M5 réutilise le pattern et les composants posés aux [M1 clients](../M1-clients/spec-front.md) → [M4 transporteurs](../M4-transporteurs/spec-front.md).
> **Nouveau module `Logistique`** (DÉCISION Matthieu 17/06). La maquette montre une section sidebar **Logistique** plus large (Réception, Expédition, Validations, Triage, **Ticket de pesée**, Bons…) ; **le M5 ne livre que l'écran « Ticket de pesée »**. Les autres items sont hors périmètre (modules/écrans ultérieurs).
> **Décisions (17/06)** : (1) **pont bascule = stub** renvoyant un poids aléatoire ∈ [10000, 50000] kg (pas de liaison matérielle — [`spec-back.md § 2.6`](./spec-back.md)) ; (2) **DSD = compteur de pesée** par site, +1 par pesée ([`§ 2.7`](./spec-back.md)) ; (3) **net = plein vide** ([`§ 2.8`](./spec-back.md)) ; (4) numéro **`{siteCode}-TP-{NNNN}` par site** ([`§ 2.5`](./spec-back.md)).
## But
Lister les tickets de pesée et accéder à leur fiche : consultation, création (pesée à vide + pesée à plein au pont bascule), modification, impression. Chaque ticket porte un **numéro unique par site** (ex. `86-TP-0001`) et une **contrepartie** Client / Fournisseur / Autre.
## Accès
- **Depuis** : menu principal → section **Logistique** → item **« Ticket de pesée »** (route `/weighing-tickets`).
- **Site** : l'écran dépend du **site courant** (sélecteur de site en haut de l'app — onglets `CHÂTELLERAULT` / `SAINT-JEAN` / `POMMEVIC`). Le site pilote la numérotation et (par défaut) le cloisonnement de la liste ([`spec-back.md § 2.3 / § 2.5`](./spec-back.md)).
- **Rôles autorisés** (tableau « Rôles & permissions » du docx p.3, V0.2) :
| Rôle | Consultation | Ajout / Modification |
|---|---|---|
| **Admin** | ✅ Tout | ✅ Tout |
| **Bureau** | ✅ Tout | ✅ Tout |
| **Usine** | ✅ Tout | ✅ Tout |
| **Compta** | ❌ | ❌ |
| **Commerciale** | ❌ | ❌ |
> **Notes** :
> - RBAC transposée sur `logistique.weighing_tickets.view` / `.manage` ([`spec-back.md § 5`](./spec-back.md)).
> - ⚠ **Changement vs M5 V0.1** : en **V0.2, Usine = Tout / Tout**. **Compta** et **Commerciale** n'ont **aucun** accès (item sidebar masqué).
## Navigation
Page d'entrée de l'écran : **datatable** « Tickets de pesées ».
- **Clic sur une ligne** → écran **Modification d'un ticket de pesée** (le docx ne prévoit pas d'écran de consultation séparé — clic = édition).
- **Bouton « + Ajouter »** (haut droite, si `manage`) → écran **Ajouter un ticket de pesée**.
- **Bouton « Exporter »** (bas de liste, maquette) → télécharge un **XLSX** de **toute la liste** (filtres + site courant appliqués). Format dans [`spec-back.md § 4.5`](./spec-back.md).
## Datatable des tickets
Composant : `<MalioDataTable>` branché sur `usePaginatedList<WeighingTicket>({ url: '/weighing_tickets' })` *(URL API en `snake_case` ; la route Nuxt reste `/weighing-tickets`)* (règle frontend obligatoire — pagination Hydra, état 100 % local). Colonnes (docx p.3 + maquette) :
| Colonne | Source | Tri |
|---|---|---|
| **Numéro** | `ticket.number` (`{siteCode}-TP-{NNNN}`) | DESC par défaut (plus récents en tête) |
| **Client** | `ticket.client.companyName` (vide si contrepartie ≠ Client) | Non |
| **Fournisseur** | `ticket.supplier.companyName` (vide si ≠ Fournisseur) | Non |
| **Autre** | `ticket.otherLabel` (vide si ≠ Autre) | Non |
| **Date** | `ticket.displayDate` (`fullDate ?? emptyDate`, format `JJ-MM-AAAA`) | Oui |
| **Poids** | `ticket.netWeight` (kg, = plein vide — RG-5.05) | Oui |
> **Clic ligne** → écran Modification. **Pagination** : standard Starseed 10 / 25 / 50 (défaut 10). Liste **cloisonnée par site courant** par défaut ([`spec-back.md § 2.3`](./spec-back.md)).
## Écran « Ajouter un ticket de pesée »
**Accès** : bouton « + Ajouter ». **Rôles** : Admin, Bureau, Usine.
**Titre** : « ← Ticket de pesée » (flèche retour vers la liste).
L'écran (maquette) est composé de **deux blocs empilés****« Poids à vide »** puis **« Poids à plein »** — et d'un bouton **« Valider »** en bas.
### Bloc « Poids à vide »
Boutons en haut à droite du bloc : **« Pesée bascule »** (`<MalioButton>` secondaire) + **« Pesée manuelle »** (`<MalioButton>` primaire).
**Champs** :
| Champ | Type composant | Obligatoire | Règle |
|---|---|---|---|
| **Fournisseur / Client / Autre** | `<MalioSelect>` (3 valeurs) | Oui | RG-5.03 — pilote le champ suivant |
| **Nom du fournisseur** | `<MalioSelect>` (liste fournisseurs M2) | Conditionnel | RG-5.03 — visible + obligatoire si « Fournisseur » |
| **Nom du client** | `<MalioSelect>` (liste clients M1) | Conditionnel | RG-5.03 — visible + obligatoire si « Client » |
| **Autre** | `<MalioInputText>` | Conditionnel | RG-5.03 — visible + obligatoire si « Autre » |
| **Date** | `<MalioInputText>` type `date` *(cf. note)* | Oui | RG-5.07 — **date du jour par défaut** |
| **Poids** | `<MalioInputNumber>` (suffixe « Kg ») | Oui | RG-5.07 — **readonly**, rempli par la pesée |
| **DSD** | `<MalioInputNumber>` | Oui | RG-5.04 / RG-5.07 — **readonly**, rempli par la pesée |
| **Immatriculation** | `<MalioInputText>` (masque `XX-000-XX`) | Oui | RG-5.01 |
| **Tout format** | `<MalioCheckbox>` | Non | RG-5.01 — désactive le masque |
> **La contrepartie (Fournisseur/Client/Autre) + son champ associé est portée par le bloc « Poids à vide » uniquement** (maquette) — c'est une donnée du ticket, pas répétée sur le bloc plein. Côté back : champs `counterpartyType` / `client` / `supplier` / `otherLabel` du ticket ([`spec-back.md § 2.9`](./spec-back.md)).
**Action « Enregistrer »** (sous le bloc, maquette) : POST `/api/weighing_tickets` (création initiale du ticket avec la pesée à vide) — [`spec-back.md § 4.3`](./spec-back.md). Le numéro `{siteCode}-TP-{NNNN}` est attribué serveur.
### Bloc « Poids à plein »
Mêmes boutons **« Pesée bascule »** + **« Pesée manuelle »**. **Champs** : Date (date du jour par défaut), Poids (readonly, Kg), DSD (readonly), Immatriculation (`XX-000-XX`), « Tout format ».
> **Immatriculation + « Tout format » connectés entre les 2 blocs** (RG-5.01) : une seule valeur partagée — modifier l'un met à jour l'autre (même véhicule). Géré dans `useWeighingTicketForm()` (état partagé).
### Boutons de pesée — comportement
| Bouton | Déclencheur | Comportement |
|---|---|---|
| **Pesée bascule** | clic | Ouvre une **modal de confirmation** « Êtes-vous sûr de vouloir déclencher une pesée ? » (`<MalioButton>` « Valider »). Si confirmé → `POST /api/weighbridge_readings { mode: 'AUTO' }` ([`spec-back.md § 4.2`](./spec-back.md)) → remplit **Poids** et **DSD** du bloc, ferme la modal. **En cas d'erreur** (RG-5.06) : le message d'erreur s'affiche **dans la modal** et invite à passer en **pesée manuelle**. *(Au M5, le stub renvoie toujours un poids ∈ [10000,50000] — le chemin d'erreur est néanmoins géré.)* |
| **Pesée manuelle** | clic | Ouvre une **modal « Pesée manuelle »** avec **Poids** et **Numéro de pesée** à saisir (`<MalioInputNumber>` + `<MalioInputText>`), bouton « Enregistrer ». Une fois validé → le **Poids** du bloc est rempli ; le **DSD** est **calculé automatiquement** = dernier dsd du site + 1 (`POST /api/weighbridge_readings { mode: 'MANUAL', weight, manualNumber }` — RG-5.04). |
### Action « Valider » (bas d'écran)
`<MalioButton>` « Valider » → finalise le ticket (PATCH `/api/weighing_tickets/{id}` avec la pesée à plein + recalcul du net — [`spec-back.md § 4.4`](./spec-back.md)) puis **ouvre la modal d'impression** du ticket (RG-5.08 — **bon d'impression réalisé par Tristan**, cf. § Modales).
## Écran « Modification d'un ticket de pesée »
**But** : modifier un ticket existant et/ou **imprimer** le ticket.
**Accès** : clic sur une ligne de la liste. **Rôles** : Admin, Bureau, Usine.
**Identique à l'écran d'ajout** — mêmes 2 blocs, mêmes règles (RG-5.01 → RG-5.10) — **sauf** (docx + maquette) :
- Les champs sont **pré-remplis** avec les valeurs actuelles.
- Le **bouton « Enregistrer » du bloc « Poids à vide » disparaît** (RG-5.08) — on enregistre via le bas d'écran.
- En bas : **« Enregistrer »** (remplace « Valider ») + **« Imprimer »** (bouton d'impression **absent à l'ajout**, RG-5.08).
- Le numéro et le site sont **immuables** (lecture seule).
## Modales
| Modale | Contenu | Source |
|---|---|---|
| **Confirmation pesée bascule** | « Êtes-vous sûr de vouloir déclencher une pesée ? » + bouton « Valider ». Erreur affichée inline → invite pesée manuelle (RG-5.06). | docx p.5 + maquette |
| **Pesée manuelle** | Champs « Poids » + « Numéro de pesée » + bouton « Enregistrer ». DSD auto = dernier +1 (RG-5.04). | docx p.5 + maquette |
| **Impression du ticket / bon de pesée** | Aperçu imprimable du ticket (numéro, contrepartie, immat, pesée vide/plein, net, DSD, date). **Réalisé par Tristan** (voir encadré ci-dessous). | docx p.5 / RG-5.08 ; [`spec-back.md § 2.12`](./spec-back.md) |
> **⚠ Bon d'impression = Tristan.** La conception et la réalisation du **bon d'impression** (gabarit du ticket de pesée, mise en page, déclenchement) sont **prises en charge par Tristan lui-même**, hors de la découpe front standard du M5. Le reste de l'écran (modale de confirmation, modale pesée manuelle, formulaires) reste dans la découpe M5.
> - **Déclencheur attendu** : modale d'impression à la **validation** (création) ; bouton **« Imprimer »** en **modification** (absent à l'ajout — RG-5.08).
> - **Données disponibles** : toute la réponse `GET /api/weighing_tickets/{id}` (numéro, site, contrepartie, immat, pesées vide/plein, net, DSD, dates) — [`spec-back.md § 2.12 / § 4.0`](./spec-back.md).
> - **Modales** : réutiliser le wrapper de modal partagé `frontend/shared/` (comme M1→M4).
## Composants UI à utiliser (`@malio/layer-ui`)
- **Datatable** : `<MalioDataTable>` (+ `usePaginatedList`)
- **Select** : `<MalioSelect>` (contrepartie, nom client, nom fournisseur)
- **Input texte** : `<MalioInputText>` (Autre, Immatriculation, Numéro de pesée)
- **Input nombre** : `<MalioInputNumber>` (Poids, DSD)
- **Checkbox** : `<MalioCheckbox>` (« Tout format »)
- **Bouton** : `<MalioButton>`, `<MalioButtonIcon>` (Pesée bascule, Pesée manuelle, Valider, Enregistrer, Imprimer, + Ajouter, Exporter)
- **Validation par champ** : `useFormErrors` (mapping 422 inline — règle frontend obligatoire)
- **Toasts** : standards via `useApi()`
**Exceptions autorisées** (commenter `// TODO migrer quand Malio couvre`) :
- **Date** : `<MalioInput>` ne couvrant pas `date` nativement, utiliser un `<input type="date">` encapsulé OU `MalioDate` si dispo (cf. exceptions @.claude/rules/frontend.md — type `date` explicitement listé comme exception tolérée).
- **Masque immatriculation `XX-000-XX`** : si non couvert par `<MalioInputText>`, masque local (directive) + `// TODO`. La validation de format reste **autoritaire côté serveur** (RG-5.01 / RG-5.10).
- **Modales** : wrapper partagé `frontend/shared/`.
## Composables & appels API
- `usePaginatedList<WeighingTicket>({ url: '/weighing_tickets' })` — liste paginée (obligatoire). Consomme `number`, `client`/`supplier`/`otherLabel`, `displayDate`, `netWeight` ([`spec-back.md § 4.0`](./spec-back.md)).
- `useWeighingTicket(id)` — charge le détail via `GET /api/weighing_tickets/{id}` (pesées vide + plein embarquées, client/supplier/site imbriqués). **DoD avant intégration** : vérifier le JSON réel ([`spec-back.md § 4.0.bis`](./spec-back.md)).
- `useWeighingTicketForm()` — workflow 2 blocs (POST à l'« Enregistrer » du bloc vide, PATCH au « Valider ») + **état partagé** immatriculation/« Tout format » entre les 2 blocs (RG-5.01) + gestion des champs conditionnels de contrepartie (RG-5.03).
- `useWeighbridge()` — déclenche la pesée : `POST /api/weighbridge_readings` (AUTO ou MANUAL), gère la modal de confirmation et le chemin d'erreur → pesée manuelle (RG-5.06).
- `useClientOptions()` / `useSupplierOptions()` — alimentent les selects (référentiels M1/M2 via `?pagination=false` — échappatoire selects).
- `useCurrentSite()` — site courant (sélecteur) — déjà exposé côté front (Sites). Le back lit le site courant pour la numérotation ; le front n'a pas à l'envoyer.
- `usePermissions()` — masque l'item sidebar et les boutons selon `logistique.weighing_tickets.view/manage`.
- Tous les appels passent par `useApi()` (jamais `$fetch` direct — règle ABSOLUE n°4).
## Règles de formatage et normalisation
Le serveur normalise systématiquement ([`spec-back.md § 6`](./spec-back.md)) :
| Champ | Normalisation serveur | Affichage front |
|---|---|---|
| Immatriculation | trim + UPPER ; format `XX-000-XX` sauf « Tout format » (RG-5.01) | UPPER, masqué |
| Autre (`otherLabel`) | trim | identique |
| Poids / DSD | entiers (kg) | « 7 150 Kg », DSD brut |
| Numéro de ticket | `{siteCode}-TP-{NNNN}` (serveur) | affiché tel quel |
> Le front **ne normalise pas** : il envoie la valeur saisie, le serveur normalise et renvoie la valeur que l'UI affiche.
## Différences notables avec les modules précédents
| Zone | M1→M4 | M5 tickets de pesée |
|---|---|---|
| Module | Commercial / Transport… | **Logistique** (nouveau, ERP à venir) |
| Saisie poids | — | **Pesée au pont bascule** (stub random) + pesée manuelle |
| Cloisonnement par site | M3 oui / M4 non | **Oui** (site courant) + numéro par site |
| Numérotation métier | id technique | **`{siteCode}-TP-{NNNN}`** par site (RG-5.02) |
| Onglets | présents | **Aucun onglet** : 2 blocs empilés (vide + plein) |
| Impression | aucune | **Modal d'impression** du ticket (RG-5.08) |
| Contrepartie | — | **Client / Fournisseur / Autre** (conditionnel, RG-5.03) |
## Points résolus côté back
| # | Zone d'ombre | Résolution (cf. `spec-back.md`) |
|---|---|---|
| 1 | Module | **Nouveau module `Logistique`** (§ 2.1) |
| 2 | Pont bascule | **Stub** poids aléatoire ∈ [10000,50000], interface réutilisable, driver réel HP (§ 2.6) |
| 3 | DSD | **Compteur de pesée par site**, +1 par pesée ; manuel = dernier +1 (§ 2.7) |
| 4 | Poids net | **plein vide**, calculé serveur (§ 2.8) |
| 5 | Numérotation | **`{siteCode}-TP-{NNNN}`** par site, séquence verrouillée (§ 2.5) ; ajout `site.code` |
| 6 | Contrepartie | `counterpartyType` + FK Client/Supplier ou `otherLabel` (RG-5.03, § 2.9) |
| 7 | Deux pesées | Colonnes plates `empty_*` / `full_*` ; les 2 blocs supportent bascule + manuelle (§ 2.4) |
| 8 | Impression | Modal d'impression front ; bouton dispo en modif seulement (RG-5.08, § 2.12) |
| 9 | Masque immat | `XX-000-XX` + « Tout format », connectés entre blocs (RG-5.01, § 2.10) |
| 10 | RBAC | `logistique.weighing_tickets.view/manage` ; Usine = Tout ; Compta + Commerciale sans accès (§ 5.2) |
---
## 📦 Tickets Lesstime générés
**TaskGroup Lesstime** : **#33 — M5 — Tickets de pesée** (projet `ERP / Starseed`, projectId=6) — créé le 17/06/2026, 12 tickets au statut « Prêt à dev ».
| # | ERP | Ticket | Effort | Tag |
|---|---|---|---|---|
| 1.1 | ERP-181 | Scaffolder le module Logistique + RBAC | M | Backend |
| 1.2 | ERP-182 | Migrer le schéma M5 (site.code, compteurs, weighing_ticket) | M | Backend |
| 1.3 | ERP-183 | Créer l'entité WeighingTicket + repository + contrat sérialisation | M | Backend |
| 1.4 | ERP-184 | Implémenter la pesée pont bascule (stub + DSD + endpoint) | M | Backend |
| 1.5 | ERP-185 | Créer Provider + Processor (numérotation, RG, normalisation) | L | Backend |
| 1.6 | ERP-186 | Implémenter l'export XLSX | S | Backend |
| 1.7 | ERP-187 | Tests PHPUnit RG-5.01→5.10 + capture contrat JSON | M | Backend |
| 1.8 | ERP-188 | Créer la page liste `/weighing-tickets` + export | M | Frontend |
| 1.9 | ERP-189 | Implémenter l'écran Ajouter (blocs vide+plein, pesée, masque immat) | L | Frontend |
| 1.10 | ERP-190 | Implémenter l'écran Modification + déclenchement impression | M | Frontend |
| 1.11 | ERP-191 | i18n + libellés + branchement site courant | S | Frontend |
| 1.12 | ERP-192 | **Bon d'impression du ticket de pesée — OWNER Tristan** | — | Frontend |