feat(commercial) : valide tous les blocs contacts/adresses/RIB et affiche les erreurs par bloc (ERP-110)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Has been cancelled
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Has been cancelled

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