feat(front) : onglet contact prestataire (ERP-142) (#104)
Auto Tag Develop / tag (push) Successful in 8s
Auto Tag Develop / tag (push) Successful in 8s
Empilée sur ERP-141 (#103). ## Périmètre ERP-142 Onglet **Contact** de l'écran `/providers/new` — saisie multi-contacts (blocs ajoutables) via la sous-ressource contacts. - **`ProviderContactBlock.vue`** (miroir `SupplierContactBlock`) : Nom / Prénom / Fonction / Email / Téléphone (x1, +1 révélable, **max 2**), erreurs 422 par champ (prop `:errors`). - **`useProviderForm`** étendu : état `contacts`, `canAddContact` (RG-3.04), `addContact`/`removeContact`, `submitContacts` (POST `/providers/{id}/contacts` pour les nouveaux, PATCH `/provider_contacts/{id}` pour les existants, groupe `provider:write:contacts`), `submitRows` (erreurs collectées **par ligne**, non bloquant). - **RG-3.04** : « + Nouveau contact » désactivé tant que le bloc courant est vide (≥1 champ parmi prénom/nom/fonction/tél/email — aligné back). - **RG-3.12** : onglet non validable vide ; une amorce vide est soumise pour déclencher la 422 `firstName` inline. - Suppression d'un bloc → modal de confirmation. - Helpers purs `utils/forms/providerContact.ts` (`isProviderContactBlank`, `buildProviderContactPayload`). - i18n `technique.providers.form.contact/confirmDelete` + `toast.updateSuccess`. ## Vérifications - Vitest : 418/418 (16 nouveaux : helpers, bloc, workflow contacts). - ESLint : OK. - `nuxi typecheck` : 0 erreur sur les fichiers source du ticket. - Golden path navigateur : bloc Contact rendu, « Nouveau contact » désactivé tant que vide puis activé après saisie, révélation du 2e téléphone (max 2). Reviewed-on: #104 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #104.
This commit is contained in:
@@ -41,10 +41,17 @@ vi.stubGlobal('usePermissions', () => ({
|
||||
}))
|
||||
|
||||
const { useProviderForm, buildProviderCreateTabKeys } = await import('../useProviderForm')
|
||||
const { emptyProviderContact } = await import('~/modules/technique/types/providerForm')
|
||||
type ProviderForm = ReturnType<typeof useProviderForm>
|
||||
|
||||
const SITE_86 = '/api/sites/1'
|
||||
const CAT_MAINT = '/api/categories/7'
|
||||
|
||||
/** Accede a un bloc contact (cast : sous noUncheckedIndexedAccess l'index est optionnel). */
|
||||
function contactAt(form: ProviderForm, index = 0) {
|
||||
return form.contacts.value[index] ?? emptyProviderContact()
|
||||
}
|
||||
|
||||
describe('useProviderForm', () => {
|
||||
beforeEach(() => {
|
||||
mockPost.mockReset()
|
||||
@@ -190,3 +197,110 @@ describe('useProviderForm', () => {
|
||||
expect(mockPatch).toHaveBeenCalledWith('/providers/9', { siren: '123456789' }, { toast: false })
|
||||
})
|
||||
})
|
||||
|
||||
describe('useProviderForm — onglet Contact (ERP-142)', () => {
|
||||
beforeEach(() => {
|
||||
mockPost.mockReset()
|
||||
mockPatch.mockReset()
|
||||
permState.accountingView = false
|
||||
})
|
||||
|
||||
/** Place le formulaire en etat « prestataire cree » (onglet Contact accessible). */
|
||||
function createdForm() {
|
||||
const form = useProviderForm()
|
||||
form.providerId.value = 7
|
||||
return form
|
||||
}
|
||||
|
||||
it('RG-3.04 : « + Nouveau contact » desactive tant que le dernier bloc est vide', () => {
|
||||
const form = createdForm()
|
||||
expect(form.canAddContact.value).toBe(false)
|
||||
|
||||
// addContact est un no-op tant que le bloc est vide.
|
||||
form.addContact()
|
||||
expect(form.contacts.value).toHaveLength(1)
|
||||
|
||||
contactAt(form).lastName = 'Doe'
|
||||
expect(form.canAddContact.value).toBe(true)
|
||||
form.addContact()
|
||||
expect(form.contacts.value).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('removeContact retire le bloc et son erreur de ligne', () => {
|
||||
const form = createdForm()
|
||||
contactAt(form).lastName = 'Doe'
|
||||
form.addContact()
|
||||
form.contactErrors.value = [{}, { lastName: 'x' }]
|
||||
|
||||
form.removeContact(1)
|
||||
expect(form.contacts.value).toHaveLength(1)
|
||||
expect(form.contactErrors.value).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('submitContacts : POST des nouveaux, capture id + IRI, finalise l\'onglet', async () => {
|
||||
mockPost.mockResolvedValueOnce({ '@id': '/api/provider_contacts/55', id: 55 })
|
||||
const form = createdForm()
|
||||
contactAt(form).lastName = 'Doe'
|
||||
|
||||
const ok = await form.submitContacts(vi.fn())
|
||||
|
||||
expect(ok).toBe(true)
|
||||
const [url, body, opts] = mockPost.mock.calls[0] ?? []
|
||||
expect(url).toBe('/providers/7/contacts')
|
||||
expect(body).toMatchObject({ lastName: 'Doe' })
|
||||
expect(opts).toMatchObject({ toast: false, headers: { Accept: 'application/ld+json' } })
|
||||
expect(contactAt(form).id).toBe(55)
|
||||
expect(contactAt(form).iri).toBe('/api/provider_contacts/55')
|
||||
expect(form.isValidated('contact')).toBe(true)
|
||||
})
|
||||
|
||||
it('submitContacts : PATCH des contacts existants sur /provider_contacts/{id}', async () => {
|
||||
mockPatch.mockResolvedValueOnce({})
|
||||
const form = createdForm()
|
||||
contactAt(form).id = 55
|
||||
contactAt(form).lastName = 'Doe'
|
||||
|
||||
await form.submitContacts(vi.fn())
|
||||
|
||||
expect(mockPost).not.toHaveBeenCalled()
|
||||
expect(mockPatch).toHaveBeenCalledWith('/provider_contacts/55', expect.objectContaining({ lastName: 'Doe' }), { toast: false })
|
||||
})
|
||||
|
||||
it('RG-3.12 : onglet vide -> soumet l\'amorce pour declencher la 422 firstName inline', async () => {
|
||||
mockPost.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: 422,
|
||||
_data: { violations: [{ propertyPath: 'firstName', message: 'Au moins un champ du contact est obligatoire.' }] },
|
||||
},
|
||||
})
|
||||
const form = createdForm()
|
||||
|
||||
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('contact')).toBe(false)
|
||||
})
|
||||
|
||||
it('mappe les erreurs 422 PAR LIGNE (le bloc 2 echoue, le bloc 1 passe)', async () => {
|
||||
mockPost
|
||||
.mockResolvedValueOnce({ '@id': '/api/provider_contacts/1', id: 1 })
|
||||
.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: 422,
|
||||
_data: { violations: [{ propertyPath: 'email', message: 'L\'adresse email n\'est pas valide.' }] },
|
||||
},
|
||||
})
|
||||
const form = createdForm()
|
||||
contactAt(form).lastName = 'Doe'
|
||||
form.addContact()
|
||||
contactAt(form, 1).email = 'invalide'
|
||||
|
||||
const ok = await form.submitContacts(vi.fn())
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(form.contactErrors.value[0]).toBeUndefined()
|
||||
expect(form.contactErrors.value[1]?.email).toBe('L\'adresse email n\'est pas valide.')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user