From 3b2b441e5f8b8c628ec49cc546b2ecbab9d756a9 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 3 Jun 2026 16:13:36 +0200 Subject: [PATCH] refactor(front) : retirer le bloc contact principal des ecrans client Les 5 champs inline (nom, prenom, telephones, email) sont retires des ecrans creation / consultation / modification du Client. Les coordonnees sont saisies exclusivement dans l'onglet Contacts (ClientContactBlock). Types, mappeurs, validations, payloads et cles i18n form.main.* associes nettoyes ; tests Vitest clientEdit ajustes. Ticket M1 2/3 (front), refonte-contact. --- frontend/i18n/locales/fr.json | 6 -- .../commercial/pages/clients/[id]/edit.vue | 38 -------- .../commercial/pages/clients/[id]/index.vue | 34 +------ .../modules/commercial/pages/clients/new.vue | 92 ++----------------- .../utils/__tests__/clientEdit.spec.ts | 26 ++---- .../commercial/utils/clientConsultation.ts | 5 - .../modules/commercial/utils/clientEdit.ts | 31 ++----- 7 files changed, 21 insertions(+), 211 deletions(-) diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index c09f4e9..6e99ccc 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -133,12 +133,6 @@ "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", "relationDistributor": "Dépend du distributeur", diff --git a/frontend/modules/commercial/pages/clients/[id]/edit.vue b/frontend/modules/commercial/pages/clients/[id]/edit.vue index 06a2f77..a109988 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" /> - - - - - { || (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..18909fa 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..41b015d 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" /> - - - - - (['']) - -/** 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 +426,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 +440,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 +464,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 +475,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 +589,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 +874,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,