--- # === IDENTITÉ === module: M2 nom: "Répertoire fournisseurs" ecran: repertoire-fournisseurs owner_spec: Matthieu backup_spec: Tristan version: V0.2 date_redaction: 2026-06-02 # Historique : V0.2 (2026-06-03) — Refonte contact : suppression du bloc contact principal inline # du formulaire Supplier (Nom/Prénom/Téléphone/Téléphone 2/Email). Saisie via l'onglet Contacts. # Aligné sur M1. Cf. docs/specs/M1-clients/refonte-contact/README.md # === LIENS === maquette_figma: "https://www.figma.com/design/jRYgT0T9c03VsEbjGhCwwS/Composants---Design-System?node-id=1132-36987&p=f&m=dev" regles_metier: [RG-2.01, RG-2.02, RG-2.03, RG-2.04, RG-2.05, RG-2.06, RG-2.07, RG-2.08, RG-2.09, RG-2.10, RG-2.11, RG-2.12, RG-2.13, RG-2.14, RG-2.15, RG-2.16, RG-2.17] roles: [Admin, Bureau, Compta, Commerciale, Usine] lien_spec_back: ./spec-back.md # === VALIDATION CLIENT === client_validation_1: statut: validee date: 2026-05-22 version: V0 valide_par: "Matthieu (CP MALIO)" client_validation_2: statut: validee date: 2026-06-01 version: V0.1 valide_par: "Matthieu (CP MALIO)" resume: "Module 2 — Répertoire fournisseurs. Page d'entrée Commercial. Datatable + 3 écrans (Ajouter / Consulter / Modifier). Création par onglets : Information / Contact / Adresse / Comptabilité (Transport, Statistiques, Rapports, Échanges = placeholders 'À venir')." trace_archivee: "uploads/M2-reportoire-fournisseurs.docx (V0.1) + M2-reportoire-fournisseurs-V01.pdf" # === LIEN LESSTIME === lesstime_taskgroup_id: 26 lesstime_project_id: 6 statut_global: a_dev --- # Module 2 — Répertoire fournisseurs (V0.1 front) > **Origine** : spec front livrée le 22/05/2026 (V0), amendée le 01/06/2026 (V0.1) — `M2-reportoire-fournisseurs.docx`. Restitution Markdown pour intégration au workflow MALIO. Le contenu fonctionnel original n'est pas modifié ; toute décision technique (back) vit dans [`spec-back.md`](./spec-back.md). Le M2 réutilise massivement le pattern et les composants posés au [M1 clients](../M1-clients/spec-front.md). ## But Permettre aux utilisateurs Starseed (selon rôle) de gérer le **répertoire des fournisseurs** de l'organisation : consultation, création, modification, archivage. C'est la **deuxième porte d'entrée du module Commercial** (aux côtés des Clients). ## Accès - **Depuis** : menu principal → section **Commercial** → entrée « Répertoire fournisseurs » (route `/suppliers`). - **Rôles autorisés** : | Rôle | Consultation | Création / Modification | Archivage | |---|---|---|---| | **Admin** | ✅ Tout | ✅ Tout | ✅ | | **Bureau** | ✅ Tout | ✅ Tout sauf onglet Comptabilité | ❌ | | **Compta** | ✅ Tout | ✅ Onglet Comptabilité uniquement | ❌ | | **Commerciale** | ✅ Tout sauf Comptabilité | ✅ Tout sauf Comptabilité | ❌ | | **Usine** | ❌ (pas d'accès) | ❌ | ❌ | > **Note** : RBAC identique au M1, transposée sur `commercial.suppliers.*`. Compta édite uniquement l'onglet Comptabilité (SIREN / N° compte / TVA / Délai / Type de règlement / Banque / RIBs) d'un fournisseur existant ; Compta ne peut pas **créer** un fournisseur. **L'archivage est réservé à Admin** (cf. tableau du docx). ## Navigation Page d'entrée du module **Commercial** (route `/suppliers`). Titre : « **Répertoire fournisseurs** ». - Affichage principal : un **datatable** listant tous les fournisseurs **actifs** (les archivés sont masqués par défaut — toggle UI dédié). - **Clic sur une ligne** → écran **Consultation fournisseur** (page dédiée). - **Bouton « + Ajouter »** (haut droite) → écran **Ajouter un fournisseur**. - **Bouton « Filtrer »** (haut droite, **à côté de « + Ajouter »**) → ouvre le **panneau de filtres** (cf. ci-dessous). Un badge/compteur indique le nombre de filtres actifs ; un bouton « Réinitialiser » les vide. - **Bouton « Exporter »** (haut droite) → télécharge un **XLSX** des fournisseurs **affichés** (cf. filtres actifs). Format dans [`spec-back.md § 4.6`](./spec-back.md). ### Panneau de filtres (bouton « Filtrer ») Ouvre un drawer/popover (composant à confirmer côté équipe front — réutiliser le pattern M1 s'il existe). Filtres proposés, branchés sur les query params de `GET /api/suppliers` (cf. [`spec-back.md § 4.1`](./spec-back.md)) : | Filtre | Composant | Query param back | |---|---|---| | **Recherche** (nom entreprise / contact / email — recherche contact via `supplier_contact`, décision D1) | `` | `?search=` | | **Catégorie** | `` (multi, type FOURNISSEUR) | `?categoryCode=` | | **Site** | `` (86 / 17 / 82) | `?siteId=` | | **Inclure les archivés** | `` | `?includeArchived=true` | - À l'application des filtres → `setFilters(...)` de `usePaginatedList` (retombe en **page 1**), qui relance `GET /api/suppliers` avec les params. - **État 100 % local** (jamais dans l'URL — règle ABSOLUE n°6). Le bouton « Filtrer » + son panneau remplacent/regroupent l'ancien toggle « archivés » isolé. ## Datatable du Répertoire Composant : `` branché sur `usePaginatedList({ url: '/suppliers' })` (règle frontend obligatoire — pagination Hydra, état 100 % local). Colonnes **conformes à la maquette Figma** (4 colonnes) : | Colonne | Source | Tri | |---|---|---| | **Nom** | `supplier.companyName` | ASC par défaut | | **Catégories** | `supplier.categories[].name` (embarquées en liste — cohérence M1/ERP-62 ; libellé = `name`, pas `label`) | Non | | **Site** | `supplier.sites[].name` (agrégat des adresses via `getSites()` ; `Site` n'a pas de `code`) | Non | | **Dernière activité** | `supplier.updatedAt` (format `JJ-MM-AAAA`) — exposé dans `supplier:read` | Oui | > **Clic sur une ligne** (texte en bleu lien) → écran Consultation. > **Filtres** : regroupés dans le panneau du bouton « Filtrer » (cf. section précédente), dont l'inclusion des archivés (désactivée par défaut). **État local** (jamais dans l'URL — règle ABSOLUE n°6). > **Pagination** : `` + `usePaginatedList`, options **standard Starseed 10 / 25 / 50 (défaut 10)** — on **n'applique pas** le « Ligne : 20 » de la maquette (décision Matthieu : on reste sur le standard). Tri serveur `companyName ASC` par défaut. ## Écran « Ajouter un fournisseur » Création par **onglets successifs avec validation incrémentale** : pour passer à l'onglet suivant, il faut avoir validé l'onglet en cours. **Une fois un onglet validé, on passe automatiquement au suivant** ; les champs validés passent en lecture seule + bouton « Valider » désactivé (disabled). Cf. [`spec-back.md § 2.10`](./spec-back.md) (PATCH partiels par groupe de sérialisation). **Barre d'onglets en création (5 onglets, conforme maquette)** : `Information` · `Contacts` · `Adresses` · `Transport` · `Comptabilité`. L'onglet `Information` est actif par défaut juste après validation du formulaire principal. Les onglets `Statistiques`, `Rapports` et `Échanges` **n'apparaissent PAS dans le flux de création** — ils ne sont présents qu'en Consultation / Modification. ### Formulaire principal (pré-onglets) 1er bloc à remplir. Sans validation, les onglets ne sont pas accessibles. Une fois validé → POST `/api/suppliers`, puis bascule sur l'onglet Information ; les champs passent en readonly. > **V0.2 — refonte-contact** : le contact principal (Nom / Prénom / Téléphone / Téléphone 2 / Email) a été **retiré** du formulaire principal. Les coordonnées se saisissent dans l'onglet **Contacts** (RG-2.04 / RG-2.13). Le formulaire principal ne contient plus que Entreprise + Catégorie. | Champ | Type composant | Obligatoire | Règle | |---|---|---|---| | **Nom du fournisseur (Entreprise)** | `` | Oui | RG-2.12 (UPPERCASE serveur) | | **Catégorie** | `` (multi) | Oui | `Category` de **type FOURNISSEUR** via `GET /api/categories?typeCode=FOURNISSEUR` (RG-2.10). Libellé affiché = `category.name`. ⚠️ Le type + le filtre `?typeCode=` sont **à créer** côté back (n'existent pas en prod — cf. spec-back § 2.4). | **Action** : « Valider » (``) → POST `/api/suppliers` ([`spec-back.md § 4.3`](./spec-back.md)). Succès → onglet « Information ». ### Onglet « Information » Saisir les informations du fournisseur. | Champ | Type | Obligatoire | Règle | |---|---|---|---| | **Description** | `` | Conditionnel | RG-2.03 (obligatoire rôle Commerciale) | | **Concurrent** | `` | Conditionnel | RG-2.03 | | **Date création** (entreprise) | `` (exception Malio — `// TODO migrer`) | Conditionnel | RG-2.03 | | **Nombre de salariés** | `` | Conditionnel | RG-2.03 | | **CA €** | `` | Conditionnel | RG-2.03 | | **Dirigeant** | `` | Conditionnel | RG-2.03 | | **Résultat €** | `` | Conditionnel | RG-2.03 | | **Volume Prévisionnel** | `` | Conditionnel | RG-2.03 (champ spécifique fournisseur) | > **Disposition maquette** : 3 colonnes — ligne 1 (Description / Concurrent / Date création), ligne 2 (Nombre de salariés / CA / Dirigeant), ligne 3 (Résultat / Volume Prévisionnel). **Action** : « Valider » → PATCH `/api/suppliers/{id}` (groupe `supplier:write:information`). ### Onglet « Contact » Saisir un ou plusieurs contacts. **(V0.2 — refonte-contact : plus de pré-remplissage depuis le formulaire principal ; les coordonnées du contact se saisissent directement ici.)** Au moins un bloc Contact valide est requis (RG-2.13). **Bloc Contact** : | Champ | Type | Obligatoire | Règle | |---|---|---|---| | **Nom** | `` | Conditionnel | RG-2.04 + RG-2.12 (Capitalize) | | **Prénom** | `` | Conditionnel | RG-2.04 + RG-2.12 (Capitalize) | | **Fonction** | `` | Non | — | | **Téléphone** (x1, +1 possible) | `` | Non | RG-2.12 (format) | | **Email** | `` type email | Non | RG-2.12 (lowercase) | **RG-2.04 / RG-2.13** : au moins 1 bloc Contact valide (Nom OU Prénom rempli) pour valider l'onglet — l'onglet Contact ne peut pas être finalisé vide. **Actions** : - « + Nouveau contact » : ajoute un bloc. **Désactivé tant que le bloc précédent n'a pas Prénom OU Nom** (RG-2.04). - « Supprimer » (icône) : modal de confirmation, puis suppression du bloc. - « Valider » → PATCH `/api/suppliers/{id}/contacts`. ### Onglet « Adresse » Saisir une ou plusieurs adresses, rattachées à un ou plusieurs sites (86 / 17 / 82) et à des contacts. **Bloc Adresse** : | Champ | Type | Obligatoire | Règle | |---|---|---|---| | **Type d'adresse** | `` — `Prospect` / `Départ` / `Rendu` | Oui | RG-2.09 (exclusif, enum `PROSPECT`/`DEPART`/`RENDU`) | | **Pays** | `` (saisie assistée — préremplie « France ») | Oui | — | | **Code postal** | `` (saisie assistée) | Oui | RG-2.05 — déclenche autocomplete ville (BAN) | | **Ville** | `` (saisie assistée) | Oui | RG-2.05 — alimentée par api-adresse.data.gouv.fr suivant le CP | | **Adresse** | `` (saisie assistée) | Oui | RG-2.05 — autocomplete BAN | | **Adresse complémentaire** | `` | Non | — | | **Sélecteur de site** | `` (86 / 17 / 82) | Oui | RG-2.06 — ≥ 1 site. Les 3 cases = les 3 `Site` fixes ; libellés « 86/17/82 » = **préfixe du `postalCode`** (86100/17400/82400), pas un `Site.code` (qui n'existe pas). La sélection stocke des **IDs de Site** (M2M). | | **Catégories** | `` (multi) | Oui | Catégories de type FOURNISSEUR (RG-2.10), liées aux catégories du fournisseur | | **Contact** | `` (multi) | Non | Liste = blocs Contact saisis dans l'onglet Contact | | **Benne(s)** | `` (stepper −/+ , défaut 0) | Non | Champ spécifique fournisseur | | **Prestation de triage** | `` | Non | Champ spécifique fournisseur (porté par l'adresse — colonne back `triage_provider`) | > **Disposition maquette par bloc** : ligne 1 = radio (Prospect / Départ / Rendu) + Pays + Code postal ; ligne 2 = Ville + Adresse + Adresse complémentaire ; ligne 3 = sites (86 / 17 / 82) + Catégories + Contact ; ligne 4 = Benne(s) + Prestation de triage. Icône corbeille en haut à droite de chaque bloc pour le supprimer. **Actions** : - « + Nouvelle Adresse » : ajoute un bloc identique au premier. - « Supprimer » : modal de confirmation puis suppression. - « Valider » → PATCH `/api/suppliers/{id}/addresses`. ### Onglet « Transport » 🚧 **Onglet placeholder minimal au M2.** Conforme à la maquette : la frame est **vide** (aucun champ, aucun bouton de validation, aucune API back). L'onglet reste navigable. Un libellé discret « À venir » est toléré mais non requis (la maquette ne l'affiche pas). Cet onglet **fait partie de la barre de création** (entre Adresses et Comptabilité). ### Onglet « Comptabilité » ⚠ **Accessible aux rôles avec `commercial.suppliers.accounting.view`** (Admin + Compta au M2). Bureau et Commerciale ne voient pas l'onglet. **Compta peut éditer** cet onglet (`accounting.manage`). Compta ne peut pas créer un fournisseur (pas de `manage` global). **Champs comptables** : | Champ | Type | Obligatoire | Règle | |---|---|---|---| | **SIREN** | `` (masque 9 chiffres) | Oui | 9 chiffres. **Pas d'unicité** (cf. § 2.6) | | **Numéro de compte** | `` | Oui | — | | **Mode de TVA** | `` | Oui | Liste depuis `/api/tva_modes` (référentiel M1) | | **N° de TVA** | `` | Oui | — | | **Délai de règlement** | `` | Oui | Liste depuis `/api/payment_delays` | | **Type de règlement** | `` | Oui | Liste depuis `/api/payment_types` | | **Banque** | `` | Conditionnel | RG-2.07 — visible et obligatoire **si** Type de règlement = `VIREMENT`. Liste depuis `/api/banks` (SG / CIC / CA). | **Bloc RIB** (0..n, présence obligatoire conditionnée par RG-2.08) : | Champ | Type | Obligatoire | Règle | |---|---|---|---| | **Libellé** | `` | Oui (si LCR) | RG-2.08 | | **BIC** | `` | Oui (si LCR) | RG-2.08 | | **IBAN** | `` | Oui (si LCR) | RG-2.08 | **Actions** : - « + RIB » : ajoute un bloc. - « Supprimer » (icône) : modal de confirmation. - « Valider » → PATCH `/api/suppliers/{id}` (groupe `supplier:write:accounting`) + sous-ressource RIBs. ### Onglets « Statistiques » / « Rapports » / « Échanges » 🚧 **Placeholders minimaux au M2 — uniquement en Consultation / Modification** (ils n'apparaissent **pas** dans le flux de création, cf. maquette). Frames vides, pas de validation, pas d'API. ## Écran « Consultation fournisseur » Tous les champs en **lecture seule**. Layout identique à l'écran Ajouter mais sans bouton « Valider », sans `+` pour ajouter des blocs. - **Flèche retour** (gauche) → revient au Répertoire. - **Bouton « Modifier »** (droite, visible si `commercial.suppliers.manage`) → écran Modification. - **Bouton « Archiver »** (droite, visible **uniquement Admin** via `commercial.suppliers.archive`) → modal de confirmation, puis PATCH `/api/suppliers/{id}` `{ "isArchived": true }`. > Un fournisseur archivé peut être restauré (`isArchived: false`) — bouton « Restaurer » remplace « Archiver » dans la consultation d'un archivé. ### Onglets affichés en consultation Information / Contacts / Adresses / Transport / Statistiques / Rapports / Échanges / Comptabilité (les 4 derniers métiers en placeholder « À venir », Comptabilité selon permission). L'utilisateur navigue **librement** entre les onglets (pas de séquence forcée en consultation). ## Écran « Modification fournisseur » Comportement identique à l'écran Ajouter sauf : - **Pas de formulaire principal** réaffiché (champs principaux édités via les onglets correspondants). - Les champs sont **pré-remplis** avec les valeurs actuelles. - **Validation par onglet** : on peut modifier UN onglet sans toucher aux autres (PATCH partiel). - Les onglets pour lesquels l'utilisateur n'a **pas** la permission `manage` (ou `accounting.manage`) restent en **lecture seule** (pas de bouton Valider, pas d'icône suppression). - Les onglets placeholders « À venir » restent non éditables. ## Composants UI à utiliser (`@malio/layer-ui`) - **Datatable** : `` (+ `usePaginatedList`) - **Input texte** : `` - **Input numérique** : `` (Nombre de salariés, Volume prévisionnel, Bennes) - **Input montant** : `` (CA, Résultat) - **TextArea** : `` (Description) - **Select simple** : `` (Pays, Ville, référentiels comptables) - **Select multi (cases à cocher)** : `` (Catégorie, Sites, Contacts rattachés) - **Radio** : `` (Type d'adresse Prospect / Départ / Rendu — RG-2.09) - **Checkbox** : `` (Prestataire de triage) - **Bouton** : ``, `` - **Toasts** : standards via `useApi()` **Exceptions autorisées** (commenter `// TODO migrer quand Malio couvre`) : - `` pour « Date Création » (`MalioDate` non couvert). - Modal de confirmation : `` ou wrapper partagé dans `frontend/shared/` (réutiliser celui du M1 si présent). ## Composables & appels API - `usePaginatedList({ url: '/suppliers' })` — liste paginée (obligatoire, règle frontend). La liste consomme `categories[]` (libellé = `name`) et `sites[]` (libellé = `name`, pas de `code`) **embarqués** + `updatedAt` (cohérence M1/ERP-62, cf. [`spec-back.md § 2.12 / § 4.0`](./spec-back.md)). Côté back, fetch-joins anti-N+1. - `useSupplier(id)` — charge le détail via `GET /api/suppliers/{id}`, qui **embarque** `contacts`, `addresses` (avec `sites` / `categories` / `contacts` imbriqués) et, si permission, `ribs` + scalaires compta. Les écrans Consultation et Modification se peuplent depuis cette seule réponse (RETEX M1 §2 : embed borné, pas de N+1 d'appels). **DoD avant intégration** : vérifier que le JSON réel contient bien ces blocs (cf. [`spec-back.md § 4.0.bis`](./spec-back.md)). - `useSupplierForm()` — workflow par onglet (POST principal + PATCH partiels par groupe), miroir de `useClientForm()`. - `useAddressAutocomplete()` — **réutilisé du M1** (BAN), pas de réécriture. - `usePermissions()` — masque l'onglet Comptabilité et le bouton Archiver. - Tous les appels passent par `useApi()` (jamais `$fetch` direct — règle ABSOLUE n°4). - Filter `formatPhoneFR()` — **réutilisé du M1** pour l'affichage `XX XX XX XX XX`. ## Règles de formatage et normalisation Le serveur normalise systématiquement (RG-2.12 — cf. [`spec-back.md`](./spec-back.md)) : | Champ | Normalisation serveur | Affichage front | |---|---|---| | Nom fournisseur (`companyName`) | UPPERCASE intégral | UPPERCASE | | Nom + Prénom contact | Capitalize | identique | | Téléphones (blocs `SupplierContact`) | Chiffres uniquement en BDD | Formaté `XX XX XX XX XX` (filter Vue) | | Email | lowercase intégral | identique | > Le front **ne normalise pas** : il envoie la valeur saisie, le serveur normalise et renvoie la valeur normalisée que l'UI affiche. Cohérent avec `useApi()`. ## API adresse postale Code postal + Ville + Adresse branchés sur **api-adresse.data.gouv.fr** (BAN) via le composable `useAddressAutocomplete()` **déjà créé au M1** (réutilisé tel quel) : - À la saisie du CP (5 chiffres) : `GET https://api-adresse.data.gouv.fr/search/?q={cp}&type=municipality` → alimente le select Ville. - À la saisie d'adresse : `?q={addr}&postcode={cp}&type=housenumber` → suggestions. - Cas dégradé (timeout / offline) : Ville en `` libre + toast d'avertissement. ## Différences notables avec le M1 (clients) | Zone | M1 clients | M2 fournisseurs | |---|---|---| | Distributeur / Courtier | Auto-référence Client (RG-1.03) | **Absent** | | Prestation de triage | Booléen sur le client (formulaire principal) | **Booléen sur l'adresse** (`triage_provider`) | | Type d'adresse | 3 checkboxes Prospect / Livraison / Facturation | **Radio exclusif** Prospect / Départ / Rendu (RG-2.09) | | Email facturation sur adresse | Oui (conditionnel) | **Absent** | | Champ adresse « Bennes » | — | **Présent** (nombre) | | Onglet Information | 7 champs | **8 champs** (ajout « Volume prévisionnel ») | | Catégories | type unique `CLIENT` (codes ERP-78) | **nouveau type `FOURNISSEUR`** | | Archivage | Admin | **Admin uniquement** (idem) | | Onglets « À venir » | frames blanches | **placeholder « À venir »** (minimal) | ## Points résolus côté back | # | Zone d'ombre | Résolution (cf. `spec-back.md`) | |---|---|---| | 1 | Catégorie multi-select | M2M `supplier_category`, `Category` de type **FOURNISSEUR** (RG-2.10) | | 2 | Type d'adresse Prospect/Départ/Rendu | Enum exclusif `address_type` (RG-2.09) | | 3 | Onglet Comptabilité : qui édite ? | Admin + Compta (`accounting.manage`) ; Bureau/Commerciale ne le voient pas | | 4 | Workflow par onglet | Sauvegarde incrémentale (POST principal + PATCH partiels) — pas d'état « draft » | | 5 | Onglets « À venir » | Placeholder minimal « À venir » (Transport / Stats / Rapports / Échanges) | | 6 | Archive vs delete | Flag `is_archived` séparé de `deleted_at` ; archivage Admin seul ; soft delete = HP | | 7 | Unicité métier | Nom de fournisseur uniquement (à valider — § 2.6). SIREN/email non uniques | | 8 | Référentiels comptables | Réutilisés du M1 (zéro duplication) | | 9 | API code postal | BAN via `useAddressAutocomplete()` du M1 | | 10 | Format export | XLSX uniquement (CSV = HP) | --- ## 📦 Tickets Lesstime **TaskGroup Lesstime** : à créer — `M2 — Répertoire fournisseurs` (projet `ERP / Starseed`, projectId=6). > Détail complet et action manuelle → voir [`spec-back.md § Tickets Lesstime`](./spec-back.md#-tickets-lesstime-à-découper).