diff --git a/frontend/modules/transport/composables/__tests__/useCarrierForm.test.ts b/frontend/modules/transport/composables/__tests__/useCarrierForm.test.ts index e9b5335..0608410 100644 --- a/frontend/modules/transport/composables/__tests__/useCarrierForm.test.ts +++ b/frontend/modules/transport/composables/__tests__/useCarrierForm.test.ts @@ -37,7 +37,7 @@ vi.stubGlobal('useToast', () => ({ })) const { useCarrierForm, CARRIER_TAB_KEYS } = await import('../useCarrierForm') -const { buildCarrierContactPayload, isCarrierContactBlank } = await import('../../utils/forms/carrierContact') +const { buildCarrierContactPayload, isCarrierContactBlank, isCarrierContactNamed } = await import('../../utils/forms/carrierContact') const { emptyCarrierContact } = await import('../../types/carrierForm') describe('useCarrierForm', () => { @@ -558,11 +558,22 @@ describe('useCarrierForm — onglet Adresses (ERP-167)', () => { }) }) -describe('carrierContact (util) — RG-4.08 + max 2 téléphones', () => { - it('isCarrierContactBlank : vrai si aucun champ, faux dès un champ rempli', () => { +describe('carrierContact (util) — validité alignée M1/M2/M3 + max 2 téléphones', () => { + it('isCarrierContactBlank : vrai si vide, faux dès un champ comptant rempli (phoneSecondary exclu)', () => { expect(isCarrierContactBlank(emptyCarrierContact())).toBe(true) expect(isCarrierContactBlank({ ...emptyCarrierContact(), jobTitle: 'Acheteur' })).toBe(false) expect(isCarrierContactBlank({ ...emptyCarrierContact(), phonePrimary: '0102030405' })).toBe(false) + // phoneSecondary seul ne compte pas (aligné M1/M2/M3). + expect(isCarrierContactBlank({ ...emptyCarrierContact(), phoneSecondary: '0605040302', hasSecondaryPhone: true })).toBe(true) + }) + + it('isCarrierContactNamed : nommé seulement avec un prénom OU un nom', () => { + expect(isCarrierContactNamed(emptyCarrierContact())).toBe(false) + expect(isCarrierContactNamed({ ...emptyCarrierContact(), firstName: 'Jean' })).toBe(true) + expect(isCarrierContactNamed({ ...emptyCarrierContact(), lastName: 'Doe' })).toBe(true) + // Fonction / téléphone / email seuls ne « nomment » pas (≠ RG-4.08 large). + expect(isCarrierContactNamed({ ...emptyCarrierContact(), jobTitle: 'Acheteur' })).toBe(false) + expect(isCarrierContactNamed({ ...emptyCarrierContact(), email: 'a@b.fr' })).toBe(false) }) it('buildCarrierContactPayload : phones = 1 numéro sans secondaire', () => { @@ -605,15 +616,22 @@ describe('useCarrierForm — onglet Contacts (ERP-168)', () => { return form } - it('RG-4.08 : « + Nouveau contact » désactivé tant que le bloc est vide', () => { + it('« + Nouveau contact » désactivé tant que le bloc n\'est pas nommé (prénom OU nom, aligné M1/M2/M3)', () => { const form = createdForm() expect(form.canAddContact.value).toBe(false) - // addContact est un no-op tant que le bloc est vide. + // addContact est un no-op tant que le bloc n'est pas nommé. form.addContact() expect(form.contacts.value).toHaveLength(1) + // Fonction seule ne suffit PAS à ajouter un nouveau bloc (≠ RG-4.08 large). const first = form.contacts.value[0] + if (first) first.jobTitle = 'Acheteur' + expect(form.canAddContact.value).toBe(false) + form.addContact() + expect(form.contacts.value).toHaveLength(1) + + // Un nom (ou prénom) débloque l'ajout. if (first) first.lastName = 'Doe' expect(form.canAddContact.value).toBe(true) form.addContact() diff --git a/frontend/modules/transport/composables/useCarrierForm.ts b/frontend/modules/transport/composables/useCarrierForm.ts index 29bc403..49f0a37 100644 --- a/frontend/modules/transport/composables/useCarrierForm.ts +++ b/frontend/modules/transport/composables/useCarrierForm.ts @@ -14,7 +14,7 @@ import { type CarrierMainResponse, } from '~/modules/transport/types/carrierForm' import { buildCarrierAddressPayload, isCarrierAddressValid } from '~/modules/transport/utils/forms/carrierAddress' -import { buildCarrierContactPayload, isCarrierContactBlank } from '~/modules/transport/utils/forms/carrierContact' +import { buildCarrierContactPayload, isCarrierContactBlank, isCarrierContactNamed } from '~/modules/transport/utils/forms/carrierContact' import type { QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch' /** Nom du cas spécial « compte-propre » LIOT (comparaison insensible à la casse, RG-4.01). */ @@ -390,11 +390,12 @@ export function useCarrierForm() { // Erreurs 422 par ligne (alignées sur l'index du v-for), peuplées par submitRows. const contactErrors = ref[]>([]) - // RG-4.08 : « + Nouveau contact » désactivé tant que le DERNIER bloc est vide - // (aucun champ rempli). + // « + Nouveau contact » désactivé tant que le DERNIER bloc n'est pas « nommé » + // (prénom OU nom) — aligné sur M1/M2/M3 (fonction / téléphone / email seuls ne + // suffisent pas à ajouter un nouveau bloc). const canAddContact = computed(() => { const last = contacts.value[contacts.value.length - 1] - return last !== undefined && !isCarrierContactBlank(last) + return last !== undefined && isCarrierContactNamed(last) }) function addContact(): void { diff --git a/frontend/modules/transport/utils/forms/carrierContact.ts b/frontend/modules/transport/utils/forms/carrierContact.ts index c006367..3d434f4 100644 --- a/frontend/modules/transport/utils/forms/carrierContact.ts +++ b/frontend/modules/transport/utils/forms/carrierContact.ts @@ -1,10 +1,9 @@ /** - * Helpers purs de l'onglet Contact transporteur (M4 Transport, ERP-168) — miroir - * de `providerContact.ts` (M3), avec deux spécificités M4 : - * - RG-4.08 : un bloc est valide dès qu'AU MOINS UN champ est rempli (n'importe - * lequel) — ≠ M3 qui n'exigeait que le nom. - * - les téléphones partent au back dans le tableau virtuel `phones` (max 2), - * pas en `phonePrimary` / `phoneSecondary` (mappés par le CarrierContactProcessor). + * Helpers purs de l'onglet Contact transporteur (M4 Transport, ERP-168) — ALIGNÉ + * sur `providerContact.ts` (M3) / les autres modules : mêmes règles de validité et + * de gating « + Nouveau contact » (un contact est « nommé » dès qu'il porte un + * prénom OU un nom). Seule spécificité M4 conservée : les téléphones partent au back + * dans le tableau virtuel `phones` (max 2), mappés par le CarrierContactProcessor. * Testables sans Vue ni API. */ @@ -16,10 +15,10 @@ function isFilled(value: string | null | undefined): boolean { } /** - * RG-4.08 : un bloc Contact est VIDE tant qu'aucun de ses champs n'est rempli - * (prénom / nom / fonction / téléphone(s) / email). Sert le gating « + Nouveau - * contact » (on n'ajoute pas de bloc tant que le précédent est vide) et reflète la - * garde back (CarrierContactProcessor + CHECK chk_carrier_contact_filled). + * Un bloc Contact est VIDE tant qu'aucun champ comptant pour la validité n'est + * rempli — prénom / nom / fonction / téléphone principal / email. `phoneSecondary` + * est EXCLU (aligné M1/M2/M3 : un bloc ne portant qu'un 2e numéro reste vide). Sert + * le filtrage des amorces vides à la soumission. */ export function isCarrierContactBlank(contact: CarrierContactFormDraft): boolean { return ![ @@ -27,11 +26,19 @@ export function isCarrierContactBlank(contact: CarrierContactFormDraft): boolean contact.lastName, contact.jobTitle, contact.phonePrimary, - contact.phoneSecondary, contact.email, ].some(isFilled) } +/** + * Un contact est « nommé » (valide) dès qu'il porte un prénom OU un nom — aligné + * sur M1/M2/M3. Pilote le gating « + Nouveau contact » : la fonction / le téléphone + * / l'email seuls ne suffisent pas pour ajouter un nouveau bloc. + */ +export function isCarrierContactNamed(contact: CarrierContactFormDraft): boolean { + return isFilled(contact.firstName) || isFilled(contact.lastName) +} + /** * Payload de la sous-ressource contacts (groupe `carrier:write:contacts`). Les * chaînes vides partent à null (le serveur normalise/trim). Les téléphones sont