diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 1032c45..4df66ab 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -88,6 +88,7 @@ "toast": { "createSuccess": "Client créé avec succès", "updateSuccess": "Client mis à jour avec succès", + "addComplete": "Client ajouté", "archiveSuccess": "Client archivé avec succès", "restoreSuccess": "Client restauré avec succès", "error": "Une erreur est survenue. Réessayez.", diff --git a/frontend/modules/commercial/pages/clients/new.vue b/frontend/modules/commercial/pages/clients/new.vue index a2c12e2..6c19208 100644 --- a/frontend/modules/commercial/pages/clients/new.vue +++ b/frontend/modules/commercial/pages/clients/new.vue @@ -399,6 +399,7 @@ import { isRibBlank, isRibComplete, isRibRequiredForPaymentType, + lastFillableTabKey, MAIN_REQUIRED_NON_NULLABLE_KEYS, omitEmptyRequired, RIB_REQUIRED_NON_NULLABLE_KEYS, @@ -588,6 +589,12 @@ const validated = reactive>({}) const tabKeys = computed(() => buildClientFormTabKeys(canAccountingView.value)) +// Dernier onglet REMPLISSABLE par le role (cf. lastFillableTabKey) : deja role-aware +// via tabKeys (accounting present ssi accounting.view, et a la creation « present » = +// « editable » : aucun role createur n'a la Compta en lecture seule). Sa validation +// cloture l'ajout -> redirection vers la liste. +const lastFillableTab = computed(() => lastFillableTabKey(tabKeys.value)) + // Icone (Iconify) affichee dans l'onglet, par cle. A ajuster librement. const TAB_ICONS: Record = { information: 'mdi:account-outline', @@ -615,12 +622,23 @@ function tabIndex(key: string): number { return tabKeys.value.indexOf(key) } -/** Marque l'onglet valide, deverrouille et avance automatiquement au suivant. */ -function completeTab(key: string): void { +/** + * Marque l'onglet valide. Si c'est le dernier onglet remplissable, l'ajout est + * termine : toast final + redirection vers la liste, et on retourne true pour que + * l'appelant n'affiche pas son toast « mis a jour ». Sinon, deverrouille et avance + * a l'onglet suivant, et retourne false. + */ +function completeTab(key: string): boolean { validated[key] = true + if (key === lastFillableTab.value) { + toast.success({ title: t('commercial.clients.toast.addComplete') }) + router.push('/clients') + return true + } const next = tabKeys.value[tabIndex(key) + 1] unlockedIndex.value = Math.max(unlockedIndex.value, tabIndex(key) + 1) if (next) activeTab.value = next + return false } // Passage automatique sur les onglets coquille (Transport, Stats, Rapports, Echanges). @@ -658,7 +676,7 @@ async function submitInformation(): Promise { profitAmount: information.profitAmount || null, directorName: information.directorName || null, }, { toast: false }) - completeTab('information') + if (completeTab('information')) return toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } catch (error) { @@ -737,7 +755,7 @@ async function submitContacts(): Promise { ) // Tant qu'un bloc reste en erreur : pas de validation d'onglet ni de toast succes. if (hasError) return - completeTab('contact') + if (completeTab('contact')) return toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } finally { @@ -839,7 +857,7 @@ async function submitAddresses(): Promise { error => toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }), ) if (hasError) return - completeTab('address') + if (completeTab('address')) return toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } finally { @@ -967,7 +985,7 @@ async function submitAccounting(): Promise { return } - completeTab('accounting') + if (completeTab('accounting')) return toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } finally { diff --git a/frontend/modules/commercial/utils/__tests__/clientFormRules.spec.ts b/frontend/modules/commercial/utils/__tests__/clientFormRules.spec.ts index 83fe6d4..f5e2b10 100644 --- a/frontend/modules/commercial/utils/__tests__/clientFormRules.spec.ts +++ b/frontend/modules/commercial/utils/__tests__/clientFormRules.spec.ts @@ -18,6 +18,7 @@ import { isRibBlank, isRibComplete, isRibRequiredForPaymentType, + lastFillableTabKey, omitEmptyRequired, showsRelationAndTriageFields, type AddressFlagsDraft, @@ -70,6 +71,24 @@ describe('buildClientFormTabKeys (gating onglet Comptabilite + onglets edit-only }) }) +describe('lastFillableTabKey (redirection fin d\'ajout, role-aware)', () => { + it('Adresse pour un role sans Comptabilite (Bureau / Commerciale)', () => { + expect(lastFillableTabKey(buildClientFormTabKeys(false))).toBe('address') + }) + + it('Comptabilite pour un role avec accounting.view (Admin)', () => { + expect(lastFillableTabKey(buildClientFormTabKeys(true))).toBe('accounting') + }) + + it('ignore les onglets placeholder (Transport en dernier ne compte pas)', () => { + expect(lastFillableTabKey(['information', 'contact', 'address', 'transport'])).toBe('address') + }) + + it('undefined si aucun onglet remplissable (que des placeholders)', () => { + expect(lastFillableTabKey(['transport', 'statistics'])).toBeUndefined() + }) +}) + describe('isContactNamed (RG-1.05)', () => { it('vrai si le prenom est renseigne', () => { expect(isContactNamed({ firstName: 'Alice', lastName: null })).toBe(true) diff --git a/frontend/modules/commercial/utils/clientFormRules.ts b/frontend/modules/commercial/utils/clientFormRules.ts index 35a8274..2a26c9f 100644 --- a/frontend/modules/commercial/utils/clientFormRules.ts +++ b/frontend/modules/commercial/utils/clientFormRules.ts @@ -50,6 +50,18 @@ export function buildClientFormTabKeys( return keys } +/** + * Dernier onglet REMPLISSABLE d'un jeu d'onglets : le dernier qui n'est pas un + * placeholder (coquille). Role-aware sans regle ad hoc — il suffit de lui passer + * les `tabKeys` deja filtres par permission (l'onglet Comptabilite n'y figure que + * si accounting.view). Sa validation marque la fin de l'ajout (redirection liste). + */ +export function lastFillableTabKey(tabKeys: string[]): string | undefined { + return [...tabKeys].reverse().find( + key => !(CLIENT_FORM_PLACEHOLDER_TABS as readonly string[]).includes(key), + ) +} + /** * Codes de categorie « intermediaire » : un client dont la categorie est * Distributeur ou Courtier n'a ni relation amont (il EST le distributeur /