fix(transport) : onglet Contact transporteur non obligatoire + navigation onglets (ERP-193)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 46s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m24s

- retrait de la regle « prenom OU nom » sur le bloc Contact : garde
  CarrierContactProcessor::validateName supprimee, CHECK chk_carrier_contact_name
  droppe (migration Version20260619120000), commentaires SQL/catalogue alignes
- front : gating « + Nouveau contact » sur bloc non vide (au lieu de « nomme »),
  onglet Contact vide finalisable sans creer de contact
- Prix accessible des la validation des Adresses (Contacts optionnel ne bloque plus)
- consultation <-> edition : on retombe sur le meme onglet via ?tab=
This commit is contained in:
2026-06-19 14:53:52 +02:00
parent c11d7822ce
commit 833d992ebb
12 changed files with 131 additions and 124 deletions
@@ -37,7 +37,7 @@ vi.stubGlobal('useToast', () => ({
}))
const { useCarrierForm, CARRIER_TAB_KEYS } = await import('../useCarrierForm')
const { buildCarrierContactPayload, isCarrierContactBlank, isCarrierContactNamed } = await import('../../utils/forms/carrierContact')
const { buildCarrierContactPayload, isCarrierContactBlank } = await import('../../utils/forms/carrierContact')
const { buildCarrierPricePayload, isCarrierPriceValid } = await import('../../utils/forms/carrierPrice')
const { emptyCarrierContact, emptyCarrierPrice } = await import('../../types/carrierForm')
@@ -545,6 +545,9 @@ describe('useCarrierForm — onglet Adresse (ERP-167 / ERP-172 : adresse unique)
expect(opts).toMatchObject({ toast: false, headers: { Accept: 'application/ld+json' } })
expect(form.address.value.id).toBe(88)
expect(form.isValidated('addresses')).toBe(true)
// ERP-193 : Contact optionnel → valider Adresses déverrouille jusqu'à Prix
// (dernier onglet), sans étape bloquante par Contacts.
expect(form.unlockedIndex.value).toBe(CARRIER_TAB_KEYS.length - 1)
})
it('submitAddress : PATCH de l\'adresse existante sur /carrier_addresses/{id}', async () => {
@@ -577,7 +580,7 @@ describe('useCarrierForm — onglet Adresse (ERP-167 / ERP-172 : adresse unique)
})
})
describe('carrierContact (util) — validité alignée M1/M2/M3 + max 2 téléphones', () => {
describe('carrierContact (util) — bloc optionnel (ERP-193) + 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)
@@ -586,15 +589,6 @@ describe('carrierContact (util) — validité alignée M1/M2/M3 + max 2 téléph
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', () => {
const body = buildCarrierContactPayload({ ...emptyCarrierContact(), phonePrimary: '0102030405' })
expect(body.phones).toEqual(['0102030405'])
@@ -635,23 +629,18 @@ describe('useCarrierForm — onglet Contacts (ERP-168)', () => {
return form
}
it('« + Nouveau contact » désactivé tant que le bloc n\'est pas nommé (prénom OU nom, aligné M1/M2/M3)', () => {
it('ERP-193 : « + Nouveau contact » désactivé tant que le bloc est VIDE (plus de règle prénom/nom)', () => {
const form = createdForm()
expect(form.canAddContact.value).toBe(false)
// addContact est un no-op tant que le bloc n'est pas nommé.
// addContact est un no-op tant que le bloc est totalement vide.
form.addContact()
expect(form.contacts.value).toHaveLength(1)
// Fonction seule ne suffit PAS à ajouter un nouveau bloc (≠ RG-4.08 large).
// ERP-193 : un seul champ rempli (ici la fonction, sans prénom ni nom) suffit
// désormais à débloquer l'ajout — la règle « prénom OU nom » est retirée.
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()
expect(form.contacts.value).toHaveLength(2)
@@ -686,21 +675,15 @@ describe('useCarrierForm — onglet Contacts (ERP-168)', () => {
expect(mockPatch).toHaveBeenCalledWith('/carrier_contacts/55', expect.objectContaining({ lastName: 'Doe' }), { toast: false })
})
it('RG-4.08 : onglet vide → soumet l\'amorce pour déclencher la 422 inline', async () => {
mockPost.mockRejectedValueOnce({
response: {
status: 422,
_data: { violations: [{ propertyPath: 'firstName', message: 'Au moins un champ du contact est obligatoire.' }] },
},
})
it('ERP-193 : onglet Contact vide → aucun POST, onglet finalisé (bloc optionnel)', async () => {
const form = createdForm()
// Bloc vide → rien n'est soumis, l'onglet se finalise et déverrouille Prix.
const ok = await form.submitContacts(vi.fn())
expect(ok).toBe(false)
expect(mockPost).toHaveBeenCalledTimes(1)
expect(form.contactErrors.value[0]?.firstName).toBe('Au moins un champ du contact est obligatoire.')
expect(form.isValidated('contacts')).toBe(false)
expect(ok).toBe(true)
expect(mockPost).not.toHaveBeenCalled()
expect(form.isValidated('contacts')).toBe(true)
})
it('removeContact : DELETE /carrier_contacts/{id} puis retrait du bloc', async () => {