From b3cc7e4ced751877b13482aefc6c3b5448831afd Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 3 Jun 2026 10:17:06 +0200 Subject: [PATCH] fix(front) : corrections review ecran ajouter client (ERP-63) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Relation Distributeur/Courtier : libelles « Depend du distributeur/courtier », select optionnel ; le nom (distributeur ou courtier) devient requis quand la relation correspondante est choisie. - Categorie : au moins 1 obligatoire dans le formulaire principal (aligne sur Assert\Count(min:1) du back). - Bouton « Valider » de l'onglet Information desactive tant que le client n'est pas cree (l'onglet est actif par defaut) : evite tout PATCH premature. - Gestion d'erreur : les toasts d'erreur passent toujours une chaine (corrige un crash izitoast sur message undefined) et remontent le message de validation du serveur (violations 422) sur tous les onglets. --- frontend/i18n/locales/fr.json | 5 +- .../modules/commercial/pages/clients/new.vue | 62 ++++++++++++++----- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index c085ff4..c21d4ab 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -115,9 +115,8 @@ "addPhone": "Ajouter un numéro", "categories": "Catégorie", "relation": "Distributeur / Courtier", - "relationNone": "Aucun", - "relationDistributor": "Distributeur", - "relationBroker": "Courtier", + "relationDistributor": "Dépend du distributeur", + "relationBroker": "Dépend du courtier", "distributorName": "Nom du distributeur", "brokerName": "Nom du courtier", "triageService": "Prestation de triage" diff --git a/frontend/modules/commercial/pages/clients/new.vue b/frontend/modules/commercial/pages/clients/new.vue index 2438e22..b7768ea 100644 --- a/frontend/modules/commercial/pages/clients/new.vue +++ b/frontend/modules/commercial/pages/clients/new.vue @@ -149,10 +149,13 @@ />
+
@@ -386,6 +389,7 @@ import { type RibFormDraft, } from '~/modules/commercial/types/clientForm' import { formatPhoneFR } from '~/shared/utils/phone' +import { extractApiErrorMessage } from '~/shared/utils/api' // Masques de saisie (la normalisation finale reste serveur). const PHONE_MASK = '## ## ## ## ##' @@ -407,6 +411,17 @@ function goBack(): void { router.push('/clients') } +/** + * Message d'erreur a afficher dans un toast a partir d'une erreur d'API. + * Retourne TOUJOURS une chaine (le composant de toast plante sur `undefined`) : + * le message de validation renvoye par le serveur (violations 422 / detail), + * sinon un libelle generique. + */ +function apiErrorMessage(error: unknown): string { + const data = (error as { data?: unknown })?.data + return extractApiErrorMessage(data) || t('commercial.clients.toast.error') +} + useHead({ title: t('commercial.clients.form.title') }) // Gating de la route : la creation est reservee a `manage`. Compta (accounting @@ -433,7 +448,7 @@ const main = reactive({ lastName: null as string | null, email: null as string | null, categoryIris: [] as string[], - relationType: 'aucun' as 'aucun' | 'distributeur' | 'courtier', + relationType: null as 'distributeur' | 'courtier' | null, distributorIri: null as string | null, brokerIri: null as string | null, triageService: false, @@ -450,23 +465,37 @@ function addMainPhone(): void { } } +// Pas d'option « Aucun » : le select est vide par defaut (relationType = null). const relationOptions = computed(() => [ - { value: 'aucun', label: t('commercial.clients.form.main.relationNone') }, { value: 'distributeur', label: t('commercial.clients.form.main.relationDistributor') }, { value: 'courtier', label: t('commercial.clients.form.main.relationBroker') }, ]) -// RG-1.01 : firstName OU lastName, + companyName / email / telephone principal requis. +// Validation du formulaire principal (gate le bouton « Valider ») : +// - companyName / email / telephone principal / >= 1 categorie obligatoires ; +// - RG-1.01 : nom OU prenom du contact principal ; +// - relation Distributeur/Courtier obligatoire (un des deux), ET le nom +// correspondant obligatoire selon le choix (spec fonctionnelle). const isMainValid = computed(() => { const filled = (v: string | null | undefined) => v !== null && v !== undefined && v.trim() !== '' + // Relation Distributeur/Courtier OPTIONNELLE ; mais si « Depend du + // distributeur/courtier » est choisi, le nom correspondant devient requis. + const relationValid + = main.relationType === null + || (main.relationType === 'distributeur' && filled(main.distributorIri)) + || (main.relationType === 'courtier' && filled(main.brokerIri)) return filled(main.companyName) && filled(main.email) && filled(mainPhones.value[0]) && (filled(main.firstName) || filled(main.lastName)) + && main.categoryIris.length >= 1 + && relationValid }) async function onRelationChange(value: string | number | null): Promise { - const relation = String(value ?? 'aucun') as typeof main.relationType + const relation = (value === null || value === '') + ? null + : (String(value) as 'distributeur' | 'courtier') main.relationType = relation // Reinitialise la FK non concernee (une seule remplie a la fois, RG-1.03). if (relation !== 'distributeur') main.distributorIri = null @@ -518,11 +547,14 @@ async function submitMain(): Promise { toast.success({ title: t('commercial.clients.toast.createSuccess') }) } catch (error) { - // 409 = doublon nom de societe (RG d'unicite) → message explicite. + // 409 = doublon nom de societe (RG d'unicite) → message explicite ; + // sinon on remonte le message de validation du serveur (ex: 422). const status = (error as { response?: { status?: number } })?.response?.status toast.error({ title: t('commercial.clients.toast.error'), - message: status === 409 ? t('commercial.clients.form.duplicateCompany') : undefined, + message: status === 409 + ? t('commercial.clients.form.duplicateCompany') + : apiErrorMessage(error), }) } finally { @@ -611,8 +643,8 @@ async function submitInformation(): Promise { completeTab('information') toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } - catch { - toast.error({ title: t('commercial.clients.toast.error') }) + catch (error) { + toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }) } finally { tabSubmitting.value = false @@ -685,8 +717,8 @@ async function submitContacts(): Promise { completeTab('contact') toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } - catch { - toast.error({ title: t('commercial.clients.toast.error') }) + catch (error) { + toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }) } finally { tabSubmitting.value = false @@ -783,8 +815,8 @@ async function submitAddresses(): Promise { completeTab('address') toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } - catch { - toast.error({ title: t('commercial.clients.toast.error') }) + catch (error) { + toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }) } finally { tabSubmitting.value = false @@ -878,8 +910,8 @@ async function submitAccounting(): Promise { completeTab('accounting') toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } - catch { - toast.error({ title: t('commercial.clients.toast.error') }) + catch (error) { + toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }) } finally { tabSubmitting.value = false