# M4 — Répertoire transporteurs · Découpe en tickets Lesstime

> **Statut** : ✅ **poussé dans Lesstime** — TaskGroup **#31 « M4 — Répertoire transporteurs »** (projet STARSEED), 19 tickets **ERP-153 → ERP-171** au statut **Prêt à dev**.
> **Assignation** : tickets **Backend (1.1→1.11, ERP-153→163) → Matthieu** · tickets **Frontend (1.12→1.19, ERP-164→171) → Tristan**.
>
> | Pos | Ticket | Réf |
> |---|---|---|
> | 1.1 | Permissions transport.carriers.* + sidebar | ERP-153 |
> | 1.2 | Infra upload générique Shared | ERP-154 |
> | 1.3 | Migration BDD M4 | ERP-155 |
> | 1.4 | QualimatCarrier + endpoint recherche | ERP-156 |
> | 1.5 | Entités Carrier* + ApiResource + Provider | ERP-157 |
> | 1.6 | CarrierProcessor (RG-4.01/02/03 + LIOT) | ERP-158 |
> | 1.7 | Sous-ressource Adresses | ERP-159 |
> | 1.8 | Sous-ressource Contacts | ERP-160 |
> | 1.9 | Sous-ressource Prix + branches | ERP-161 |
> | 1.10 | Export XLSX | ERP-162 |
> | 1.11 | Tests PHPUnit + contrat JSON | ERP-163 |
> | 1.12 | Page Répertoire /carriers | ERP-164 |
> | 1.13 | Page Ajouter (layout + formulaire) | ERP-165 |
> | 1.14 | Saisie assistée QUALIMAT + conditionnels | ERP-166 |
> | 1.15 | Onglet Adresses (BAN) | ERP-167 |
> | 1.16 | Onglet Contacts | ERP-168 |
> | 1.17 | Onglet Prix | ERP-169 |
> | 1.18 | Consultation + Modification | ERP-170 |
> | 1.19 | Upload front + i18n + audit | ERP-171 |
> **Specs sources** : [`spec-back.md`](./spec-back.md) · [`spec-front.md`](./spec-front.md) — validées (docx V0 du 27/05/2026).
> **Maquette Figma** : node `1132-45376` ([lien](https://www.figma.com/design/jRYgT0T9c03VsEbjGhCwwS/Composants---Design-System?node-id=1132-45376&p=f&m=dev)).

## ⚠️ Dépendance amont (socle Tristan — en cours de merge)

Le M4 s'appuie sur le module `Transport` et le référentiel QUALIMAT, livrés par les PR de Tristan **en cours de merge** dans `develop` :

- **ERP-150** (PR #97) — module `Transport` (`TransportModule`, layer front, `config/modules.php`). **Requis** par tout le M4.
- **ERP-39** (PR #99) — sync QUALIMAT (`qualimat_carrier` + commande `app:qualimat:sync`). **Requis** par la saisie assistée (ticket 1.4).
- **ERP-149** (PR #101) — sync IDTF (`idtf_product`). **NON requis** par le M4 (référentiel autonome, hors écrans transporteurs).

> Les 3 PR sont **empilées** (`develop → ERP-150 → ERP-39 → ERP-149`). Démarrer le M4 une fois **ERP-150 + ERP-39 dans `develop`** (DoR des tickets 1.1 et 1.4). Brancher le M4 sur `develop` post-merge.

## Vue d'ensemble (ordre d'exécution)

| # | Ticket | Tag | Effort | RG / dépend |
|---|---|---|---|---|
| 1.1 | Déclarer permissions `transport.carriers.*` + sidebar | Backend | S | DoR : ERP-150 mergé |
| 1.2 | Créer l'infra d'upload générique `Shared` | Backend | M | § 2.7 |
| 1.3 | Migrer le schéma BDD M4 (carrier + sous-tables) | Backend | M | § 3.2 |
| 1.4 | Exposer `QualimatCarrier` (lecture seule) + endpoint recherche | Backend | S | RG-4.01 · DoR : ERP-39 mergé |
| 1.5 | Créer entités `Carrier*` + repos + `ApiResource` + `CarrierProvider` | Backend | M | § 3.3 / 4.0 |
| 1.6 | Implémenter `CarrierProcessor` (RG-4.01/4.02/4.03 + LIOT + normalisation + archive) | Backend | M | RG-4.01→4.03, 4.13, 4.14 |
| 1.7 | Sous-ressource Adresses (`carrier_address`) | Backend | S | RG-4.05→4.07 |
| 1.8 | Sous-ressource Contacts (`carrier_contact`) | Backend | S | RG-4.08 |
| 1.9 | Sous-ressource Prix (`carrier_price`) + RG branches | Backend | M | RG-4.09→4.11 |
| 1.10 | Export XLSX (répertoire + onglet Prix regroupé) | Backend | M | § 4.6 |
| 1.11 | Tests PHPUnit RG-4.01→4.14 + capture contrat JSON (DoD) | Backend | M | § 4.0.bis / 8.1 |
| 1.12 | Page Répertoire `/carriers` (datatable, filtres, export) | Frontend | M | RG-4.04 |
| 1.13 | Page Ajouter `/carriers/new` (layout, onglets, formulaire principal POST) | Frontend | M | RG-4.12 |
| 1.14 | Saisie assistée QUALIMAT + champs conditionnels (Affréter / AUTRE→Décharge / LIOT) | Frontend | M | RG-4.01→4.03 |
| 1.15 | Onglet Adresses (autocomplete BAN) | Frontend | M | RG-4.05→4.07 |
| 1.16 | Onglet Contacts | Frontend | S | RG-4.08 |
| 1.17 | Onglet Prix (Client/Fournisseur, sites) | Frontend | M | RG-4.09→4.11 |
| 1.18 | Pages Consultation + Modification | Frontend | M | — |
| 1.19 | Upload front (`useUpload`) + i18n + libellés audit | Frontend | S | § 2.8 |

**Total** : 19 tickets · ~11 back / 8 front · mini-MR de 1 à 4h.

---

## Tickets — détail

### 1.1 — Déclarer permissions `transport.carriers.*` + sidebar
**Position** : 1.1 • Suit : — • Précède : Migrer le schéma BDD M4
**Tag** : Backend • **Effort** : S
**Contexte** : `TransportModule::permissions()` renvoie aujourd'hui `[]`. Ce ticket pose le socle RBAC du module et son entrée de menu, prérequis de toute opération sécurisée.
**Spec liée** : [`spec-back.md § 5`](./spec-back.md) · [`spec-front.md § Accès`](./spec-front.md)
**Critères d'acceptation** :
- [ ] `TransportModule::permissions()` déclare `transport.carriers.view`, `transport.carriers.manage`, `transport.carriers.archive` ; `app:sync-permissions` les enregistre.
- [ ] **Matrice § 5.2** : Admin (view+manage+archive), Bureau (view+manage), Commerciale (view), Compta + Usine (aucune).
- [ ] **3 sources RBAC alignées dans le même commit** (règle ABSOLUE n°8) : `config/sidebar.php` (section Transport + item `/carriers` + permission), `personas.ts`, `SeedE2ECommand.php`.
- [ ] Item sidebar masqué pour Compta/Usine ; visible Admin/Bureau/Commerciale.
**Tests à prévoir** : permissions sync OK ; personas e2e cohérents (pas de drift).
**Tips** : DoR — ERP-150 mergé (module Transport présent). Section sidebar « Transport » (ou « Logistique » — à trancher, cosmétique).

### 1.2 — Créer l'infra d'upload générique `Shared`
**Position** : 1.2 • Suit : permissions • Précède : Migration M4
**Tag** : Backend • **Effort** : M
**Contexte** : la « Décharge » (RG-4.02) est le 1er d'une série d'uploads à venir. On pose une infra réutilisable, pas un upload ad hoc.
**Spec liée** : [`spec-back.md § 2.7`](./spec-back.md)
**Critères d'acceptation** :
- [ ] Table `uploaded_document` (`original_filename`, `stored_path`, `mime_type`, `size_bytes`, `checksum`, `created_at`, `created_by`) + COMMENT ON COLUMN.
- [ ] Service `Shared\Infrastructure\Upload\FileUploader` : validation MIME **server-side via `$file->getMimeType()`** (jamais `getClientMimeType()`), bornage taille, checksum sha256, écriture disque (`var/uploads/{yyyy}/{mm}/`).
- [ ] Endpoint `POST /api/uploaded_documents` (multipart) → renvoie l'IRI ; whitelist MIME explicite (PDF + images) ; hors whitelist → 422.
**Tests à prévoir** : PHPUnit — MIME hors whitelist → 422 ; MIME valide → IRI + ligne persistée ; checksum calculé.
**Tips** : générique et réutilisable (autres modules la consommeront). Antivirus / S3 / purge = HP (§ 9).

### 1.3 — Migrer le schéma BDD M4 (carrier + sous-tables)
**Position** : 1.3 • Suit : infra upload • Précède : QualimatCarrier
**Tag** : Backend • **Effort** : M
**Contexte** : créer le schéma du répertoire (entité éditable distincte du référentiel `qualimat_carrier`).
**Spec liée** : [`spec-back.md § 3.2`](./spec-back.md)
**Critères d'acceptation** :
- [ ] Migration namespace racine `DoctrineMigrations`, **postérieure** à `Version20260612160000`.
- [ ] Tables `carrier`, `carrier_address`, `carrier_contact`, `carrier_price` + FK (`qualimat_carrier`, `uploaded_document`, `client`, `client_address`, `supplier`, `supplier_address`, `site`, `user`).
- [ ] `certification_type` **nullable** (null seulement en cas LIOT) + CHECK enum ; CHECK `container_type`, `direction`, `pricing_unit`, `price_state`, branches Prix client/fournisseur.
- [ ] Index partiel `uq_carrier_name_active` (LOWER(name), WHERE non archivé & non supprimé).
- [ ] **`COMMENT ON COLUMN` sur TOUTES les colonnes** (règle n°12) + helper Timestampable/Blamable. `ColumnsHaveSqlCommentTest` vert.
- [ ] `make db-reset` passe ; schéma conforme.
**Tests à prévoir** : `make db-reset` OK ; `ColumnsHaveSqlCommentTest` vert ; index partiel présent.
**Tips** : PK `BIGINT` (cohérence module Transport) — à confirmer vs `INT`.

### 1.4 — Exposer `QualimatCarrier` (lecture seule) + endpoint recherche
**Position** : 1.4 • Suit : migration • Précède : entités Carrier*
**Tag** : Backend • **Effort** : S
**Contexte** : la saisie assistée du nom (RG-4.01) a besoin d'un endpoint de recherche sur le référentiel QUALIMAT, aujourd'hui alimenté en console mais non exposé.
**Spec liée** : [`spec-back.md § 4.7`](./spec-back.md) · RG-4.01
**Critères d'acceptation** :
- [ ] Entité `QualimatCarrier` (lecture seule) mappée sur la table existante `qualimat_carrier` (aucune écriture exposée).
- [ ] `GET /api/qualimat_carriers?search=` : fuzzy sur `name` (+ `siret`), **seulement `is_active = true`**, tri `name`, paginé (règle n°13).
- [ ] **Security** `is_granted('transport.carriers.view')`. Champs exposés : `id, siret, name, address, postalCode, city, phone, department, status, validityDate, isActive`.
**Tests à prévoir** : PHPUnit — recherche ne renvoie que les actifs ; pagination Hydra ; 403 sans permission.
**Tips** : DoR — ERP-39 mergé. Ne pas toucher la commande de sync.

### 1.5 — Créer entités `Carrier*` + repos + `ApiResource` + `CarrierProvider`
**Position** : 1.5 • Suit : QualimatCarrier • Précède : CarrierProcessor
**Tag** : Backend • **Effort** : M
**Contexte** : poser les entités, le contrat de sérialisation (groupes) et la lecture (liste + détail).
**Spec liée** : [`spec-back.md § 3.3 / 3.4 / 4.0 / 4.1 / 4.2`](./spec-back.md)
**Critères d'acceptation** :
- [ ] Entités `Carrier`, `CarrierAddress`, `CarrierContact`, `CarrierPrice` (`#[Auditable]`, `TimestampableBlamableTrait`), repos Doctrine.
- [ ] `ApiResource` Carrier : `GetCollection` + `Get` + `Post` + `Patch` avec `security` (§ 3.3) ; **pas de Delete**.
- [ ] Groupes de sérialisation : `carrier:read`, `carrier:item:read`, `qualimat:read`, embed `client:read`/`client_address:read`/`supplier:read`/`supplier_address:read`/`site:read` au détail (3 maillons § 4.0 — ⚠ les adresses de l'onglet Prix sont des entités `ClientAddress`/`SupplierAddress` distinctes).
- [ ] `CarrierProvider` paginé (`ApiPlatform\Doctrine\Orm\Paginator`) ; liste **sans cloisonnement site** (§ 2.3) ; anti-N+1 (§ 2.11).
- [ ] Piège booléen `isArchived` : `#[SerializedName('isArchived')]` sur le getter.
**Tests à prévoir** : liste exclut archivés par défaut ; `?includeArchived=true` ; enveloppe Hydra ; `isArchived` présent dans le JSON.
**Tips** : miroir `Supplier`/`Provider`. Pas d'onglet Comptabilité (≠ M2/M3).

### 1.6 — Implémenter `CarrierProcessor`
**Position** : 1.6 • Suit : entités • Précède : sous-ressource Adresses
**Tag** : Backend • **Effort** : M
**Contexte** : logique d'écriture du formulaire principal (POST/PATCH) : normalisation, champs conditionnels, archivage.
**Spec liée** : [`spec-back.md § 4.3 / 4.4 / 7`](./spec-back.md)
**Critères d'acceptation** :
- [ ] **RG-4.01** : POST avec `qualimatCarrier` → `certificationType=QUALIMAT` + FK persistée ; cas LIOT : `name='LIOT'` ⇒ `certificationType` non requis, `liotPlates` accepté.
- [ ] **RG-4.02** : `certificationType='AUTRE'` sans `dischargeDocument` → **422** (`#[Assert\Callback]`).
- [ ] **RG-4.03** : `isChartered=true` sans `indexationRate`/`containerType`/`volumeM3` → **422**.
- [ ] **RG-4.13** : normalisation (`name` UPPER, contacts Capitalize, phones digits, email lower, `liotPlates`).
- [ ] **RG-4.12** : doublon `name` (actifs) → **409**.
- [ ] **RG-4.14** : PATCH `isArchived` exige `transport.carriers.archive` (Admin) ; mode strict (403 sinon).
**Tests à prévoir** : PHPUnit sur chaque RG ci-dessus (cf. § 8.1).
**Tips** : `CarrierFieldNormalizer` miroir `SupplierFieldNormalizer`.

### 1.7 — Sous-ressource Adresses (`carrier_address`)
**Position** : 1.7 • Suit : CarrierProcessor • Précède : Contacts
**Tag** : Backend • **Effort** : S
**Spec liée** : [`spec-back.md § 4.5`](./spec-back.md) · RG-4.05→4.07
**Critères d'acceptation** :
- [ ] `POST /api/carriers/{id}/addresses`, `PATCH`/`DELETE /api/carrier_addresses/{id}` (security `manage`).
- [ ] **RG-4.06** : `postalCode` matche `^[0-9]{4,5}$` (autocomplete ville = front).
- [ ] **RG-4.05** : si affrété, adresse obligatoire (Pays/CP/Ville/Adresse) — validation conditionnelle.
**Tests à prévoir** : PHPUnit — CP invalide → 422 ; adresse affrété incomplète → 422.
**Tips** : RG-4.07 (bouton Valider masqué si QUALIMAT) = front, back accepte le PATCH.

### 1.8 — Sous-ressource Contacts (`carrier_contact`)
**Position** : 1.8 • Suit : Adresses • Précède : Prix
**Tag** : Backend • **Effort** : S
**Spec liée** : [`spec-back.md § 4.5`](./spec-back.md) · RG-4.08
**Critères d'acceptation** :
- [ ] `POST /api/carriers/{id}/contacts`, `PATCH`/`DELETE /api/carrier_contacts/{id}` (security `manage`).
- [ ] **RG-4.08** : bloc valide si ≥ 1 champ rempli (CHECK `chk_carrier_contact_filled` + Processor) ; **max 2 téléphones**.
**Tests à prévoir** : PHPUnit — contact vide → 422 ; 1 champ → 200.
**Tips** : miroir contacts M2/M3.

### 1.9 — Sous-ressource Prix (`carrier_price`) + RG branches
**Position** : 1.9 • Suit : Contacts • Précède : Export
**Tag** : Backend • **Effort** : M
**Spec liée** : [`spec-back.md § 4.5 / 7`](./spec-back.md) · RG-4.09→4.11
**Critères d'acceptation** :
- [ ] `POST /api/carriers/{id}/prices`, `PATCH`/`DELETE /api/carrier_prices/{id}` (security `manage`).
- [ ] **RG-4.10** (CLIENT) : `client`, `clientDeliveryAddress`, `departureSite` requis ; `clientDeliveryAddress` doit appartenir au `client` → sinon 422.
- [ ] **RG-4.11** (FOURNISSEUR) : `supplier`, `supplierSupplyAddress`, `deliverySite` requis ; `supplierSupplyAddress` appartient au `supplier` → sinon 422.
- [ ] Communs obligatoires : `containerType`, `pricingUnit`, `price`, `priceState` ; CHECK branches respectées.
**Tests à prévoir** : PHPUnit — branche CLIENT/FOURNISSEUR incomplète → 422 ; adresse étrangère → 422.
**Tips** : « Adresse départ/livraison 86/17/82 » = `Site` (FK) ; livraison client = `ClientAddress`, appro = `SupplierAddress` (relations ORM partagées).

### 1.10 — Export XLSX (répertoire + onglet Prix regroupé)
**Position** : 1.10 • Suit : Prix • Précède : Tests PHPUnit
**Tag** : Backend • **Effort** : M
**Spec liée** : [`spec-back.md § 4.6`](./spec-back.md)
**Critères d'acceptation** :
- [ ] `GET /api/carriers/export.xlsx` : transporteurs affichés (mêmes filtres) ; colonnes § 4.6.
- [ ] `GET /api/carriers/{id}/prices/export.xlsx` : tableau Prix regroupé Benne / Fond Mouvant (colonnes docx p.10).
- [ ] Controllers custom `#[Route(priority: 1)]` (conflit API Platform `{id}`) ; `Content-Disposition`.
**Tests à prévoir** : PHPUnit — 200 + en-tête fichier ; respect des filtres.
**Tips** : PhpSpreadsheet déjà présent.

### 1.11 — Tests PHPUnit RG-4.01→4.14 + capture contrat JSON (DoD)
**Position** : 1.11 • Suit : Export • Précède : Page Répertoire
**Tag** : Backend • **Effort** : M
**Spec liée** : [`spec-back.md § 4.0.bis / 8.1`](./spec-back.md)
**Critères d'acceptation** :
- [ ] Matrice RG-4.01→4.14 couverte (§ 8.1) + RBAC par rôle (Compta/Usine → 403).
- [ ] `CarrierSerializationContractTest` : capture JSON réel **liste + détail** ; `prices[].client`/`.supplier`/sites **embarqués** (pas IRI) ; `qualimatCarrier` embarqué ; `isArchived` présent.
- [ ] Anti-N+1 liste ; pagination Hydra ; audit (`entity_type='Carrier'`) ; `AuditableEntitiesHaveI18nLabelTest` vert.
- [ ] `CarrierFixtures` idempotent (§ 8.4) : transporteur QUALIMAT (validité passée), AUTRE+décharge, affrété, LIOT, complet (contacts/adresses/prix CLIENT+FOURNISSEUR), 1 archivé.
**Tests à prévoir** : suite complète `make test` verte.
**Tips** : coller les JSON capturés dans § 4.0.bis (DoD avant front).

### 1.12 — Page Répertoire `/carriers` (datatable, filtres, export)
**Position** : 1.12 • Suit : Tests back • Précède : Page Ajouter
**Tag** : Frontend • **Effort** : M
**Spec liée** : [`spec-front.md § Datatable / Filtres`](./spec-front.md) · Figma `1132-45377`
**Critères d'acceptation** :
- [ ] `<MalioDataTable>` + `usePaginatedList<Carrier>({url:'/carriers'})` ; colonnes Nom / Certification / Date de validité / Dernière activité.
- [ ] **RG-4.04** : date de validité QUALIMAT < aujourd'hui → **fond rouge**.
- [ ] Filtres (`search`, `certificationType`, `includeArchived`) → `setFilters` (page 1) ; **état 100 % local** (règle n°6).
- [ ] Boutons « + Ajouter » (si `manage`) / « Filtrer » / « Exporter » (XLSX) ; clic ligne → Consultation.
**Tests à prévoir** : Vitest — `usePaginatedList` (Hydra, exclusion archivés).
**Tips** : `useApi()` obligatoire ; pas de persistance URL.

### 1.13 — Page Ajouter `/carriers/new` (layout, onglets, formulaire principal POST)
**Position** : 1.13 • Suit : Répertoire • Précède : Saisie assistée QUALIMAT
**Tag** : Frontend • **Effort** : M
**Spec liée** : [`spec-front.md § Écran Ajouter / Formulaire principal`](./spec-front.md) · Figma node `1132-45382` (Ajouter – Qualimat)
**Critères d'acceptation** :
- [ ] Layout + barre d'onglets `Qualimat · Adresses · Contacts · Prix` ; validation incrémentale (onglet suivant accessible après validation).
- [ ] Formulaire principal (Nom, Liste certification, Affréter, …) → `POST /api/carriers` ; succès → bascule onglet + champs readonly.
- [ ] `useFormErrors` : mapping 422 inline par champ ; `{ toast:false }`.
**Tests à prévoir** : Vitest — `useCarrierForm` (workflow par onglet, POST principal).
**Tips** : miroir `useSupplierForm`/`useProviderForm`.

### 1.14 — Saisie assistée QUALIMAT + champs conditionnels
**Position** : 1.14 • Suit : Page Ajouter • Précède : Onglet Adresses
**Tag** : Frontend • **Effort** : M
**Spec liée** : [`spec-front.md § Formulaire principal / Onglet Qualimat`](./spec-front.md) · RG-4.01→4.03 · Figma nodes `1132-50717` (Affréter), `1132-50982` (AUTRE→Décharge), `1132-45593` (LIOT)
**Critères d'acceptation** :
- [ ] **RG-4.01** : saisie du nom → `GET /api/qualimat_carriers?search=` → modal « Êtes-vous sûr… » → copie Nom + certification (`QUALIMAT`, readonly) + adresse + FK conservée.
- [ ] **Cas LIOT** : nom `LIOT` → champ immatriculations seul, autres masqués.
- [ ] **RG-4.02** : certification `AUTRE` → champ Décharge visible **et obligatoire** (upload).
- [ ] **RG-4.03** : « Affréter » coché → indexation / benne-fond mouvant / volume visibles et obligatoires.
**Tests à prévoir** : Vitest — affichage conditionnel (Affréter, AUTRE, LIOT) ; copie QUALIMAT.
**Tips** : `useQualimatSearch()` ; `useUpload()` (ticket 1.19) pour la décharge.

### 1.15 — Onglet Adresses (autocomplete BAN)
**Position** : 1.15 • Suit : Saisie QUALIMAT • Précède : Onglet Contacts
**Tag** : Frontend • **Effort** : M
**Spec liée** : [`spec-front.md § Onglet Adresses`](./spec-front.md) · RG-4.05→4.07 · Figma node `1132-45670`
**Critères d'acceptation** :
- [ ] Bloc adresse (Pays/CP/Ville/Adresse/complément) → `PATCH /api/carriers/{id}/addresses`.
- [ ] **RG-4.06** : `useAddressAutocomplete()` (BAN) — ville auto depuis CP, dégradé texte libre.
- [ ] **RG-4.05** : champs préremplis si QUALIMAT ; obligatoires si affrété. **RG-4.07** : pas de bouton Valider si QUALIMAT.
**Tests à prévoir** : Vitest — autocomplete nominal + dégradé (réutilisation M1/M2/M3).
**Tips** : ne pas réécrire `useAddressAutocomplete()`.

### 1.16 — Onglet Contacts
**Position** : 1.16 • Suit : Adresses • Précède : Onglet Prix
**Tag** : Frontend • **Effort** : S
**Spec liée** : [`spec-front.md § Onglet Contacts`](./spec-front.md) · RG-4.08 · Figma node `1132-45756`
**Critères d'acceptation** :
- [ ] Blocs contact (Nom/Prénom/Fonction/Téléphone x1-2/Email) → `PATCH /api/carriers/{id}/contacts`.
- [ ] **RG-4.08** : « + Nouveau contact » bloqué tant que le bloc courant est vide ; suppression avec modal.
**Tests à prévoir** : Vitest — règle « ≥ 1 champ », max 2 téléphones.
**Tips** : `mapViolationsToRecord` par ligne (pattern collections M1/M2/M3).

### 1.17 — Onglet Prix (Client/Fournisseur, sites)
**Position** : 1.17 • Suit : Contacts • Précède : Consultation/Modification
**Tag** : Frontend • **Effort** : M
**Spec liée** : [`spec-front.md § Onglet Prix`](./spec-front.md) · RG-4.09→4.11 · Figma node `1132-45859`
**Critères d'acceptation** :
- [ ] Radio `direction` (Client/Fournisseur) → bascule des champs (**RG-4.09**).
- [ ] **RG-4.10** (Client) : Client + Adresse de livraison (du client) + Adresse de départ (86/17/82).
- [ ] **RG-4.11** (Fournisseur) : Fournisseur + Adresse d'approvisionnement + Adresse de livraison (86/17/82).
- [ ] Communs : Benne/FM, Forfait/Tonne, Prix (`MalioInputAmount`), État du prix → `PATCH /api/carriers/{id}/prices`.
**Tests à prévoir** : Vitest — bascule Client/Fournisseur, champs requis.
**Tips** : selects clients/fournisseurs/sites via endpoints existants (security élargie § 4.8).

### 1.18 — Pages Consultation + Modification
**Position** : 1.18 • Suit : Onglet Prix • Précède : Upload/i18n
**Tag** : Frontend • **Effort** : M
**Spec liée** : [`spec-front.md § Consultation / Modification`](./spec-front.md)
**Critères d'acceptation** :
- [ ] Consultation readonly (ouvre sur Adresses) ; flèche retour ; « Modifier » (si `manage`) ; « Archiver » (Admin) → PATCH `isArchived`.
- [ ] Onglet Prix consultation = tableau regroupé Benne/FM + bouton Exporter (XLSX).
- [ ] Modification = mêmes formulaires, champs pré-remplis, PATCH partiel par onglet.
**Tests à prévoir** : Vitest — `useCarrier(id)` peuple les écrans depuis une seule réponse ; visibilité boutons par permission.
**Tips** : « Restaurer » remplace « Archiver » sur un archivé.

### 1.19 — Upload front (`useUpload`) + i18n + libellés audit
**Position** : 1.19 • Suit : Consultation/Modification • Précède : —
**Tag** : Frontend • **Effort** : S
**Spec liée** : [`spec-back.md § 2.7 / 2.8`](./spec-back.md) · [`spec-front.md § Composables`](./spec-front.md)
**Critères d'acceptation** :
- [ ] Composable `useUpload()` : `POST /api/uploaded_documents` (multipart) → IRI posée sur `carrier.dischargeDocument` (RG-4.02).
- [ ] Clés i18n : libellés certification, sidebar (`sidebar.transport.*`), **libellés audit** `audit.entity.transport_carrier/carrieraddress/carriercontact/carrierprice`.
- [ ] `<MalioInputUpload>` (exception documentée si type non couvert).
**Tests à prévoir** : Vitest — `useUpload` (succès + erreur MIME).
**Tips** : `AuditableEntitiesHaveI18nLabelTest` exige les clés audit.

---

## Actions Lesstime (à exécuter au feu vert de Matthieu)

1. `create-group` projectId 6, title « M4 — Répertoire transporteurs » → récupérer l'`id`.
2. `create-task` ×19 (statut `Prêt à dev` = 6, priorité Moyen=2, effort dans la description), dans l'ordre 1.1 → 1.19 :
   - Tickets **1.1 → 1.11** (Backend, tag `3`) → **assigné à Matthieu**.
   - Tickets **1.12 → 1.19** (Frontend, tag `2`) → **assigné à Tristan**.
3. Mettre à jour le frontmatter des specs (`lesstime_taskgroup_id`) + lien du groupe.

> Au push : récupérer les `userId` via `list-users` (Matthieu = `5` selon le référentiel ; Tristan à confirmer) pour renseigner l'assignation à la création.
