From ec952896baecb0cc247059a85bf9be562a5505ce Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 3 Jun 2026 14:48:55 +0000 Subject: [PATCH] =?UTF-8?q?M1=20=C2=B7=202/3=20(Front)=20=E2=80=94=20Retir?= =?UTF-8?q?er=20le=20bloc=20contact=20principal=20des=20ecrans=20Client=20?= =?UTF-8?q?(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 passe désormais exclusivement par l'onglet **Contacts** (`ClientContactBlock`, inchangé). Dépend du ticket **1/3 (back)** : l'API ne renvoie/n'accepte plus ces 5 champs sur `client`. Contexte : `docs/specs/M1-clients/refonte-contact/README.md`. ## Changements - **`pages/clients/new.vue`** : bloc principal réduit à Nom entreprise / Catégories / Relation / Triage. Suppression de `main.firstName/lastName/email`, `mainPhones`, `addMainPhone()`, `prefillFirstContact()`. `isMainValid` ne dépend plus que de `companyName` + ≥ 1 catégorie + relation valide. Payload POST et `ClientResponse` nettoyés. - **`pages/clients/[id]/edit.vue`** : mêmes champs retirés, `isMainValid` simplifié. - **`pages/clients/[id]/index.vue`** : affichage lecture seule des 5 champs retiré. - **`utils/clientEdit.ts`** : `MainFormDraft`, `mapMainDraft()`, `buildMainPayload()` débarrassés des 5 champs + `hasSecondaryPhone`. - **`utils/clientConsultation.ts`** : `ClientDetail` débarrassé des champs inline (`ContactRead` conservé). - **`i18n/locales/fr.json`** : clés `form.main.firstName/lastName/email/phonePrimary/phoneSecondary/addPhone` supprimées. `form.contact.*` conservé. - **Tests** : `clientEdit.spec.ts` ajusté (factory, `MAIN_KEYS`, assertions `mapMainDraft`, test téléphone secondaire obsolète retiré). ## Vérifications - `make nuxt-test` : suites `clientEdit` / `clientConsultation` / `clientFormRules` vertes. Les 2 échecs restants (`useClientReferentials.spec.ts`, libellé de site) sont **pré-existants** sur `develop` (confirmé par `git stash`), sans rapport avec ce ticket. - `eslint` sur les fichiers touchés : OK, aucun import/variable mort. - Zéro référence orpheline aux clés `form.main.*` supprimées ; JSON i18n valide. ## Reste à faire - Golden path navigateur (création → consultation → modification sans bloc inline) à valider manuellement. Reviewed-on: https://gitea.malio.fr/MALIO-DEV/Starseed/pulls/57 Co-authored-by: tristan Co-committed-by: tristan --- frontend/i18n/locales/fr.json | 7 +- .../__tests__/useClientReferentials.spec.ts | 10 +- .../commercial/pages/clients/[id]/edit.vue | 39 +------- .../commercial/pages/clients/[id]/index.vue | 39 +------- .../modules/commercial/pages/clients/new.vue | 93 ++----------------- .../utils/__tests__/clientEdit.spec.ts | 26 ++---- .../commercial/utils/clientConsultation.ts | 5 - .../modules/commercial/utils/clientEdit.ts | 31 ++----- 8 files changed, 34 insertions(+), 216 deletions(-) diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index c09f4e9..86651f0 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -133,14 +133,9 @@ "duplicateCompany": "Un client portant ce nom de société existe déjà.", "main": { "companyName": "Nom du client (Entreprise)", - "firstName": "Prénom du contact principal", - "lastName": "Nom du contact principal", - "email": "Email", - "phonePrimary": "Téléphone", - "phoneSecondary": "Téléphone (2)", - "addPhone": "Ajouter un numéro", "categories": "Catégorie", "relation": "Distributeur / Courtier", + "relationNone": "Aucun", "relationDistributor": "Dépend du distributeur", "relationBroker": "Dépend du courtier", "distributorName": "Nom du distributeur", diff --git a/frontend/modules/commercial/composables/__tests__/useClientReferentials.spec.ts b/frontend/modules/commercial/composables/__tests__/useClientReferentials.spec.ts index 67202f6..3d92495 100644 --- a/frontend/modules/commercial/composables/__tests__/useClientReferentials.spec.ts +++ b/frontend/modules/commercial/composables/__tests__/useClientReferentials.spec.ts @@ -28,7 +28,7 @@ describe('useClientReferentials.loadCommon (resilience ERP-102)', () => { return Promise.reject(new Error('403 Forbidden')) } if (url === '/sites') { - return Promise.resolve({ member: [{ '@id': '/api/sites/1', name: 'Chatellerault' }] }) + return Promise.resolve({ member: [{ '@id': '/api/sites/1', name: 'Chatellerault', postalCode: '86100' }] }) } return Promise.resolve({ member: [{ '@id': '/api/x/1', code: 'X', label: 'Libelle X' }], @@ -40,7 +40,8 @@ describe('useClientReferentials.loadCommon (resilience ERP-102)', () => { await refs.loadCommon() // Resilience : les referentiels OK sont peuples malgre l'echec de /categories. - expect(refs.sites.value).toEqual([{ value: '/api/sites/1', label: 'Chatellerault' }]) + // Le libelle d'un site est son numero de departement (2 premiers chiffres du code postal). + expect(refs.sites.value).toEqual([{ value: '/api/sites/1', label: '86' }]) expect(refs.tvaModes.value).toEqual([{ value: '/api/x/1', label: 'Libelle X' }]) expect(refs.banks.value).toEqual([{ value: '/api/x/1', label: 'Libelle X' }]) @@ -56,7 +57,7 @@ describe('useClientReferentials.loadCommon (resilience ERP-102)', () => { }) } if (url === '/sites') { - return Promise.resolve({ member: [{ '@id': '/api/sites/1', name: 'Chatellerault' }] }) + return Promise.resolve({ member: [{ '@id': '/api/sites/1', name: 'Chatellerault', postalCode: '86100' }] }) } return Promise.resolve({ member: [] }) }) @@ -67,6 +68,7 @@ describe('useClientReferentials.loadCommon (resilience ERP-102)', () => { expect(refs.categories.value).toEqual([ { value: '/api/categories/1', label: 'Secteur', code: 'SECTEUR' }, ]) - expect(refs.sites.value).toEqual([{ value: '/api/sites/1', label: 'Chatellerault' }]) + // Le libelle d'un site est son numero de departement (2 premiers chiffres du code postal). + expect(refs.sites.value).toEqual([{ value: '/api/sites/1', label: '86' }]) }) }) diff --git a/frontend/modules/commercial/pages/clients/[id]/edit.vue b/frontend/modules/commercial/pages/clients/[id]/edit.vue index 06a2f77..23b955f 100644 --- a/frontend/modules/commercial/pages/clients/[id]/edit.vue +++ b/frontend/modules/commercial/pages/clients/[id]/edit.vue @@ -29,16 +29,6 @@ :required="true" :readonly="businessReadonly" /> - - - - - @@ -424,7 +391,6 @@ import { import { extractApiErrorMessage } from '~/shared/utils/api' // Masques de saisie (la normalisation finale reste serveur). -const PHONE_MASK = '## ## ## ## ##' const SIREN_MASK = '#########' const EMPLOYEES_MASK = '#######' @@ -620,9 +586,6 @@ const isMainValid = computed(() => { || (main.relationType === 'distributeur' && filled(main.distributorIri)) || (main.relationType === 'courtier' && filled(main.brokerIri)) return filled(main.companyName) - && filled(main.email) - && filled(main.phonePrimary) - && (filled(main.firstName) || filled(main.lastName)) && main.categoryIris.length >= 1 && relationValid }) diff --git a/frontend/modules/commercial/pages/clients/[id]/index.vue b/frontend/modules/commercial/pages/clients/[id]/index.vue index 1951700..ab74193 100644 --- a/frontend/modules/commercial/pages/clients/[id]/index.vue +++ b/frontend/modules/commercial/pages/clients/[id]/index.vue @@ -52,16 +52,6 @@ :label="t('commercial.clients.form.main.companyName')" readonly /> - - - - + + client.value?.companyName ?? t('commercial.cl const relation = computed(() => (client.value ? relationOf(client.value) : { type: null, name: null })) const categoryIris = computed(() => (client.value?.categories ?? []).map(c => c['@id'])) -// Telephones du formulaire principal, formates XX XX XX XX XX (RG d'affichage). -const mainPhones = computed(() => - [client.value?.phonePrimary, client.value?.phoneSecondary] - .filter((p): p is string => Boolean(p)) - .map(formatPhoneFR), -) - const information = computed(() => ({ description: client.value?.description ?? null, competitors: client.value?.competitors ?? null, diff --git a/frontend/modules/commercial/pages/clients/new.vue b/frontend/modules/commercial/pages/clients/new.vue index 7bbdbe6..e29da36 100644 --- a/frontend/modules/commercial/pages/clients/new.vue +++ b/frontend/modules/commercial/pages/clients/new.vue @@ -23,16 +23,6 @@ :required="true" :readonly="mainLocked" /> - - - - - @@ -388,11 +359,9 @@ import { type ContactFormDraft, type RibFormDraft, } from '~/modules/commercial/types/clientForm' -import { formatPhoneFR } from '~/shared/utils/phone' import { extractApiErrorMessage } from '~/shared/utils/api' // Masques de saisie (la normalisation finale reste serveur). -const PHONE_MASK = '## ## ## ## ##' const SIREN_MASK = '#########' // Masque « nombre » du champ Nombre de salaries : chiffres uniquement (max 7). const EMPLOYEES_MASK = '#######' @@ -444,9 +413,6 @@ const tabSubmitting = ref(false) // ── Formulaire principal ──────────────────────────────────────────────────── const main = reactive({ companyName: null as string | null, - firstName: null as string | null, - lastName: null as string | null, - email: null as string | null, categoryIris: [] as string[], relationType: null as 'distributeur' | 'courtier' | null, distributorIri: null as string | null, @@ -454,17 +420,6 @@ const main = reactive({ triageService: false, }) -// Telephones du formulaire principal : 1 par defaut, 2 au maximum (RG-1.02). -// L'index 0 alimente phonePrimary, l'index 1 phoneSecondary au POST. -const mainPhones = ref(['']) - -/** Revele le 2e numero (le bouton « + » disparait une fois a 2, RG-1.02). */ -function addMainPhone(): void { - if (mainPhones.value.length === 1) { - mainPhones.value.push('') - } -} - // Pas d'option « Aucun » : le select est vide par defaut (relationType = null). const relationOptions = computed(() => [ { value: 'distributeur', label: t('commercial.clients.form.main.relationDistributor') }, @@ -472,10 +427,11 @@ const relationOptions = computed(() => [ ]) // Validation du formulaire principal (gate le bouton « Valider ») : -// - companyName / email / telephone principal / >= 1 categorie obligatoires ; -// - RG-1.01 : nom OU prenom du contact principal ; -// - relation Distributeur/Courtier obligatoire (un des deux), ET le nom -// correspondant obligatoire selon le choix (spec fonctionnelle). +// - companyName / >= 1 categorie obligatoires ; +// - relation Distributeur/Courtier optionnelle, mais le nom correspondant +// devient requis si l'un des deux est choisi (spec fonctionnelle). +// Les coordonnees de contact ne sont plus saisies ici : elles vivent dans +// l'onglet Contacts (RG-1.05/1.14 garantissent >= 1 contact valide). const isMainValid = computed(() => { const filled = (v: string | null | undefined) => v !== null && v !== undefined && v.trim() !== '' // Relation Distributeur/Courtier OPTIONNELLE ; mais si « Depend du @@ -485,9 +441,6 @@ const isMainValid = computed(() => { || (main.relationType === 'distributeur' && filled(main.distributorIri)) || (main.relationType === 'courtier' && filled(main.brokerIri)) return filled(main.companyName) - && filled(main.email) - && filled(mainPhones.value[0]) - && (filled(main.firstName) || filled(main.lastName)) && main.categoryIris.length >= 1 && relationValid }) @@ -512,11 +465,6 @@ async function submitMain(): Promise { try { const payload: Record = { companyName: main.companyName, - firstName: main.firstName || null, - lastName: main.lastName || null, - email: main.email, - phonePrimary: mainPhones.value[0] || null, - phoneSecondary: mainPhones.value[1] || null, categories: main.categoryIris, distributor: main.relationType === 'distributeur' ? main.distributorIri : null, broker: main.relationType === 'courtier' ? main.brokerIri : null, @@ -528,18 +476,8 @@ async function submitMain(): Promise { }) clientId.value = created.id - // Reaffiche les valeurs normalisees renvoyees par le serveur. + // Reaffiche la valeur normalisee renvoyee par le serveur. main.companyName = created.companyName ?? main.companyName - main.firstName = created.firstName ?? null - main.lastName = created.lastName ?? null - main.email = created.email ?? main.email - // Reaffiche les telephones normalises (reformates via formatPhoneFR). - const normalizedPhones = [formatPhoneFR(created.phonePrimary), formatPhoneFR(created.phoneSecondary)] - .filter(p => p !== '') - mainPhones.value = normalizedPhones.length > 0 ? normalizedPhones : [''] - - // Pre-remplit le 1er contact a partir du formulaire principal (editable). - prefillFirstContact() mainLocked.value = true unlockedIndex.value = 0 @@ -652,18 +590,10 @@ async function submitInformation(): Promise { } // ── Onglet Contact ────────────────────────────────────────────────────────── +// Au moins un bloc Contact vide au depart : c'est desormais le seul point de +// saisie des coordonnees (le bloc principal ne porte plus de contact inline). const contacts = ref([emptyContact()]) -/** Pre-remplit le 1er contact depuis le formulaire principal (apres creation). */ -function prefillFirstContact(): void { - const first = contacts.value[0] - if (!first) return - first.lastName = main.lastName - first.firstName = main.firstName - first.email = main.email - first.phonePrimary = mainPhones.value[0] ?? null -} - // « + Nouveau contact » desactive tant que le dernier bloc n'a ni nom ni prenom. const canAddContact = computed(() => { const last = contacts.value[contacts.value.length - 1] @@ -945,11 +875,6 @@ function runConfirm(): void { interface ClientResponse { id: number companyName: string | null - firstName: string | null - lastName: string | null - email: string | null - phonePrimary: string | null - phoneSecondary: string | null } interface ContactResponse { diff --git a/frontend/modules/commercial/utils/__tests__/clientEdit.spec.ts b/frontend/modules/commercial/utils/__tests__/clientEdit.spec.ts index 5de9d8b..b0e5ea4 100644 --- a/frontend/modules/commercial/utils/__tests__/clientEdit.spec.ts +++ b/frontend/modules/commercial/utils/__tests__/clientEdit.spec.ts @@ -22,12 +22,6 @@ import type { AddressFormDraft, ContactFormDraft, RibFormDraft } from '~/modules function mainDraft(overrides: Partial = {}): MainFormDraft { return { companyName: 'ACME', - firstName: 'Jean', - lastName: 'Dupont', - email: 'jean@acme.fr', - phonePrimary: '05 49 11 22 33', - phoneSecondary: null, - hasSecondaryPhone: false, categoryIris: ['/api/categories/1'], relationType: null, distributorIri: null, @@ -64,9 +58,10 @@ function accountingDraft(overrides: Partial = {}): Accounti } // Champs de chaque groupe de serialisation (miroir back ClientProcessor). +// Le contact inline (nom/prenom/telephones/email) ne fait plus partie du groupe +// main : les coordonnees vivent desormais sur la sous-ressource ClientContact. const MAIN_KEYS = [ - 'companyName', 'firstName', 'lastName', 'email', 'phonePrimary', - 'phoneSecondary', 'categories', 'distributor', 'broker', 'triageService', + 'companyName', 'categories', 'distributor', 'broker', 'triageService', ] const INFORMATION_KEYS = [ 'description', 'competitors', 'foundedAt', 'employeesCount', @@ -104,11 +99,6 @@ describe('buildMainPayload — scoping strict groupe client:write:main', () => { expect(payload.distributor).toBeNull() expect(payload.broker).toBeNull() }) - - it('telephone secondaire non revele : envoie null meme si une valeur traine', () => { - const payload = buildMainPayload(mainDraft({ hasSecondaryPhone: false, phoneSecondary: '06 00 00 00 00' })) - expect(payload.phoneSecondary).toBeNull() - }) }) describe('buildInformationPayload — scoping strict groupe client:write:information', () => { @@ -168,19 +158,16 @@ describe('buildContactPayload / buildAddressPayload / buildRibPayload', () => { }) describe('mapMainDraft — pre-remplissage bloc principal', () => { - it('formate les telephones, resout la relation et extrait les IRI', () => { + it('resout la relation et extrait les IRI (sans contact inline)', () => { const client = { '@id': '/api/clients/1', id: 1, - companyName: 'ACME', firstName: 'Jean', lastName: 'Dupont', email: 'jean@acme.fr', - phonePrimary: '0549112233', phoneSecondary: '0600000000', triageService: true, + companyName: 'ACME', triageService: true, categories: [{ '@id': '/api/categories/1', code: 'SECTEUR' }], distributor: { '@id': '/api/clients/9', companyName: 'DISTRIB' }, } as ClientDetail const draft = mapMainDraft(client) - expect(draft.phonePrimary).toBe('05 49 11 22 33') - expect(draft.phoneSecondary).toBe('06 00 00 00 00') - expect(draft.hasSecondaryPhone).toBe(true) + expect(draft.companyName).toBe('ACME') expect(draft.categoryIris).toEqual(['/api/categories/1']) expect(draft.relationType).toBe('distributeur') expect(draft.distributorIri).toBe('/api/clients/9') @@ -191,7 +178,6 @@ describe('mapMainDraft — pre-remplissage bloc principal', () => { it('gere les cles omises (skip_null_values) sans planter', () => { const draft = mapMainDraft({ '@id': '/api/clients/2', id: 2 } as ClientDetail) expect(draft.companyName).toBeNull() - expect(draft.hasSecondaryPhone).toBe(false) expect(draft.categoryIris).toEqual([]) expect(draft.relationType).toBeNull() expect(draft.triageService).toBe(false) diff --git a/frontend/modules/commercial/utils/clientConsultation.ts b/frontend/modules/commercial/utils/clientConsultation.ts index a5b0ada..1b3761f 100644 --- a/frontend/modules/commercial/utils/clientConsultation.ts +++ b/frontend/modules/commercial/utils/clientConsultation.ts @@ -93,11 +93,6 @@ export interface RelatedClientRead extends HydraRef { export interface ClientDetail extends HydraRef { id: number companyName?: string | null - firstName?: string | null - lastName?: string | null - phonePrimary?: string | null - phoneSecondary?: string | null - email?: string | null triageService?: boolean isArchived?: boolean categories?: CategoryRead[] diff --git a/frontend/modules/commercial/utils/clientEdit.ts b/frontend/modules/commercial/utils/clientEdit.ts index 9031076..6b949d7 100644 --- a/frontend/modules/commercial/utils/clientEdit.ts +++ b/frontend/modules/commercial/utils/clientEdit.ts @@ -24,23 +24,16 @@ import { type ClientDetail, } from '~/modules/commercial/utils/clientConsultation' import type { AddressFormDraft, ContactFormDraft, RibFormDraft } from '~/modules/commercial/types/clientForm' -import { formatPhoneFR } from '~/shared/utils/phone' /** * Etat « plat » du bloc principal (groupe client:write:main). Distinct des * brouillons Contact : ces champs vivent sur le Client lui-meme (companyName, - * contact principal, telephones, email, categories, relation, triage), pas sur - * une sous-ressource ClientContact. + * categories, relation, triage), pas sur une sous-ressource ClientContact. Les + * coordonnees de contact (nom, prenom, telephones, email) ne sont plus portees + * par le Client : elles vivent exclusivement dans l'onglet Contacts. */ export interface MainFormDraft { companyName: string | null - firstName: string | null - lastName: string | null - email: string | null - phonePrimary: string | null - phoneSecondary: string | null - /** UI : le 2e numero a ete revele (ou existait deja au chargement). */ - hasSecondaryPhone: boolean /** IRI des categories rattachees (M2M). */ categoryIris: string[] relationType: 'distributeur' | 'courtier' | null @@ -96,22 +89,15 @@ export interface TabEditability { // ── Pre-remplissage (GET detail -> brouillons) ────────────────────────────── /** - * Mappe le detail client vers le brouillon du bloc principal. Les telephones - * sont reformates XX XX XX XX XX (RG d'affichage). La relation Distributeur/ - * Courtier est resolue par exclusivite (RG-1.03) et son IRI extrait de l'embed. + * Mappe le detail client vers le brouillon du bloc principal. La relation + * Distributeur/Courtier est resolue par exclusivite (RG-1.03) et son IRI extrait + * de l'embed. */ export function mapMainDraft(client: ClientDetail): MainFormDraft { const relation = relationOf(client) - const phoneSecondary = client.phoneSecondary ?? null return { companyName: client.companyName ?? null, - firstName: client.firstName ?? null, - lastName: client.lastName ?? null, - email: client.email ?? null, - phonePrimary: client.phonePrimary ? formatPhoneFR(client.phonePrimary) : null, - phoneSecondary: phoneSecondary ? formatPhoneFR(phoneSecondary) : null, - hasSecondaryPhone: phoneSecondary !== null && phoneSecondary !== '', categoryIris: (client.categories ?? []).map(c => c['@id']), relationType: relation.type, distributorIri: iriOf(client.distributor), @@ -157,11 +143,6 @@ export function mapAccountingFormDraft(client: ClientDetail): AccountingFormDraf export function buildMainPayload(main: MainFormDraft): Record { return { companyName: main.companyName, - firstName: main.firstName || null, - lastName: main.lastName || null, - email: main.email, - phonePrimary: main.phonePrimary || null, - phoneSecondary: main.hasSecondaryPhone ? (main.phoneSecondary || null) : null, categories: main.categoryIris, distributor: main.relationType === 'distributeur' ? main.distributorIri : null, broker: main.relationType === 'courtier' ? main.brokerIri : null,