[ERP-63] Page Ajouter un client (formulaire principal + onglets) #46

Merged
tristan merged 6 commits from feature/ERP-63-page-ajouter-client into develop 2026-06-03 08:49:27 +00:00
2 changed files with 49 additions and 18 deletions
Showing only changes of commit b3cc7e4ced - Show all commits
+2 -3
View File
@@ -115,9 +115,8 @@
"addPhone": "Ajouter un numéro", "addPhone": "Ajouter un numéro",
"categories": "Catégorie", "categories": "Catégorie",
"relation": "Distributeur / Courtier", "relation": "Distributeur / Courtier",
"relationNone": "Aucun", "relationDistributor": "Dépend du distributeur",
"relationDistributor": "Distributeur", "relationBroker": "Dépend du courtier",
"relationBroker": "Courtier",
"distributorName": "Nom du distributeur", "distributorName": "Nom du distributeur",
"brokerName": "Nom du courtier", "brokerName": "Nom du courtier",
"triageService": "Prestation de triage" "triageService": "Prestation de triage"
@@ -149,10 +149,13 @@
/> />
</div> </div>
<div v-if="!isValidated('information')" class="mt-12 flex justify-center"> <div v-if="!isValidated('information')" class="mt-12 flex justify-center">
<!-- Desactive tant que le client n'est pas cree : evite un PATCH
avant le POST si l'utilisateur clique trop tot (le panneau
Information est l'onglet actif par defaut). -->
<MalioButton <MalioButton
variant="primary" variant="primary"
:label="t('commercial.clients.form.submit')" :label="t('commercial.clients.form.submit')"
:disabled="tabSubmitting" :disabled="tabSubmitting || clientId === null"
@click="submitInformation" @click="submitInformation"
/> />
</div> </div>
@@ -386,6 +389,7 @@ import {
type RibFormDraft, type RibFormDraft,
} from '~/modules/commercial/types/clientForm' } from '~/modules/commercial/types/clientForm'
import { formatPhoneFR } from '~/shared/utils/phone' import { formatPhoneFR } from '~/shared/utils/phone'
import { extractApiErrorMessage } from '~/shared/utils/api'
// Masques de saisie (la normalisation finale reste serveur). // Masques de saisie (la normalisation finale reste serveur).
const PHONE_MASK = '## ## ## ## ##' const PHONE_MASK = '## ## ## ## ##'
@@ -407,6 +411,17 @@ function goBack(): void {
router.push('/clients') 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') }) useHead({ title: t('commercial.clients.form.title') })
// Gating de la route : la creation est reservee a `manage`. Compta (accounting // Gating de la route : la creation est reservee a `manage`. Compta (accounting
@@ -433,7 +448,7 @@ const main = reactive({
lastName: null as string | null, lastName: null as string | null,
email: null as string | null, email: null as string | null,
categoryIris: [] as string[], categoryIris: [] as string[],
relationType: 'aucun' as 'aucun' | 'distributeur' | 'courtier', relationType: null as 'distributeur' | 'courtier' | null,
distributorIri: null as string | null, distributorIri: null as string | null,
brokerIri: null as string | null, brokerIri: null as string | null,
triageService: false, triageService: false,
@@ -450,23 +465,37 @@ function addMainPhone(): void {
} }
} }
// Pas d'option « Aucun » : le select est vide par defaut (relationType = null).
const relationOptions = computed<RefOption[]>(() => [ const relationOptions = computed<RefOption[]>(() => [
{ value: 'aucun', label: t('commercial.clients.form.main.relationNone') },
{ value: 'distributeur', label: t('commercial.clients.form.main.relationDistributor') }, { value: 'distributeur', label: t('commercial.clients.form.main.relationDistributor') },
{ value: 'courtier', label: t('commercial.clients.form.main.relationBroker') }, { 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 isMainValid = computed(() => {
const filled = (v: string | null | undefined) => v !== null && v !== undefined && v.trim() !== '' 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) return filled(main.companyName)
&& filled(main.email) && filled(main.email)
&& filled(mainPhones.value[0]) && filled(mainPhones.value[0])
&& (filled(main.firstName) || filled(main.lastName)) && (filled(main.firstName) || filled(main.lastName))
&& main.categoryIris.length >= 1
&& relationValid
}) })
async function onRelationChange(value: string | number | null): Promise<void> { async function onRelationChange(value: string | number | null): Promise<void> {
const relation = String(value ?? 'aucun') as typeof main.relationType const relation = (value === null || value === '')
? null
: (String(value) as 'distributeur' | 'courtier')
main.relationType = relation main.relationType = relation
// Reinitialise la FK non concernee (une seule remplie a la fois, RG-1.03). // Reinitialise la FK non concernee (une seule remplie a la fois, RG-1.03).
if (relation !== 'distributeur') main.distributorIri = null if (relation !== 'distributeur') main.distributorIri = null
@@ -518,11 +547,14 @@ async function submitMain(): Promise<void> {
toast.success({ title: t('commercial.clients.toast.createSuccess') }) toast.success({ title: t('commercial.clients.toast.createSuccess') })
} }
catch (error) { 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 const status = (error as { response?: { status?: number } })?.response?.status
toast.error({ toast.error({
title: t('commercial.clients.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 { finally {
@@ -611,8 +643,8 @@ async function submitInformation(): Promise<void> {
completeTab('information') completeTab('information')
toast.success({ title: t('commercial.clients.toast.updateSuccess') }) toast.success({ title: t('commercial.clients.toast.updateSuccess') })
} }
catch { catch (error) {
toast.error({ title: t('commercial.clients.toast.error') }) toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) })
} }
finally { finally {
tabSubmitting.value = false tabSubmitting.value = false
@@ -685,8 +717,8 @@ async function submitContacts(): Promise<void> {
completeTab('contact') completeTab('contact')
toast.success({ title: t('commercial.clients.toast.updateSuccess') }) toast.success({ title: t('commercial.clients.toast.updateSuccess') })
} }
catch { catch (error) {
toast.error({ title: t('commercial.clients.toast.error') }) toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) })
} }
finally { finally {
tabSubmitting.value = false tabSubmitting.value = false
@@ -783,8 +815,8 @@ async function submitAddresses(): Promise<void> {
completeTab('address') completeTab('address')
toast.success({ title: t('commercial.clients.toast.updateSuccess') }) toast.success({ title: t('commercial.clients.toast.updateSuccess') })
} }
catch { catch (error) {
toast.error({ title: t('commercial.clients.toast.error') }) toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) })
} }
finally { finally {
tabSubmitting.value = false tabSubmitting.value = false
@@ -878,8 +910,8 @@ async function submitAccounting(): Promise<void> {
completeTab('accounting') completeTab('accounting')
toast.success({ title: t('commercial.clients.toast.updateSuccess') }) toast.success({ title: t('commercial.clients.toast.updateSuccess') })
} }
catch { catch (error) {
toast.error({ title: t('commercial.clients.toast.error') }) toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) })
} }
finally { finally {
tabSubmitting.value = false tabSubmitting.value = false