feat(commercial) : onglet adresse — Select « Type d'adresse » + Sites en multiselect, ligne 1 réagencée (ERP-110)
- Sites Starseed : 3 cases -> multiselect a tags « Sites » (required). - Usage adresse : 3 cases Prospect/Livraison/Facturation -> Select unique « Type d'adresse » (Prospect / Livraison / Facturation / Adresse + Facturation), obligatoire sans option vide, conditionnant le bouton « Valider ». Pur sucre front : le back recoit toujours isProspect/isDelivery/isBilling (aucune RG modifiee), exclusivite Prospect devenue structurelle. - Email de facturation conditionnel (Facturation / Adresse + Facturation) deplace en ligne 1. - Ligne 1 : Type d'adresse | Sites | Contact rattache | Email ; le reste (Categorie, Pays, CP, Ville, Adresse...) en lignes suivantes. - Email : MalioInputText -> MalioInputEmail (lowercase, ERP-101/RG-1.21) sur facturation ET contact. - Helpers front testables addressFlagsFromType / addressTypeFromFlags + gating canValidateAddresses (type obligatoire) dans new.vue / edit.vue.
This commit is contained in:
@@ -10,34 +10,53 @@
|
||||
@click="$emit('remove')"
|
||||
/>
|
||||
|
||||
<!-- Usage de l'adresse : Prospect exclusif de Livraison/Facturation
|
||||
(RG-1.06/07/08). L'exclusivite est appliquee au toggle (cocher l'un
|
||||
decoche l'autre) plutot qu'en masquant les options. -->
|
||||
<MalioCheckbox
|
||||
:model-value="model.isProspect"
|
||||
:label="t('commercial.clients.form.address.prospect')"
|
||||
group-class="self-center"
|
||||
<!-- Usage de l'adresse : Select unique (plus simple pour l'utilisateur)
|
||||
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). -->
|
||||
<MalioSelect
|
||||
:model-value="addressType"
|
||||
:options="addressTypeOptions"
|
||||
:label="t('commercial.clients.form.address.addressType')"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: boolean) => toggleFlag('isProspect', v)"
|
||||
/>
|
||||
<MalioCheckbox
|
||||
:model-value="model.isDelivery"
|
||||
:label="t('commercial.clients.form.address.delivery')"
|
||||
group-class="self-center"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: boolean) => toggleFlag('isDelivery', v)"
|
||||
/>
|
||||
<MalioCheckbox
|
||||
:model-value="model.isBilling"
|
||||
:label="t('commercial.clients.form.address.billing')"
|
||||
group-class="self-center"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: boolean) => toggleFlag('isBilling', v)"
|
||||
:required="true"
|
||||
@update:model-value="onAddressTypeChange"
|
||||
/>
|
||||
|
||||
<!-- Cellule vide : laisse un trou en position 4 (ligne 1) pour que
|
||||
Categorie reparte au debut de la ligne suivante. -->
|
||||
<div aria-hidden="true" />
|
||||
<!-- Sites Starseed : multiselect a tags (>= 1 obligatoire, RG-1.10). -->
|
||||
<MalioSelectCheckbox
|
||||
:model-value="model.siteIris"
|
||||
:options="siteOptions"
|
||||
:label="t('commercial.clients.form.address.sites')"
|
||||
:display-tag="true"
|
||||
:readonly="readonly"
|
||||
:required="true"
|
||||
@update:model-value="(v: (string | number)[]) => update('siteIris', v.map(String))"
|
||||
/>
|
||||
|
||||
<MalioSelectCheckbox
|
||||
:model-value="model.contactIris"
|
||||
:options="contactOptions"
|
||||
:label="t('commercial.clients.form.address.contacts')"
|
||||
:display-tag="true"
|
||||
:readonly="readonly"
|
||||
@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. -->
|
||||
<MalioInputEmail
|
||||
v-if="isBillingEmailRequired(model)"
|
||||
:model-value="model.billingEmail"
|
||||
:label="t('commercial.clients.form.address.billingEmail')"
|
||||
:required="true"
|
||||
:readonly="readonly"
|
||||
:lowercase="true"
|
||||
:error="errors?.billingEmail"
|
||||
@update:model-value="(v: string) => update('billingEmail', v)"
|
||||
/>
|
||||
<div v-else aria-hidden="true" />
|
||||
|
||||
<MalioSelectCheckbox
|
||||
:model-value="model.categoryIris"
|
||||
@@ -134,47 +153,15 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Sites Starseed : cases a cocher inline (>= 1 obligatoire, RG-1.10). -->
|
||||
<div class="flex justify-between">
|
||||
<MalioCheckbox
|
||||
v-for="site in siteOptions"
|
||||
:key="site.value"
|
||||
:model-value="model.siteIris.includes(site.value)"
|
||||
:label="site.label"
|
||||
group-class="w-auto self-center"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: boolean) => toggleSite(site.value, v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MalioSelectCheckbox
|
||||
:model-value="model.contactIris"
|
||||
:options="contactOptions"
|
||||
:label="t('commercial.clients.form.address.contacts')"
|
||||
:display-tag="true"
|
||||
:readonly="readonly"
|
||||
@update:model-value="(v: (string | number)[]) => update('contactIris', v.map(String))"
|
||||
/>
|
||||
|
||||
<!-- Email de facturation : visible/obligatoire seulement si Facturation
|
||||
est coche (RG-1.11). -->
|
||||
<MalioInputText
|
||||
v-if="isBillingEmailRequired(model)"
|
||||
:model-value="model.billingEmail"
|
||||
:label="t('commercial.clients.form.address.billingEmail')"
|
||||
:required="true"
|
||||
:readonly="readonly"
|
||||
:error="errors?.billingEmail"
|
||||
@update:model-value="(v: string) => update('billingEmail', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
applyProspectExclusivity,
|
||||
addressFlagsFromType,
|
||||
addressTypeFromFlags,
|
||||
isBillingEmailRequired,
|
||||
type AddressFlagsDraft,
|
||||
type AddressType,
|
||||
} from '~/modules/commercial/utils/clientFormRules'
|
||||
import { useAddressAutocomplete, type AddressSuggestion } from '~/shared/composables/useAddressAutocomplete'
|
||||
import type { CategoryOption, RefOption } from '~/modules/commercial/composables/useClientReferentials'
|
||||
@@ -213,6 +200,23 @@ const autocomplete = useAddressAutocomplete()
|
||||
|
||||
const model = computed(() => props.modelValue)
|
||||
|
||||
// Type d'adresse (Select unique) derive des drapeaux back. null tant qu'aucun
|
||||
// drapeau n'est pose -> champ vide + bouton « Valider » bloque (cf. parent).
|
||||
const addressType = computed<AddressType | null>(() => addressTypeFromFlags(model.value))
|
||||
|
||||
const addressTypeOptions = computed<RefOption[]>(() => [
|
||||
{ value: 'prospect', label: t('commercial.clients.form.address.addressTypeProspect') },
|
||||
{ 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') },
|
||||
])
|
||||
|
||||
/** Applique le type choisi en repercutant les 3 drapeaux back (immutabilite). */
|
||||
function onAddressTypeChange(value: string | number | null): void {
|
||||
if (value === null) return
|
||||
emit('update:modelValue', { ...props.modelValue, ...addressFlagsFromType(value as AddressType) })
|
||||
}
|
||||
|
||||
// Mode degrade : service BAN indisponible → Ville/Adresse en saisie libre.
|
||||
const degraded = ref(false)
|
||||
// Villes proposees par la BAN (alimentees a la saisie du code postal).
|
||||
@@ -254,25 +258,6 @@ function update<K extends keyof AddressFormDraft>(field: K, value: AddressFormDr
|
||||
emit('update:modelValue', { ...props.modelValue, [field]: value })
|
||||
}
|
||||
|
||||
/** Coche/decoche un site Starseed rattache a l'adresse (M2M par IRI, RG-1.10). */
|
||||
function toggleSite(siteIri: string, selected: boolean): void {
|
||||
const current = props.modelValue.siteIris
|
||||
const next = selected
|
||||
? [...current, siteIri]
|
||||
: current.filter(iri => iri !== siteIri)
|
||||
update('siteIris', next)
|
||||
}
|
||||
|
||||
/** Applique l'exclusivite Prospect / (Livraison|Facturation) au changement. */
|
||||
function toggleFlag(field: keyof AddressFlagsDraft, value: boolean): void {
|
||||
const flags = applyProspectExclusivity(
|
||||
{ isProspect: model.value.isProspect, isDelivery: model.value.isDelivery, isBilling: model.value.isBilling },
|
||||
field,
|
||||
value,
|
||||
)
|
||||
emit('update:modelValue', { ...props.modelValue, ...flags })
|
||||
}
|
||||
|
||||
/** Bascule définitivement en mode degrade et previent le parent (toast unique). */
|
||||
function enterDegraded(): void {
|
||||
if (!degraded.value) {
|
||||
|
||||
Reference in New Issue
Block a user