diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 86651f0..5744828 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -228,7 +228,10 @@ }, "sites": { "notAuthorized": "Vous n'êtes pas autorisé à sélectionner ce site." - } + }, + "title": "Erreur", + "generic": "Une erreur est survenue.", + "unknown": "Erreur inconnue." }, "sites": { "selector": { @@ -285,7 +288,8 @@ "success": { "auth": { "logout": "Deconnexion reussie" - } + }, + "title": "Succès" }, "admin": { "roles": { diff --git a/frontend/modules/catalog/composables/__tests__/useCategoryForm.spec.ts b/frontend/modules/catalog/composables/__tests__/useCategoryForm.spec.ts index bdd5a4a..5a14387 100644 --- a/frontend/modules/catalog/composables/__tests__/useCategoryForm.spec.ts +++ b/frontend/modules/catalog/composables/__tests__/useCategoryForm.spec.ts @@ -220,7 +220,7 @@ describe('useCategoryForm', () => { await form.submitCreate() expect(mockToastSuccess).toHaveBeenCalledWith({ - title: 'Succès', + title: 'success.title', message: 'admin.categories.toast.created', }) }) @@ -302,7 +302,7 @@ describe('useCategoryForm', () => { // Pas d'erreur inline par champ : l'erreur transverse part en toast. expect(form.errors).toEqual({}) expect(mockToastError).toHaveBeenCalledWith({ - title: 'Erreur', + title: 'errors.title', message: 'Boom server', }) }) @@ -378,7 +378,7 @@ describe('useCategoryForm', () => { await form.submitUpdate(42) expect(mockToastSuccess).toHaveBeenCalledWith({ - title: 'Succès', + title: 'success.title', message: 'admin.categories.toast.updated', }) }) @@ -409,7 +409,7 @@ describe('useCategoryForm', () => { expect(mockDelete).toHaveBeenCalledWith('/categories/42', {}, { toast: false }) expect(ok).toBe(true) expect(mockToastSuccess).toHaveBeenCalledWith({ - title: 'Succès', + title: 'success.title', message: 'admin.categories.toast.deleted', }) }) diff --git a/frontend/modules/catalog/composables/useCategoryForm.ts b/frontend/modules/catalog/composables/useCategoryForm.ts index fda7660..689b9f3 100644 --- a/frontend/modules/catalog/composables/useCategoryForm.ts +++ b/frontend/modules/catalog/composables/useCategoryForm.ts @@ -129,11 +129,11 @@ export function useCategoryForm() { name: attemptedName, }) formErrors.setError('name', duplicateMessage) - toast.error({ title: 'Erreur', message: duplicateMessage }) + toast.error({ title: t('errors.title'), message: duplicateMessage }) return true } - return formErrors.handleApiError(e, { fallbackMessage: 'Une erreur est survenue.' }) + return formErrors.handleApiError(e, { fallbackMessage: t('errors.generic') }) } /** @@ -150,7 +150,7 @@ export function useCategoryForm() { toast: false, }) toast.success({ - title: 'Succès', + title: t('success.title'), message: t('admin.categories.toast.created'), }) return created @@ -189,7 +189,7 @@ export function useCategoryForm() { toast: false, }) toast.success({ - title: 'Succès', + title: t('success.title'), message: t('admin.categories.toast.updated'), }) return updated @@ -215,7 +215,7 @@ export function useCategoryForm() { try { await api.delete(`/categories/${id}`, {}, { toast: false }) toast.success({ - title: 'Succès', + title: t('success.title'), message: t('admin.categories.toast.deleted'), }) return true diff --git a/frontend/modules/commercial/composables/__tests__/useClientFormErrors.spec.ts b/frontend/modules/commercial/composables/__tests__/useClientFormErrors.spec.ts new file mode 100644 index 0000000..c3f8470 --- /dev/null +++ b/frontend/modules/commercial/composables/__tests__/useClientFormErrors.spec.ts @@ -0,0 +1,54 @@ +import { describe, it, expect, vi } from 'vitest' +import { useFormErrors } from '~/shared/composables/useFormErrors' +import { useClientFormErrors } from '../useClientFormErrors' + +// useFormErrors (auto-import) expose l'implementation reelle ; elle consomme +// useToast + useI18n, stubbes ici. +vi.stubGlobal('useToast', () => ({ error: vi.fn(), success: vi.fn() })) +vi.stubGlobal('useI18n', () => ({ t: (key: string) => key })) +vi.stubGlobal('useFormErrors', useFormErrors) + +/** + * Tests du composable partage `useClientFormErrors` — factorisation du cablage + * d'erreurs des ecrans client (creation/edition), suggestion de revue ERP-101. + * `mapRowError` ne toaste plus : il retourne un booleen et chaque page garde son + * propre fallback (toast.error en creation, showError en edition). + */ +describe('useClientFormErrors', () => { + it('expose les 3 etats scalaires (vides) et les 3 tableaux d\'erreurs par ligne', () => { + const f = useClientFormErrors() + expect(f.mainErrors.errors).toEqual({}) + expect(f.informationErrors.errors).toEqual({}) + expect(f.accountingErrors.errors).toEqual({}) + expect(f.contactErrors.value).toEqual([]) + expect(f.addressErrors.value).toEqual([]) + expect(f.ribErrors.value).toEqual([]) + }) + + it('mapRowError mappe une 422 sur target[index] et retourne true', () => { + const f = useClientFormErrors() + const error = { + response: { + status: 422, + _data: { violations: [{ propertyPath: 'email', message: 'Adresse invalide.' }] }, + }, + } + const mapped = f.mapRowError(error, f.contactErrors, 0) + expect(mapped).toBe(true) + expect(f.contactErrors.value[0]).toEqual({ email: 'Adresse invalide.' }) + }) + + it('mapRowError retourne false et ne touche pas la cible pour une erreur non-422', () => { + const f = useClientFormErrors() + const error = { response: { status: 500, _data: {} } } + expect(f.mapRowError(error, f.ribErrors, 0)).toBe(false) + expect(f.ribErrors.value[0]).toBeUndefined() + }) + + it('mapRowError retourne false pour une 422 sans violation exploitable', () => { + const f = useClientFormErrors() + const error = { response: { status: 422, _data: { 'hydra:description': 'Donnees invalides.' } } } + expect(f.mapRowError(error, f.addressErrors, 0)).toBe(false) + expect(f.addressErrors.value[0]).toBeUndefined() + }) +}) diff --git a/frontend/modules/commercial/composables/useClientFormErrors.ts b/frontend/modules/commercial/composables/useClientFormErrors.ts new file mode 100644 index 0000000..dcb10d6 --- /dev/null +++ b/frontend/modules/commercial/composables/useClientFormErrors.ts @@ -0,0 +1,55 @@ +/** + * Composable d'erreurs partage des ecrans client (creation + edition, M1 + * Commercial). Factorise le cablage identique entre `clients/new.vue` et + * `clients/[id]/edit.vue` (suggestion de revue ERP-101) : + * - un `useFormErrors` par groupe scalaire (Principal / Information / + * Comptabilite) : violations 422 affichees inline sous chaque champ ; + * - un tableau d'erreurs PAR LIGNE pour chaque collection (contacts / + * adresses / RIB), aligne sur l'index du `v-for`. + * + * `mapRowError` ne toaste PAS lui-meme : il retourne un booleen (true = mappe + * inline). Chaque page conserve ainsi son propre fallback dans le `catch` + * (toast generique en creation, `showError` en edition) sans imposer un + * comportement commun. + */ +import { ref, type Ref } from 'vue' +import { mapViolationsToRecord } from '~/shared/utils/api' + +export function useClientFormErrors() { + const mainErrors = useFormErrors() + const informationErrors = useFormErrors() + const accountingErrors = useFormErrors() + const contactErrors = ref[]>([]) + const addressErrors = ref[]>([]) + const ribErrors = ref[]>([]) + + /** + * Mappe l'erreur d'une ligne de collection sur le tableau cible (par index). + * 422 avec violations exploitables → erreurs inline sous les champs de la + * ligne + retourne true. Sinon → ne touche pas la cible et retourne false + * (le caller decide du fallback toast). + */ + function mapRowError( + error: unknown, + target: Ref[]>, + index: number, + ): boolean { + const response = (error as { response?: { status?: number, _data?: unknown } })?.response + const mapped = response?.status === 422 ? mapViolationsToRecord(response._data) : {} + if (Object.keys(mapped).length > 0) { + target.value[index] = mapped + return true + } + return false + } + + return { + mainErrors, + informationErrors, + accountingErrors, + contactErrors, + addressErrors, + ribErrors, + mapRowError, + } +} diff --git a/frontend/modules/commercial/pages/clients/[id]/edit.vue b/frontend/modules/commercial/pages/clients/[id]/edit.vue index b6b1297..0dce2ca 100644 --- a/frontend/modules/commercial/pages/clients/[id]/edit.vue +++ b/frontend/modules/commercial/pages/clients/[id]/edit.vue @@ -379,9 +379,10 @@