feat : M5 — Tickets de pesée (ERP-188 → ERP-193) #144

Merged
tristan merged 20 commits from feat/erp-191-i18n-site-courant into develop 2026-06-24 14:38:02 +00:00
Owner

MR unique regroupant tout le module M5 « Tickets de pesée » (remplace les MR empilées #140/#141/#142/#143).

Périmètre

  • ERP-188 — Page liste des tickets de pesée + export XLSX (colonnes Fournisseur/Client/Autre + Statut).
  • ERP-189 — Écran « Ajouter » (4 champs en haut, 2 blocs de pesée, pesée bascule/manuelle, date+heure horodatée à la validation).
  • ERP-190 — Écran « Modifier » + bouton Imprimer.
  • ERP-191 — i18n + libellés + branchement site courant.
  • ERP-192 — Bon de pesée PDF généré côté back (template Twig → Dompdf), endpoint GET /api/weighing_tickets/{id}/print.pdf.
  • ERP-193 — Cycle de vie brouillon/validé (status DRAFT/VALIDATED, numéro attribué à la validation), DSD saisi conservé en pesée manuelle, retours métier design.

Vérifications

  • Back : tests Logistique + architecture verts, php-cs-fixer propre, migrations appliquées (dev + test).
  • Front : suite Vitest complète verte, ESLint propre.

Base : develop — contient les 16 commits du M5 (rien d'autre).

MR unique regroupant tout le module M5 « Tickets de pesée » (remplace les MR empilées #140/#141/#142/#143). ## Périmètre - **ERP-188** — Page liste des tickets de pesée + export XLSX (colonnes Fournisseur/Client/Autre + Statut). - **ERP-189** — Écran « Ajouter » (4 champs en haut, 2 blocs de pesée, pesée bascule/manuelle, date+heure horodatée à la validation). - **ERP-190** — Écran « Modifier » + bouton Imprimer. - **ERP-191** — i18n + libellés + branchement site courant. - **ERP-192** — Bon de pesée PDF généré côté back (template Twig → Dompdf), endpoint `GET /api/weighing_tickets/{id}/print.pdf`. - **ERP-193** — Cycle de vie brouillon/validé (status DRAFT/VALIDATED, numéro attribué à la validation), DSD saisi conservé en pesée manuelle, retours métier design. ## Vérifications - Back : tests Logistique + architecture verts, php-cs-fixer propre, migrations appliquées (dev + test). - Front : suite Vitest complète verte, ESLint propre. Base : `develop` — contient les 16 commits du M5 (rien d'autre).
tristan added 16 commits 2026-06-24 13:38:32 +00:00
style(front) : section Logistique en tête de sidebar + icône camion (ERP-188)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m55s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m30s
ef7bf69980
- collectDenormalizationErrors sur Post/Patch : les erreurs de dénormalisation
  (date/type/IRI) reviennent en 422 avec propertyPath (et non 400 opaque), donc
  mappables inline côté front (miroir M1 Client).
- NotBlank sur emptyWeight : le poids à vide est obligatoire à la création, sa
  violation est renvoyée avec counterpartyType / immatriculation d'un seul coup.
- Poids/DSD en champs texte verrouillés sur les chiffres et désactivés.
- Boutons de pesée : icône mdi:weight à gauche + gap-8.
- Bloc « Poids à vide » réagencé en 3 lignes (contrepartie / Date-Poids-DSD-Immat / Tout format).
- Omission des clés null dans les payloads (compact) : requis vides → message NotBlank métier au lieu d'une erreur de type.
- Pesée obligatoire (RG-5.07) signalée inline sous Poids/DSD ; toutes les violations affichées d'un seul aller-retour.
- Erreur d'immatriculation affichée uniquement sur le bloc « Poids à vide » (plus de doublon sur le bloc plein).
« Tout format » n'est plus un champ libre total : masque maska charset
(lettres/chiffres/espace/tiret, MAJ, longueur libre) pour les plaques anciennes
ou étrangères, filtrant accents/ponctuation/symboles. Format autoritaire côté serveur.
- Modale « Pesée manuelle » : champ Poids passé en MalioInputText verrouillé sur
  les chiffres (NUMERIC_MASK), comme le formulaire.
- Masques de pesée factorisés dans utils/weighingMasks (NUMERIC / PLATE / FREE_PLATE).
- Écran Modification : suppression des champs lecture seule « Numéro » et « Site »
  en tête (le numéro reste rappelé dans le titre de l'écran).
Le DSD est l'index de pesée du pont, déjà verrouillé (FOR UPDATE) à l'émission
par l'endpoint de pesée. Le WeighingTicketProcessor ne le réalloue plus à la
création : il conserve la valeur reçue, n'allouant qu'en filet de sécurité si
elle est absente. Supprime le double comptage AUTO, aligne le DSD prévisionnel
sur le DSD enregistré, et reste compatible avec le futur driver matériel
(le DSD viendra du pont et sera persisté à l'identique).
- Modale pesée manuelle : titre UPPERCASE, marges 24px haut / 28px latéral /
  12px titre-bordure / 36px bordure-formulaire, bordure insérée sous le header,
  champs resserrés (gap-2), Annuler retiré, Enregistrer centré.
- Modale pesée bascule : question portée par le titre (corps sans texte),
  Annuler retiré, Valider centré.
- Marge bottom de 24px sous le bouton dans les deux modales.
- Nettoyage des clés i18n devenues inutiles (cancel / confirmMessage).
Endpoint API Platform GET /api/weighing_tickets/{id}/print.pdf (provider
renvoyant un binaire, pas de controller) sécurisé par
logistique.weighing_tickets.view. Rendu d'un template Twig hydraté avec le
ticket converti en PDF via Dompdf. Reproduit le modèle fourni : en-tête fixe
(logo + identité société, indépendant du site), pesées à vide/plein avec le
numéro de pesée affiché comme un DSD, poids net = plein − vide.
Le champ Date des blocs de pesée passe de MalioDate (date seule, heure perdue
-> 00:00:00 en base) à MalioDateTime (date + heure). Défaut = instant courant
(nowIsoDateTime) et ré-horodatage à la validation d'une pesée (bascule ou
manuelle) via applyReading : la date du ticket reflète le moment réel de la
pesée. L'hydratation en modification conserve l'heure du back (TIMESTAMP).
Une pesée (bascule ou manuelle) s'enregistre désormais dès la validation de sa
modale, sans exiger la contrepartie ni l'immatriculation : le ticket naît
« brouillon » (status DRAFT, sans numéro). Le bouton « Valider » finalise quand
les 3 champs du haut (contrepartie + champ associé + immatriculation) ET les 2
pesées sont renseignés : attribution du numéro {siteCode}-TP-{NNNN} et passage
en VALIDATED, puis ouverture du bon de pesée PDF.

Back : counterparty_type/immatriculation/number nullables + colonne status
(migration racine), contraintes strictes déplacées en groupe de validation
finalize, opération PATCH /weighing_tickets/{id}/validate, numéro attribué à la
validation. Front : 4 champs en haut hors blocs, persistance immédiate des
pesées, écrans Ajouter/Modifier refondus, colonne Statut dans la liste, form à
plat pleine largeur. Tests back (lifecycle brouillon/validate) + front à jour.
Remplace les colonnes « Type contrepartie » + « Contrepartie » par 3 colonnes
mutuellement exclusives Fournisseur / Client / Autre (miroir de la liste), et
ajoute une colonne Statut (« En attente » / « Terminée »).
fix : DSD saisi conservé en pesée manuelle (ERP-193)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 2m12s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m45s
9e2206a7d6
En pesée manuelle, le serveur incrémentait automatiquement le DSD et ignorait la
saisie de l'opérateur. Désormais l'opérateur saisit le poids ET le DSD (le numéro
du pont réellement utilisé), conservés tels quels — plus d'auto-incrément. Le
champ « Numéro de pesée » séparé (manualNumber) est supprimé : pour le client
c'est la même chose que le DSD. Pas de contrainte d'unicité sur le DSD (doublons
autorisés). Colonnes empty_manual_number/full_manual_number droppées.
tristan reviewed 2026-06-24 13:59:33 +00:00
tristan left a comment
Author
Owner

Revue automatisée de la MR (recall élevé). PR globalement très propre côté conventions (aucune violation CLAUDE.md relevée : strict_types, #[Auditable], i18n d'audit, COMMENT ON COLUMN, priority:1 sur l'export, useApi/useFormErrors/usePaginatedList/Malio*, pagination, tests fournis).

8 points ci-dessous en commentaires inline, du plus grave au moins grave. Le #1 est le seul bloquant (500 atteignable en parcours nominal). Points volontaires non signalés : DSD non unique (ERP-193) et brouillons dans l'export avec colonnes vides + statut « En attente » (miroir de la liste).

Revue automatisée de la MR (recall élevé). PR globalement **très propre** côté conventions (aucune violation CLAUDE.md relevée : strict_types, #[Auditable], i18n d'audit, COMMENT ON COLUMN, priority:1 sur l'export, useApi/useFormErrors/usePaginatedList/Malio*, pagination, tests fournis). 8 points ci-dessous en commentaires inline, du plus grave au moins grave. Le **#1** est le seul bloquant (500 atteignable en parcours nominal). Points volontaires non signalés : DSD non unique (ERP-193) et brouillons dans l'export avec colonnes vides + statut « En attente » (miroir de la liste).
@@ -0,0 +48,4 @@
* faire échouer l'autre).
*/
async function load(): Promise<void> {
await Promise.allSettled([
Author
Owner

🟢 Robustesse — selects référentiels : 403 silencieux → cul-de-sac

Les erreurs de /clients et /suppliers sont avalées par Promise.allSettled. Si un rôle porteur de logistique.weighing_tickets.manage (ex. Usine) ne dispose pas de commercial.clients.view/suppliers, le select est vide sans message et la validation échoue ensuite en 422 sur client/supplier sans moyen de corriger. À confirmer vs la matrice RBAC ; sinon afficher un message explicite quand le référentiel est inaccessible.

🟢 **Robustesse — selects référentiels : 403 silencieux → cul-de-sac** Les erreurs de `/clients` et `/suppliers` sont avalées par `Promise.allSettled`. Si un rôle porteur de `logistique.weighing_tickets.manage` (ex. Usine) ne dispose pas de `commercial.clients.view`/suppliers, le select est **vide sans message** et la validation échoue ensuite en 422 sur `client`/`supplier` sans moyen de corriger. À confirmer vs la matrice RBAC ; sinon afficher un message explicite quand le référentiel est inaccessible.
@@ -0,0 +371,4 @@
* pesée PDF (RG-5.08) aussi bien à la validation d'un brouillon qu'à
* l'enregistrement d'un ticket déjà validé. Retour à la liste au succès.
*/
async function submitPrimary(): Promise<void> {
Author
Owner

🟡 UX — « Enregistrer » sur un ticket déjà validé rejoue /validate et ré-ouvre le PDF

Pour un ticket VALIDATED le bouton s'intitule « Enregistrer » mais submitPrimary appelle quand même saveDraft() puis PATCH …/validate, puis window.open(…/print.pdf). Une simple modif (ex. immatriculation) repasse donc par la validation stricte finalize et rouvre le bon de pesée dans un nouvel onglet — surprenant pour un « Enregistrer ». Côté serveur rien n'interdit non plus de re-déclencher /validate sur un ticket déjà validé. Prévoir un chemin de sauvegarde « draft-only » (PATCH standard) pour un ticket validé.

🟡 **UX — « Enregistrer » sur un ticket déjà validé rejoue `/validate` et ré-ouvre le PDF** Pour un ticket `VALIDATED` le bouton s'intitule « Enregistrer » mais `submitPrimary` appelle quand même `saveDraft()` puis `PATCH …/validate`, puis `window.open(…/print.pdf)`. Une simple modif (ex. immatriculation) repasse donc par la validation stricte `finalize` et **rouvre le bon de pesée dans un nouvel onglet** — surprenant pour un « Enregistrer ». Côté serveur rien n'interdit non plus de re-déclencher `/validate` sur un ticket déjà validé. Prévoir un chemin de sauvegarde « draft-only » (PATCH standard) pour un ticket validé.
@@ -0,0 +250,4 @@
}
/** Déclenche la pesée bascule puis enregistre le brouillon (ERP-193). */
async function confirmAuto(): Promise<void> {
Author
Owner

🟡 Duplication — new.vue et [id]/edit.vue partagent ~250 lignes quasi identiques

Corps du formulaire (grille contrepartie + champ conditionnel + immat + « Tout format »), les deux modales (auto + manuelle), counterpartyOptions, confirmAuto/confirmManual, saveDraft et le cycle de vie sont copiés-collés entre les deux pages, avec des divergences cosmétiques déjà présentes (catch (error) vs catch (e)). Toute évolution devra être faite deux fois. À extraire dans un WeighingTicketFormBody.vue + la persistance/modales dans le composable (qui ne fait aujourd'hui que le GET) — réduirait mécaniquement le risque des deux points UX ci-dessus.

🟡 **Duplication — `new.vue` et `[id]/edit.vue` partagent ~250 lignes quasi identiques** Corps du formulaire (grille contrepartie + champ conditionnel + immat + « Tout format »), les **deux** modales (auto + manuelle), `counterpartyOptions`, `confirmAuto`/`confirmManual`, `saveDraft` et le cycle de vie sont copiés-collés entre les deux pages, avec des divergences cosmétiques déjà présentes (`catch (error)` vs `catch (e)`). Toute évolution devra être faite deux fois. À extraire dans un `WeighingTicketFormBody.vue` + la persistance/modales dans le composable (qui ne fait aujourd'hui que le GET) — réduirait mécaniquement le risque des deux points UX ci-dessus.
@@ -0,0 +309,4 @@
await saveDraft()
}
catch (error) {
manualModal.errors = { weight: weighbridge.extractWeighbridgeError(error) }
Author
Owner

🟡 Bug UX — un 422 sur le DSD s'affiche sous le champ Poids

dsd porte #[Assert\Positive] côté WeighbridgeReadingResource. Si l'opérateur saisit 0 (ou ≤ 0), le POST /weighbridge_readings renvoie une 422 de propertyPath dsd, mais le catch mappe toujours le message sous weight ({ weight: extractWeighbridgeError(error) }). L'erreur DSD apparaît sous Poids, le champ DSD reste vierge. Router le message vers le bon champ (réutiliser mapViolationsToRecord). Idem dans [id]/edit.vue.

🟡 **Bug UX — un 422 sur le DSD s'affiche sous le champ Poids** `dsd` porte `#[Assert\Positive]` côté `WeighbridgeReadingResource`. Si l'opérateur saisit `0` (ou ≤ 0), le `POST /weighbridge_readings` renvoie une 422 de `propertyPath` **`dsd`**, mais le `catch` mappe **toujours** le message sous `weight` (`{ weight: extractWeighbridgeError(error) }`). L'erreur DSD apparaît sous Poids, le champ DSD reste vierge. Router le message vers le bon champ (réutiliser `mapViolationsToRecord`). Idem dans `[id]/edit.vue`.
@@ -0,0 +10,4 @@
* absente ou invalide. Lit les composantes locales (cohérent avec l'affichage
* des autres répertoires M1M4).
*/
export function formatDateFr(value: string | null | undefined): string {
Author
Owner

🟢 Mineur — formatDateFr dupliqué + lecture en fuseau local

Fonction identique à celle de transport/pages/carriers/index.vue (3ᵉ variante de rendu date FR dans le repo). De plus elle lit getDate()/getMonth() en fuseau navigateur : sur une date proche de minuit avec offset +02:00, un runtime UTC (CI/Vitest, poste hors Europe/Paris) décale d'un jour, alors que l'écran d'édition (slice de la chaîne brute) et l'export XLSX (format('d/m/Y H:i') serveur) gardent le bon jour → divergence liste vs édition vs export. À hoister dans shared/utils/date.ts à côté de nowIsoDateTime/todayIso.

🟢 **Mineur — `formatDateFr` dupliqué + lecture en fuseau local** Fonction identique à celle de `transport/pages/carriers/index.vue` (3ᵉ variante de rendu date FR dans le repo). De plus elle lit `getDate()/getMonth()` en fuseau **navigateur** : sur une date proche de minuit avec offset `+02:00`, un runtime UTC (CI/Vitest, poste hors Europe/Paris) décale d'un jour, alors que l'écran d'édition (slice de la chaîne brute) et l'export XLSX (`format('d/m/Y H:i')` serveur) gardent le bon jour → divergence liste vs édition vs export. À hoister dans `shared/utils/date.ts` à côté de `nowIsoDateTime`/`todayIso`.
@@ -260,3 +310,3 @@
* champs hors-branche ERP-185).
*/
#[Assert\Callback]
#[Assert\Callback(groups: ['finalize'])]
Author
Owner

🔴 Bug — brouillon avec contrepartie partielle → 500 (au lieu d'une 422 mappable)

validateCounterpartyConsistency est passé en groups: ['finalize'] : il ne joue plus sur les POST/PATCH brouillon.

Parcours réel : l'opérateur choisit « Client » dans le menu contrepartie avant d'avoir sélectionné le client, puis effectue une pesée → confirmAuto/confirmManual appelle saveDraft(). buildDraftPayload() (via compact()) retire client: null mais conserve counterpartyType: 'CLIENT'. En groupe Default, Assert\Choice passe, NotBlank/callback sont finalize-only → la validation passe. Au persist, chk_wt_client_branch (counterparty_type <> 'CLIENT' OR client_id IS NOT NULL …) est violée → HTTP 500, toast générique, aucune erreur inline, retry impossible (contraire à ERP-101).

applyCounterpartyExclusivity ne peut pas inventer la FK manquante. Piste : faire jouer une variante « présence cohérente » du callback aussi en Default (message inline sur le champ), ou nullifier counterpartyType côté brouillon quand le champ de branche est absent.

🔴 **Bug — brouillon avec contrepartie partielle → 500 (au lieu d'une 422 mappable)** `validateCounterpartyConsistency` est passé en `groups: ['finalize']` : il ne joue plus sur les POST/PATCH brouillon. Parcours réel : l'opérateur choisit « Client » dans le menu contrepartie **avant** d'avoir sélectionné le client, puis effectue une pesée → `confirmAuto/confirmManual` appelle `saveDraft()`. `buildDraftPayload()` (via `compact()`) retire `client: null` **mais conserve `counterpartyType: 'CLIENT'`**. En groupe Default, `Assert\Choice` passe, `NotBlank`/callback sont `finalize`-only → la validation passe. Au persist, `chk_wt_client_branch` (`counterparty_type <> 'CLIENT' OR client_id IS NOT NULL …`) est violée → **HTTP 500**, toast générique, aucune erreur inline, retry impossible (contraire à ERP-101). `applyCounterpartyExclusivity` ne peut pas inventer la FK manquante. Piste : faire jouer une variante « présence cohérente » du callback aussi en Default (message inline sur le champ), ou nullifier `counterpartyType` côté brouillon quand le champ de branche est absent.
@@ -88,3 +87,4 @@
$this->allocateAutoDsd($data, $site);
}
$this->computeNetWeight($data);
Author
Owner

🟠 Bug — poids net jamais borné, un net négatif est persisté/affiché/imprimé

computeNetWeight() (l. 204-209) fait full - empty sans garantir full >= empty. Si l'opérateur intervertit les deux pesées (saisit la pesée chargée dans le bloc « vide »), net_weight devient négatif, est stocké, affiché en liste (-7 150 Kg) et imprimé sur le bon de pesée — rien ne le signale. Ajouter une garde (422 si full < empty, ou au moins un avertissement).

🟠 **Bug — poids net jamais borné, un net négatif est persisté/affiché/imprimé** `computeNetWeight()` (l. 204-209) fait `full - empty` sans garantir `full >= empty`. Si l'opérateur intervertit les deux pesées (saisit la pesée chargée dans le bloc « vide »), `net_weight` devient négatif, est stocké, affiché en liste (`-7 150 Kg`) et imprimé sur le bon de pesée — rien ne le signale. Ajouter une garde (422 si `full < empty`, ou au moins un avertissement).
@@ -0,0 +92,4 @@
* (user `sites.bypass_scope`, ou pas de site courant). Miroir de
* WeighingTicketProvider::currentScopeSite().
*/
private function currentScopeSite(): ?Site
Author
Owner

🟢 Altitude — règle de visibilité/cloisonnement recopiée 3 fois

findVisibleTicket()/currentScopeSite() (soft-delete + scope site + anti-énumération) sont des « miroirs » manuels de WeighingTicketProvider, et applySiteScope est mirroré une 3ᵉ fois dans WeighingTicketExportController. C'est une frontière de sécurité (§2.3) : un changement de scoping oublié dans l'un des trois fuiterait des tickets cross-site. De même, DsdAllocator et WeighingTicketNumberAllocator réimplémentent le même compteur par site (ON CONFLICT DO NOTHING + SELECT … FOR UPDATE + UPDATE). Un service de visibilité partagé + un SiteCounterAllocator(table) paramétré seraient préférables.

🟢 **Altitude — règle de visibilité/cloisonnement recopiée 3 fois** `findVisibleTicket()`/`currentScopeSite()` (soft-delete + scope site + anti-énumération) sont des « miroirs » manuels de `WeighingTicketProvider`, et `applySiteScope` est mirroré une 3ᵉ fois dans `WeighingTicketExportController`. C'est une frontière de sécurité (§2.3) : un changement de scoping oublié dans l'un des trois fuiterait des tickets cross-site. De même, `DsdAllocator` et `WeighingTicketNumberAllocator` réimplémentent le même compteur par site (`ON CONFLICT DO NOTHING` + `SELECT … FOR UPDATE` + `UPDATE`). Un service de visibilité partagé + un `SiteCounterAllocator(table)` paramétré seraient préférables.
tristan added 2 commits 2026-06-24 14:16:08 +00:00
Un brouillon dont le type de contrepartie est choisi sans son champ associé
(client/fournisseur null, ou libellé « Autre » vide) violait chk_wt_*_branch
et levait une 500 : le callback de cohérence RG-5.03 ne joue qu'au groupe
finalize, laissant passer l'incohérence à l'enregistrement du brouillon.

- back : WeighingTicketProcessor retire la contrepartie entière quand le champ
  de branche est absent (clearCounterparty) au lieu de persister un état
  incohérent. N'affecte que le brouillon (à la validation, le callback finalize
  lève déjà une 422 avant le processor).
- front : buildDraftPayload n'émet le type que si son champ associé est rempli ;
  la validation continue d'envoyer toujours le type pour la 422 métier.
- tests : 2 cas back (CLIENT sans client, AUTRE libellé vide) + 2 cas front.
fix(front) : 422 de pesée manuelle mappé sous le bon champ (poids/DSD) (ERP-193)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m56s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 5m21s
9b476f22bb
La modale de pesée manuelle plaçait toute erreur du POST /weighbridge_readings
sous le champ Poids : un 422 Assert\Positive sur le DSD (DSD ≤ 0) s'affichait
donc sous Poids, le champ DSD restant vierge.

confirmManual mappe désormais les violations par propertyPath via
mapViolationsToRecord (le nom du champ front = le propertyPath back), et ne
retombe sur le message générique sous Poids que pour les erreurs sans
violations (503 pont indisponible, réseau). Appliqué à new.vue et [id]/edit.vue.
tristan added 1 commit 2026-06-24 14:22:29 +00:00
refactor(front) : formatDateFr mutualisé dans shared/utils/date + rendu déterministe (ERP-191)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m33s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 5m19s
87513d130a
Trois copies identiques de formatDateFr (logistique weighingTicketFormat,
transport carriers/index, CarrierQualimatTab) fusionnées en un seul helper
partagé. La nouvelle version lit la date directement dans la chaîne ISO (10
premiers caractères) au lieu de new Date(value).getDate() : un datetime porteur
d'un offset (…+02:00, …Z) ne bascule plus d'un jour selon le fuseau du
navigateur / runner CI, et reste cohérent avec l'écran d'édition (slice) et
l'export serveur (format d/m/Y).

weighingTicketFormat ré-exporte le helper (imports inchangés côté écrans).
Tests de déterminisme fuseau ajoutés dans shared/utils/date.test.ts.
tristan added 1 commit 2026-06-24 14:34:09 +00:00
feat(front) : liste des tickets de pesée à 25 lignes/page par défaut (ERP-188)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 49s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m20s
998368f1f6
Le tableau des tickets démarre désormais à 25 items/page (au lieu de 10) via
defaultItemsPerPage de usePaginatedList. 25 fait partie des options [10, 25, 50]
et reste sous le max serveur (50) ; l'utilisateur peut toujours rebasculer via
le sélecteur. Test de contrat du repository ajouté.
tristan merged commit faafd99ef8 into develop 2026-06-24 14:38:02 +00:00
tristan deleted branch feat/erp-191-i18n-site-courant 2026-06-24 14:38:02 +00:00
Sign in to join this conversation.