d6790dd37d
Auto Tag Develop / tag (push) Successful in 7s
ERP-94 (etape front 7/7 du M2). **Stack sur #97** (base = `feature/ERP-97-suppliers-i18n-sidebar`, elle-meme sur #93) pour un diff isole. A recibler sur `develop` une fois #93 (MR #81) et #97 (MR #82) mergees. Page « Ajouter un fournisseur » — **replique a l'identique le fonctionnement de l'ecran Client** (workflow inline par onglets, blocs reutilisables, validation 422 inline ERP-101), avec les specificites M2. ## Architecture (miroir Client) - Workflow par onglets **inline dans `suppliers/new.vue`** (comme `clients/new.vue` — il n'existe pas de `useClientForm` monolithique). Helpers paralleles : `useSupplierReferentials`, `useSupplierFormErrors`, `supplierFormRules`, `supplierEdit` (payloads), `types/supplierForm`. - Blocs `SupplierContactBlock` / `SupplierAddressBlock` (miroir des blocs Client). - POST `/suppliers` puis PATCH partiels par onglet (mode strict, groupes de serialisation). Sous-ressources : `/suppliers/{id}/contacts|addresses|ribs`. - Validation ERP-101 : 422 `violations[].propertyPath` mappees inline par champ (`useFormErrors` / `mapViolationsToRecord`), `{ toast: false }`, bouton Valider toujours actif. ## Specificites M2 (vs M1) - Formulaire principal **sans contact inline** (ERP-106) : Entreprise + Categorie (type FOURNISSEUR, `?typeCode=FOURNISSEUR`). - Adresse : **radio exclusif** Prospect/Depart/Rendu (`addressType` enum, RG-2.09), champs **Bennes** (stepper) + **Prestation de triage**, **pas d'email de facturation**. - Information : champ **Volume previsionnel** (8e champ). - Compta (Admin+Compta) : banque si VIREMENT (RG-2.07), RIB si LCR (RG-2.08) ; RIB sous-ressource gardee par `accounting.manage`. ## Tests (mirroir strategie Client) - `make nuxt-test` : 338 passed (specs ajoutees : supplierFormRules, supplierEdit, useSupplierReferentials, SupplierContactBlock, SupplierAddressBlock). - ESLint propre ; `nuxi typecheck` (lance en container) : **0 erreur**. - Golden path navigateur valide end-to-end : POST /suppliers OK, companyName normalise UPPERCASE (RG-2.12), gating des onglets (Information actif, Contacts deverrouille). ## Note de revue ~30 `WARN Duplicated imports` au typecheck : les helpers Supplier exportent les memes noms generiques que leurs equivalents Client (`buildMainPayload`, `omitEmptyRequired`, `RefOption`...), tous deux auto-importes par Nuxt. **Sans impact runtime** : tous les consommateurs utilisent des imports explicites (qui priment). Consequence directe du miroir 1:1 ; une factorisation des generiques dans `shared/` pourrait etre un suivi. Reviewed-on: #83 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
105 lines
4.0 KiB
Vue
105 lines
4.0 KiB
Vue
<template>
|
|
<div class="relative grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
|
<!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si
|
|
non supprimable (1er bloc, RG-2.13) ou en lecture seule. -->
|
|
<MalioButtonIcon
|
|
v-if="removable && !readonly"
|
|
icon="mdi:delete-outline"
|
|
variant="ghost"
|
|
button-class="absolute top-3 right-3"
|
|
v-bind="{ ariaLabel: t('commercial.suppliers.form.contact.remove') }"
|
|
@click="$emit('remove')"
|
|
/>
|
|
|
|
<MalioInputText
|
|
:model-value="model.lastName"
|
|
:label="t('commercial.suppliers.form.contact.lastName')"
|
|
:readonly="readonly"
|
|
:error="errors?.lastName"
|
|
@update:model-value="(v: string) => update('lastName', v)"
|
|
/>
|
|
<MalioInputText
|
|
:model-value="model.firstName"
|
|
:label="t('commercial.suppliers.form.contact.firstName')"
|
|
:readonly="readonly"
|
|
:error="errors?.firstName"
|
|
@update:model-value="(v: string) => update('firstName', v)"
|
|
/>
|
|
<MalioInputText
|
|
:model-value="model.jobTitle"
|
|
:label="t('commercial.suppliers.form.contact.jobTitle')"
|
|
:readonly="readonly"
|
|
:error="errors?.jobTitle"
|
|
@update:model-value="(v: string) => update('jobTitle', v)"
|
|
/>
|
|
<MalioInputEmail
|
|
:model-value="model.email"
|
|
:label="t('commercial.suppliers.form.contact.email')"
|
|
:readonly="readonly"
|
|
:lowercase="true"
|
|
:error="errors?.email"
|
|
@update:model-value="(v: string) => update('email', v)"
|
|
/>
|
|
<MalioInputPhone
|
|
:model-value="model.phonePrimary"
|
|
:label="t('commercial.suppliers.form.contact.phonePrimary')"
|
|
:mask="PHONE_MASK"
|
|
:readonly="readonly"
|
|
:error="errors?.phonePrimary"
|
|
:addable="!model.hasSecondaryPhone && !readonly"
|
|
:add-button-label="t('commercial.suppliers.form.contact.addPhone')"
|
|
@update:model-value="(v: string) => update('phonePrimary', v)"
|
|
@add="revealSecondaryPhone"
|
|
/>
|
|
<MalioInputPhone
|
|
v-if="model.hasSecondaryPhone"
|
|
:model-value="model.phoneSecondary"
|
|
:label="t('commercial.suppliers.form.contact.phoneSecondary')"
|
|
:mask="PHONE_MASK"
|
|
:readonly="readonly"
|
|
:error="errors?.phoneSecondary"
|
|
@update:model-value="(v: string) => update('phoneSecondary', v)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { SupplierContactFormDraft } from '~/modules/commercial/types/supplierForm'
|
|
|
|
// Masque telephone FR : 5 groupes de 2 chiffres (la normalisation finale reste serveur).
|
|
const PHONE_MASK = '## ## ## ## ##'
|
|
|
|
const props = defineProps<{
|
|
/** Brouillon du contact (v-model). */
|
|
modelValue: SupplierContactFormDraft
|
|
/** Titre du bloc (ex: « Contact 1 »). */
|
|
title: string
|
|
/** Affiche l'icone de suppression (1er bloc non supprimable, RG-2.13). */
|
|
removable?: boolean
|
|
/** Bloc en lecture seule (onglet valide). */
|
|
readonly?: boolean
|
|
/** Erreurs serveur 422 de cette ligne, indexees par champ (ERP-101). */
|
|
errors?: Record<string, string>
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: SupplierContactFormDraft]
|
|
'remove': []
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
|
|
// Alias local pour la lisibilite du template.
|
|
const model = computed(() => props.modelValue)
|
|
|
|
/** Emet un nouveau brouillon avec le champ modifie (immutabilite). */
|
|
function update<K extends keyof SupplierContactFormDraft>(field: K, value: SupplierContactFormDraft[K]): void {
|
|
emit('update:modelValue', { ...props.modelValue, [field]: value })
|
|
}
|
|
|
|
/** Revele le 2e numero (max 1 secondaire, le « + » disparait). */
|
|
function revealSecondaryPhone(): void {
|
|
emit('update:modelValue', { ...props.modelValue, hasSecondaryPhone: true })
|
|
}
|
|
</script>
|