--- # === IDENTITÉ === module: M1 nom: "Répertoire clients" ecran: repertoire-clients owner_spec: Matthieu backup_spec: Tristan version: V0 date_redaction: 2026-05-28 # === LIENS === maquette_figma: "https://www.figma.com/design/jRYgT0T9c03VsEbjGhCwwS/Composants---Design-System?node-id=1132-31898" regles_metier: [RG-1.01, RG-1.02, RG-1.03, RG-1.04, RG-1.05, RG-1.06, RG-1.07, RG-1.08, RG-1.09, RG-1.10, RG-1.11, RG-1.12, RG-1.13, RG-1.14, RG-1.15, RG-1.16, RG-1.17, RG-1.18, RG-1.19, RG-1.20, RG-1.21] roles: [Admin, Bureau, Compta, Commerciale, Usine] lien_spec_back: ./spec-back.md # === VALIDATION CLIENT #1 === client_validation_1: statut: validee date: 2026-05-22 canal: ecrit valide_par: "Matthieu (CP MALIO) — validation implicite, périmètre projet" resume: "Module 1 — Répertoire clients. Page d'entrée Commercial. Datatable + 3 écrans (Ajouter / Consulter / Modifier). Création par onglets : Information / Contact / Adresse / Comptabilité (Transport, Statistiques, Rapports, Échanges = placeholders blancs)." trace_archivee: "uploads/4a1b026f-M1-reportoire-clients.docx (V0 d'origine .docx)" # === LIEN LESSTIME === lesstime_taskgroup_id: 23 lesstime_project_id: 6 statut_global: en_dev --- # Module 1 — Répertoire clients (V0 front) > **Origine** : spec front V0 livrée le 22/05/2026 (`M1-reportoire-clients.docx`). Restitution Markdown pour intégration au workflow MALIO. Le contenu original n'est pas modifié — toute précision et toute décision (en particulier côté back) vit dans [`spec-back.md`](./spec-back.md). ## But Permettre aux utilisateurs Starseed (selon rôle) de gérer le **répertoire des clients** de l'organisation : consultation, création, modification, archivage. Cette page est la **porte d'entrée du module Commercial**. ## Accès - **Depuis** : menu principal → section **Commercial** → entrée « Répertoire clients » - **Rôles autorisés** : | Rôle | Consultation | Création / Modification | Archivage | |---|---|---|---| | **Admin** | ✅ Tout | ✅ Tout | ✅ | | **Bureau** | ✅ Tout | ✅ Tout sauf onglet Comptabilité | ❌ | | **Compta** | ✅ Tout | ❌ (lecture seule) | ❌ | | **Commerciale** | ✅ Tout sauf Comptabilité | ✅ Tout sauf Comptabilité | ❌ | | **Usine** | ❌ | ❌ | ❌ | > **⚠ Décision validée par Tristan (28/05/2026)** : le rôle **Compta est en lecture seule** sur l'ensemble du module clients, y compris l'onglet Comptabilité. Le tableau d'origine du `.docx` indiquait « Compta = Ajout / Modification : Onglet Comptabilité uniquement » — cette ligne est **invalidée** par cette spec. Si un besoin métier d'édition apparaît plus tard, une décision archi dédiée sera prise (cf. HP-X de [`spec-back.md`](./spec-back.md)). ## Navigation L'écran est la page d'entrée du module **Commercial**. Titre : « **Répertoire clients** ». - Affichage principal : un **datatable** listant tous les clients **actifs** de l'organisation (les clients archivés sont masqués par défaut — filtre UI dédié pour les voir). - **Clic sur une ligne** → bascule sur l'écran **Consultation client** (page dédiée, pas un drawer — cf. maquette Figma). - **Bouton « + Ajouter »** (en haut à droite) → bascule sur l'écran **Ajouter un client**. - **Bouton « Exporter »** (en haut à droite) → télécharge un **fichier XLSX** des clients **affichés** (cf. filtre actif). Format détaillé dans [`spec-back.md` § Export](./spec-back.md). ## Datatable du Répertoire Composant : ``. Colonnes (à raffiner avec Tristan en revue maquette) : | Colonne | Source | Tri | |---|---|---| | **Nom entreprise** | `client.companyName` | ASC par défaut | | **Contact principal** | `firstName + lastName` | Oui | | **Téléphone principal** | `phonePrimary` (formaté `XX XX XX XX XX`) | Non | | **Email principal** | `email` | Oui | | **Catégories** | liste des codes catégories séparés par `,` | Non | | **Site(s)** | sites rattachés à au moins une adresse (badges colorés) | Non | > **Filtre archivés** : toggle UI en haut du datatable. Désactivé par défaut. État local (pas dans l'URL — cf. règle ABSOLUE Starseed n°6). > **Pagination** : front via `` (volumétrie cible faible — quelques centaines). Tri serveur `companyName ASC` par défaut. ## Écran « Ajouter un client » Création par **onglets successifs avec validation incrémentale** : pour pouvoir passer à l'onglet suivant, il faut avoir validé l'onglet en cours. **Une fois un onglet validé, on passe automatiquement au suivant**, et les champs de l'onglet validé passent en lecture seule + bouton « Valider » désactivé (disabled). ### Formulaire principal (pré-onglets) C'est le 1er bloc à remplir. Sans validation de ce formulaire, les onglets ne sont pas accessibles. | Champ | Type composant | Obligatoire | Règle | |---|---|---|---| | **Nom du client (Entreprise)** | `` | Oui | RG-1.18 (normalisation UPPERCASE serveur) | | **Nom du contact principal** | `` | Conditionnel | RG-1.01 + RG-1.19 (Capitalize) | | **Prénom du contact principal** | `` | Conditionnel | RG-1.01 + RG-1.19 (Capitalize) | | **Catégorie** | `` (multi) | Oui | Liste des `Category` de l'API ; M2M Client ↔ Category | | **Téléphone principal** | `` (masque tel) | Oui | RG-1.02 + RG-1.20 (format `XX XX XX XX XX`) | | **Téléphone secondaire** | `` (masque tel) | Non | Apparaît au clic sur le bouton `+` (RG-1.02). Max 2 — bouton `+` disparaît une fois rempli. | | **Email** | `` type email | Oui | RG-1.21 (lowercase) | | **Distributeur / Courtier** | `` | Non | Valeurs : `Dépend du distributeur` / `Dépend du courtier` / `Aucun`. RG-1.03 conditionne les 2 champs suivants. | | **Nom du distributeur** | `` | Conditionnel | Visible si « Dépend du distributeur ». Liste = clients ayant ≥ 1 catégorie de type `DISTRIBUTEUR`. RG-1.03. | | **Nom du courtier** | `` | Conditionnel | Visible si « Dépend du courtier ». Liste = clients ayant ≥ 1 catégorie de type `COURTIER`. RG-1.03. | | **Prestation de triage** | `` | Non | — | **Action** : « Valider » (``) → POST `/api/clients` ([`spec-back.md` § 4.3](./spec-back.md)). Si succès, on passe automatiquement à l'onglet « Information ». ### Onglet « Information » Saisir les informations de l'entreprise. | Champ | Type | Obligatoire | Règle | |---|---|---|---| | **Description** | `` | Conditionnel | RG-1.04 (obligatoire pour rôle Commerciale) | | **Concurrents** | `` | Conditionnel | RG-1.04 | | **Date de création** (de l'entreprise) | `` (exception Malio — pas de composant date couvert) | Conditionnel | RG-1.04 | | **Nombre de salariés** | `` | Conditionnel | RG-1.04 | | **CA €** | `` | Conditionnel | RG-1.04 | | **Dirigeant** | `` | Conditionnel | RG-1.04 | | **Résultat €** | `` | Conditionnel | RG-1.04 | **Action** : « Valider » → PATCH partiel `/api/clients/{id}` (groupe `client:write:information`). ### Onglet « Contact » Saisir un ou plusieurs contacts associés au client. Le 1er bloc est **pré-rempli** depuis les champs du formulaire principal (Nom, Prénom, Téléphone, Email — édition autorisée). **Bloc Contact** : | Champ | Type | Obligatoire | Règle | |---|---|---|---| | **Nom** | `` | Conditionnel | RG-1.05 + RG-1.19 (Capitalize) | | **Prénom** | `` | Conditionnel | RG-1.05 + RG-1.19 (Capitalize) | | **Fonction** | `` | Non | — | | **Téléphone** (x1, +1 possible) | `` | Non | RG-1.20 (format) | | **Email** | `` type email | Non | RG-1.21 (lowercase) | **RG-1.14 (renforcement validée par Tristan le 28/05)** : **au moins 1 bloc Contact valide** (au moins Nom OU Prénom rempli) est obligatoire pour valider l'onglet. Donc l'onglet Contact ne peut pas être finalisé vide. **Actions** : - « + Nouveau contact » : ajoute un bloc. Bouton **désactivé tant que le bloc précédent n'a pas Prénom OU Nom rempli** (RG-1.05). - « Supprimer » (icône) sur un bloc : modal de confirmation (`` Annuler / Confirmer). Si Oui → suppression du bloc. - « Valider » → PATCH `/api/clients/{id}/contacts` (création/mise à jour de la collection). ### Onglet « Adresse » Saisir une ou plusieurs adresses du client, rattachées à un ou plusieurs sites Starseed (Châtellerault 86 / Saint-Jean 17 / Pommevic 82) et à des contacts. **Bloc Adresse** : | Champ | Type | Obligatoire | Règle | |---|---|---|---| | **Prospect** | `` | Non | RG-1.06 — masque Adresse de livraison + Facturation si coché | | **Adresse de livraison** | `` | Non | RG-1.07 — masque Prospect si coché | | **Facturation** | `` | Non | RG-1.08 — masque Prospect si coché ; affiche le champ Email (RG-1.11) | | **Catégorie** | `` (multi) | Oui | Liste des `Category` | | **Pays** | `` | Oui | Préremplie « France » | | **Code postal** | `` (masque numérique) | Oui | RG-1.09 — déclenche autocomplete ville via BAN | | **Ville** | `` | Oui | RG-1.09 — alimentée par api-adresse.data.gouv.fr suivant le CP | | **Adresse** | `` (saisie assistée) | Oui | RG-1.09 — autocomplete BAN | | **Adresse complémentaire** | `` | Non | — | | **Sites Starseed** | `` (multi-checkbox 86 / 17 / 82) | Oui | RG-1.10 — ≥ 1 site obligatoire | | **Contact(s) rattaché(s)** | `` (multi) | Non | Liste = blocs Contact saisis dans l'onglet Contact | | **Email (facturation)** | `` type email | Conditionnel | RG-1.11 — visible/obligatoire uniquement si « Facturation » coché | **Actions** : - « + Nouvelle Adresse » : ajoute un bloc identique. - « Supprimer » : modal de confirmation puis suppression. - « Valider » → PATCH `/api/clients/{id}/addresses`. ### Onglet « Transport » 🚧 **Placeholder blanc au M1.** Frame vide. Aucun champ. Aucun bouton de validation. L'utilisateur passe automatiquement à l'onglet suivant. **Pas de mention « En cours »** — c'est juste blanc (décision Tristan 28/05). ### Onglet « Comptabilité » ⚠ **Accessible uniquement aux rôles avec `commercial.clients.accounting.manage`** (Admin seul au M1). Bureau et Commerciale ne voient pas l'onglet. Compta voit l'onglet **en lecture seule** (cf. décision Compta lecture seule). **Champs comptables** : | Champ | Type | Obligatoire | Règle | |---|---|---|---| | **SIREN** | `` (masque 9 chiffres) | Oui | RG-1.15 (unicité) | | **Numéro de compte** | `` | Oui | — | | **Mode de TVA** | `` | Oui | Liste depuis `/api/tva_modes` | | **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-1.12 — visible et obligatoire **si** Type de règlement = `VIREMENT`. Liste depuis `/api/banks`. | **Bloc RIB** (0..n blocs, présence obligatoire conditionnée par RG-1.13) : | Champ | Type | Obligatoire | Règle | |---|---|---|---| | **Libellé** | `` | Oui (si LCR) | RG-1.13 | | **BIC** | `` | Oui (si LCR) | RG-1.13 — `#[AuditIgnore]` (champ sensible) | | **IBAN** | `` | Oui (si LCR) | RG-1.13 — `#[AuditIgnore]` (champ sensible) | **Actions** : - « + RIB » : ajoute un bloc. - « Supprimer » (icône) : modal de confirmation. - « Valider » → PATCH `/api/clients/{id}/accounting`. ### Onglets « Statistiques » / « Rapports » / « Échanges » 🚧 **Placeholders blancs au M1.** Mêmes règles que Transport (frames vides, pas de validation). ## Écran « Consultation client » Tous les champs en **lecture seule**. Layout identique à l'écran Ajouter mais sans bouton « Valider », sans bouton `+` pour ajouter des blocs Contact / Adresse / RIB. - **Flèche retour** (à gauche) → revient au Répertoire. - **Bouton « Modifier »** (à droite, visible si l'utilisateur a la permission `commercial.clients.manage`) → bascule sur l'écran Modification. - **Bouton « Archiver »** (à droite, visible **uniquement pour Admin** via permission `commercial.clients.archive`) → ouvre une modal de confirmation, puis PATCH `/api/clients/{id}` `{ "isArchived": true }`. Le client passe en archivé (cf. flag `is_archived`). > Le client archivé peut être restauré (`isArchived: false`) — bouton « Restaurer » remplace « Archiver » dans la consultation d'un archivé. Décision validée Tristan 28/05. ### Onglets affichés en consultation Mêmes onglets qu'en création, **plus** les 4 placeholders blancs. L'utilisateur navigue librement entre les onglets (pas de séquence forcée en consultation). ## Écran « Modification client » Comportement identique à l'écran Ajouter sauf : - **Pas de formulaire principal** (les champs principaux sont é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` restent en lecture seule (pas de bouton Valider, pas d'icône suppression de bloc). - Les onglets placeholders restent inaccessibles à l'édition (blancs). ## Composants UI à utiliser (`@malio/layer-ui`) - **Datatable** : `` (Répertoire) - **Input texte** : `` - **Input numérique** : `` - **Input montant** : `` (CA, Résultat) - **TextArea** : `` (Description) - **Select simple** : `` (Pays, Ville, distributeur/courtier, refs comptables) - **Select multi (cases à cocher)** : `` (Catégorie, Sites, Contacts rattachés) - **Checkbox** : `` (Prospect, Adresse livraison, Facturation, Prestation de triage) - **Bouton** : ``, `` - **Toasts** : standards via `useApi()` **Exceptions autorisées** (à commenter `// TODO migrer quand Malio couvre`) : - `` pour « Date de création » (composant `MalioDate` non couvert) - Modal de confirmation : composant à confirmer côté équipe front (probablement `` ou un wrapper à créer dans `frontend/shared/`) ## Règles de formatage et normalisation Le serveur normalise systématiquement (cf. RG-1.18 à RG-1.21 dans [`spec-back.md`](./spec-back.md)) : | Champ | Normalisation serveur | Affichage front | |---|---|---| | Nom entreprise (`companyName`) | UPPERCASE intégral | UPPERCASE | | Nom + Prénom contact | Capitalize (1ère lettre majuscule + reste minuscule) | identique | | Téléphone (`phonePrimary`, `phoneSecondary`, contact phones) | Chiffres uniquement en BDD | Formaté `XX XX XX XX XX` à l'affichage (filter Vue) | | Email | lowercase intégral | identique | > **Le front ne fait pas la normalisation** — il envoie la valeur saisie, le serveur normalise puis renvoie la valeur normalisée. L'UI affiche immédiatement la valeur normalisée renvoyée par l'API. Cohérent avec le pattern `useApi()`. ## API adresse postale Le composant `Code postal` + `Ville` + `Adresse` est branché sur **api-adresse.data.gouv.fr** (Base Adresse Nationale, gratuite, française). - Composable dédié `useAddressAutocomplete()` (à créer en M1). - Appel HTTP **direct depuis le front** (CORS OK), pas de proxy back. - Pattern : à la saisie du code postal (5 chiffres), GET `https://api-adresse.data.gouv.fr/search/?q={cp}&type=municipality` → alimente le select Ville. Sur saisie d'adresse : `?q={addr}&postcode={cp}&type=housenumber` → suggestions adresse. - Cas dégradé : si l'API ne répond pas (offline, timeout), le champ Ville devient un `` libre éditable + toast d'avertissement. Validation serveur acceptera la saisie libre. ## Points laissés ouverts par la V0 (résolus côté back) | # | Zone d'ombre V0 | Résolution (cf. `spec-back.md`) | |---|---|---| | 1 | Catégorie en multi-select non clarifiée (1 ou n par client) | **M2M `client_category`** validée. CategoryType seedé avec `DISTRIBUTEUR`, `COURTIER`, `SECTEUR`, `AUTRE` (HP-3 du M0 levé). | | 2 | Distributeur / Courtier : liste de quoi ? | **Auto-référence Client** via 2 FK nullables `distributor_id` et `broker_id` (cf. RG-1.03). Une seule des deux est remplie à la fois. | | 3 | Onglet « Comptabilité » : qui édite ? | **Admin uniquement au M1.** Compta lecture seule (décision validée par Tristan 28/05). Bureau / Commerciale ne voient pas l'onglet. | | 4 | Workflow par onglet | **Sauvegarde incrémentale**. POST formulaire principal crée le `Client` (status implicite « actif »). Chaque onglet validé = PATCH partiel par groupe de sérialisation dédié. Pas d'état « draft ». | | 5 | Onglets « À venir » | **Placeholders blancs** (frames vides, pas de message). Ré-activables sans rebuild quand les modules associés arriveront. | | 6 | Archive vs soft delete | **Flag `is_archived` séparé de `deleted_at`**. Archive ≠ delete : un client archivé est masqué par défaut mais reste en BDD éditable (Admin seul). Filtres UI distincts. Soft delete = HP M2. | | 7 | Unicité métier | **SIREN, Nom entreprise, Email principal — tous uniques parmi non-archivés.** Index partiels Postgres. Tentative de doublon → 409 Conflict. | | 8 | Téléphones (max 2) | **2 colonnes plates** `phone_primary` + `phone_secondary`. Pas de table séparée. | | 9 | API code postal | **api-adresse.data.gouv.fr** (BAN). Appel direct front via composable dédié. Cas dégradé : saisie libre + toast. | | 10 | Référentiels comptables | **4 entités CRUD-ables** (`TvaMode`, `PaymentDelay`, `PaymentType`, `Bank`) seedées au M1, CRUD admin futur (HP-M2). | | 11 | Format de l'export | **XLSX uniquement** au M1. CSV à étudier en HP. | --- ## 📦 Tickets Lesstime générés **TaskGroup Lesstime** : à créer — `M1 — Répertoire clients` (projet `ERP / Starseed`, projectId=6). > Détail complet, table des tickets et action manuelle dans Lesstime → voir [`spec-back.md § Tickets Lesstime générés`](./spec-back.md#-tickets-lesstime-générés).