From 41d391eebf4405332bf54c23dd1c1694f9204178 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 4 Jun 2026 11:42:08 +0200 Subject: [PATCH] feat(commercial) : valide tous les blocs contacts/adresses/RIB et affiche les erreurs par bloc (ERP-110) --- .../commercial/pages/clients/[id]/edit.vue | 78 +++++------ .../modules/commercial/pages/clients/new.vue | 128 ++++++++---------- 2 files changed, 94 insertions(+), 112 deletions(-) diff --git a/frontend/modules/commercial/pages/clients/[id]/edit.vue b/frontend/modules/commercial/pages/clients/[id]/edit.vue index 0dce2ca..9c29de4 100644 --- a/frontend/modules/commercial/pages/clients/[id]/edit.vue +++ b/frontend/modules/commercial/pages/clients/[id]/edit.vue @@ -628,7 +628,7 @@ const { contactErrors, addressErrors, ribErrors, - mapRowError, + submitRows, } = useClientFormErrors() // ── Bloc principal ─────────────────────────────────────────────────────────── @@ -742,11 +742,13 @@ async function submitContacts(): Promise { } removedContactIds.value = [] - for (let index = 0; index < contacts.value.length; index++) { - const contact = contacts.value[index] - if (!isContactNamed(contact)) continue - const body = buildContactPayload(contact) - try { + // On tente TOUS les blocs (collecte des erreurs par index, ERP-110) ; les + // blocs vides (ni nom ni prenom) sont ignores. + const hasError = await submitRows( + contacts.value, + contactErrors, + async (contact) => { + const body = buildContactPayload(contact) if (contact.id === null) { const created = await api.post<{ '@id'?: string, id: number }>( `/clients/${clientId}/contacts`, @@ -759,15 +761,12 @@ async function submitContacts(): Promise { else { await api.patch(`/client_contacts/${contact.id}`, body, { toast: false }) } - } - catch (error) { - // 422 → erreurs inline sous les champs de CETTE ligne ; on stoppe. - if (!mapRowError(error, contactErrors, index)) { - showError(error) - } - return - } - } + }, + error => showError(error), + contact => !isContactNamed(contact), + ) + // Tant qu'un bloc reste en erreur : pas de toast succes. + if (hasError) return toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } catch (e) { @@ -824,10 +823,12 @@ async function submitAddresses(): Promise { } removedAddressIds.value = [] - for (let index = 0; index < addresses.value.length; index++) { - const address = addresses.value[index] - const body = buildAddressPayload(address, isBillingEmailRequired(address)) - try { + // On tente TOUS les blocs d'adresse (collecte des erreurs par index, ERP-110). + const hasError = await submitRows( + addresses.value, + addressErrors, + async (address) => { + const body = buildAddressPayload(address, isBillingEmailRequired(address)) if (address.id === null) { const created = await api.post<{ id: number }>( `/clients/${clientId}/addresses`, @@ -839,14 +840,10 @@ async function submitAddresses(): Promise { else { await api.patch(`/client_addresses/${address.id}`, body, { toast: false }) } - } - catch (error) { - if (!mapRowError(error, addressErrors, index)) { - showError(error) - } - return - } - } + }, + error => showError(error), + ) + if (hasError) return toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } catch (e) { @@ -905,7 +902,6 @@ async function submitAccounting(): Promise { if (accountingReadonly.value || !canValidateAccounting.value || tabSubmitting.value) return tabSubmitting.value = true accountingErrors.clearErrors() - ribErrors.value = [] try { // 1) PATCH des scalaires comptables (erreurs inline sur leurs champs). try { @@ -921,12 +917,13 @@ async function submitAccounting(): Promise { } removedRibIds.value = [] - // 2) POST/PATCH des RIB (erreurs inline par ligne). - for (let index = 0; index < ribs.value.length; index++) { - const rib = ribs.value[index] - if (!ribIsComplete(rib)) continue - const body = buildRibPayload(rib) - try { + // 2) POST/PATCH des RIB (erreurs inline par ligne, tous les blocs tentes — + // les blocs RIB incomplets sont ignores). + const ribHasError = await submitRows( + ribs.value, + ribErrors, + async (rib) => { + const body = buildRibPayload(rib) if (rib.id === null) { const created = await api.post<{ id: number }>( `/clients/${clientId}/ribs`, @@ -938,14 +935,11 @@ async function submitAccounting(): Promise { else { await api.patch(`/client_ribs/${rib.id}`, body, { toast: false }) } - } - catch (error) { - if (!mapRowError(error, ribErrors, index)) { - showError(error) - } - return - } - } + }, + error => showError(error), + rib => !ribIsComplete(rib), + ) + if (ribHasError) return toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } catch (e) { diff --git a/frontend/modules/commercial/pages/clients/new.vue b/frontend/modules/commercial/pages/clients/new.vue index e4680e0..366fb9e 100644 --- a/frontend/modules/commercial/pages/clients/new.vue +++ b/frontend/modules/commercial/pages/clients/new.vue @@ -441,7 +441,7 @@ const { contactErrors, addressErrors, ribErrors, - mapRowError, + submitRows, } = useClientFormErrors() useHead({ title: t('commercial.clients.form.title') }) @@ -676,23 +676,21 @@ function askRemoveContact(index: number): void { async function submitContacts(): Promise { if (clientId.value === null || !canValidateContacts.value || tabSubmitting.value) return tabSubmitting.value = true - contactErrors.value = [] try { - for (let index = 0; index < contacts.value.length; index++) { - const contact = contacts.value[index] - // On ignore les blocs totalement vides (ni nom ni prenom). - if (!isContactNamed(contact)) continue - - const body = { - firstName: contact.firstName || null, - lastName: contact.lastName || null, - jobTitle: contact.jobTitle || null, - phonePrimary: contact.phonePrimary || null, - phoneSecondary: contact.hasSecondaryPhone ? (contact.phoneSecondary || null) : null, - email: contact.email || null, - } - - try { + // On tente TOUS les blocs (collecte des erreurs par index, ERP-110) ; les + // blocs vides (ni nom ni prenom) sont ignores. + const hasError = await submitRows( + contacts.value, + contactErrors, + async (contact) => { + const body = { + firstName: contact.firstName || null, + lastName: contact.lastName || null, + jobTitle: contact.jobTitle || null, + phonePrimary: contact.phonePrimary || null, + phoneSecondary: contact.hasSecondaryPhone ? (contact.phoneSecondary || null) : null, + email: contact.email || null, + } if (contact.id === null) { const created = await api.post( `/clients/${clientId.value}/contacts`, @@ -705,16 +703,12 @@ async function submitContacts(): Promise { else { await api.patch(`/client_contacts/${contact.id}`, body, { toast: false }) } - } - catch (error) { - // 422 → erreurs inline sous les champs de CETTE ligne ; on stoppe - // a la premiere ligne en echec (les suivantes ne sont pas tentees). - if (!mapRowError(error, contactErrors, index)) { - toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }) - } - return - } - } + }, + error => toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }), + contact => !isContactNamed(contact), + ) + // Tant qu'un bloc reste en erreur : pas de validation d'onglet ni de toast succes. + if (hasError) return completeTab('contact') toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } @@ -784,26 +778,26 @@ function onAddressDegraded(): void { async function submitAddresses(): Promise { if (clientId.value === null || !canValidateAddresses.value || tabSubmitting.value) return tabSubmitting.value = true - addressErrors.value = [] try { - for (let index = 0; index < addresses.value.length; index++) { - const address = addresses.value[index] - const body = { - isProspect: address.isProspect, - isDelivery: address.isDelivery, - isBilling: address.isBilling, - country: address.country, - postalCode: address.postalCode || null, - city: address.city || null, - street: address.street || null, - streetComplement: address.streetComplement || null, - categories: address.categoryIris, - sites: address.siteIris, - contacts: address.contactIris, - billingEmail: isBillingEmailRequired(address) ? (address.billingEmail || null) : null, - } - - try { + // On tente TOUS les blocs d'adresse (collecte des erreurs par index, ERP-110). + const hasError = await submitRows( + addresses.value, + addressErrors, + async (address) => { + const body = { + isProspect: address.isProspect, + isDelivery: address.isDelivery, + isBilling: address.isBilling, + country: address.country, + postalCode: address.postalCode || null, + city: address.city || null, + street: address.street || null, + streetComplement: address.streetComplement || null, + categories: address.categoryIris, + sites: address.siteIris, + contacts: address.contactIris, + billingEmail: isBillingEmailRequired(address) ? (address.billingEmail || null) : null, + } if (address.id === null) { const created = await api.post<{ id: number }>( `/clients/${clientId.value}/addresses`, @@ -815,14 +809,10 @@ async function submitAddresses(): Promise { else { await api.patch(`/client_addresses/${address.id}`, body, { toast: false }) } - } - catch (error) { - if (!mapRowError(error, addressErrors, index)) { - toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }) - } - return - } - } + }, + error => toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }), + ) + if (hasError) return completeTab('address') toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } @@ -893,7 +883,6 @@ async function submitAccounting(): Promise { if (clientId.value === null || !canValidateAccounting.value || tabSubmitting.value) return tabSubmitting.value = true accountingErrors.clearErrors() - ribErrors.value = [] try { // 1) PATCH des scalaires comptables (erreurs inline sur leurs champs). try { @@ -912,30 +901,29 @@ async function submitAccounting(): Promise { return } - // 2) POST/PATCH des RIB (erreurs inline par ligne). - for (let index = 0; index < ribs.value.length; index++) { - const rib = ribs.value[index] - if (!ribIsComplete(rib)) continue - try { + // 2) POST/PATCH des RIB (erreurs inline par ligne, tous les blocs tentes — + // les blocs RIB incomplets sont ignores). + const ribHasError = await submitRows( + ribs.value, + ribErrors, + async (rib) => { + const body = { label: rib.label, bic: rib.bic, iban: rib.iban } if (rib.id === null) { const created = await api.post<{ id: number }>( `/clients/${clientId.value}/ribs`, - { label: rib.label, bic: rib.bic, iban: rib.iban }, + body, { headers: { Accept: 'application/ld+json' }, toast: false }, ) rib.id = created.id } else { - await api.patch(`/client_ribs/${rib.id}`, { label: rib.label, bic: rib.bic, iban: rib.iban }, { toast: false }) + await api.patch(`/client_ribs/${rib.id}`, body, { toast: false }) } - } - catch (error) { - if (!mapRowError(error, ribErrors, index)) { - toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }) - } - return - } - } + }, + error => toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }), + rib => !ribIsComplete(rib), + ) + if (ribHasError) return completeTab('accounting') toast.success({ title: t('commercial.clients.toast.updateSuccess') })