feat(commercial) : redirection vers la liste a la fin de l'ajout d'un client (ERP-119)
Quand le dernier onglet remplissable par le role est valide (Adresse pour Bureau/Commerciale, Comptabilite pour Admin), l'ajout est termine : toast « Client ajoute » + redirection vers /clients. Le dernier onglet remplissable est derive de tabKeys via lastFillableTabKey (dernier onglet non-placeholder), deja role-aware, sans regle RBAC custom. completeTab retourne un booleen pour eviter le double toast.
This commit is contained in:
@@ -88,6 +88,7 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"createSuccess": "Client créé avec succès",
|
"createSuccess": "Client créé avec succès",
|
||||||
"updateSuccess": "Client mis à jour avec succès",
|
"updateSuccess": "Client mis à jour avec succès",
|
||||||
|
"addComplete": "Client ajouté",
|
||||||
"archiveSuccess": "Client archivé avec succès",
|
"archiveSuccess": "Client archivé avec succès",
|
||||||
"restoreSuccess": "Client restauré avec succès",
|
"restoreSuccess": "Client restauré avec succès",
|
||||||
"error": "Une erreur est survenue. Réessayez.",
|
"error": "Une erreur est survenue. Réessayez.",
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ import {
|
|||||||
isRibBlank,
|
isRibBlank,
|
||||||
isRibComplete,
|
isRibComplete,
|
||||||
isRibRequiredForPaymentType,
|
isRibRequiredForPaymentType,
|
||||||
|
lastFillableTabKey,
|
||||||
MAIN_REQUIRED_NON_NULLABLE_KEYS,
|
MAIN_REQUIRED_NON_NULLABLE_KEYS,
|
||||||
omitEmptyRequired,
|
omitEmptyRequired,
|
||||||
RIB_REQUIRED_NON_NULLABLE_KEYS,
|
RIB_REQUIRED_NON_NULLABLE_KEYS,
|
||||||
@@ -588,6 +589,12 @@ const validated = reactive<Record<string, boolean>>({})
|
|||||||
|
|
||||||
const tabKeys = computed(() => buildClientFormTabKeys(canAccountingView.value))
|
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.
|
// Icone (Iconify) affichee dans l'onglet, par cle. A ajuster librement.
|
||||||
const TAB_ICONS: Record<string, string> = {
|
const TAB_ICONS: Record<string, string> = {
|
||||||
information: 'mdi:account-outline',
|
information: 'mdi:account-outline',
|
||||||
@@ -615,12 +622,23 @@ function tabIndex(key: string): number {
|
|||||||
return tabKeys.value.indexOf(key)
|
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
|
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]
|
const next = tabKeys.value[tabIndex(key) + 1]
|
||||||
unlockedIndex.value = Math.max(unlockedIndex.value, tabIndex(key) + 1)
|
unlockedIndex.value = Math.max(unlockedIndex.value, tabIndex(key) + 1)
|
||||||
if (next) activeTab.value = next
|
if (next) activeTab.value = next
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Passage automatique sur les onglets coquille (Transport, Stats, Rapports, Echanges).
|
// Passage automatique sur les onglets coquille (Transport, Stats, Rapports, Echanges).
|
||||||
@@ -658,7 +676,7 @@ async function submitInformation(): Promise<void> {
|
|||||||
profitAmount: information.profitAmount || null,
|
profitAmount: information.profitAmount || null,
|
||||||
directorName: information.directorName || null,
|
directorName: information.directorName || null,
|
||||||
}, { toast: false })
|
}, { toast: false })
|
||||||
completeTab('information')
|
if (completeTab('information')) return
|
||||||
toast.success({ title: t('commercial.clients.toast.updateSuccess') })
|
toast.success({ title: t('commercial.clients.toast.updateSuccess') })
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -737,7 +755,7 @@ async function submitContacts(): Promise<void> {
|
|||||||
)
|
)
|
||||||
// Tant qu'un bloc reste en erreur : pas de validation d'onglet ni de toast succes.
|
// Tant qu'un bloc reste en erreur : pas de validation d'onglet ni de toast succes.
|
||||||
if (hasError) return
|
if (hasError) return
|
||||||
completeTab('contact')
|
if (completeTab('contact')) return
|
||||||
toast.success({ title: t('commercial.clients.toast.updateSuccess') })
|
toast.success({ title: t('commercial.clients.toast.updateSuccess') })
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -839,7 +857,7 @@ async function submitAddresses(): Promise<void> {
|
|||||||
error => toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }),
|
error => toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }),
|
||||||
)
|
)
|
||||||
if (hasError) return
|
if (hasError) return
|
||||||
completeTab('address')
|
if (completeTab('address')) return
|
||||||
toast.success({ title: t('commercial.clients.toast.updateSuccess') })
|
toast.success({ title: t('commercial.clients.toast.updateSuccess') })
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -967,7 +985,7 @@ async function submitAccounting(): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
completeTab('accounting')
|
if (completeTab('accounting')) return
|
||||||
toast.success({ title: t('commercial.clients.toast.updateSuccess') })
|
toast.success({ title: t('commercial.clients.toast.updateSuccess') })
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
isRibBlank,
|
isRibBlank,
|
||||||
isRibComplete,
|
isRibComplete,
|
||||||
isRibRequiredForPaymentType,
|
isRibRequiredForPaymentType,
|
||||||
|
lastFillableTabKey,
|
||||||
omitEmptyRequired,
|
omitEmptyRequired,
|
||||||
showsRelationAndTriageFields,
|
showsRelationAndTriageFields,
|
||||||
type AddressFlagsDraft,
|
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)', () => {
|
describe('isContactNamed (RG-1.05)', () => {
|
||||||
it('vrai si le prenom est renseigne', () => {
|
it('vrai si le prenom est renseigne', () => {
|
||||||
expect(isContactNamed({ firstName: 'Alice', lastName: null })).toBe(true)
|
expect(isContactNamed({ firstName: 'Alice', lastName: null })).toBe(true)
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ export function buildClientFormTabKeys(
|
|||||||
return keys
|
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
|
* Codes de categorie « intermediaire » : un client dont la categorie est
|
||||||
* Distributeur ou Courtier n'a ni relation amont (il EST le distributeur /
|
* Distributeur ou Courtier n'a ni relation amont (il EST le distributeur /
|
||||||
|
|||||||
Reference in New Issue
Block a user