ERP-119 : revue validation front clients + évolutions écran client (types d'adresse, 2e email, saisies manuelles, redirection) (#80)
Auto Tag Develop / tag (push) Successful in 7s

## Contexte
Branche ERP-119 — revue de la validation des formulaires clients (déclencheur : écran « Ajouter un client »), accompagnée de plusieurs évolutions de l'écran client (M1).

## Contenu

### Validation front (clients)
- Boutons « Valider » toujours actifs (retrait du gating de validité) : c'est le back qui renvoie les 422, mappées en rouge par champ.
- Champs requis adossés à une colonne non-nullable : la clé est omise du payload si vide (companyName, RIB, adresse) → 422 NotBlank au lieu d'un 400 de type.
- Onglet Contact : au moins un contact requis (l'amorce vide est soumise → 422 RG-1.05).
- Onglet Adresse : affichage inline des erreurs type / sites / catégories + RG back « au moins un type d'adresse obligatoire ».

### Nouveaux types d'adresse
- Courtier / Distributeur, types autonomes exclusifs : colonnes `is_broker` / `is_distributor` (migration + CHECK miroir d'exclusivité), entité + Callback, et front (select, drapeaux, payloads).

### Saisies manuelles
- Adresse : `allow-create` sur le champ Adresse → saisie libre si la BAN ne propose rien.
- Date de création : `MalioDate :editable` → saisie clavier JJ/MM/AAAA en plus du calendrier.

### 2e email de facturation
- Colonne `billing_email_secondary` (optionnel, max 2), miroir du téléphone secondaire. Bump `@malio/layer-ui` 1.7.8 (prop `addable`).

### Fin d'ajout d'un client
- Redirection vers la liste à la validation du dernier onglet remplissable par le rôle (Adresse pour Bureau/Commerciale, Comptabilité pour Admin) + toast « Client ajouté ». Dérivé de `tabKeys`, sans règle RBAC custom.

## Vérifications
- Back : suites Module/Commercial + Architecture vertes (Client : 124/124). Migrations appliquées (dev + test).
- Front : Vitest vert (272), ESLint OK.

> Note : le hook pré-commit flake aléatoirement (JWT 401 / timeout DB) sur des tests sans rapport (Supplier) ; les commits ont été faits après vérification des suites concernées en isolation.

Reviewed-on: #80
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #80.
This commit is contained in:
2026-06-09 19:47:40 +00:00
committed by Autin
parent b3ab23ee8f
commit 8490de99da
25 changed files with 1011 additions and 245 deletions
@@ -14,12 +14,15 @@
remplacant les 3 cases. Les options encodent les combinaisons valides
(exclusivite Prospect, RG-1.06/07/08) ; le back recoit toujours les
drapeaux isProspect / isDelivery / isBilling (aucune RG modifiee). -->
<!-- Erreur portee sur `isProspect` cote back (Callback type obligatoire +
exclusivite prospect) -> affichee sous le select Type d'adresse. -->
<MalioSelect
:model-value="addressType"
:options="addressTypeOptions"
:label="t('commercial.clients.form.address.addressType')"
:readonly="readonly"
:required="true"
:error="errors?.isProspect"
@update:model-value="onAddressTypeChange"
/>
@@ -31,6 +34,7 @@
:display-tag="true"
:readonly="readonly"
:required="true"
:error="errors?.sites"
@update:model-value="(v: (string | number)[]) => update('siteIris', v.map(String))"
/>
@@ -43,9 +47,10 @@
@update:model-value="(v: (string | number)[]) => update('contactIris', v.map(String))"
/>
<!-- Email de facturation : ligne 1 colonne 4, visible/obligatoire
seulement si Facturation (RG-1.11). Sinon un filler comble la
colonne pour que Categorie reparte au debut de la ligne 2. -->
<!-- Email(s) de facturation : visible/obligatoire seulement si Facturation
(RG-1.11). Le « + » revele un 2e email optionnel (max 2, pendant du
telephone secondaire) qui coule dans la grille. Sinon un filler comble
la colonne pour que Categorie reparte au debut de la ligne suivante. -->
<MalioInputEmail
v-if="isBillingEmailRequired(model)"
:model-value="model.billingEmail"
@@ -54,10 +59,23 @@
:readonly="readonly"
:lowercase="true"
:error="errors?.billingEmail"
:addable="!model.hasSecondaryBillingEmail && !readonly"
:add-button-label="t('commercial.clients.form.address.addBillingEmail')"
@update:model-value="(v: string) => update('billingEmail', v)"
@add="revealSecondaryBillingEmail"
/>
<div v-else aria-hidden="true" />
<MalioInputEmail
v-if="isBillingEmailRequired(model) && model.hasSecondaryBillingEmail"
:model-value="model.billingEmailSecondary"
:label="t('commercial.clients.form.address.billingEmailSecondary')"
:readonly="readonly"
:lowercase="true"
:error="errors?.billingEmailSecondary"
@update:model-value="(v: string) => update('billingEmailSecondary', v)"
/>
<MalioSelectCheckbox
:model-value="model.categoryIris"
:options="categoryOptions"
@@ -65,6 +83,7 @@
:display-tag="true"
:readonly="readonly"
:required="true"
:error="errors?.categories"
@update:model-value="(v: (string | number)[]) => update('categoryIris', v.map(String))"
/>
@@ -118,10 +137,10 @@
<div class="col-span-2">
<!-- Adresse : saisie assistee (BAN) en edition ; champ texte simple
seulement en lecture seule (MalioInputAutocomplete ne reaffiche pas
sa valeur liee, il n'afficherait rien en readonly). Une erreur BAN
ne bascule PAS en saisie libre : l'autocompletion reste montee et
chaque frappe relance la recherche (l'utilisateur peut aussi taper
une rue librement). -->
sa valeur liee, il n'afficherait rien en readonly). allow-create :
si la BAN ne propose rien (ou erreur), le texte saisi est CONSERVE au
blur/Entree (saisie manuelle) — sinon il serait efface. La ville reste
pilotee par le code postal ; choisir une suggestion remplit rue+ville+CP. -->
<MalioInputAutocomplete
v-if="!readonly"
:model-value="model.street"
@@ -132,6 +151,8 @@
:readonly="readonly"
:required="true"
:error="errors?.street"
:allow-create="true"
:no-results-text="t('commercial.clients.form.address.streetNotFound')"
@update:model-value="(v: string | number | null) => update('street', v === null ? null : String(v))"
@search="onAddressSearch"
@select="onAddressSelect"
@@ -147,7 +168,7 @@
/>
</div>
<div class="col-span-2">
<div class="col-span-1">
<MalioInputText
:model-value="model.streetComplement"
:label="t('commercial.clients.form.address.streetComplement')"
@@ -213,6 +234,8 @@ const addressTypeOptions = computed<RefOption[]>(() => [
{ value: 'delivery', label: t('commercial.clients.form.address.addressTypeDelivery') },
{ value: 'billing', label: t('commercial.clients.form.address.addressTypeBilling') },
{ value: 'delivery_billing', label: t('commercial.clients.form.address.addressTypeDeliveryBilling') },
{ value: 'broker', label: t('commercial.clients.form.address.addressTypeBroker') },
{ value: 'distributor', label: t('commercial.clients.form.address.addressTypeDistributor') },
])
/** Applique le type choisi en repercutant les 3 drapeaux back (immutabilite). */
@@ -266,6 +289,11 @@ function update<K extends keyof AddressFormDraft>(field: K, value: AddressFormDr
emit('update:modelValue', { ...props.modelValue, [field]: value })
}
/** Revele le 2e champ email de facturation (clic sur le « + »). */
function revealSecondaryBillingEmail(): void {
emit('update:modelValue', { ...props.modelValue, hasSecondaryBillingEmail: true })
}
/** Previent le parent (toast unique) que l'autocompletion est indisponible. */
function notifyUnavailable(): void {
if (!unavailableNotified) {