--- # === 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 : `` branché sur `usePaginatedList({ 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 »** (`` secondaire) + **« Pesée manuelle »** (`` primaire). **Champs** : | Champ | Type composant | Obligatoire | Règle | |---|---|---|---| | **Fournisseur / Client / Autre** | `` (3 valeurs) | Oui | RG-5.03 — pilote le champ suivant | | **Nom du fournisseur** | `` (liste fournisseurs M2) | Conditionnel | RG-5.03 — visible + obligatoire si « Fournisseur » | | **Nom du client** | `` (liste clients M1) | Conditionnel | RG-5.03 — visible + obligatoire si « Client » | | **Autre** | `` | Conditionnel | RG-5.03 — visible + obligatoire si « Autre » | | **Date** | `` type `date` *(cf. note)* | Oui | RG-5.07 — **date du jour par défaut** | | **Poids** | `` (suffixe « Kg ») | Oui | RG-5.07 — **readonly**, rempli par la pesée | | **DSD** | `` | Oui | RG-5.04 / RG-5.07 — **readonly**, rempli par la pesée | | **Immatriculation** | `` (masque `XX-000-XX`) | Oui | RG-5.01 | | **Tout format** | `` | 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 ? » (`` « 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 (`` + ``), 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) `` « 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** : `` (+ `usePaginatedList`) - **Select** : `` (contrepartie, nom client, nom fournisseur) - **Input texte** : `` (Autre, Immatriculation, Numéro de pesée) - **Input nombre** : `` (Poids, DSD) - **Checkbox** : `` (« Tout format ») - **Bouton** : ``, `` (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** : `` ne couvrant pas `date` nativement, utiliser un `` 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 ``, 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({ 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 |