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
@@ -21,6 +21,12 @@ import {
relationOf,
type ClientDetail,
} from '~/modules/commercial/utils/clientConsultation'
import {
ADDRESS_REQUIRED_NON_NULLABLE_KEYS,
MAIN_REQUIRED_NON_NULLABLE_KEYS,
omitEmptyRequired,
RIB_REQUIRED_NON_NULLABLE_KEYS,
} from '~/modules/commercial/utils/clientFormRules'
import type { AddressFormDraft, ContactFormDraft, RibFormDraft } from '~/modules/commercial/types/clientForm'
/**
@@ -139,13 +145,21 @@ export function mapAccountingFormDraft(client: ClientDetail): AccountingFormDraf
* que la FK correspondant au type choisi, l'autre est forcee a null.
*/
export function buildMainPayload(main: MainFormDraft): Record<string, unknown> {
return {
// companyName omis si vide -> 422 NotBlank au lieu d'un 400 de type (ERP-119).
// relationType : champ transitoire (non persiste cote back) qui porte
// l'intention UI « ce client depend d'un distributeur / courtier ». Il sert
// a la validation croisee serveur (RG-1.03 bis) : si une relation est choisie,
// la FK correspondante devient obligatoire -> 422 sur distributor / broker.
// Sans equivalent derivable cote back (FK nullable), c'est la seule facon de
// rester sur « on soumet, le back tranche » plutot qu'une garde front-only.
return omitEmptyRequired({
companyName: main.companyName,
categories: main.categoryIris,
relationType: main.relationType,
distributor: main.relationType === 'distributeur' ? main.distributorIri : null,
broker: main.relationType === 'courtier' ? main.brokerIri : null,
triageService: main.triageService,
}
}, MAIN_REQUIRED_NON_NULLABLE_KEYS)
}
/** Payload de l'onglet Information — groupe client:write:information UNIQUEMENT. */
@@ -198,10 +212,13 @@ export function buildAddressPayload(
address: AddressFormDraft,
isBillingEmailRequired: boolean,
): Record<string, unknown> {
return {
// postalCode / city / street omis si vides -> 422 NotBlank (ERP-119).
return omitEmptyRequired({
isProspect: address.isProspect,
isDelivery: address.isDelivery,
isBilling: address.isBilling,
isBroker: address.isBroker,
isDistributor: address.isDistributor,
country: address.country,
postalCode: address.postalCode || null,
city: address.city || null,
@@ -211,16 +228,19 @@ export function buildAddressPayload(
sites: address.siteIris,
contacts: address.contactIris,
billingEmail: isBillingEmailRequired ? (address.billingEmail || null) : null,
}
billingEmailSecondary: isBillingEmailRequired && address.hasSecondaryBillingEmail ? (address.billingEmailSecondary || null) : null,
}, ADDRESS_REQUIRED_NON_NULLABLE_KEYS)
}
/** Payload d'un RIB (sous-ressource client_rib). */
export function buildRibPayload(rib: RibFormDraft): Record<string, unknown> {
return {
// label / bic / iban omis si vides -> 422 NotBlank au lieu d'un 400 de type
// sur un RIB partiel (ex. IBAN seul). ERP-119.
return omitEmptyRequired({
label: rib.label,
bic: rib.bic,
iban: rib.iban,
}
}, RIB_REQUIRED_NON_NULLABLE_KEYS)
}
// ── Gating par permission ────────────────────────────────────────────────────