fix(front) : toast de succes a la suppression d'un bloc (contact / adresse / RIB / prix) sur les 4 modules (ERP-193)

removeCollectionRow expose un callback onSuccess declenche uniquement apres une
suppression serveur confirmee (pas sur le retrait d'un brouillon local). Cable
sur Client / Fournisseur / Prestataire / Transporteur via notifyRemovalSuccess,
avec un message i18n generique success.deleted.
This commit is contained in:
2026-06-19 15:24:38 +02:00
parent 0786e4f461
commit 1e86d9745c
7 changed files with 54 additions and 4 deletions
+2 -1
View File
@@ -785,7 +785,8 @@
"auth": { "auth": {
"logout": "Deconnexion reussie" "logout": "Deconnexion reussie"
}, },
"title": "Succès" "title": "Succès",
"deleted": "Suppression effectuée"
}, },
"admin": { "admin": {
"roles": { "roles": {
@@ -701,6 +701,11 @@ function showError(e: unknown, opts: { duplicateCompany?: boolean } = {}): void
}) })
} }
/** Toast de succès après suppression serveur confirmée d'un bloc (contact / adresse / RIB). */
function notifyRemovalSuccess(): void {
toast.success({ title: t('success.title'), message: t('success.deleted') })
}
// ── Erreurs de validation par champ (ERP-101) ─────────────────────────────── // ── Erreurs de validation par champ (ERP-101) ───────────────────────────────
// Etat d'erreurs factorise avec l'ecran de creation (cf. useClientFormErrors) : // Etat d'erreurs factorise avec l'ecran de creation (cf. useClientFormErrors) :
// un `useFormErrors` par groupe scalaire + un tableau d'erreurs par ligne pour // un `useFormErrors` par groupe scalaire + un tableau d'erreurs par ligne pour
@@ -800,6 +805,7 @@ function askRemoveContact(index: number): void {
deleteRow: url => api.delete(url, {}, { toast: false }), deleteRow: url => api.delete(url, {}, { toast: false }),
makeEmpty: emptyContact, makeEmpty: emptyContact,
onError: showError, onError: showError,
onSuccess: notifyRemovalSuccess,
})) }))
} }
@@ -877,6 +883,7 @@ function askRemoveAddress(index: number): void {
deleteRow: url => api.delete(url, {}, { toast: false }), deleteRow: url => api.delete(url, {}, { toast: false }),
makeEmpty: emptyAddress, makeEmpty: emptyAddress,
onError: showError, onError: showError,
onSuccess: notifyRemovalSuccess,
})) }))
} }
@@ -977,6 +984,7 @@ function askRemoveRib(index: number): void {
deleteRow: url => api.delete(url, {}, { toast: false }), deleteRow: url => api.delete(url, {}, { toast: false }),
makeEmpty: emptyRib, makeEmpty: emptyRib,
onError: showError, onError: showError,
onSuccess: notifyRemovalSuccess,
})) }))
} }
@@ -616,6 +616,11 @@ function showError(e: unknown): void {
toast.error({ title: t('commercial.suppliers.toast.error'), message: apiErrorMessage(e) }) toast.error({ title: t('commercial.suppliers.toast.error'), message: apiErrorMessage(e) })
} }
/** Toast de succès après suppression serveur confirmée d'un bloc (contact / adresse / RIB). */
function notifyRemovalSuccess(): void {
toast.success({ title: t('success.title'), message: t('success.deleted') })
}
// ── Erreurs de validation par champ (ERP-101) ─────────────────────────────── // ── Erreurs de validation par champ (ERP-101) ───────────────────────────────
const { const {
mainErrors, mainErrors,
@@ -699,6 +704,7 @@ function askRemoveContact(index: number): void {
deleteRow: url => api.delete(url, {}, { toast: false }), deleteRow: url => api.delete(url, {}, { toast: false }),
makeEmpty: emptyContact, makeEmpty: emptyContact,
onError: showError, onError: showError,
onSuccess: notifyRemovalSuccess,
})) }))
} }
@@ -767,6 +773,7 @@ function askRemoveAddress(index: number): void {
deleteRow: url => api.delete(url, {}, { toast: false }), deleteRow: url => api.delete(url, {}, { toast: false }),
makeEmpty: emptyAddress, makeEmpty: emptyAddress,
onError: showError, onError: showError,
onSuccess: notifyRemovalSuccess,
})) }))
} }
@@ -866,6 +873,7 @@ function askRemoveRib(index: number): void {
deleteRow: url => api.delete(url, {}, { toast: false }), deleteRow: url => api.delete(url, {}, { toast: false }),
makeEmpty: emptyRib, makeEmpty: emptyRib,
onError: showError, onError: showError,
onSuccess: notifyRemovalSuccess,
})) }))
} }
@@ -84,6 +84,11 @@ export function useProviderForm() {
}) })
} }
/** Toast de succès après suppression serveur confirmée d'une sous-ressource. */
function notifyRemovalSuccess(): void {
toast.success({ title: t('success.title'), message: t('success.deleted') })
}
// ── Etat du prestataire cree ──────────────────────────────────────────── // ── Etat du prestataire cree ────────────────────────────────────────────
const providerId = ref<number | null>(null) const providerId = ref<number | null>(null)
const mainLocked = ref(false) const mainLocked = ref(false)
@@ -339,6 +344,7 @@ export function useProviderForm() {
deleteRow: url => api.delete(url, {}, { toast: false }), deleteRow: url => api.delete(url, {}, { toast: false }),
makeEmpty: emptyProviderContact, makeEmpty: emptyProviderContact,
onError: notifyRemovalError, onError: notifyRemovalError,
onSuccess: notifyRemovalSuccess,
}) })
} }
@@ -417,6 +423,7 @@ export function useProviderForm() {
deleteRow: url => api.delete(url, {}, { toast: false }), deleteRow: url => api.delete(url, {}, { toast: false }),
makeEmpty: emptyProviderAddress, makeEmpty: emptyProviderAddress,
onError: notifyRemovalError, onError: notifyRemovalError,
onSuccess: notifyRemovalSuccess,
}) })
} }
@@ -518,6 +525,7 @@ export function useProviderForm() {
deleteRow: url => api.delete(url, {}, { toast: false }), deleteRow: url => api.delete(url, {}, { toast: false }),
makeEmpty: emptyProviderRib, makeEmpty: emptyProviderRib,
onError: notifyRemovalError, onError: notifyRemovalError,
onSuccess: notifyRemovalSuccess,
}) })
} }
@@ -416,6 +416,11 @@ export function useCarrierForm() {
}) })
} }
/** Toast de succès après suppression serveur confirmée d'une sous-ressource. */
function notifyRemovalSuccess(): void {
toast.success({ title: t('success.title'), message: t('success.deleted') })
}
/** /**
* Soumet TOUS les blocs d'une collection en collectant les erreurs PAR INDEX : * Soumet TOUS les blocs d'une collection en collectant les erreurs PAR INDEX :
* on n'arrête pas au premier bloc en échec (décision ERP-101). Réinitialise la * on n'arrête pas au premier bloc en échec (décision ERP-101). Réinitialise la
@@ -540,6 +545,7 @@ export function useCarrierForm() {
deleteRow: url => api.delete(url, {}, { toast: false }), deleteRow: url => api.delete(url, {}, { toast: false }),
makeEmpty: emptyCarrierContact, makeEmpty: emptyCarrierContact,
onError: notifyRemovalError, onError: notifyRemovalError,
onSuccess: notifyRemovalSuccess,
}) })
} }
@@ -653,6 +659,7 @@ export function useCarrierForm() {
deleteRow: url => api.delete(url, {}, { toast: false }), deleteRow: url => api.delete(url, {}, { toast: false }),
makeEmpty: emptyCarrierPrice, makeEmpty: emptyCarrierPrice,
onError: notifyRemovalError, onError: notifyRemovalError,
onSuccess: notifyRemovalSuccess,
}) })
} }
@@ -22,11 +22,12 @@ describe('removeCollectionRow', () => {
const errors: Record<string, string>[] = [{}, {}] const errors: Record<string, string>[] = [{}, {}]
const deleteRow = vi.fn().mockResolvedValue(undefined) const deleteRow = vi.fn().mockResolvedValue(undefined)
const onError = vi.fn() const onError = vi.fn()
const onSuccess = vi.fn()
const removed = await removeCollectionRow({ const removed = await removeCollectionRow({
rows, errors, index: 0, rows, errors, index: 0,
endpoint: '/client_contacts', endpoint: '/client_contacts',
deleteRow, makeEmpty, onError, deleteRow, makeEmpty, onError, onSuccess,
}) })
expect(deleteRow).toHaveBeenCalledOnce() expect(deleteRow).toHaveBeenCalledOnce()
@@ -35,6 +36,8 @@ describe('removeCollectionRow', () => {
expect(rows).toEqual([{ id: 11, label: 'B' }]) expect(rows).toEqual([{ id: 11, label: 'B' }])
expect(errors).toHaveLength(1) expect(errors).toHaveLength(1)
expect(onError).not.toHaveBeenCalled() expect(onError).not.toHaveBeenCalled()
// Toast de succes uniquement sur suppression serveur confirmee.
expect(onSuccess).toHaveBeenCalledOnce()
}) })
it('ne fait AUCUN appel reseau pour un bloc jamais persiste (id null) — retrait local', async () => { it('ne fait AUCUN appel reseau pour un bloc jamais persiste (id null) — retrait local', async () => {
@@ -42,16 +45,19 @@ describe('removeCollectionRow', () => {
const errors: Record<string, string>[] = [{}, {}] const errors: Record<string, string>[] = [{}, {}]
const deleteRow = vi.fn().mockResolvedValue(undefined) const deleteRow = vi.fn().mockResolvedValue(undefined)
const onError = vi.fn() const onError = vi.fn()
const onSuccess = vi.fn()
const removed = await removeCollectionRow({ const removed = await removeCollectionRow({
rows, errors, index: 1, rows, errors, index: 1,
endpoint: '/client_contacts', endpoint: '/client_contacts',
deleteRow, makeEmpty, onError, deleteRow, makeEmpty, onError, onSuccess,
}) })
expect(deleteRow).not.toHaveBeenCalled() expect(deleteRow).not.toHaveBeenCalled()
expect(removed).toBe(true) expect(removed).toBe(true)
expect(rows).toEqual([{ id: 10, label: 'A' }]) expect(rows).toEqual([{ id: 10, label: 'A' }])
// Retrait d'un simple brouillon local : pas de toast « supprime ».
expect(onSuccess).not.toHaveBeenCalled()
}) })
it('conserve le bloc et remonte l\'erreur si le DELETE serveur echoue (ex. 409 dernier RIB LCR)', async () => { it('conserve le bloc et remonte l\'erreur si le DELETE serveur echoue (ex. 409 dernier RIB LCR)', async () => {
+13 -1
View File
@@ -33,6 +33,12 @@ export interface RemoveCollectionRowOptions<T extends DeletableRow> {
makeEmpty: () => T makeEmpty: () => T
/** Remontee d'erreur 409/422 mappee proprement (message back, pas de toast fourre-tout). */ /** Remontee d'erreur 409/422 mappee proprement (message back, pas de toast fourre-tout). */
onError: (error: unknown) => void onError: (error: unknown) => void
/**
* Callback de succes (toast) appele UNIQUEMENT apres une suppression serveur
* confirmee d'un bloc persiste (`id` non null). Pas appele sur le simple retrait
* d'un brouillon local non enregistre (aucune suppression reelle).
*/
onSuccess?: () => void
} }
/** /**
@@ -55,8 +61,9 @@ export interface RemoveCollectionRowOptions<T extends DeletableRow> {
export async function removeCollectionRow<T extends DeletableRow>( export async function removeCollectionRow<T extends DeletableRow>(
options: RemoveCollectionRowOptions<T>, options: RemoveCollectionRowOptions<T>,
): Promise<boolean> { ): Promise<boolean> {
const { rows, errors, index, endpoint, deleteRow, makeEmpty, onError } = options const { rows, errors, index, endpoint, deleteRow, makeEmpty, onError, onSuccess } = options
const removed = rows[index] const removed = rows[index]
let serverDeleted = false
// Bloc existant : suppression serveur d'abord, retrait local seulement si OK. // Bloc existant : suppression serveur d'abord, retrait local seulement si OK.
if (removed?.id != null) { if (removed?.id != null) {
@@ -67,6 +74,7 @@ export async function removeCollectionRow<T extends DeletableRow>(
onError(error) onError(error)
return false return false
} }
serverDeleted = true
} }
rows.splice(index, 1) rows.splice(index, 1)
@@ -75,5 +83,9 @@ export async function removeCollectionRow<T extends DeletableRow>(
if (rows.length === 0) { if (rows.length === 0) {
rows.push(makeEmpty()) rows.push(makeEmpty())
} }
// Toast de succes uniquement quand le serveur a confirme une vraie suppression.
if (serverDeleted) {
onSuccess?.()
}
return true return true
} }