docs(commercial) : refonte contact — suppression du contact inline (specs M1 + M2) (#54)
Auto Tag Develop / tag (push) Successful in 6s
Auto Tag Develop / tag (push) Successful in 6s
Acte la décision refonte-contact dans les specs : le contact principal inline (firstName/lastName/phonePrimary/phoneSecondary/email) est retiré des entités tiers (Client, Supplier). Les contacts vivent uniquement dans ClientContact / SupplierContact (onglet Contacts). Garantie « >=1 contact nommé » préservée par RG-1.05/1.14 (M1) et RG-2.04/2.13 (M2). - M1 (spec-back/spec-front/cahier) : modèle Client sans contact inline ; RG-1.01/1.02 supprimées ; D1 (recherche) / D2 (export) décrites ; version V1. - M2 (spec-back/spec-front) : FICHIERS NOUVEAUX (non versionnés sur develop), introduits déjà corrigés (Supplier sans contact inline, RG-2.01/2.02 supprimées) ; version V0.2. - docs/specs/M1-clients/refonte-contact/ : décision (README) + tickets (M1 back/front/specs, M2 specs) + prompts + amendement des tickets M2. Lesstime : tâches #103 (M1 back), #104 (M1 front), #105 (M1 specs), #106 (M2 specs) ; tickets M2 #85-#97 amendés. --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #54 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
This commit was merged in pull request #54.
This commit is contained in:
@@ -18,8 +18,8 @@ merge de la stack.
|
||||
|
||||
| RG | Intitulé | Test(s) | Source |
|
||||
|----|----------|---------|--------|
|
||||
| RG-1.01 | Prénom OU nom obligatoire → 422 | `ClientApiTest::testPostWithoutFirstOrLastNameReturns422` ; `ClientProcessorTest` (unit) | ERP-55 |
|
||||
| RG-1.02 | phoneSecondary persisté ; max 2 téléphones | `ClientFormulaireMainTest::testPostPersistsSecondaryPhoneNormalized` ; `::testThirdPhoneFieldIsIgnored` | **ERP-60** |
|
||||
| ~~RG-1.01~~ | _(supprimée V1 — refonte-contact)_ contact inline retiré du Client ; complétude couverte par RG-1.05 / RG-1.14 (`ClientContact`) | — | refonte-contact |
|
||||
| ~~RG-1.02~~ | _(supprimée du Client V1)_ téléphones inline retirés du Client (testés sur `ClientContact`) | — | refonte-contact |
|
||||
| RG-1.03 | distributor/broker exclusifs + type catégorie | `ClientApiTest::testPostWithDistributorAndBrokerReturns422` ; `::testPostDistributorReferencingNonDistributorReturns422` ; `::testPostValidDistributorReturns201` ; `ClientProcessorTest` (unit) | ERP-55 |
|
||||
| RG-1.04 | Onglet Information obligatoire pour rôle Commerciale | `ClientProcessorTest::testCommercialeIncompleteInformationIsUnprocessable` ; `::testNonCommercialeSkipsInformationCompleteness` (unit, dormant). **Test fonctionnel + durcissement → ERP-74** | ERP-55 / **ERP-74** |
|
||||
| RG-1.05 | Contact : prénom OU nom → 422 (CHECK) | `ClientSubResourceApiTest::testPostContactWithoutNameReturns422` | ERP-57 |
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# M1 · Ticket 1/3 (Backend) — Supprimer le contact inline du `Client`
|
||||
|
||||
## 1. Objectif
|
||||
|
||||
Retirer de l'entité `Client` (et de la table `client`) les **5 champs du contact
|
||||
principal inline** : `firstName`, `lastName`, `phonePrimary`, `phoneSecondary`, `email`.
|
||||
La gestion des contacts passe désormais **exclusivement** par la sous-entité
|
||||
`ClientContact` (onglet « Contacts »), déjà en place et déjà porteuse des mêmes champs.
|
||||
|
||||
Le code M1 est **déjà livré en prod** : ce ticket inclut donc une **migration de données**
|
||||
(backfill) pour ne perdre aucune information de contact existante avant de supprimer les
|
||||
colonnes.
|
||||
|
||||
Contexte et justification : voir `README.md` du dossier `refonte-contact`.
|
||||
|
||||
## 2. Périmètre
|
||||
|
||||
### IN
|
||||
|
||||
- Migration Doctrine : **backfill puis suppression** des 5 colonnes de `client`.
|
||||
- `Client` (entité) : supprimer les 5 propriétés, getters/setters, annotations ORM /
|
||||
`Assert` / `Groups`.
|
||||
- `ClientProcessor` : retirer les 5 champs de `MAIN_FIELDS`, `changedBusinessFields()`,
|
||||
`normalize()` ; supprimer `validateMainContact()` (RG-1.01 — n'a plus d'objet).
|
||||
- `DoctrineClientRepository::applySearch()` : trancher D1 (recherche) et l'appliquer.
|
||||
- `ClientExportController` : trancher D2 (colonnes export) et l'appliquer.
|
||||
- `ClientFixtures` : retirer les 5 paramètres inline de `ensureClient()` ; garantir que
|
||||
chaque client seedé possède au moins 1 `ClientContact` (déjà géré par `addContact()`).
|
||||
- Tests PHPUnit : mettre à jour / retirer les cas qui exercent ces 5 champs sur `Client`.
|
||||
|
||||
### OUT
|
||||
|
||||
- Toute modification de `ClientContact` / `ClientContactProcessor` : **inchangés** (c'est la
|
||||
cible, les champs y restent). `ClientFieldNormalizer` reste tel quel (toujours appelé par
|
||||
`ClientContactProcessor`).
|
||||
- Le front (formulaires, vues, types, i18n) → **ticket 2/3**.
|
||||
- Les specs (`spec-back.md`, `spec-front.md`, cahier de test) → **ticket 3/3**.
|
||||
|
||||
## 3. Fichiers à modifier
|
||||
|
||||
| Fichier | Action |
|
||||
|---|---|
|
||||
| `src/Module/Commercial/Domain/Entity/Client.php` | Supprimer props `firstName` (~l.158), `lastName` (~l.163), `phonePrimary` (~l.168), `phoneSecondary` (~l.172), `email` (~l.178) + leurs getters/setters (~l.329-382) + groupes `client:read`/`client:write:main` + `Assert\*`. |
|
||||
| `src/Module/Commercial/Infrastructure/ApiPlatform/State/Processor/ClientProcessor.php` | Retirer les 5 clés de `MAIN_FIELDS` (~l.63) ; de `changedBusinessFields()` (~l.277-281) ; les 6 lignes de `normalize()` qui touchent email/phone/first/last/secondary (~l.433-441) ; supprimer `validateMainContact()` (~l.447-456) et son appel. |
|
||||
| `src/Module/Commercial/Infrastructure/Doctrine/DoctrineClientRepository.php` | `applySearch()` (~l.110-124) : appliquer **D1**. |
|
||||
| `src/Module/Commercial/Infrastructure/Controller/ClientExportController.php` | `buildHeaders()` (~l.94-114) + `buildRows()` (~l.121-143) : appliquer **D2**. |
|
||||
| `src/Module/Commercial/Infrastructure/DataFixtures/ClientFixtures.php` | `ensureClient()` (~l.357-395) : retirer firstName/lastName/phonePrimary/phoneSecondary/email ; conserver `addContact()`. |
|
||||
| `migrations/Version<timestamp>.php` (NOUVELLE) | Backfill + `DROP COLUMN` (cf. § 4). |
|
||||
| `tests/Module/Commercial/**` | Voir § 5. |
|
||||
|
||||
## 4. Migration Doctrine — backfill puis suppression
|
||||
|
||||
> Migration **modulaire** (`src/Module/Commercial/Infrastructure/Doctrine/Migrations/`) : ce
|
||||
> n'est PAS une migration d'initialisation, le schéma `client` / `client_contact` existe
|
||||
> déjà (règle ABSOLUE n°11).
|
||||
|
||||
### `up()`
|
||||
|
||||
1. **Backfill — ne créer un contact que pour les clients qui n'en ont aucun**, afin de ne
|
||||
pas dupliquer le contact déjà recopié à la création (`prefillFirstContact`) :
|
||||
|
||||
```sql
|
||||
INSERT INTO client_contact
|
||||
(client_id, first_name, last_name, phone_primary, phone_secondary, email, position, created_at, updated_at)
|
||||
SELECT c.id, c.first_name, c.last_name, c.phone_primary, c.phone_secondary, c.email, 0, NOW(), NOW()
|
||||
FROM client c
|
||||
WHERE NOT EXISTS (SELECT 1 FROM client_contact cc WHERE cc.client_id = c.id)
|
||||
AND (c.first_name IS NOT NULL OR c.last_name IS NOT NULL);
|
||||
```
|
||||
|
||||
> Le `WHERE ... first_name OU last_name IS NOT NULL` respecte le CHECK
|
||||
> `chk_client_contact_name`. Les rares clients sans nom de contact ET sans contact
|
||||
> existant ne reçoivent pas de ligne (cas théorique : `phone_primary`/`email` étaient
|
||||
> `NOT NULL` mais les noms nullables).
|
||||
|
||||
2. **Supprimer les 5 colonnes** :
|
||||
|
||||
```sql
|
||||
ALTER TABLE client
|
||||
DROP COLUMN first_name,
|
||||
DROP COLUMN last_name,
|
||||
DROP COLUMN phone_primary,
|
||||
DROP COLUMN phone_secondary,
|
||||
DROP COLUMN email;
|
||||
```
|
||||
|
||||
> Pas de `COMMENT ON COLUMN` à poser (on supprime). Vérifier qu'aucun index ne portait
|
||||
> sur `email` (l'index unique `uq_client_email_active` a déjà été supprimé — décision Q4 /
|
||||
> RG-1.17, cf. `ClientMigrationTest`).
|
||||
|
||||
### `down()` (best-effort)
|
||||
|
||||
1. Recréer les 5 colonnes (`phone_primary`/`email` en `NOT NULL` impose un défaut transitoire
|
||||
ou un re-remplissage depuis le contact `position = 0`).
|
||||
2. Re-remplir depuis `client_contact` (`position = 0`) si possible.
|
||||
3. Reposer les `COMMENT ON COLUMN` d'origine (textes RG-1.19/1.20/1.21/1.01/1.17 — cf.
|
||||
`migrations/Version20260601000000.php` l.251-255).
|
||||
|
||||
> `down()` ne peut pas restaurer parfaitement les données (ambiguïté si plusieurs contacts).
|
||||
> Documenter cette limite dans le docblock de la migration.
|
||||
|
||||
## 5. Tests à mettre à jour
|
||||
|
||||
| Fichier | Action |
|
||||
|---|---|
|
||||
| `tests/Module/Commercial/Api/ClientApiTest.php` | Retirer firstName/lastName/phone/email des payloads POST/PATCH `client` et des assertions JSON. |
|
||||
| `tests/.../ClientFormulaireMainTest.php` | Supprimer les tests RG-1.01 (firstName/lastName) et RG-1.02 (téléphones) **côté Client** — ils basculent côté `ClientContact` (couverts ailleurs). |
|
||||
| `tests/.../ClientExportControllerTest.php` | Aligner les en-têtes/lignes attendus sur **D2**. |
|
||||
| `tests/.../ClientMigrationTest.php` | Asserter que les 5 colonnes **n'existent plus** sur `client` ; vérifier le backfill (un client sans contact obtient bien 1 `client_contact`). |
|
||||
| `tests/.../ClientFieldNormalizerTest.php` | Conserver les tests du normalizer (toujours utilisé par `ClientContact`) ; retirer les cas spécifiques aux champs `Client` s'il y en a. |
|
||||
| RG-1.01/1.02 (matrice) | Ne plus tester sur `Client` ; vérifier qu'ils restent couverts sur `ClientContact` (RG-1.05). |
|
||||
|
||||
## 6. Décisions à trancher (cf. README § 3)
|
||||
|
||||
- **D1 — recherche** : recommandé = `LEFT JOIN client_contact` (fuzzy sur
|
||||
`companyName` + contact `first_name`/`last_name`/`email`). Attention au `DISTINCT` /
|
||||
risque de doublons de lignes si plusieurs contacts matchent (grouper par `client.id`).
|
||||
- **D2 — export** : recommandé = alimenter les colonnes contact depuis le contact de plus
|
||||
petit `position` (fetch-join `contacts` pour éviter le N+1).
|
||||
|
||||
## 7. Critères d'acceptation (DoD)
|
||||
|
||||
- [ ] Les colonnes `first_name`, `last_name`, `phone_primary`, `phone_secondary`, `email`
|
||||
n'existent plus sur la table `client`.
|
||||
- [ ] La migration est jouable sur une base seedée sans perte de contact (backfill vérifié)
|
||||
et `down()` documenté comme best-effort.
|
||||
- [ ] `Client`, `ClientProcessor`, `DoctrineClientRepository`, `ClientExportController`,
|
||||
`ClientFixtures` ne référencent plus les 5 champs.
|
||||
- [ ] D1 et D2 implémentées conformément à la décision validée.
|
||||
- [ ] `ClientContact` / `ClientContactProcessor` / `ClientFieldNormalizer` inchangés.
|
||||
- [ ] `make test` vert (notamment `tests/Architecture/ColumnsHaveSqlCommentTest` et
|
||||
`EntitiesAreTimestampableBlamableTest`).
|
||||
- [ ] `make php-cs-fixer-allow-risky` ne signale rien sur les fichiers touchés.
|
||||
- [ ] Aucune régression du contrat de sérialisation : capturer le JSON réel de
|
||||
`GET /api/clients/{id}` et vérifier l'absence des 5 champs (réflexe RETEX M1).
|
||||
@@ -0,0 +1,58 @@
|
||||
# Prompt d'implémentation — M1 · Ticket 1/3 (Backend)
|
||||
|
||||
Tu travailles sur le projet **Starseed** (Symfony 8 / API Platform 4 / Doctrine / PostgreSQL).
|
||||
Lis `CLAUDE.md` et `.claude/rules/backend.md` avant de coder. Commentaires en français,
|
||||
code en anglais, `declare(strict_types=1);` partout.
|
||||
|
||||
## Mission
|
||||
|
||||
Supprimer le **contact principal inline** de l'entité `Client` : les 5 champs
|
||||
`firstName`, `lastName`, `phonePrimary`, `phoneSecondary`, `email`. Les contacts sont gérés
|
||||
uniquement via la sous-entité `ClientContact` (onglet Contacts), déjà en place. Le code est
|
||||
déjà en prod → migration avec **backfill** avant `DROP`.
|
||||
|
||||
La spec détaillée du ticket est dans `docs/specs/M1-clients/refonte-contact/M1-ticket-01-back.md`.
|
||||
Lis-la en entier, ainsi que le `README.md` du même dossier (décision + RG impactées + D1/D2).
|
||||
|
||||
## Étapes
|
||||
|
||||
1. **Explorer** : `Client.php`, `ClientProcessor.php`, `DoctrineClientRepository.php`,
|
||||
`ClientExportController.php`, `ClientFixtures.php`, et `ClientContact.php` (pour confirmer
|
||||
que la cible porte bien les mêmes champs).
|
||||
2. **Demander la validation des décisions D1 (recherche) et D2 (export)** avant de coder —
|
||||
défauts recommandés : D1 = LEFT JOIN sur `client_contact`, D2 = colonnes export depuis le
|
||||
contact `position` minimal. Ne pas inventer un autre comportement.
|
||||
3. **Migration** (`src/Module/Commercial/Infrastructure/Doctrine/Migrations/`) : backfill
|
||||
`INSERT INTO client_contact ... WHERE NOT EXISTS(...)` puis `ALTER TABLE client DROP COLUMN ...`
|
||||
(les 5). `down()` best-effort documenté. Voir le SQL exact dans la spec § 4.
|
||||
4. **Entité** : retirer les 5 props + getters/setters + `#[ORM\Column]` + `#[Assert\*]` +
|
||||
`#[Groups(['client:read','client:write:main'])]`.
|
||||
5. **Processor** : retirer de `MAIN_FIELDS`, `changedBusinessFields()`, `normalize()` ;
|
||||
supprimer `validateMainContact()` et son appel.
|
||||
6. **Repository** : `applySearch()` selon D1.
|
||||
7. **Export** : `buildHeaders()` / `buildRows()` selon D2.
|
||||
8. **Fixtures** : alléger `ensureClient()` ; garder `addContact()`.
|
||||
9. **Tests** : mettre à jour `ClientApiTest`, `ClientFormulaireMainTest`,
|
||||
`ClientExportControllerTest`, `ClientMigrationTest`, `ClientFieldNormalizerTest`
|
||||
(cf. spec § 5). Ajouter une assertion que le backfill crée bien un contact pour un client
|
||||
qui n'en avait pas.
|
||||
|
||||
## Garde-fous
|
||||
|
||||
- Ne touche **pas** `ClientContact`, `ClientContactProcessor`, `ClientFieldNormalizer`.
|
||||
- Respecte les règles ABSOLUES : pagination, `#[Auditable]`, COMMENT ON COLUMN (ici on
|
||||
supprime → pas de commentaire à poser, mais ne pas casser le garde-fou).
|
||||
- Les RG-1.01 et RG-1.02 disparaissent **du Client** : leur équivalent (RG-1.05 / RG-1.14)
|
||||
vit déjà sur `ClientContact`, ne le duplique pas.
|
||||
|
||||
## Vérification finale (obligatoire avant de dire « fini »)
|
||||
|
||||
```bash
|
||||
make db-reset && make migration-migrate # migration rejouable sur base fraîche
|
||||
make test # PHPUnit vert
|
||||
make php-cs-fixer-allow-risky # lint
|
||||
```
|
||||
|
||||
Puis capture le JSON réel de `GET /api/clients/{id}` (avec un JWT) et confirme que les 5
|
||||
champs ont disparu de la réponse et que `contacts[]` porte bien l'info (réflexe RETEX M1 :
|
||||
on valide sur le contrat réel, pas sur les annotations).
|
||||
@@ -0,0 +1,74 @@
|
||||
# M1 · Ticket 2/3 (Frontend) — Retirer le bloc contact principal des écrans Client
|
||||
|
||||
## 1. Objectif
|
||||
|
||||
Retirer le **bloc « contact principal »** (Nom, Prénom, Téléphone, Téléphone 2, Email) des
|
||||
trois écrans Client — **création**, **consultation**, **modification** — ainsi que des
|
||||
types, mappeurs, validations et clés i18n associés. La saisie des contacts se fait
|
||||
désormais uniquement dans l'**onglet « Contacts »** (composant `ClientContactBlock`, déjà
|
||||
en place et inchangé).
|
||||
|
||||
Dépend du **ticket 1/3 (back)** : l'API ne renvoie/n'accepte plus ces 5 champs sur `client`.
|
||||
Contexte : voir `README.md` du dossier `refonte-contact`.
|
||||
|
||||
## 2. Périmètre
|
||||
|
||||
### IN — fichiers `frontend/modules/commercial/`
|
||||
|
||||
| Fichier | Action |
|
||||
|---|---|
|
||||
| `pages/clients/new.vue` | Supprimer le bloc principal Nom/Prénom/Téléphones/Email (~l.27-63), l'état `main.firstName/lastName/email`, `mainPhones` (~l.445-459), la fonction `prefillFirstContact()` (~l.658-665) et son appel, le mapping payload POST `phonePrimary/phoneSecondary` (~l.513-524). Adapter `isMainValid` (~l.479-493) : la validation principale ne porte plus que sur `companyName` (+ relation/catégories selon RG existantes). L'onglet **Contacts** devient le point de saisie des coordonnées ; garantir au moins un `ClientContactBlock` vide au départ. |
|
||||
| `pages/clients/[id]/edit.vue` | Supprimer les 5 champs du bloc principal (~l.32-73). `mapMainDraft()` et `buildMainPayload()` ne portent plus ces champs. L'onglet Contacts reste éditable. |
|
||||
| `pages/clients/[id]/index.vue` | Supprimer l'affichage lecture seule des 5 champs du bloc principal (~l.49-104, partie contact). Conserver l'onglet Contacts (lecture seule). |
|
||||
| `types/clientForm.ts` | `MainFormDraft` : retirer `firstName`, `lastName`, `email`, `phonePrimary`, `phoneSecondary`, `hasSecondaryPhone`. Garder `ContactFormDraft` (inchangé). |
|
||||
| `types/clientConsultation.ts` | `ClientDetail` : retirer `firstName/lastName/phonePrimary/phoneSecondary/email` (les commentaires « Contact principal »). Garder `ContactRead`. |
|
||||
| `utils/clientEdit.ts` | `mapMainDraft()` et `buildMainPayload()` : retirer les 5 champs. Garder `buildContactPayload()`. |
|
||||
| `utils/clientConsultation.ts` | Retirer toute lecture des 5 champs inline du client (garder `mapContactToDraft`, `contactOptionsOf`). |
|
||||
| `i18n/locales/fr.json` | Retirer `commercial.clients.form.main.firstName/lastName/email/phonePrimary/phoneSecondary/addPhone`. **Conserver** tout le bloc `commercial.clients.form.contact.*`. Vérifier qu'aucune autre vue ne référence les clés retirées. |
|
||||
| `**/__tests__/*.spec.ts` | Mettre à jour `clientFormRules.spec.ts`, `clientEdit.spec.ts`, `clientConsultation.spec.ts` (cf. § 4). |
|
||||
|
||||
### OUT
|
||||
|
||||
- `ClientContactBlock.vue`, l'onglet Contacts, `useClient`, la liste/répertoire
|
||||
(`pages/clients/index.vue` — ses colonnes n'affichent déjà pas le contact inline) :
|
||||
**inchangés**.
|
||||
- Le back → ticket 1/3. Les specs → ticket 3/3.
|
||||
|
||||
## 3. Comportement attendu après modification
|
||||
|
||||
- **Création** : le formulaire principal demande l'entreprise (et relation/catégories selon
|
||||
l'existant), plus de Nom/Prénom/Téléphone/Email inline. L'utilisateur renseigne les
|
||||
coordonnées dans l'onglet **Contacts**. La création reste valide tant qu'il y a
|
||||
`companyName` **et** ≥ 1 bloc Contact valide (Nom OU Prénom) — RG-1.05/RG-1.14 inchangées.
|
||||
- **Consultation** : plus de bloc contact principal ; l'onglet Contacts affiche les
|
||||
contacts.
|
||||
- **Modification** : idem ; le PATCH du groupe `client:write:main` n'envoie plus les 5
|
||||
champs.
|
||||
|
||||
## 4. Tests Vitest à mettre à jour
|
||||
|
||||
- `clientFormRules.spec.ts` : la validité du « principal » ne dépend plus de
|
||||
firstName/email/phone ; conserver `isContactNamed()` (RG-1.05) sur les blocs Contacts.
|
||||
- `clientEdit.spec.ts` : `buildMainPayload()` ne contient plus les 5 champs ; `mapMainDraft()`
|
||||
non plus.
|
||||
- `clientConsultation.spec.ts` : retirer les assertions sur les 5 champs inline.
|
||||
|
||||
## 5. Tips & rappels projet
|
||||
|
||||
- `useApi()` obligatoire (jamais `$fetch`/`ofetch`). Composants `Malio*` obligatoires.
|
||||
- État de tableau jamais dans l'URL (règle inchangée).
|
||||
- Les valeurs sont **normalisées côté serveur** (Capitalize / chiffres / lowercase) : le
|
||||
front envoie la saisie et réaffiche la valeur renvoyée — ne pas réintroduire de
|
||||
normalisation front.
|
||||
- Ne pas créer de clé i18n orpheline ni laisser de clé `form.main.*` morte.
|
||||
|
||||
## 6. Critères d'acceptation (DoD)
|
||||
|
||||
- [ ] Les 3 écrans n'affichent plus Nom/Prénom/Téléphone/Téléphone 2/Email en bloc principal.
|
||||
- [ ] Le parcours de création fonctionne avec `companyName` + onglet Contacts (≥ 1 contact).
|
||||
- [ ] `MainFormDraft` / `ClientDetail` ne déclarent plus les 5 champs ; `mapMainDraft` /
|
||||
`buildMainPayload` non plus.
|
||||
- [ ] Aucune clé i18n `form.main.firstName/lastName/email/phone*` restante ni référencée.
|
||||
- [ ] `make nuxt-test` vert.
|
||||
- [ ] Vérification visuelle du golden path (`make dev-nuxt`, port 3004) : création →
|
||||
consultation → modification d'un client sans bloc contact inline.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Prompt d'implémentation — M1 · Ticket 2/3 (Frontend)
|
||||
|
||||
Projet **Starseed** (Nuxt 4 / Vue 3 Composition API / TypeScript / @malio/layer-ui).
|
||||
Lis `CLAUDE.md` et `.claude/rules/frontend.md` avant de coder. Commentaires en français,
|
||||
code en anglais, 4 espaces d'indentation.
|
||||
|
||||
## Mission
|
||||
|
||||
Retirer le **bloc « contact principal »** (Nom, Prénom, Téléphone, Téléphone 2, Email) des
|
||||
écrans Client (création / consultation / modification) et de tout le code associé (types,
|
||||
mappeurs, validations, i18n). Les contacts restent gérés par l'onglet **Contacts**
|
||||
(`ClientContactBlock`, inchangé).
|
||||
|
||||
Spec détaillée : `docs/specs/M1-clients/refonte-contact/M1-ticket-02-front.md` (lis-la en
|
||||
entier + le `README.md` du dossier). Ce ticket dépend du ticket back (l'API ne porte plus
|
||||
les 5 champs sur `client`).
|
||||
|
||||
## Étapes
|
||||
|
||||
1. Explorer `frontend/modules/commercial/` : `pages/clients/new.vue`, `[id]/edit.vue`,
|
||||
`[id]/index.vue`, `types/clientForm.ts`, `types/clientConsultation.ts`,
|
||||
`utils/clientEdit.ts`, `utils/clientConsultation.ts`, `i18n/locales/fr.json`.
|
||||
2. Supprimer le bloc principal des 3 écrans + l'état réactif `main.firstName/lastName/email`,
|
||||
`mainPhones`, `prefillFirstContact()`.
|
||||
3. Adapter `isMainValid` : ne dépend plus que de `companyName` (+ relation/catégories selon
|
||||
l'existant). La garantie « ≥ 1 contact valide » reste portée par l'onglet Contacts.
|
||||
4. Nettoyer les types (`MainFormDraft`, `ClientDetail`) et les mappeurs (`mapMainDraft`,
|
||||
`buildMainPayload`, `clientConsultation`).
|
||||
5. Retirer les clés i18n `form.main.firstName/lastName/email/phonePrimary/phoneSecondary/addPhone` ;
|
||||
vérifier par recherche qu'aucune vue ne les utilise plus. **Garder** `form.contact.*`.
|
||||
6. Mettre à jour les specs Vitest (`clientFormRules`, `clientEdit`, `clientConsultation`).
|
||||
|
||||
## Garde-fous
|
||||
|
||||
- `useApi()` uniquement ; composants `Malio*` uniquement ; pas d'état tableau dans l'URL.
|
||||
- Ne touche pas `ClientContactBlock.vue`, l'onglet Contacts, ni la liste/répertoire.
|
||||
- Pas de normalisation front (le serveur normalise).
|
||||
|
||||
## Vérification finale
|
||||
|
||||
```bash
|
||||
make nuxt-test # Vitest vert
|
||||
make dev-nuxt # port 3004 — golden path manuel
|
||||
```
|
||||
|
||||
Golden path à vérifier dans le navigateur : créer un client (entreprise + 1 contact dans
|
||||
l'onglet Contacts), le consulter, le modifier — sans aucun bloc contact inline.
|
||||
@@ -0,0 +1,51 @@
|
||||
# M1 · Ticket 3/3 (Specs) — Acter la suppression du contact inline dans les specs M1
|
||||
|
||||
## 1. Objectif
|
||||
|
||||
Mettre à jour la **documentation fonctionnelle/technique M1** pour refléter la décision :
|
||||
le contact principal inline est supprimé du `Client`, les contacts vivent uniquement dans
|
||||
`ClientContact`. Les specs sont la **source de vérité** du projet (cf. `workflow.md`) : elles
|
||||
doivent décrire le modèle cible, pas l'ancien.
|
||||
|
||||
> Idéalement réalisé **avant** les tickets 1 et 2 (la spec guide le code), mais peut être
|
||||
> fait en parallèle. À minima, ne pas merger le code sans aligner la spec.
|
||||
|
||||
## 2. Fichiers à modifier
|
||||
|
||||
| Fichier | Sections concernées |
|
||||
|---|---|
|
||||
| `docs/specs/M1-clients/spec-back.md` | § 3.1 diagramme E-R (retirer les 5 colonnes du bloc `client`) ; § 3.2 migration SQL `CREATE TABLE client` (retirer `first_name`/`last_name`/`phone_primary`/`phone_secondary`/`email` + leurs COMMENT) ; § 3.4 squelette entité `Client` (retirer les 5 props) ; § 4.3 exemple payload `POST /api/clients` (retirer les 5 champs) ; § 4.1 filtre `?search=` (refléter D1) ; § 4.6 export (refléter D2) ; § 7 RG (voir § 3 ci-dessous) ; § 8 cahier de tests (déplacer RG-1.01/1.02 vers ClientContact). |
|
||||
| `docs/specs/M1-clients/spec-front.md` | « Formulaire principal » (l.85-103) : retirer les lignes Nom/Prénom/Téléphone/Téléphone 2/Email ; écrans Consultation / Modification ; règles de formatage. Préciser que les coordonnées se saisissent dans l'onglet Contact. |
|
||||
| `docs/specs/M1-clients/cahier-test-back-M1.md` | Retirer / requalifier les lignes RG-1.01 et RG-1.02 (désormais couvertes par RG-1.05 sur `ClientContact`). |
|
||||
|
||||
## 3. Traitement des règles de gestion
|
||||
|
||||
- **RG-1.01** (firstName OU lastName obligatoire sur Client) → marquer **supprimée** :
|
||||
« Remplacée par RG-1.05 (≥ 1 contact valide) + RG-1.14 (≥ 1 bloc Contact). Le contact
|
||||
principal inline n'existe plus. »
|
||||
- **RG-1.02** (max 2 téléphones sur Client) → marquer **supprimée du Client** (reste
|
||||
applicable aux blocs `ClientContact`).
|
||||
- **RG-1.19 / RG-1.20 / RG-1.21** (normalisation) → préciser que le **scope `Client`
|
||||
disparaît** ; la normalisation reste sur `ClientContact` (et `ClientAddress.billingEmail`
|
||||
pour RG-1.21).
|
||||
- Ne **pas renuméroter** les RG existantes (éviter le drift avec le code/tests) : marquer
|
||||
« supprimée / requalifiée » en place, avec date et renvoi à la décision.
|
||||
|
||||
## 4. Forme
|
||||
|
||||
- Bumper la version des deux specs (`version: V0` → `V1`) dans le frontmatter, avec une
|
||||
entrée d'historique : date `2026-06-03`, motif « Suppression du contact inline du Client
|
||||
(refonte-contact) », auteur.
|
||||
- Ajouter un encadré « Décision » en tête de la section modèle de données, renvoyant au
|
||||
`README.md` du dossier `refonte-contact`.
|
||||
- Conserver le style des specs (sections numérotées, tableaux RG, exemples JSON).
|
||||
|
||||
## 5. Critères d'acceptation (DoD)
|
||||
|
||||
- [ ] `spec-back.md` : aucune mention des 5 colonnes inline dans le modèle `client`
|
||||
(E-R + SQL + entité + payload) ; RG-1.01/1.02 marquées supprimées ; D1/D2 décrites.
|
||||
- [ ] `spec-front.md` : le formulaire principal ne liste plus les champs de contact ;
|
||||
l'onglet Contact est présenté comme seul lieu de saisie des coordonnées.
|
||||
- [ ] `cahier-test-back-M1.md` : RG-1.01/1.02 retirées/requalifiées.
|
||||
- [ ] Versions bumpées (V1) + historique daté dans les deux specs.
|
||||
- [ ] Cohérence vérifiée avec les tickets 1 et 2 (mêmes décisions D1/D2).
|
||||
@@ -0,0 +1,38 @@
|
||||
# Prompt d'implémentation — M1 · Ticket 3/3 (Specs)
|
||||
|
||||
Projet **Starseed**. Tâche **documentaire** : mettre à jour les specs M1 Clients pour acter
|
||||
la suppression du contact principal inline du `Client`. Les specs sont la source de vérité ;
|
||||
elles doivent décrire le modèle cible.
|
||||
|
||||
## Mission
|
||||
|
||||
Modifier `docs/specs/M1-clients/spec-back.md`, `spec-front.md` et `cahier-test-back-M1.md`
|
||||
pour retirer le contact inline du `Client` (5 champs `firstName/lastName/phonePrimary/
|
||||
phoneSecondary/email`) — les contacts vivent uniquement dans `ClientContact`.
|
||||
|
||||
Spec du ticket : `docs/specs/M1-clients/refonte-contact/M1-ticket-03-specs.md` (lis-la + le
|
||||
`README.md` du dossier, qui contient la décision, les RG impactées et les décisions D1/D2).
|
||||
|
||||
## Étapes
|
||||
|
||||
1. Lire les 3 fichiers de specs M1 visés, repérer toutes les occurrences des 5 champs
|
||||
(diagramme E-R, CREATE TABLE client, squelette entité, payload POST, filtre search,
|
||||
export, RG, cahier de test).
|
||||
2. Retirer les 5 colonnes du modèle `client` (E-R + SQL + entité + exemple JSON).
|
||||
3. Marquer **supprimées** RG-1.01 et RG-1.02 (renvoi à RG-1.05/RG-1.14 sur `ClientContact`),
|
||||
restreindre le scope de RG-1.19/1.20/1.21 à `ClientContact`. **Ne pas renuméroter** les RG.
|
||||
4. Refléter les décisions D1 (recherche) et D2 (export) une fois tranchées.
|
||||
5. Côté `spec-front.md` : retirer les champs de contact du formulaire principal ; présenter
|
||||
l'onglet Contact comme seul lieu de saisie.
|
||||
6. Bumper `version: V0 → V1` + ajouter une entrée d'historique datée (2026-06-03).
|
||||
|
||||
## Garde-fous
|
||||
|
||||
- Ne touche pas au code, uniquement aux `.md` de specs.
|
||||
- Garde le style existant (sections numérotées, tableaux RG, exemples JSON).
|
||||
- Cohérence stricte avec les tickets 1 (back) et 2 (front) : mêmes décisions D1/D2.
|
||||
|
||||
## Vérification
|
||||
|
||||
Relire les 3 fichiers : plus aucune mention des 5 champs inline dans le modèle `client` ;
|
||||
RG-1.01/1.02 marquées supprimées ; versions à V1 avec historique.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Amendement des tickets M2 existants — suppression du contact inline du `Supplier`
|
||||
|
||||
Les 14 tickets M2 (n° 84–97, groupe Lesstime « M2 — Répertoire fournisseurs ») ont été
|
||||
rédigés sur le modèle initial **avec** contact inline. La décision `refonte-contact` les
|
||||
amende : `Supplier` ne porte **plus** les 5 champs `firstName/lastName/phonePrimary/
|
||||
phoneSecondary/email` ; les contacts vivent uniquement dans `SupplierContact` (onglet
|
||||
Contacts). Comme M2 n'est pas codé, il suffit de **ne jamais créer** ces colonnes/champs.
|
||||
|
||||
## Bandeau injecté en tête des tickets impactés
|
||||
|
||||
> ⚠️ **AMENDEMENT 2026-06-03 — refonte-contact.** Le contact principal inline est
|
||||
> **supprimé** du `Supplier` : ne pas créer/saisir les colonnes ni les champs `firstName`,
|
||||
> `lastName`, `phonePrimary`, `phoneSecondary`, `email` sur l'entité/le formulaire
|
||||
> `Supplier`. Les contacts sont gérés **uniquement** via `SupplierContact` (onglet
|
||||
> Contacts). RG-2.01 et RG-2.02 sont supprimées (équivalent assuré par RG-2.04 / RG-2.13).
|
||||
> RG-2.12 ne s'applique qu'à `companyName` + `SupplierContact`. Décisions transverses
|
||||
> recherche (D1) et export (D2) : cf. `docs/specs/M1-clients/refonte-contact/README.md`.
|
||||
|
||||
## Tickets à amender
|
||||
|
||||
### Back
|
||||
|
||||
| Ticket | n° | Impact |
|
||||
|---|---|---|
|
||||
| migration BDD M2 (supplier + sous-collections) | #85 | Retirer `first_name/last_name/phone_primary/phone_secondary/email` du `CREATE TABLE supplier` et leurs `COMMENT ON COLUMN`. `supplier_contact` inchangé. |
|
||||
| entités + repositories M2 | #86 | `Supplier` : retirer les 5 props + `Assert\Callback` RG-2.01. `SupplierContact` inchangé. |
|
||||
| SupplierProvider + SupplierProcessor | #87 | Retirer la validation RG-2.01, la normalisation des champs inline, leur présence dans `MAIN_FIELDS` / changedFields. Recherche selon D1. |
|
||||
| export XLSX fournisseurs | #91 | Colonnes contact selon D2 (depuis le contact principal, ou supprimées). |
|
||||
| tests PHPUnit M2 | #92 | RG-2.01/2.02 testées sur `SupplierContact` (pas `Supplier`) ; contrat de sérialisation sans les 5 champs inline sur le supplier. |
|
||||
|
||||
### Front
|
||||
|
||||
| Ticket | n° | Impact |
|
||||
|---|---|---|
|
||||
| page Ajouter un fournisseur (`/suppliers/new`) + `useSupplierForm` | #94 | Retirer le bloc contact principal du formulaire + le pré-remplissage du 1er contact. Saisie des coordonnées dans l'onglet Contacts. |
|
||||
| page Consultation fournisseur (`/suppliers/{id}`) | #95 | Retirer l'affichage du bloc contact principal. |
|
||||
| page Modification fournisseur (`/suppliers/{id}/edit`) | #96 | Retirer les 5 champs du bloc principal ; payload `supplier:write:main` sans ces champs. |
|
||||
|
||||
### Léger
|
||||
|
||||
| Ticket | n° | Impact |
|
||||
|---|---|---|
|
||||
| page Répertoire fournisseurs + datatable | #93 | Recherche « nom / contact / email » selon D1. Datatable : colonnes inchangées (pas de contact inline en colonne). |
|
||||
| i18n + sidebar fournisseurs | #97 | Ne pas créer les clés i18n `form.main.firstName/lastName/email/phone*` (garder `form.contact.*`). |
|
||||
|
||||
## Tickets NON impactés
|
||||
|
||||
- #84 (taxonomie FOURNISSEUR), #88 (sous-ressources contacts/adresses/ribs —
|
||||
`SupplierContact` est la cible, inchangé), #89 (validators Information Commerciale /
|
||||
catégorie / RG-2.07-2.08), #90 (RBAC fournisseurs).
|
||||
|
||||
## Méthode d'amendement
|
||||
|
||||
Pour chaque ticket impacté : **préfixer** la description existante du bandeau ci-dessus
|
||||
(sans rien supprimer du contenu d'origine), via `mcp__lesstime__update-task`
|
||||
(`description` = bandeau + description actuelle). La méthode préserve l'historique et reste
|
||||
réversible (retirer le bandeau).
|
||||
@@ -0,0 +1,55 @@
|
||||
# M2 · Ticket Specs — Retirer le contact inline du `Supplier` dans les specs M2
|
||||
|
||||
## 1. Objectif
|
||||
|
||||
Mettre à jour les specs **M2 Fournisseurs** déjà rédigées pour **ne plus inclure** le contact
|
||||
principal inline sur le `Supplier`. M2 est le jumeau strict de M1 (`Supplier` /
|
||||
`SupplierContact` / `SupplierAddress` / `SupplierRib`) et n'est **pas encore codé** : il faut
|
||||
donc corriger la conception **en amont**, pour que les 14 tickets M2 « prêts à dev » soient
|
||||
implémentés directement sans les 5 colonnes inline.
|
||||
|
||||
> Pendant de M1 ticket 3/3, mais côté M2 : **aucune migration de suppression ni backfill** —
|
||||
> on retire simplement le contact inline du modèle cible. Contexte : `README.md` du dossier
|
||||
> `refonte-contact`.
|
||||
|
||||
## 2. Fichiers à modifier
|
||||
|
||||
| Fichier | Sections concernées |
|
||||
|---|---|
|
||||
| `docs/specs/M2-suppliers/spec-back.md` | § 3.1 diagramme E-R (l.175-179 : retirer les 5 colonnes du bloc `supplier`) ; § 3.2 `CREATE TABLE supplier` (l.227-231) ; § 3.4 squelette entité `Supplier` (l.496-517 : props + `Assert\Callback` RG-2.01) ; § 4 exemples payload POST/GET (l.782-805, 867-871) ; recherche `?search=` (l.847 : refléter D1) ; export (refléter D2) ; § contrat de sérialisation (l.725, 729) ; § 7 RG (voir § 3). |
|
||||
| `docs/specs/M2-suppliers/spec-front.md` | « Formulaire principal » (l.105-117 : retirer Nom/Prénom/Téléphone/Téléphone 2/Email) ; onglet « Contact » (l.140-157 : retirer la phrase de pré-remplissage depuis le formulaire principal, l.142) ; écrans Consultation/Modification ; règles de formatage (l.283-285) ; recherche (l.76 : refléter D1). |
|
||||
|
||||
## 3. Traitement des règles de gestion M2
|
||||
|
||||
- **RG-2.01** (firstName OU lastName obligatoire sur Supplier) → **supprimée** : remplacée
|
||||
par RG-2.04 (≥ 1 contact valide) + RG-2.13 (≥ 1 bloc Contact). Le contact inline n'existe
|
||||
plus sur `Supplier`.
|
||||
- **RG-2.02** (max 2 téléphones sur Supplier) → **supprimée du Supplier** (reste sur
|
||||
`SupplierContact`).
|
||||
- **RG-2.12** (normalisation Capitalize / chiffres / lowercase) → restreindre le scope :
|
||||
s'applique à `companyName` (UPPERCASE) et aux champs de `SupplierContact` ; **plus** aux
|
||||
champs inline du `Supplier` (qui disparaissent).
|
||||
- Ne pas renuméroter les RG : marquer « supprimée / requalifiée » en place, avec date.
|
||||
|
||||
## 4. Forme
|
||||
|
||||
- Bumper la version des deux specs M2 + entrée d'historique datée (2026-06-03, motif
|
||||
« Suppression du contact inline du Supplier — alignement refonte-contact M1 »).
|
||||
- Encadré « Décision » renvoyant au `README.md` du dossier `refonte-contact`.
|
||||
- Garder le style des specs M2.
|
||||
|
||||
## 5. Lien avec les tickets M2 existants
|
||||
|
||||
La mise à jour des specs doit être cohérente avec l'**amendement des tickets M2** (voir
|
||||
`M2-amendement-tickets.md`) : tickets back #85/#86/#87/#91/#92 et front #94/#95/#96 (+ #93/#97
|
||||
légers). Specs et tickets décrivent le **même** modèle cible (sans contact inline).
|
||||
|
||||
## 6. Critères d'acceptation (DoD)
|
||||
|
||||
- [ ] `spec-back.md` M2 : aucune mention des 5 colonnes inline dans le modèle `supplier`
|
||||
(E-R + SQL + entité + payloads + sérialisation) ; RG-2.01/2.02 marquées supprimées ;
|
||||
D1/D2 décrites.
|
||||
- [ ] `spec-front.md` M2 : formulaire principal sans champs de contact ; onglet Contact
|
||||
présenté comme seul lieu de saisie (sans pré-remplissage depuis le principal).
|
||||
- [ ] Versions bumpées + historique daté.
|
||||
- [ ] Cohérence avec l'amendement des tickets M2.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Prompt d'implémentation — M2 · Ticket Specs
|
||||
|
||||
Projet **Starseed**. Tâche **documentaire**. Mettre à jour les specs M2 Fournisseurs
|
||||
(`docs/specs/M2-suppliers/spec-back.md` + `spec-front.md`) pour retirer le contact principal
|
||||
inline du `Supplier` (5 champs `firstName/lastName/phonePrimary/phoneSecondary/email`).
|
||||
|
||||
M2 n'est **pas encore codé** : on corrige la conception en amont, **sans** migration ni
|
||||
backfill (contrairement à M1). Les contacts vivent uniquement dans `SupplierContact`.
|
||||
|
||||
Spec du ticket : `docs/specs/M1-clients/refonte-contact/M2-ticket-specs.md` (lis-la + le
|
||||
`README.md` du dossier).
|
||||
|
||||
## Étapes
|
||||
|
||||
1. Lire `spec-back.md` et `spec-front.md` M2 ; repérer toutes les occurrences des 5 champs
|
||||
(E-R l.175-179, CREATE TABLE supplier l.227-231, entité l.496-517, payloads l.782-805 /
|
||||
867-871, sérialisation l.725-729, RG-2.01/2.02/2.12, recherche, export, formulaire
|
||||
principal front l.105-117, pré-remplissage onglet Contact l.142).
|
||||
2. Retirer les 5 colonnes du modèle `supplier`.
|
||||
3. Marquer **supprimées** RG-2.01 et RG-2.02 (renvoi RG-2.04/RG-2.13) ; restreindre RG-2.12
|
||||
à `companyName` + `SupplierContact`. Ne pas renuméroter.
|
||||
4. Refléter D1 (recherche : LEFT JOIN supplier_contact recommandé) et D2 (export depuis le
|
||||
contact principal recommandé).
|
||||
5. Front : retirer les champs de contact du formulaire principal ; retirer la phrase de
|
||||
pré-remplissage du 1er bloc Contact ; présenter l'onglet Contact comme seul lieu de saisie.
|
||||
6. Bumper la version + historique daté (2026-06-03).
|
||||
|
||||
## Garde-fous
|
||||
|
||||
- Uniquement les `.md` de specs M2. Style existant conservé.
|
||||
- Cohérence stricte avec l'amendement des tickets M2 et avec la décision M1 (jumeau).
|
||||
|
||||
## Vérification
|
||||
|
||||
Relire les 2 specs : plus aucune mention des 5 champs inline dans le modèle `supplier` ;
|
||||
RG-2.01/2.02 supprimées ; versions bumpées.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Refonte « contact » — suppression du contact inline des tiers (Client M1 + Supplier M2)
|
||||
|
||||
> Dossier de tickets transverse. Source de vérité de la décision et de son découpage.
|
||||
> Rédigé le 2026-06-03. Owner : Matthieu.
|
||||
|
||||
## 1. Décision
|
||||
|
||||
Le **contact « principal » inline** (les 5 colonnes plates `first_name`, `last_name`,
|
||||
`phone_primary`, `phone_secondary`, `email`) est **supprimé de l'entité tier** (`Client`,
|
||||
puis `Supplier`). La gestion des contacts passe **exclusivement** par la sous-entité
|
||||
dédiée (`ClientContact` / `SupplierContact`), c.-à-d. l'**onglet « Contacts »**.
|
||||
|
||||
### Pourquoi
|
||||
|
||||
- **Modèle unique, zéro duplication.** Aujourd'hui le contact est saisi deux fois : une
|
||||
fois dans le bloc principal (inline sur le tier) et une fois dans l'onglet Contacts
|
||||
(sous-entité). À la création, le front recopie même l'un dans l'autre
|
||||
(`prefillFirstContact`). Deux sources pour la même information = risque de divergence.
|
||||
- **Cohérence métier.** Un tier peut avoir plusieurs contacts ; il n'y a pas de raison
|
||||
qu'un seul soit « privilégié » au niveau de la table tier. La notion de contact
|
||||
appartient à la collection de contacts.
|
||||
- **Garantie préservée.** L'invariant « il y a toujours au moins un contact » est déjà
|
||||
assuré par la sous-entité : RG-1.05/RG-1.14 (M1) et RG-2.04/RG-2.13 (M2) imposent
|
||||
**≥ 1 bloc Contact valide** (Nom OU Prénom). Supprimer le contact inline ne crée donc
|
||||
aucun trou : le contact reste obligatoire, mais au bon endroit.
|
||||
|
||||
### Règles de gestion impactées
|
||||
|
||||
| RG | Avant | Après |
|
||||
|---|---|---|
|
||||
| RG-1.01 / RG-2.01 (firstName OU lastName obligatoire **sur le tier**) | sur `Client` / `Supplier` | **supprimée** du tier — équivalent assuré par RG-1.05 / RG-2.04 sur la sous-entité |
|
||||
| RG-1.02 / RG-2.02 (max 2 téléphones **sur le tier**) | sur le tier | **supprimée** du tier — reste sur la sous-entité |
|
||||
| RG-1.19/1.20/1.21 — RG-2.12 (normalisation Capitalize / chiffres / lowercase) | appliquée aux champs **du tier ET** de la sous-entité | ne s'applique plus aux champs du tier (qui n'existent plus) — **inchangée** sur la sous-entité |
|
||||
|
||||
## 2. Périmètre & découpage
|
||||
|
||||
### M1 — Clients (code DÉJÀ livré → suppression + migration de données)
|
||||
|
||||
| # | Ticket | Tag | Effort |
|
||||
|---|--------|-----|--------|
|
||||
| 1 | `M1-ticket-01-back` — supprimer le contact inline du `Client` (migration + backfill + entité + processor + provider + export + fixtures + tests) | Backend | M |
|
||||
| 2 | `M1-ticket-02-front` — retirer le bloc contact principal des écrans création / consultation / modification | Frontend | M |
|
||||
| 3 | `M1-ticket-03-specs` — acter la décision dans les specs M1 (back + front + cahier de test) | Maintenance | S |
|
||||
|
||||
### M2 — Fournisseurs (NON codé → on retire le contact inline dès la conception)
|
||||
|
||||
| # | Action | Tag | Effort |
|
||||
|---|--------|-----|--------|
|
||||
| 4 | `M2-ticket-specs` — mettre à jour les specs M2 déjà écrites (back + front) pour retirer le contact inline du `Supplier` | Maintenance | S |
|
||||
| — | `M2-amendement-tickets` — amender les tickets M2 existants (n° 84–97) impactés (migration, entités, processor, export, front, tests, i18n) | — | — |
|
||||
|
||||
> M2 ne nécessite **pas** de migration de suppression ni de backfill : il suffit de **ne
|
||||
> jamais créer** les 5 colonnes inline sur `supplier`. Le travail M2 est donc un
|
||||
> ajustement de specs + un amendement des tickets « prêts à dev ».
|
||||
|
||||
## 3. Décisions transverses à trancher (mêmes pour M1 et M2)
|
||||
|
||||
Deux comportements s'appuyaient sur les colonnes inline du tier. À la suppression, il faut
|
||||
choisir leur nouvelle source. Recommandation par défaut entre parenthèses.
|
||||
|
||||
- **D1 — Recherche serveur** (`?search=`). Aujourd'hui : fuzzy sur `companyName` +
|
||||
`lastName` + `email` **du tier**. Après suppression, deux options :
|
||||
- (a) restreindre la recherche à `companyName` seul (simple, mais perte de la recherche
|
||||
par contact) ;
|
||||
- (b) **[recommandé]** étendre la recherche en `LEFT JOIN` sur la sous-entité contact
|
||||
(`first_name` / `last_name` / `email` du contact), pour préserver l'UX « recherche par
|
||||
nom / contact / email » annoncée dans la barre de recherche.
|
||||
- **D2 — Colonnes de l'export XLSX** (Nom contact / Prénom / Téléphone / Téléphone 2 /
|
||||
Email). Après suppression :
|
||||
- (a) supprimer ces colonnes ;
|
||||
- (b) **[recommandé]** les alimenter depuis le **contact principal** (le contact de plus
|
||||
petit `position`), pour garder un export utile.
|
||||
|
||||
Ces deux décisions sont à valider par le métier (Matthieu) avant implémentation et sont
|
||||
rappelées dans chaque ticket concerné.
|
||||
|
||||
## 4. Fichiers de ce dossier
|
||||
|
||||
- `README.md` (ce fichier) — décision + découpage.
|
||||
- `M1-ticket-01-back.md` / `.prompt.md` — description + prompt d'implémentation.
|
||||
- `M1-ticket-02-front.md` / `.prompt.md`.
|
||||
- `M1-ticket-03-specs.md` / `.prompt.md`.
|
||||
- `M2-ticket-specs.md` / `.prompt.md`.
|
||||
- `M2-amendement-tickets.md` — bandeau d'amendement + liste des tickets M2 à mettre à jour.
|
||||
@@ -5,8 +5,11 @@ nom: "Répertoire clients"
|
||||
ecran: repertoire-clients
|
||||
owner_spec: Matthieu
|
||||
backup_spec: Tristan
|
||||
version: V0
|
||||
version: V1
|
||||
date_redaction: 2026-05-28
|
||||
# Historique : V1 (2026-06-03) — Refonte contact : suppression du contact principal inline
|
||||
# du Client (firstName/lastName/phonePrimary/phoneSecondary/email retirés de la table client).
|
||||
# Les contacts vivent uniquement dans ClientContact. Cf. docs/specs/M1-clients/refonte-contact/README.md
|
||||
|
||||
# === LIENS ===
|
||||
spec_front: ./spec-front.md
|
||||
@@ -203,11 +206,11 @@ Le **formatage `XX XX XX XX XX`** est fait à l'affichage côté front (filter V
|
||||
| | +-----------------------+ | (Catalog) |
|
||||
| id (PK) | +--------------+
|
||||
| company_name |
|
||||
| first_name | +-----------------------+ +--------------+
|
||||
| last_name |--1:n-->| client_contact | | site |
|
||||
| phone_primary | +-----------------------+ | (Sites) |
|
||||
| phone_secondary | +--------------+
|
||||
| email | +-----------------------+ ^
|
||||
| (contact inline | +-----------------------+ +--------------+
|
||||
| retiré V1 — |--1:n-->| client_contact | | site |
|
||||
| firstName, | +-----------------------+ | (Sites) |
|
||||
| lastName, phones,| +--------------+
|
||||
| email) | +-----------------------+ ^
|
||||
| distributor_id |--1:n-->| client_address |--n:m---------+
|
||||
| broker_id | +-----------------------+
|
||||
| triage_service | |
|
||||
@@ -302,11 +305,8 @@ CREATE TABLE client (
|
||||
id SERIAL PRIMARY KEY,
|
||||
-- Formulaire principal
|
||||
company_name VARCHAR(180) NOT NULL,
|
||||
first_name VARCHAR(120),
|
||||
last_name VARCHAR(120),
|
||||
phone_primary VARCHAR(20) NOT NULL,
|
||||
phone_secondary VARCHAR(20),
|
||||
email VARCHAR(180) NOT NULL,
|
||||
-- Contact inline retiré (V1, refonte-contact) : first_name / last_name / phone_primary /
|
||||
-- phone_secondary / email vivent désormais uniquement dans client_contact (onglet Contacts).
|
||||
distributor_id INT REFERENCES client(id) ON DELETE SET NULL,
|
||||
broker_id INT REFERENCES client(id) ON DELETE SET NULL,
|
||||
triage_service BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
@@ -580,32 +580,9 @@ class Client implements TimestampableInterface, BlamableInterface
|
||||
#[Groups(['client:read', 'client:write:main'])]
|
||||
private ?string $companyName = null;
|
||||
|
||||
// RG-1.01 — first_name OU last_name obligatoire (validation Assert\Callback
|
||||
// au niveau de l'entite, levee dans le Processor).
|
||||
#[ORM\Column(length: 120, nullable: true)]
|
||||
#[Assert\Length(max: 120, normalizer: 'trim')]
|
||||
#[Groups(['client:read', 'client:write:main'])]
|
||||
private ?string $firstName = null;
|
||||
|
||||
#[ORM\Column(length: 120, nullable: true)]
|
||||
#[Assert\Length(max: 120, normalizer: 'trim')]
|
||||
#[Groups(['client:read', 'client:write:main'])]
|
||||
private ?string $lastName = null;
|
||||
|
||||
#[ORM\Column(length: 20)]
|
||||
#[Assert\NotBlank]
|
||||
#[Groups(['client:read', 'client:write:main'])]
|
||||
private ?string $phonePrimary = null;
|
||||
|
||||
#[ORM\Column(length: 20, nullable: true)]
|
||||
#[Groups(['client:read', 'client:write:main'])]
|
||||
private ?string $phoneSecondary = null;
|
||||
|
||||
#[ORM\Column(length: 180)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Email]
|
||||
#[Groups(['client:read', 'client:write:main'])]
|
||||
private ?string $email = null;
|
||||
// Contact inline retiré (V1, refonte-contact) : firstName / lastName / phonePrimary /
|
||||
// phoneSecondary / email ne sont plus portés par Client — ils vivent dans ClientContact
|
||||
// (onglet Contacts). La garantie « ≥ 1 contact nommé » est portée par RG-1.05 + RG-1.14.
|
||||
|
||||
// RG-1.03 — distributor / broker auto-references (mutuellement exclusives,
|
||||
// contrainte CHECK en base).
|
||||
@@ -749,7 +726,7 @@ class Client implements TimestampableInterface, BlamableInterface
|
||||
- **Query params** :
|
||||
- `includeArchived=true|false` (default `false`)
|
||||
- `categoryCode=<code>` (filtre les clients ayant ≥ 1 `Category` de ce code stable — ERP-78 ; ex. `DISTRIBUTEUR`, `COURTIER`)
|
||||
- `search=<text>` (recherche fuzzy sur companyName + lastName + email)
|
||||
- `search=<text>` (recherche fuzzy sur companyName + contacts liés `client_contact` (firstName / lastName / email) via LEFT JOIN groupé par `client.id` — décision D1, refonte-contact)
|
||||
- **Tri par défaut** : `companyName ASC`
|
||||
- **Pagination** : front via `<MalioDataTable>` (volumétrie cible faible). Pas de pagination serveur au M1.
|
||||
- **Réponse 200** (JSON-LD Hydra) : items avec champs `client:read` UNIQUEMENT (pas les champs `client:read:accounting` sauf si l'user a la permission `accounting.view`).
|
||||
@@ -768,10 +745,6 @@ class Client implements TimestampableInterface, BlamableInterface
|
||||
```json
|
||||
{
|
||||
"companyName": "ACME SAS",
|
||||
"firstName": "Jean",
|
||||
"lastName": "Dupont",
|
||||
"phonePrimary": "0612345678",
|
||||
"email": "jean.dupont@acme.fr",
|
||||
"categories": ["/api/categories/3", "/api/categories/7"],
|
||||
"distributor": null,
|
||||
"broker": null,
|
||||
@@ -783,7 +756,7 @@ class Client implements TimestampableInterface, BlamableInterface
|
||||
- `201` / `400` / `401` / `403`
|
||||
- `409 Conflict` si doublon de nom de société (`companyName` — RG-1.16). SIREN et email ne sont pas uniques (cf. Q4, § 2.4).
|
||||
- `422 Unprocessable Entity` :
|
||||
- RG-1.01 : ni firstName ni lastName
|
||||
- (RG-1.01 supprimée V1 — la complétude du contact est portée par l'onglet Contacts : RG-1.05 / RG-1.14)
|
||||
- RG-1.03 : distributor + broker remplis simultanément
|
||||
- Catégories vides (Assert\Count min=1)
|
||||
|
||||
@@ -885,8 +858,8 @@ Cf. § 2.6. Pattern Shared standard.
|
||||
|
||||
### Formulaire principal
|
||||
|
||||
- **RG-1.01** : Au moins l'un des champs `firstName` (Prénom du contact principal) ou `lastName` (Nom du contact principal) doit être renseigné. Sinon → 422.
|
||||
- **RG-1.02** : Le champ `phoneSecondary` est optionnel et apparaît au clic sur un bouton `+` côté front. Maximum 2 téléphones (primary + secondary). Comportement purement front au niveau UI ; côté serveur, les 2 colonnes existent et sont distinctes.
|
||||
- ~~**RG-1.01**~~ _(SUPPRIMÉE — V1, 2026-06-03, refonte-contact)_ : le contact principal inline est retiré du `Client`. La garantie « au moins un contact nommé » est désormais portée par **RG-1.05** (bloc Contact valide) + **RG-1.14** (≥ 1 bloc Contact) sur `ClientContact`.
|
||||
- ~~**RG-1.02**~~ _(SUPPRIMÉE du Client — V1, refonte-contact)_ : plus de téléphones inline sur le `Client`. Le « maximum 2 téléphones » reste applicable aux blocs `ClientContact` (normalisation RG-1.20).
|
||||
- **RG-1.03** : Les champs `distributor` et `broker` sont **mutuellement exclusifs** (au plus une seule des deux est renseignée). Tentative d'envoyer les deux → 422. Contrainte CHECK en base également : `NOT (distributor_id IS NOT NULL AND broker_id IS NOT NULL)`. Un `distributor` référencé doit porter une **`Category` de code `DISTRIBUTEUR`** ; un `broker` une **`Category` de code `COURTIER`** — sinon 422. _(Refonte ERP-78 : le filtrage se fait sur le `code` de la `Category`, plus sur le type — `ClientProcessor::hasCategoryCode`.)_ La liste front de `distributor` = clients ayant une catégorie de code `DISTRIBUTEUR`, via `GET /api/clients?categoryCode=DISTRIBUTEUR` ; idem `broker` avec `COURTIER`.
|
||||
|
||||
### Onglet Information
|
||||
@@ -929,9 +902,9 @@ Cf. § 2.6. Pattern Shared standard.
|
||||
### Normalisation serveur (formatage)
|
||||
|
||||
- **RG-1.18** : `companyName` est **upper-cased** intégralement côté serveur avant validation et persistance (`mb_strtoupper(trim($v), 'UTF-8')`). Le client n'a pas besoin de saisir en majuscules ; la BDD stocke en majuscules.
|
||||
- **RG-1.19** : `firstName`, `lastName` (sur `Client` et `ClientContact`) sont **capitalize**-és serveur (`mb_convert_case(trim($v), MB_CASE_TITLE, 'UTF-8')`). Exemple : `JEAN dupont` → `Jean Dupont`.
|
||||
- **RG-1.20** : Les champs téléphone (`phonePrimary`, `phoneSecondary` sur `Client`, et idem sur `ClientContact`) sont **normalisés à chiffres uniquement** côté serveur (`preg_replace('/\D+/', '', $v)`). Stockage : `0612345678`. Le **format affichage `XX XX XX XX XX`** est de la responsabilité du front via un filter Vue dédié (cf. spec-front).
|
||||
- **RG-1.21** : `email` (`Client.email`, `ClientAddress.billingEmail`, `ClientContact.email`) est **lowercase** intégralement côté serveur (`mb_strtolower(trim($v), 'UTF-8')`).
|
||||
- **RG-1.19** : `firstName`, `lastName` (sur `ClientContact` ; scope `Client` retiré en V1) sont **capitalize**-és serveur (`mb_convert_case(trim($v), MB_CASE_TITLE, 'UTF-8')`). Exemple : `JEAN dupont` → `Jean Dupont`.
|
||||
- **RG-1.20** : Les champs téléphone (`phonePrimary`, `phoneSecondary` sur `ClientContact` ; scope `Client` retiré en V1) sont **normalisés à chiffres uniquement** côté serveur (`preg_replace('/\D+/', '', $v)`). Stockage : `0612345678`. Le **format affichage `XX XX XX XX XX`** est de la responsabilité du front via un filter Vue dédié (cf. spec-front).
|
||||
- **RG-1.21** : `email` (`ClientAddress.billingEmail`, `ClientContact.email` ; `Client.email` retiré en V1) est **lowercase** intégralement côté serveur (`mb_strtolower(trim($v), 'UTF-8')`).
|
||||
|
||||
### Archivage
|
||||
|
||||
@@ -960,8 +933,8 @@ Cf. § 2.6. Pattern Shared standard.
|
||||
|
||||
### 8.1 Cas à couvrir (back — PHPUnit)
|
||||
|
||||
- [ ] **RG-1.01** : POST sans firstName ni lastName → 422
|
||||
- [ ] **RG-1.02** : POST avec phoneSecondary rempli → persistance OK ; PATCH ajoutant un 3e téléphone → côté API, 2 colonnes uniquement (test que le payload ne peut pas créer un 3e)
|
||||
- [ ] ~~RG-1.01~~ _(supprimée V1)_ : la complétude du contact est couverte par RG-1.05 / RG-1.14 sur `ClientContact`
|
||||
- [ ] ~~RG-1.02~~ _(supprimée du Client V1)_ : plus de téléphones inline sur le Client (téléphones testés sur `ClientContact`)
|
||||
- [ ] **RG-1.03** : POST avec distributor ET broker → 422 ; POST distributor seul → 201
|
||||
- [ ] **RG-1.03** : POST distributor référençant un client SANS catégorie de code DISTRIBUTEUR → 422 (validation custom `ClientProcessor::hasCategoryCode`)
|
||||
- [ ] **RG-1.04** : PATCH onglet Information par un user Commerciale avec champs incomplets → 422 ; même PATCH par Admin → 200
|
||||
@@ -975,9 +948,9 @@ Cf. § 2.6. Pattern Shared standard.
|
||||
- [ ] **RG-1.14** : front-driven uniquement, pas de test back
|
||||
- [ ] **RG-1.16** : POST avec `companyName` déjà pris → 409 ; POST avec même `companyName` après archivage de l'ancien → 201. SIREN et email dupliqués → 201 (plus d'unicité — RG-1.15/1.17 supprimées, Q4).
|
||||
- [ ] **RG-1.18** : POST `companyName="acme sas"` → BDD persiste `"ACME SAS"`
|
||||
- [ ] **RG-1.19** : POST `firstName="JEAN"`, `lastName="dupont"` → persiste `"Jean"`, `"Dupont"`
|
||||
- [ ] **RG-1.20** : POST `phonePrimary="06.12.34.56.78"` → persiste `"0612345678"`
|
||||
- [ ] **RG-1.21** : POST `email="Jean.DUPONT@ACME.FR"` → persiste `"jean.dupont@acme.fr"`
|
||||
- [ ] **RG-1.19** : POST `firstName="JEAN"`, `lastName="dupont"` (via un bloc `ClientContact`) → persiste `"Jean"`, `"Dupont"`
|
||||
- [ ] **RG-1.20** : POST `phonePrimary="06.12.34.56.78"` (via un bloc `ClientContact`) → persiste `"0612345678"`
|
||||
- [ ] **RG-1.21** : POST `email="Jean.DUPONT@ACME.FR"` (via `ClientContact` ou `ClientAddress.billingEmail`) → persiste `"jean.dupont@acme.fr"`
|
||||
- [ ] **RG-1.22/23** : PATCH isArchived=true par Bureau (sans `archive`) → 403 ; par Admin → 200 + archivedAt rempli ; PATCH isArchived=false sur un client archivé dont le SIREN a été repris → 409
|
||||
- [ ] **RG-1.24/25** : GET liste sans flag → exclut archivés ; avec `?includeArchived=true` → inclut
|
||||
- [ ] **RG-1.26** : GET liste → tri companyName ASC
|
||||
|
||||
@@ -5,7 +5,10 @@ nom: "Répertoire clients"
|
||||
ecran: repertoire-clients
|
||||
owner_spec: Matthieu
|
||||
backup_spec: Tristan
|
||||
version: V0
|
||||
version: V1
|
||||
# Historique : V1 (2026-06-03) — Refonte contact : suppression du bloc contact principal inline
|
||||
# (Nom/Prénom/Téléphone/Téléphone 2/Email retirés du formulaire principal et des écrans).
|
||||
# Saisie via l'onglet Contacts uniquement. Cf. docs/specs/M1-clients/refonte-contact/README.md
|
||||
date_redaction: 2026-05-28
|
||||
|
||||
# === LIENS ===
|
||||
@@ -68,9 +71,6 @@ Composant : `<MalioDataTable>`. Colonnes (à raffiner avec Tristan en revue maqu
|
||||
| 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 |
|
||||
|
||||
@@ -86,15 +86,12 @@ Création par **onglets successifs avec validation incrémentale** : pour pouvoi
|
||||
|
||||
C'est le 1er bloc à remplir. Sans validation de ce formulaire, les onglets ne sont pas accessibles.
|
||||
|
||||
> **V1 — 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 désormais dans l'onglet **Contacts** (RG-1.05 / RG-1.14). Le formulaire principal ne contient plus que Entreprise + Catégorie + relation Distributeur/Courtier.
|
||||
|
||||
| Champ | Type composant | Obligatoire | Règle |
|
||||
|---|---|---|---|
|
||||
| **Nom du client (Entreprise)** | `<MalioInputText>` | Oui | RG-1.18 (normalisation UPPERCASE serveur) |
|
||||
| **Nom du contact principal** | `<MalioInputText>` | Conditionnel | RG-1.01 + RG-1.19 (Capitalize) |
|
||||
| **Prénom du contact principal** | `<MalioInputText>` | Conditionnel | RG-1.01 + RG-1.19 (Capitalize) |
|
||||
| **Catégorie** | `<MalioSelectCheckbox>` (multi) | Oui | Liste des `Category` de l'API ; M2M Client ↔ Category |
|
||||
| **Téléphone principal** | `<MalioInputText>` (masque tel) | Oui | RG-1.02 + RG-1.20 (format `XX XX XX XX XX`) |
|
||||
| **Téléphone secondaire** | `<MalioInputText>` (masque tel) | Non | Apparaît au clic sur le bouton `+` (RG-1.02). Max 2 — bouton `+` disparaît une fois rempli. |
|
||||
| **Email** | `<MalioInputText>` type email | Oui | RG-1.21 (lowercase) |
|
||||
| **Distributeur / Courtier** | `<MalioSelect>` | Non | Valeurs : `Dépend du distributeur` / `Dépend du courtier` / `Aucun`. RG-1.03 conditionne les 2 champs suivants. |
|
||||
| **Nom du distributeur** | `<MalioSelect>` | Conditionnel | Visible si « Dépend du distributeur ». Liste = clients ayant ≥ 1 catégorie de **code** `DISTRIBUTEUR` (ERP-78), via `GET /api/clients?categoryCode=DISTRIBUTEUR`. RG-1.03. |
|
||||
| **Nom du courtier** | `<MalioSelect>` | Conditionnel | Visible si « Dépend du courtier ». Liste = clients ayant ≥ 1 catégorie de **code** `COURTIER` (ERP-78), via `GET /api/clients?categoryCode=COURTIER`. RG-1.03. |
|
||||
@@ -120,7 +117,7 @@ Saisir les informations de l'entreprise.
|
||||
|
||||
### 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).
|
||||
Saisir un ou plusieurs contacts associés au client. **(V1 — 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-1.14).
|
||||
|
||||
**Bloc Contact** :
|
||||
|
||||
@@ -250,7 +247,7 @@ Le serveur normalise systématiquement (cf. RG-1.18 à RG-1.21 dans [`spec-back.
|
||||
|---|---|---|
|
||||
| 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) |
|
||||
| Téléphone (téléphones des blocs `ClientContact`) | 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()`.
|
||||
@@ -275,7 +272,7 @@ Le composant `Code postal` + `Ville` + `Adresse` est branché sur **api-adresse.
|
||||
| 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 | **Nom d'entreprise uniquement** (case-insensitive, parmi non-archivés) — décision Q4. SIREN et email NON uniques. Index partiel Postgres `uq_client_company_name_active`. Doublon de nom → 409 Conflict. |
|
||||
| 8 | Téléphones (max 2) | **2 colonnes plates** `phone_primary` + `phone_secondary`. Pas de table séparée. |
|
||||
| 8 | Téléphones (max 2) | Sur les blocs `ClientContact` (`phone_primary` + `phone_secondary`). _(V1 : retirés du Client — refonte-contact.)_ |
|
||||
| 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. |
|
||||
|
||||
Reference in New Issue
Block a user