ERP-119 : revue validation front clients + évolutions écran client (types d'adresse, 2e email, saisies manuelles, redirection) #80

Merged
tristan merged 7 commits from feature/ERP-119-revoir-la-validation-front into develop 2026-06-09 19:47:41 +00:00
4 changed files with 56 additions and 6 deletions
Showing only changes of commit ada4b156fa - Show all commits
+1
View File
@@ -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.",
@@ -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<Record<string, boolean>>({})
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<string, string> = {
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<void> {
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<void> {
)
// 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<void> {
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<void> {
return
}
completeTab('accounting')
if (completeTab('accounting')) return
toast.success({ title: t('commercial.clients.toast.updateSuccess') })
}
finally {
@@ -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)
@@ -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 /