Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 817975e0b7 | |||
| efded9fd40 | |||
| 2e50a760c6 | |||
| 49e5e5548e | |||
| fd430bc123 | |||
| a6b48b1dd1 |
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.148'
|
app.version: '0.1.151'
|
||||||
|
|||||||
@@ -183,6 +183,7 @@
|
|||||||
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
|
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
|
||||||
},
|
},
|
||||||
"accounting": {
|
"accounting": {
|
||||||
|
"infoTitle": "Informations",
|
||||||
"siren": "SIREN",
|
"siren": "SIREN",
|
||||||
"accountNumber": "Numéro de compte",
|
"accountNumber": "Numéro de compte",
|
||||||
"tvaMode": "Mode de TVA",
|
"tvaMode": "Mode de TVA",
|
||||||
@@ -190,6 +191,7 @@
|
|||||||
"paymentDelay": "Délai de règlement",
|
"paymentDelay": "Délai de règlement",
|
||||||
"paymentType": "Type de règlement",
|
"paymentType": "Type de règlement",
|
||||||
"bank": "Banque",
|
"bank": "Banque",
|
||||||
|
"ribTitle": "RIB {n}",
|
||||||
"ribLabel": "Libellé",
|
"ribLabel": "Libellé",
|
||||||
"ribBic": "BIC",
|
"ribBic": "BIC",
|
||||||
"ribIban": "IBAN",
|
"ribIban": "IBAN",
|
||||||
@@ -350,6 +352,7 @@
|
|||||||
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
|
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
|
||||||
},
|
},
|
||||||
"accounting": {
|
"accounting": {
|
||||||
|
"infoTitle": "Informations",
|
||||||
"siren": "SIREN",
|
"siren": "SIREN",
|
||||||
"accountNumber": "Numéro de compte",
|
"accountNumber": "Numéro de compte",
|
||||||
"tvaMode": "Mode de TVA",
|
"tvaMode": "Mode de TVA",
|
||||||
@@ -441,6 +444,7 @@
|
|||||||
"categoryRequired": "Sélectionnez au moins une catégorie."
|
"categoryRequired": "Sélectionnez au moins une catégorie."
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
|
"title": "Contact {n}",
|
||||||
"lastName": "Nom",
|
"lastName": "Nom",
|
||||||
"firstName": "Prénom",
|
"firstName": "Prénom",
|
||||||
"jobTitle": "Fonction",
|
"jobTitle": "Fonction",
|
||||||
@@ -452,6 +456,7 @@
|
|||||||
"add": "Nouveau contact"
|
"add": "Nouveau contact"
|
||||||
},
|
},
|
||||||
"address": {
|
"address": {
|
||||||
|
"title": "Adresse {n}",
|
||||||
"sites": "Sites",
|
"sites": "Sites",
|
||||||
"contacts": "Contact(s) rattaché(s)",
|
"contacts": "Contact(s) rattaché(s)",
|
||||||
"country": "Pays",
|
"country": "Pays",
|
||||||
@@ -465,6 +470,7 @@
|
|||||||
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
|
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
|
||||||
},
|
},
|
||||||
"accounting": {
|
"accounting": {
|
||||||
|
"infoTitle": "Informations",
|
||||||
"siren": "SIREN",
|
"siren": "SIREN",
|
||||||
"accountNumber": "Numéro de compte",
|
"accountNumber": "Numéro de compte",
|
||||||
"tvaMode": "Mode de TVA",
|
"tvaMode": "Mode de TVA",
|
||||||
@@ -472,6 +478,7 @@
|
|||||||
"paymentDelay": "Délai de règlement",
|
"paymentDelay": "Délai de règlement",
|
||||||
"paymentType": "Type de règlement",
|
"paymentType": "Type de règlement",
|
||||||
"bank": "Banque",
|
"bank": "Banque",
|
||||||
|
"ribTitle": "RIB {n}",
|
||||||
"ribLabel": "Libellé",
|
"ribLabel": "Libellé",
|
||||||
"ribBic": "BIC",
|
"ribBic": "BIC",
|
||||||
"ribIban": "IBAN",
|
"ribIban": "IBAN",
|
||||||
@@ -628,6 +635,7 @@
|
|||||||
"uploadFailed": "Le téléversement de la décharge a échoué."
|
"uploadFailed": "Le téléversement de la décharge a échoué."
|
||||||
},
|
},
|
||||||
"address": {
|
"address": {
|
||||||
|
"title": "Adresse",
|
||||||
"country": "Pays",
|
"country": "Pays",
|
||||||
"postalCode": "Code postal",
|
"postalCode": "Code postal",
|
||||||
"city": "Ville",
|
"city": "Ville",
|
||||||
@@ -637,6 +645,7 @@
|
|||||||
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
|
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
|
"title": "Contact {n}",
|
||||||
"lastName": "Nom",
|
"lastName": "Nom",
|
||||||
"firstName": "Prénom",
|
"firstName": "Prénom",
|
||||||
"jobTitle": "Fonction",
|
"jobTitle": "Fonction",
|
||||||
@@ -654,6 +663,7 @@
|
|||||||
"confirm": "Supprimer"
|
"confirm": "Supprimer"
|
||||||
},
|
},
|
||||||
"price": {
|
"price": {
|
||||||
|
"title": "Prix {n}",
|
||||||
"direction": "Sens",
|
"direction": "Sens",
|
||||||
"directionClient": "Client",
|
"directionClient": "Client",
|
||||||
"directionSupplier": "Fournisseur",
|
"directionSupplier": "Fournisseur",
|
||||||
|
|||||||
@@ -1,203 +1,211 @@
|
|||||||
<template>
|
<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)]">
|
<!-- Bloc a plat (sans box-shadow) : un filet noir 1px le separe du suivant
|
||||||
<!-- ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). -->
|
(pas de bordure sous le dernier bloc). -->
|
||||||
<MalioButtonIcon
|
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
|
||||||
v-if="removable && !readonly && !disabled"
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
|
||||||
icon="mdi:delete-outline"
|
<div class="flex items-center justify-between">
|
||||||
variant="ghost"
|
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
|
||||||
button-class="absolute top-3 right-3"
|
<!-- ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). -->
|
||||||
v-bind="{ ariaLabel: t('commercial.clients.form.address.remove') }"
|
<MalioButtonIcon
|
||||||
@click="$emit('remove')"
|
v-if="removable && !readonly && !disabled"
|
||||||
/>
|
icon="mdi:delete-outline"
|
||||||
|
variant="ghost"
|
||||||
|
button-class="p-0"
|
||||||
|
v-bind="{ ariaLabel: t('commercial.clients.form.address.remove') }"
|
||||||
|
@click="$emit('remove')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Usage de l'adresse : Select unique (plus simple pour l'utilisateur)
|
<!-- Grille 4 colonnes des champs de l'adresse. -->
|
||||||
remplacant les 3 cases. Les options encodent les combinaisons valides
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
(exclusivite Prospect, RG-1.06/07/08) ; le back recoit toujours les
|
<!-- Usage de l'adresse : Select unique (plus simple pour l'utilisateur)
|
||||||
drapeaux isProspect / isDelivery / isBilling (aucune RG modifiee). -->
|
remplacant les 3 cases. Les options encodent les combinaisons valides
|
||||||
<!-- Erreur portee sur `isProspect` cote back (Callback type obligatoire +
|
(exclusivite Prospect, RG-1.06/07/08) ; le back recoit toujours les
|
||||||
exclusivite prospect) -> affichee sous le select Type d'adresse. -->
|
drapeaux isProspect / isDelivery / isBilling (aucune RG modifiee). -->
|
||||||
<MalioSelect
|
<!-- Erreur portee sur `isProspect` cote back (Callback type obligatoire +
|
||||||
:model-value="addressType"
|
exclusivite prospect) -> affichee sous le select Type d'adresse. -->
|
||||||
:options="addressTypeOptions"
|
<MalioSelect
|
||||||
:label="t('commercial.clients.form.address.addressType')"
|
:model-value="addressType"
|
||||||
:readonly="readonly"
|
:options="addressTypeOptions"
|
||||||
:disabled="disabled"
|
:label="t('commercial.clients.form.address.addressType')"
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.isProspect"
|
|
||||||
@update:model-value="onAddressTypeChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 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"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.sites"
|
|
||||||
@update:model-value="(v: (string | number)[]) => update('siteIris', v.map(String))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Contacts rattaches (M2M, facultatif). Consultation : masque si aucun (ERP-193). -->
|
|
||||||
<MalioSelectCheckbox
|
|
||||||
v-if="!hideEmpty || isFilled(model.contactIris)"
|
|
||||||
:model-value="model.contactIris"
|
|
||||||
:options="contactOptions"
|
|
||||||
:label="t('commercial.clients.form.address.contacts')"
|
|
||||||
:display-tag="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
@update:model-value="(v: (string | number)[]) => update('contactIris', v.map(String))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 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"
|
|
||||||
:label="t('commercial.clients.form.address.billingEmail')"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
: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"
|
|
||||||
/>
|
|
||||||
<!-- Filler : aligne la suite de la grille (Categorie au debut de ligne).
|
|
||||||
Inutile en consultation masquee (la grille se recompose sans les
|
|
||||||
champs vides, ERP-193). -->
|
|
||||||
<div v-else-if="!hideEmpty" aria-hidden="true" />
|
|
||||||
|
|
||||||
<MalioInputEmail
|
|
||||||
v-if="isBillingEmailRequired(model) && model.hasSecondaryBillingEmail"
|
|
||||||
:model-value="model.billingEmailSecondary"
|
|
||||||
:label="t('commercial.clients.form.address.billingEmailSecondary')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:lowercase="true"
|
|
||||||
:error="errors?.billingEmailSecondary"
|
|
||||||
@update:model-value="(v: string) => update('billingEmailSecondary', v)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MalioSelectCheckbox
|
|
||||||
:model-value="model.categoryIris"
|
|
||||||
:options="categoryOptions"
|
|
||||||
:label="t('commercial.clients.form.address.categories')"
|
|
||||||
:display-tag="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.categories"
|
|
||||||
@update:model-value="(v: (string | number)[]) => update('categoryIris', v.map(String))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MalioSelect
|
|
||||||
:model-value="model.country"
|
|
||||||
:options="countryOptions"
|
|
||||||
:label="t('commercial.clients.form.address.country')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
@update:model-value="(v: string | number | null) => update('country', String(v ?? 'France'))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MalioInputText
|
|
||||||
:model-value="model.postalCode"
|
|
||||||
:label="t('commercial.clients.form.address.postalCode')"
|
|
||||||
:mask="POSTAL_CODE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.postalCode"
|
|
||||||
@update:model-value="onPostalCodeChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Ville : MalioSelect alimente par le code postal (BAN). Si la BAN est
|
|
||||||
indisponible, bascule en saisie libre — recuperable : re-saisir le
|
|
||||||
code postal relance la recherche et repasse en select au succes. -->
|
|
||||||
<MalioSelect
|
|
||||||
v-if="!degraded"
|
|
||||||
:model-value="model.city"
|
|
||||||
:options="cityOptions"
|
|
||||||
:label="t('commercial.clients.form.address.city')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
empty-option-label=""
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.city"
|
|
||||||
@update:model-value="onCityChange"
|
|
||||||
/>
|
|
||||||
<MalioInputText
|
|
||||||
v-else
|
|
||||||
:model-value="model.city"
|
|
||||||
:label="t('commercial.clients.form.address.city')"
|
|
||||||
:mask="ADDRESS_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.city"
|
|
||||||
@update:model-value="(v: string) => update('city', v)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Adresse + Adresse complementaire sur 2 colonnes : on wrappe car
|
|
||||||
MalioInputText/Autocomplete (inheritAttrs:false) renvoient `class`
|
|
||||||
sur l'input interne, pas sur la cellule de grille. Le wrapper porte
|
|
||||||
le col-span-2, le champ le remplit (w-full). -->
|
|
||||||
<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). 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 && !disabled"
|
|
||||||
:model-value="model.street"
|
|
||||||
:options="addressOptions"
|
|
||||||
:loading="addressLoading"
|
|
||||||
:min-search-length="3"
|
|
||||||
:label="t('commercial.clients.form.address.street')"
|
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:required="!readonly && !disabled"
|
:required="!readonly && !disabled"
|
||||||
:error="errors?.street"
|
:error="errors?.isProspect"
|
||||||
:allow-create="true"
|
@update:model-value="onAddressTypeChange"
|
||||||
: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"
|
<!-- Sites Starseed : multiselect a tags (>= 1 obligatoire, RG-1.10). -->
|
||||||
@select="onAddressSelect"
|
<MalioSelectCheckbox
|
||||||
|
:model-value="model.siteIris"
|
||||||
|
:options="siteOptions"
|
||||||
|
:label="t('commercial.clients.form.address.sites')"
|
||||||
|
:display-tag="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.sites"
|
||||||
|
@update:model-value="(v: (string | number)[]) => update('siteIris', v.map(String))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Contacts rattaches (M2M, facultatif). Consultation : masque si aucun (ERP-193). -->
|
||||||
|
<MalioSelectCheckbox
|
||||||
|
v-if="!hideEmpty || isFilled(model.contactIris)"
|
||||||
|
:model-value="model.contactIris"
|
||||||
|
:options="contactOptions"
|
||||||
|
:label="t('commercial.clients.form.address.contacts')"
|
||||||
|
:display-tag="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
@update:model-value="(v: (string | number)[]) => update('contactIris', v.map(String))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 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"
|
||||||
|
:label="t('commercial.clients.form.address.billingEmail')"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
: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"
|
||||||
|
/>
|
||||||
|
<!-- Filler : aligne la suite de la grille (Categorie au debut de ligne).
|
||||||
|
Inutile en consultation masquee (la grille se recompose sans les
|
||||||
|
champs vides, ERP-193). -->
|
||||||
|
<div v-else-if="!hideEmpty" aria-hidden="true" />
|
||||||
|
|
||||||
|
<MalioInputEmail
|
||||||
|
v-if="isBillingEmailRequired(model) && model.hasSecondaryBillingEmail"
|
||||||
|
:model-value="model.billingEmailSecondary"
|
||||||
|
:label="t('commercial.clients.form.address.billingEmailSecondary')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:lowercase="true"
|
||||||
|
:error="errors?.billingEmailSecondary"
|
||||||
|
@update:model-value="(v: string) => update('billingEmailSecondary', v)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MalioSelectCheckbox
|
||||||
|
:model-value="model.categoryIris"
|
||||||
|
:options="categoryOptions"
|
||||||
|
:label="t('commercial.clients.form.address.categories')"
|
||||||
|
:display-tag="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.categories"
|
||||||
|
@update:model-value="(v: (string | number)[]) => update('categoryIris', v.map(String))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MalioSelect
|
||||||
|
:model-value="model.country"
|
||||||
|
:options="countryOptions"
|
||||||
|
:label="t('commercial.clients.form.address.country')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
@update:model-value="(v: string | number | null) => update('country', String(v ?? 'France'))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MalioInputText
|
||||||
|
:model-value="model.postalCode"
|
||||||
|
:label="t('commercial.clients.form.address.postalCode')"
|
||||||
|
:mask="POSTAL_CODE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.postalCode"
|
||||||
|
@update:model-value="onPostalCodeChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Ville : MalioSelect alimente par le code postal (BAN). Si la BAN est
|
||||||
|
indisponible, bascule en saisie libre — recuperable : re-saisir le
|
||||||
|
code postal relance la recherche et repasse en select au succes. -->
|
||||||
|
<MalioSelect
|
||||||
|
v-if="!degraded"
|
||||||
|
:model-value="model.city"
|
||||||
|
:options="cityOptions"
|
||||||
|
:label="t('commercial.clients.form.address.city')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.city"
|
||||||
|
@update:model-value="onCityChange"
|
||||||
/>
|
/>
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-else
|
v-else
|
||||||
:model-value="model.street"
|
:model-value="model.city"
|
||||||
:label="t('commercial.clients.form.address.street')"
|
:label="t('commercial.clients.form.address.city')"
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.street"
|
|
||||||
@update:model-value="(v: string) => update('street', v)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!hideEmpty || isFilled(model.streetComplement)" class="col-span-1">
|
|
||||||
<MalioInputText
|
|
||||||
:model-value="model.streetComplement"
|
|
||||||
:label="t('commercial.clients.form.address.streetComplement')"
|
|
||||||
:mask="ADDRESS_MASK"
|
:mask="ADDRESS_MASK"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:error="errors?.streetComplement"
|
:required="!readonly && !disabled"
|
||||||
@update:model-value="(v: string) => update('streetComplement', v)"
|
:error="errors?.city"
|
||||||
|
@update:model-value="(v: string) => update('city', v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<!-- Adresse + Adresse complementaire sur 2 colonnes : on wrappe car
|
||||||
|
MalioInputText/Autocomplete (inheritAttrs:false) renvoient `class`
|
||||||
|
sur l'input interne, pas sur la cellule de grille. Le wrapper porte
|
||||||
|
le col-span-2, le champ le remplit (w-full). -->
|
||||||
|
<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). 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 && !disabled"
|
||||||
|
:model-value="model.street"
|
||||||
|
:options="addressOptions"
|
||||||
|
:loading="addressLoading"
|
||||||
|
:min-search-length="3"
|
||||||
|
:label="t('commercial.clients.form.address.street')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
: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"
|
||||||
|
/>
|
||||||
|
<MalioInputText
|
||||||
|
v-else
|
||||||
|
:model-value="model.street"
|
||||||
|
:label="t('commercial.clients.form.address.street')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.street"
|
||||||
|
@update:model-value="(v: string) => update('street', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!hideEmpty || isFilled(model.streetComplement)" class="col-span-1">
|
||||||
|
<MalioInputText
|
||||||
|
:model-value="model.streetComplement"
|
||||||
|
:label="t('commercial.clients.form.address.streetComplement')"
|
||||||
|
:mask="ADDRESS_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.streetComplement"
|
||||||
|
@update:model-value="(v: string) => update('streetComplement', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -230,6 +238,8 @@ const props = defineProps<{
|
|||||||
/** Pays disponibles (France par defaut). */
|
/** Pays disponibles (France par defaut). */
|
||||||
countryOptions: RefOption[]
|
countryOptions: RefOption[]
|
||||||
removable?: boolean
|
removable?: boolean
|
||||||
|
/** Dernier bloc de la liste : supprime le filet de separation bas. */
|
||||||
|
last?: boolean
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
|||||||
@@ -1,84 +1,93 @@
|
|||||||
<template>
|
<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)]">
|
<!-- Bloc a plat (sans box-shadow) : un filet noir 1px le separe du suivant
|
||||||
<!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si
|
(pas de bordure sous le dernier bloc). -->
|
||||||
non supprimable (1er bloc obligatoire RG-1.14) ou en lecture seule.
|
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
|
||||||
ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). -->
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
|
||||||
<MalioButtonIcon
|
<div class="flex items-center justify-between">
|
||||||
v-if="removable && !readonly && !disabled"
|
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
|
||||||
icon="mdi:delete-outline"
|
<!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si
|
||||||
variant="ghost"
|
non supprimable (1er bloc obligatoire RG-1.14) ou en lecture seule.
|
||||||
button-class="absolute top-3 right-3"
|
ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). -->
|
||||||
v-bind="{ ariaLabel: t('commercial.clients.form.contact.remove') }"
|
<MalioButtonIcon
|
||||||
@click="$emit('remove')"
|
v-if="removable && !readonly && !disabled"
|
||||||
/>
|
icon="mdi:delete-outline"
|
||||||
|
variant="ghost"
|
||||||
<MalioInputText
|
button-class="p-0"
|
||||||
v-if="!hideEmpty || isFilled(model.lastName)"
|
v-bind="{ ariaLabel: t('commercial.clients.form.contact.remove') }"
|
||||||
:model-value="model.lastName"
|
@click="$emit('remove')"
|
||||||
:label="t('commercial.clients.form.contact.lastName')"
|
/>
|
||||||
:mask="PERSON_NAME_MASK"
|
</div>
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
<!-- Grille 4 colonnes des champs du contact. -->
|
||||||
:error="errors?.lastName"
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
@update:model-value="(v: string) => update('lastName', v)"
|
<MalioInputText
|
||||||
/>
|
v-if="!hideEmpty || isFilled(model.lastName)"
|
||||||
<MalioInputText
|
:model-value="model.lastName"
|
||||||
v-if="!hideEmpty || isFilled(model.firstName)"
|
:label="t('commercial.clients.form.contact.lastName')"
|
||||||
:model-value="model.firstName"
|
:mask="PERSON_NAME_MASK"
|
||||||
:label="t('commercial.clients.form.contact.firstName')"
|
:readonly="readonly"
|
||||||
:mask="PERSON_NAME_MASK"
|
:disabled="disabled"
|
||||||
:readonly="readonly"
|
:error="errors?.lastName"
|
||||||
:disabled="disabled"
|
@update:model-value="(v: string) => update('lastName', v)"
|
||||||
:error="errors?.firstName"
|
/>
|
||||||
@update:model-value="(v: string) => update('firstName', v)"
|
<MalioInputText
|
||||||
/>
|
v-if="!hideEmpty || isFilled(model.firstName)"
|
||||||
<!-- Fonction sur 2 colonnes : on wrappe car MalioInputText
|
:model-value="model.firstName"
|
||||||
(inheritAttrs:false) renvoie `class` sur l'input interne, pas sur la
|
:label="t('commercial.clients.form.contact.firstName')"
|
||||||
cellule de grille. Le wrapper porte le col-span-2, le champ le remplit. -->
|
:mask="PERSON_NAME_MASK"
|
||||||
<div v-if="!hideEmpty || isFilled(model.jobTitle)" class="col-span-2">
|
:readonly="readonly"
|
||||||
<MalioInputText
|
:disabled="disabled"
|
||||||
:model-value="model.jobTitle"
|
:error="errors?.firstName"
|
||||||
:label="t('commercial.clients.form.contact.jobTitle')"
|
@update:model-value="(v: string) => update('firstName', v)"
|
||||||
:mask="FREE_TEXT_MASK"
|
/>
|
||||||
:readonly="readonly"
|
<!-- Fonction sur 2 colonnes : on wrappe car MalioInputText
|
||||||
:disabled="disabled"
|
(inheritAttrs:false) renvoie `class` sur l'input interne, pas sur la
|
||||||
:error="errors?.jobTitle"
|
cellule de grille. Le wrapper porte le col-span-2, le champ le remplit. -->
|
||||||
@update:model-value="(v: string) => update('jobTitle', v)"
|
<div v-if="!hideEmpty || isFilled(model.jobTitle)" class="col-span-2">
|
||||||
|
<MalioInputText
|
||||||
|
:model-value="model.jobTitle"
|
||||||
|
:label="t('commercial.clients.form.contact.jobTitle')"
|
||||||
|
:mask="FREE_TEXT_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.jobTitle"
|
||||||
|
@update:model-value="(v: string) => update('jobTitle', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<MalioInputEmail
|
||||||
|
v-if="!hideEmpty || isFilled(model.email)"
|
||||||
|
:model-value="model.email"
|
||||||
|
:label="t('commercial.clients.form.contact.email')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:lowercase="true"
|
||||||
|
:error="errors?.email"
|
||||||
|
@update:model-value="(v: string) => update('email', v)"
|
||||||
|
/>
|
||||||
|
<MalioInputPhone
|
||||||
|
v-if="!hideEmpty || isFilled(model.phonePrimary)"
|
||||||
|
:model-value="model.phonePrimary"
|
||||||
|
:label="t('commercial.clients.form.contact.phonePrimary')"
|
||||||
|
:mask="PHONE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.phonePrimary"
|
||||||
|
:addable="!model.hasSecondaryPhone && !readonly"
|
||||||
|
:add-button-label="t('commercial.clients.form.contact.addPhone')"
|
||||||
|
@update:model-value="(v: string) => update('phonePrimary', v)"
|
||||||
|
@add="revealSecondaryPhone"
|
||||||
|
/>
|
||||||
|
<MalioInputPhone
|
||||||
|
v-if="model.hasSecondaryPhone && (!hideEmpty || isFilled(model.phoneSecondary))"
|
||||||
|
:model-value="model.phoneSecondary"
|
||||||
|
:label="t('commercial.clients.form.contact.phoneSecondary')"
|
||||||
|
:mask="PHONE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.phoneSecondary"
|
||||||
|
@update:model-value="(v: string) => update('phoneSecondary', v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MalioInputEmail
|
|
||||||
v-if="!hideEmpty || isFilled(model.email)"
|
|
||||||
:model-value="model.email"
|
|
||||||
:label="t('commercial.clients.form.contact.email')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:lowercase="true"
|
|
||||||
:error="errors?.email"
|
|
||||||
@update:model-value="(v: string) => update('email', v)"
|
|
||||||
/>
|
|
||||||
<MalioInputPhone
|
|
||||||
v-if="!hideEmpty || isFilled(model.phonePrimary)"
|
|
||||||
:model-value="model.phonePrimary"
|
|
||||||
:label="t('commercial.clients.form.contact.phonePrimary')"
|
|
||||||
:mask="PHONE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.phonePrimary"
|
|
||||||
:addable="!model.hasSecondaryPhone && !readonly"
|
|
||||||
:add-button-label="t('commercial.clients.form.contact.addPhone')"
|
|
||||||
@update:model-value="(v: string) => update('phonePrimary', v)"
|
|
||||||
@add="revealSecondaryPhone"
|
|
||||||
/>
|
|
||||||
<MalioInputPhone
|
|
||||||
v-if="model.hasSecondaryPhone && (!hideEmpty || isFilled(model.phoneSecondary))"
|
|
||||||
:model-value="model.phoneSecondary"
|
|
||||||
:label="t('commercial.clients.form.contact.phoneSecondary')"
|
|
||||||
:mask="PHONE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.phoneSecondary"
|
|
||||||
@update:model-value="(v: string) => update('phoneSecondary', v)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -98,6 +107,8 @@ const props = defineProps<{
|
|||||||
title: string
|
title: string
|
||||||
/** Affiche l'icone de suppression (1er bloc non supprimable, RG-1.14). */
|
/** Affiche l'icone de suppression (1er bloc non supprimable, RG-1.14). */
|
||||||
removable?: boolean
|
removable?: boolean
|
||||||
|
/** Dernier bloc de la liste : supprime le filet de separation bas. */
|
||||||
|
last?: boolean
|
||||||
/** Bloc en lecture seule (onglet valide). */
|
/** Bloc en lecture seule (onglet valide). */
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
||||||
|
|||||||
@@ -1,189 +1,198 @@
|
|||||||
<template>
|
<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)]">
|
<!-- Bloc a plat (sans box-shadow) : un filet noir 1px le separe du suivant
|
||||||
<!-- Suppression : modal de confirmation cote parent. -->
|
(pas de bordure sous le dernier bloc). -->
|
||||||
<MalioButtonIcon
|
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
|
||||||
v-if="removable && !readonly && !disabled"
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
|
||||||
icon="mdi:delete-outline"
|
<div class="flex items-center justify-between">
|
||||||
variant="ghost"
|
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
|
||||||
button-class="absolute top-3 right-3"
|
<!-- Suppression : modal de confirmation cote parent. -->
|
||||||
v-bind="{ ariaLabel: t('commercial.suppliers.form.address.remove') }"
|
<MalioButtonIcon
|
||||||
@click="$emit('remove')"
|
v-if="removable && !readonly && !disabled"
|
||||||
/>
|
icon="mdi:delete-outline"
|
||||||
|
variant="ghost"
|
||||||
|
button-class="p-0"
|
||||||
|
v-bind="{ ariaLabel: t('commercial.suppliers.form.address.remove') }"
|
||||||
|
@click="$emit('remove')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Type d'adresse : Prospect / Depart / Rendu (RG-2.09). Select en attendant
|
<!-- Grille 4 colonnes des champs de l'adresse. -->
|
||||||
l'arbitrage metier (radio vs select) ; l'erreur 422 (propertyPath
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
`addressType`) s'affiche via la prop native :error de MalioSelect. -->
|
<!-- Type d'adresse : Prospect / Depart / Rendu (RG-2.09). Select en attendant
|
||||||
<MalioSelect
|
l'arbitrage metier (radio vs select) ; l'erreur 422 (propertyPath
|
||||||
:model-value="model.addressType"
|
`addressType`) s'affiche via la prop native :error de MalioSelect. -->
|
||||||
:options="addressTypeOptions"
|
<MalioSelect
|
||||||
:label="t('commercial.suppliers.form.address.addressType')"
|
:model-value="model.addressType"
|
||||||
:readonly="readonly"
|
:options="addressTypeOptions"
|
||||||
:disabled="disabled"
|
:label="t('commercial.suppliers.form.address.addressType')"
|
||||||
empty-option-label=""
|
:readonly="readonly"
|
||||||
:required="!readonly && !disabled"
|
:disabled="disabled"
|
||||||
:error="errors?.addressType"
|
empty-option-label=""
|
||||||
@update:model-value="(v: string | number | null) => update('addressType', v === null ? null : (v as SupplierAddressType))"
|
:required="!readonly && !disabled"
|
||||||
/>
|
:error="errors?.addressType"
|
||||||
|
@update:model-value="(v: string | number | null) => update('addressType', v === null ? null : (v as SupplierAddressType))"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Sites Starseed : multiselect a tags (>= 1 obligatoire, RG-2.06). -->
|
<!-- Sites Starseed : multiselect a tags (>= 1 obligatoire, RG-2.06). -->
|
||||||
<MalioSelectCheckbox
|
<MalioSelectCheckbox
|
||||||
:model-value="model.siteIris"
|
:model-value="model.siteIris"
|
||||||
:options="siteOptions"
|
:options="siteOptions"
|
||||||
:label="t('commercial.suppliers.form.address.sites')"
|
:label="t('commercial.suppliers.form.address.sites')"
|
||||||
:display-tag="true"
|
:display-tag="true"
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.sites"
|
|
||||||
@update:model-value="(v: (string | number)[]) => update('siteIris', v.map(String))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Contacts rattaches (M2M, facultatif). -->
|
|
||||||
<MalioSelectCheckbox
|
|
||||||
v-if="!hideEmpty || isFilled(model.contactIris)"
|
|
||||||
:model-value="model.contactIris"
|
|
||||||
:options="contactOptions"
|
|
||||||
:label="t('commercial.suppliers.form.address.contacts')"
|
|
||||||
:display-tag="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
@update:model-value="(v: (string | number)[]) => update('contactIris', v.map(String))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Filler : aligne le debut de ligne suivant sur la grille (le bloc client
|
|
||||||
porte ici l'email de facturation, absent cote fournisseur). Inutile en
|
|
||||||
consultation masquee (la grille se recompose sans les champs vides). -->
|
|
||||||
<div v-if="!hideEmpty" aria-hidden="true" />
|
|
||||||
|
|
||||||
<!-- Categories de type FOURNISSEUR (>= 1 obligatoire, RG-2.10). -->
|
|
||||||
<MalioSelectCheckbox
|
|
||||||
:model-value="model.categoryIris"
|
|
||||||
:options="categoryOptions"
|
|
||||||
:label="t('commercial.suppliers.form.address.categories')"
|
|
||||||
:display-tag="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.categories"
|
|
||||||
@update:model-value="(v: (string | number)[]) => update('categoryIris', v.map(String))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MalioSelect
|
|
||||||
:model-value="model.country"
|
|
||||||
:options="countryOptions"
|
|
||||||
:label="t('commercial.suppliers.form.address.country')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
@update:model-value="(v: string | number | null) => update('country', String(v ?? 'France'))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MalioInputText
|
|
||||||
:model-value="model.postalCode"
|
|
||||||
:label="t('commercial.suppliers.form.address.postalCode')"
|
|
||||||
:mask="POSTAL_CODE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.postalCode"
|
|
||||||
@update:model-value="onPostalCodeChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Ville : MalioSelect alimente par le code postal (BAN). Saisie libre si BAN indispo. -->
|
|
||||||
<MalioSelect
|
|
||||||
v-if="!degraded"
|
|
||||||
:model-value="model.city"
|
|
||||||
:options="cityOptions"
|
|
||||||
:label="t('commercial.suppliers.form.address.city')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
empty-option-label=""
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.city"
|
|
||||||
@update:model-value="onCityChange"
|
|
||||||
/>
|
|
||||||
<MalioInputText
|
|
||||||
v-else
|
|
||||||
:model-value="model.city"
|
|
||||||
:label="t('commercial.suppliers.form.address.city')"
|
|
||||||
:mask="ADDRESS_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.city"
|
|
||||||
@update:model-value="(v: string) => update('city', v)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Adresse (BAN) sur 2 colonnes + Adresse complementaire. allow-create : le
|
|
||||||
texte saisi est conserve si la BAN ne propose rien (saisie manuelle). -->
|
|
||||||
<div class="col-span-2">
|
|
||||||
<MalioInputAutocomplete
|
|
||||||
v-if="!readonly && !disabled"
|
|
||||||
:model-value="model.street"
|
|
||||||
:options="addressOptions"
|
|
||||||
:loading="addressLoading"
|
|
||||||
:min-search-length="3"
|
|
||||||
:label="t('commercial.suppliers.form.address.street')"
|
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:required="!readonly && !disabled"
|
:required="!readonly && !disabled"
|
||||||
:error="errors?.street"
|
:error="errors?.sites"
|
||||||
:allow-create="true"
|
@update:model-value="(v: (string | number)[]) => update('siteIris', v.map(String))"
|
||||||
:no-results-text="t('commercial.suppliers.form.address.streetNotFound')"
|
/>
|
||||||
@update:model-value="(v: string | number | null) => update('street', v === null ? null : String(v))"
|
|
||||||
@search="onAddressSearch"
|
<!-- Contacts rattaches (M2M, facultatif). -->
|
||||||
@select="onAddressSelect"
|
<MalioSelectCheckbox
|
||||||
|
v-if="!hideEmpty || isFilled(model.contactIris)"
|
||||||
|
:model-value="model.contactIris"
|
||||||
|
:options="contactOptions"
|
||||||
|
:label="t('commercial.suppliers.form.address.contacts')"
|
||||||
|
:display-tag="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
@update:model-value="(v: (string | number)[]) => update('contactIris', v.map(String))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Filler : aligne le debut de ligne suivant sur la grille (le bloc client
|
||||||
|
porte ici l'email de facturation, absent cote fournisseur). Inutile en
|
||||||
|
consultation masquee (la grille se recompose sans les champs vides). -->
|
||||||
|
<div v-if="!hideEmpty" aria-hidden="true" />
|
||||||
|
|
||||||
|
<!-- Categories de type FOURNISSEUR (>= 1 obligatoire, RG-2.10). -->
|
||||||
|
<MalioSelectCheckbox
|
||||||
|
:model-value="model.categoryIris"
|
||||||
|
:options="categoryOptions"
|
||||||
|
:label="t('commercial.suppliers.form.address.categories')"
|
||||||
|
:display-tag="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.categories"
|
||||||
|
@update:model-value="(v: (string | number)[]) => update('categoryIris', v.map(String))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MalioSelect
|
||||||
|
:model-value="model.country"
|
||||||
|
:options="countryOptions"
|
||||||
|
:label="t('commercial.suppliers.form.address.country')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
@update:model-value="(v: string | number | null) => update('country', String(v ?? 'France'))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MalioInputText
|
||||||
|
:model-value="model.postalCode"
|
||||||
|
:label="t('commercial.suppliers.form.address.postalCode')"
|
||||||
|
:mask="POSTAL_CODE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.postalCode"
|
||||||
|
@update:model-value="onPostalCodeChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Ville : MalioSelect alimente par le code postal (BAN). Saisie libre si BAN indispo. -->
|
||||||
|
<MalioSelect
|
||||||
|
v-if="!degraded"
|
||||||
|
:model-value="model.city"
|
||||||
|
:options="cityOptions"
|
||||||
|
:label="t('commercial.suppliers.form.address.city')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.city"
|
||||||
|
@update:model-value="onCityChange"
|
||||||
/>
|
/>
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-else
|
v-else
|
||||||
:model-value="model.street"
|
:model-value="model.city"
|
||||||
:label="t('commercial.suppliers.form.address.street')"
|
:label="t('commercial.suppliers.form.address.city')"
|
||||||
:mask="ADDRESS_MASK"
|
:mask="ADDRESS_MASK"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:required="!readonly && !disabled"
|
:required="!readonly && !disabled"
|
||||||
:error="errors?.street"
|
:error="errors?.city"
|
||||||
@update:model-value="(v: string) => update('street', v)"
|
@update:model-value="(v: string) => update('city', v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!hideEmpty || isFilled(model.streetComplement)" class="col-span-1">
|
<!-- Adresse (BAN) sur 2 colonnes + Adresse complementaire. allow-create : le
|
||||||
<MalioInputText
|
texte saisi est conserve si la BAN ne propose rien (saisie manuelle). -->
|
||||||
:model-value="model.streetComplement"
|
<div class="col-span-2">
|
||||||
:label="t('commercial.suppliers.form.address.streetComplement')"
|
<MalioInputAutocomplete
|
||||||
:mask="ADDRESS_MASK"
|
v-if="!readonly && !disabled"
|
||||||
|
:model-value="model.street"
|
||||||
|
:options="addressOptions"
|
||||||
|
:loading="addressLoading"
|
||||||
|
:min-search-length="3"
|
||||||
|
:label="t('commercial.suppliers.form.address.street')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.street"
|
||||||
|
:allow-create="true"
|
||||||
|
:no-results-text="t('commercial.suppliers.form.address.streetNotFound')"
|
||||||
|
@update:model-value="(v: string | number | null) => update('street', v === null ? null : String(v))"
|
||||||
|
@search="onAddressSearch"
|
||||||
|
@select="onAddressSelect"
|
||||||
|
/>
|
||||||
|
<MalioInputText
|
||||||
|
v-else
|
||||||
|
:model-value="model.street"
|
||||||
|
:label="t('commercial.suppliers.form.address.street')"
|
||||||
|
:mask="ADDRESS_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.street"
|
||||||
|
@update:model-value="(v: string) => update('street', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!hideEmpty || isFilled(model.streetComplement)" class="col-span-1">
|
||||||
|
<MalioInputText
|
||||||
|
:model-value="model.streetComplement"
|
||||||
|
:label="t('commercial.suppliers.form.address.streetComplement')"
|
||||||
|
:mask="ADDRESS_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.streetComplement"
|
||||||
|
@update:model-value="(v: string) => update('streetComplement', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bennes : stepper (specifique fournisseur, defaut 0). En consultation, 0
|
||||||
|
reste affiche (valeur saisie) ; seul un champ vide serait masque. -->
|
||||||
|
<MalioInputNumber
|
||||||
|
v-if="!hideEmpty || isFilled(model.bennes)"
|
||||||
|
:model-value="model.bennes"
|
||||||
|
:label="t('commercial.suppliers.form.address.bennes')"
|
||||||
|
:min="0"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:error="errors?.streetComplement"
|
:error="errors?.bennes"
|
||||||
@update:model-value="(v: string) => update('streetComplement', v)"
|
@update:model-value="(v: string) => update('bennes', v)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Prestation de triage : booleen porte par l'adresse (specifique fournisseur).
|
||||||
|
Consultation : masquee si non cochee (ERP-193). -->
|
||||||
|
<MalioCheckbox
|
||||||
|
v-if="!hideEmpty || isFilled(model.triageProvider)"
|
||||||
|
id="address-triage-provider"
|
||||||
|
:label="t('commercial.suppliers.form.address.triageProvider')"
|
||||||
|
:model-value="model.triageProvider"
|
||||||
|
group-class="self-center"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
@update:model-value="(v: boolean) => update('triageProvider', v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bennes : stepper (specifique fournisseur, defaut 0). En consultation, 0
|
|
||||||
reste affiche (valeur saisie) ; seul un champ vide serait masque. -->
|
|
||||||
<MalioInputNumber
|
|
||||||
v-if="!hideEmpty || isFilled(model.bennes)"
|
|
||||||
:model-value="model.bennes"
|
|
||||||
:label="t('commercial.suppliers.form.address.bennes')"
|
|
||||||
:min="0"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.bennes"
|
|
||||||
@update:model-value="(v: string) => update('bennes', v)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Prestation de triage : booleen porte par l'adresse (specifique fournisseur).
|
|
||||||
Consultation : masquee si non cochee (ERP-193). -->
|
|
||||||
<MalioCheckbox
|
|
||||||
v-if="!hideEmpty || isFilled(model.triageProvider)"
|
|
||||||
id="address-triage-provider"
|
|
||||||
:label="t('commercial.suppliers.form.address.triageProvider')"
|
|
||||||
:model-value="model.triageProvider"
|
|
||||||
group-class="self-center"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
@update:model-value="(v: boolean) => update('triageProvider', v)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -210,6 +219,8 @@ const props = defineProps<{
|
|||||||
/** Pays disponibles (France par defaut). */
|
/** Pays disponibles (France par defaut). */
|
||||||
countryOptions: RefOption[]
|
countryOptions: RefOption[]
|
||||||
removable?: boolean
|
removable?: boolean
|
||||||
|
/** Dernier bloc de la liste : supprime le filet de separation bas. */
|
||||||
|
last?: boolean
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
|||||||
@@ -1,83 +1,92 @@
|
|||||||
<template>
|
<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)]">
|
<!-- Bloc a plat (sans box-shadow) : un filet noir 1px le separe du suivant
|
||||||
<!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si
|
(pas de bordure sous le dernier bloc). -->
|
||||||
non supprimable (1er bloc, RG-2.13) ou en lecture seule. -->
|
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
|
||||||
<MalioButtonIcon
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
|
||||||
v-if="removable && !readonly && !disabled"
|
<div class="flex items-center justify-between">
|
||||||
icon="mdi:delete-outline"
|
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
|
||||||
variant="ghost"
|
<!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si
|
||||||
button-class="absolute top-3 right-3"
|
non supprimable (1er bloc, RG-2.13) ou en lecture seule. -->
|
||||||
v-bind="{ ariaLabel: t('commercial.suppliers.form.contact.remove') }"
|
<MalioButtonIcon
|
||||||
@click="$emit('remove')"
|
v-if="removable && !readonly && !disabled"
|
||||||
/>
|
icon="mdi:delete-outline"
|
||||||
|
variant="ghost"
|
||||||
<MalioInputText
|
button-class="p-0"
|
||||||
v-if="!hideEmpty || isFilled(model.lastName)"
|
v-bind="{ ariaLabel: t('commercial.suppliers.form.contact.remove') }"
|
||||||
:model-value="model.lastName"
|
@click="$emit('remove')"
|
||||||
:label="t('commercial.suppliers.form.contact.lastName')"
|
/>
|
||||||
:mask="PERSON_NAME_MASK"
|
</div>
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
<!-- Grille 4 colonnes des champs du contact. -->
|
||||||
:error="errors?.lastName"
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
@update:model-value="(v: string) => update('lastName', v)"
|
<MalioInputText
|
||||||
/>
|
v-if="!hideEmpty || isFilled(model.lastName)"
|
||||||
<MalioInputText
|
:model-value="model.lastName"
|
||||||
v-if="!hideEmpty || isFilled(model.firstName)"
|
:label="t('commercial.suppliers.form.contact.lastName')"
|
||||||
:model-value="model.firstName"
|
:mask="PERSON_NAME_MASK"
|
||||||
:label="t('commercial.suppliers.form.contact.firstName')"
|
:readonly="readonly"
|
||||||
:mask="PERSON_NAME_MASK"
|
:disabled="disabled"
|
||||||
:readonly="readonly"
|
:error="errors?.lastName"
|
||||||
:disabled="disabled"
|
@update:model-value="(v: string) => update('lastName', v)"
|
||||||
:error="errors?.firstName"
|
/>
|
||||||
@update:model-value="(v: string) => update('firstName', v)"
|
<MalioInputText
|
||||||
/>
|
v-if="!hideEmpty || isFilled(model.firstName)"
|
||||||
<!-- Fonction sur 2 colonnes : on wrappe car MalioInputText
|
:model-value="model.firstName"
|
||||||
(inheritAttrs:false) renvoie `class` sur l'input interne, pas sur la
|
:label="t('commercial.suppliers.form.contact.firstName')"
|
||||||
cellule de grille. Le wrapper porte le col-span-2, le champ le remplit. -->
|
:mask="PERSON_NAME_MASK"
|
||||||
<div v-if="!hideEmpty || isFilled(model.jobTitle)" class="col-span-2">
|
:readonly="readonly"
|
||||||
<MalioInputText
|
:disabled="disabled"
|
||||||
:model-value="model.jobTitle"
|
:error="errors?.firstName"
|
||||||
:label="t('commercial.suppliers.form.contact.jobTitle')"
|
@update:model-value="(v: string) => update('firstName', v)"
|
||||||
:mask="FREE_TEXT_MASK"
|
/>
|
||||||
:readonly="readonly"
|
<!-- Fonction sur 2 colonnes : on wrappe car MalioInputText
|
||||||
:disabled="disabled"
|
(inheritAttrs:false) renvoie `class` sur l'input interne, pas sur la
|
||||||
:error="errors?.jobTitle"
|
cellule de grille. Le wrapper porte le col-span-2, le champ le remplit. -->
|
||||||
@update:model-value="(v: string) => update('jobTitle', v)"
|
<div v-if="!hideEmpty || isFilled(model.jobTitle)" class="col-span-2">
|
||||||
|
<MalioInputText
|
||||||
|
:model-value="model.jobTitle"
|
||||||
|
:label="t('commercial.suppliers.form.contact.jobTitle')"
|
||||||
|
:mask="FREE_TEXT_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.jobTitle"
|
||||||
|
@update:model-value="(v: string) => update('jobTitle', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<MalioInputEmail
|
||||||
|
v-if="!hideEmpty || isFilled(model.email)"
|
||||||
|
:model-value="model.email"
|
||||||
|
:label="t('commercial.suppliers.form.contact.email')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:lowercase="true"
|
||||||
|
:error="errors?.email"
|
||||||
|
@update:model-value="(v: string) => update('email', v)"
|
||||||
|
/>
|
||||||
|
<MalioInputPhone
|
||||||
|
v-if="!hideEmpty || isFilled(model.phonePrimary)"
|
||||||
|
:model-value="model.phonePrimary"
|
||||||
|
:label="t('commercial.suppliers.form.contact.phonePrimary')"
|
||||||
|
:mask="PHONE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
: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 && (!hideEmpty || isFilled(model.phoneSecondary))"
|
||||||
|
:model-value="model.phoneSecondary"
|
||||||
|
:label="t('commercial.suppliers.form.contact.phoneSecondary')"
|
||||||
|
:mask="PHONE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.phoneSecondary"
|
||||||
|
@update:model-value="(v: string) => update('phoneSecondary', v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MalioInputEmail
|
|
||||||
v-if="!hideEmpty || isFilled(model.email)"
|
|
||||||
:model-value="model.email"
|
|
||||||
:label="t('commercial.suppliers.form.contact.email')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:lowercase="true"
|
|
||||||
:error="errors?.email"
|
|
||||||
@update:model-value="(v: string) => update('email', v)"
|
|
||||||
/>
|
|
||||||
<MalioInputPhone
|
|
||||||
v-if="!hideEmpty || isFilled(model.phonePrimary)"
|
|
||||||
:model-value="model.phonePrimary"
|
|
||||||
:label="t('commercial.suppliers.form.contact.phonePrimary')"
|
|
||||||
:mask="PHONE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
: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 && (!hideEmpty || isFilled(model.phoneSecondary))"
|
|
||||||
:model-value="model.phoneSecondary"
|
|
||||||
:label="t('commercial.suppliers.form.contact.phoneSecondary')"
|
|
||||||
:mask="PHONE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.phoneSecondary"
|
|
||||||
@update:model-value="(v: string) => update('phoneSecondary', v)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -96,6 +105,8 @@ const props = defineProps<{
|
|||||||
title: string
|
title: string
|
||||||
/** Affiche l'icone de suppression (1er bloc non supprimable, RG-2.13). */
|
/** Affiche l'icone de suppression (1er bloc non supprimable, RG-2.13). */
|
||||||
removable?: boolean
|
removable?: boolean
|
||||||
|
/** Dernier bloc de la liste : supprime le filet de separation bas. */
|
||||||
|
last?: boolean
|
||||||
/** Bloc en lecture seule (onglet valide). */
|
/** Bloc en lecture seule (onglet valide). */
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
||||||
|
|||||||
@@ -77,4 +77,23 @@ describe('useClientReferentials.loadCommon (resilience ERP-102)', () => {
|
|||||||
// Le libelle d'un site est son numero de departement (2 premiers chiffres du code postal).
|
// Le libelle d'un site est son numero de departement (2 premiers chiffres du code postal).
|
||||||
expect(refs.sites.value).toEqual([{ value: '/api/sites/1', label: '86' }])
|
expect(refs.sites.value).toEqual([{ value: '/api/sites/1', label: '86' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('separe les categories CLIENT (formulaire) des categories ADRESSE (blocs adresse)', async () => {
|
||||||
|
// Le mock distingue les deux appels /categories par leur filtre typeCode.
|
||||||
|
mockGet.mockImplementation((url: string, query?: Record<string, unknown>) => {
|
||||||
|
if (url === '/categories' && query?.typeCode === 'CLIENT') {
|
||||||
|
return Promise.resolve({ member: [{ '@id': '/api/categories/1', code: 'SECTEUR', name: 'Secteur' }] })
|
||||||
|
}
|
||||||
|
if (url === '/categories' && query?.typeCode === 'ADRESSE') {
|
||||||
|
return Promise.resolve({ member: [{ '@id': '/api/categories/9', code: 'SIEGE', name: 'Siège' }] })
|
||||||
|
}
|
||||||
|
return Promise.resolve({ member: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
const refs = useClientReferentials()
|
||||||
|
await refs.loadCommon()
|
||||||
|
|
||||||
|
expect(refs.categories.value).toEqual([{ value: '/api/categories/1', label: 'Secteur', code: 'SECTEUR' }])
|
||||||
|
expect(refs.addressCategories.value).toEqual([{ value: '/api/categories/9', label: 'Siège', code: 'SIEGE' }])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ describe('useSupplierReferentials', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('charge les categories d\'adresse filtrees sur le type ADRESSE', async () => {
|
||||||
|
await useSupplierReferentials().loadCommon()
|
||||||
|
|
||||||
|
expect(mockGet).toHaveBeenCalledWith(
|
||||||
|
'/categories',
|
||||||
|
expect.objectContaining({ pagination: 'false', typeCode: 'ADRESSE' }),
|
||||||
|
expect.objectContaining({ toast: false }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('mappe les categories en options { value: IRI, label: name, code }', async () => {
|
it('mappe les categories en options { value: IRI, label: name, code }', async () => {
|
||||||
mockGet.mockImplementation((url: string) => {
|
mockGet.mockImplementation((url: string) => {
|
||||||
if (url === '/categories') {
|
if (url === '/categories') {
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ export function useClientReferentials() {
|
|||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const categories = ref<CategoryOption[]>([])
|
const categories = ref<CategoryOption[]>([])
|
||||||
|
// Taxonomie dediee aux blocs adresse (type ADRESSE), distincte des categories
|
||||||
|
// CLIENT du formulaire principal.
|
||||||
|
const addressCategories = ref<CategoryOption[]>([])
|
||||||
const sites = ref<RefOption[]>([])
|
const sites = ref<RefOption[]>([])
|
||||||
const tvaModes = ref<RefOption[]>([])
|
const tvaModes = ref<RefOption[]>([])
|
||||||
const paymentDelays = ref<RefOption[]>([])
|
const paymentDelays = ref<RefOption[]>([])
|
||||||
@@ -109,6 +112,9 @@ export function useClientReferentials() {
|
|||||||
// de type CLIENT (pas FOURNISSEUR) -> on filtre la collection cote API.
|
// de type CLIENT (pas FOURNISSEUR) -> on filtre la collection cote API.
|
||||||
fetchAll<CategoryMember>('/categories', { typeCode: 'CLIENT' })
|
fetchAll<CategoryMember>('/categories', { typeCode: 'CLIENT' })
|
||||||
.then((cats) => { categories.value = cats.map(c => ({ value: c['@id'], label: c.name, code: c.code })) }),
|
.then((cats) => { categories.value = cats.map(c => ({ value: c['@id'], label: c.name, code: c.code })) }),
|
||||||
|
// Categories des blocs adresse : taxonomie dediee type ADRESSE.
|
||||||
|
fetchAll<CategoryMember>('/categories', { typeCode: 'ADRESSE' })
|
||||||
|
.then((cats) => { addressCategories.value = cats.map(c => ({ value: c['@id'], label: c.name, code: c.code })) }),
|
||||||
fetchAll<SiteMember>('/sites')
|
fetchAll<SiteMember>('/sites')
|
||||||
// Libelle = numero de departement (2 premiers chiffres du code
|
// Libelle = numero de departement (2 premiers chiffres du code
|
||||||
// postal du site), ex: 86100 -> « 86 ». Le code postal est deja
|
// postal du site), ex: 86100 -> « 86 ». Le code postal est deja
|
||||||
@@ -151,6 +157,7 @@ export function useClientReferentials() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
categories,
|
categories,
|
||||||
|
addressCategories,
|
||||||
sites,
|
sites,
|
||||||
tvaModes,
|
tvaModes,
|
||||||
paymentDelays,
|
paymentDelays,
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ export function useSupplierReferentials() {
|
|||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const categories = ref<CategoryOption[]>([])
|
const categories = ref<CategoryOption[]>([])
|
||||||
|
// Taxonomie dediee aux blocs adresse (type ADRESSE), distincte des categories
|
||||||
|
// FOURNISSEUR du formulaire principal.
|
||||||
|
const addressCategories = ref<CategoryOption[]>([])
|
||||||
const sites = ref<RefOption[]>([])
|
const sites = ref<RefOption[]>([])
|
||||||
const tvaModes = ref<RefOption[]>([])
|
const tvaModes = ref<RefOption[]>([])
|
||||||
const paymentDelays = ref<RefOption[]>([])
|
const paymentDelays = ref<RefOption[]>([])
|
||||||
@@ -97,6 +100,9 @@ export function useSupplierReferentials() {
|
|||||||
// categories de type FOURNISSEUR (RG-2.10) -> on filtre cote API.
|
// categories de type FOURNISSEUR (RG-2.10) -> on filtre cote API.
|
||||||
fetchAll<CategoryMember>('/categories', { typeCode: 'FOURNISSEUR' })
|
fetchAll<CategoryMember>('/categories', { typeCode: 'FOURNISSEUR' })
|
||||||
.then((cats) => { categories.value = cats.map(c => ({ value: c['@id'], label: c.name, code: c.code })) }),
|
.then((cats) => { categories.value = cats.map(c => ({ value: c['@id'], label: c.name, code: c.code })) }),
|
||||||
|
// Categories des blocs adresse : taxonomie dediee type ADRESSE.
|
||||||
|
fetchAll<CategoryMember>('/categories', { typeCode: 'ADRESSE' })
|
||||||
|
.then((cats) => { addressCategories.value = cats.map(c => ({ value: c['@id'], label: c.name, code: c.code })) }),
|
||||||
fetchAll<SiteMember>('/sites')
|
fetchAll<SiteMember>('/sites')
|
||||||
// Libelle = numero de departement (2 premiers chiffres du code
|
// Libelle = numero de departement (2 premiers chiffres du code
|
||||||
// postal du site), ex: 86100 -> « 86 ».
|
// postal du site), ex: 86100 -> « 86 ».
|
||||||
@@ -121,6 +127,7 @@ export function useSupplierReferentials() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
categories,
|
categories,
|
||||||
|
addressCategories,
|
||||||
sites,
|
sites,
|
||||||
tvaModes,
|
tvaModes,
|
||||||
paymentDelays,
|
paymentDelays,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
<MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
<MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
||||||
<!-- Onglet Information -->
|
<!-- Onglet Information -->
|
||||||
<template #information>
|
<template #information>
|
||||||
<div class="mt-12 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)]">
|
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas
|
<!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas
|
||||||
sur les inputs (champ 40px centre dans un h-12 -> ~4px de
|
sur les inputs (champ 40px centre dans un h-12 -> ~4px de
|
||||||
coussin de chaque cote). -->
|
coussin de chaque cote). -->
|
||||||
@@ -178,6 +178,7 @@
|
|||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
:title="t('commercial.clients.form.contact.title', { n: index + 1 })"
|
:title="t('commercial.clients.form.contact.title', { n: index + 1 })"
|
||||||
:removable="isRowRemovable(contacts, index)"
|
:removable="isRowRemovable(contacts, index)"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
:disabled="businessReadonly"
|
:disabled="businessReadonly"
|
||||||
:errors="contactErrors[index]"
|
:errors="contactErrors[index]"
|
||||||
@update:model-value="(v) => contacts[index] = v"
|
@update:model-value="(v) => contacts[index] = v"
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
:key="address.id ?? `new-${index}`"
|
:key="address.id ?? `new-${index}`"
|
||||||
:model-value="address"
|
:model-value="address"
|
||||||
:title="t('commercial.clients.form.address.title', { n: index + 1 })"
|
:title="t('commercial.clients.form.address.title', { n: index + 1 })"
|
||||||
|
:last="index === addresses.length - 1"
|
||||||
:category-options="addressCategoryOptions"
|
:category-options="addressCategoryOptions"
|
||||||
:site-options="siteOptions"
|
:site-options="siteOptions"
|
||||||
:contact-options="contactOptions"
|
:contact-options="contactOptions"
|
||||||
@@ -244,8 +246,10 @@
|
|||||||
editable uniquement si accounting.manage). -->
|
editable uniquement si accounting.manage). -->
|
||||||
<template v-if="canAccountingView" #accounting>
|
<template v-if="canAccountingView" #accounting>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
<!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
|
||||||
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.infoTitle') }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="accounting.siren"
|
v-model="accounting.siren"
|
||||||
:label="t('commercial.clients.form.accounting.siren')"
|
:label="t('commercial.clients.form.accounting.siren')"
|
||||||
@@ -314,21 +318,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-1.13). -->
|
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-1.13).
|
||||||
|
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
|
||||||
<div
|
<div
|
||||||
v-for="(rib, index) in visibleRibs"
|
v-for="(rib, index) in visibleRibs"
|
||||||
:key="rib.id ?? `new-${index}`"
|
:key="rib.id ?? `new-${index}`"
|
||||||
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
class="pb-[20px]"
|
||||||
|
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
|
||||||
>
|
>
|
||||||
<MalioButtonIcon
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
|
||||||
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
<div class="flex items-center justify-between">
|
||||||
icon="mdi:delete-outline"
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
|
||||||
variant="ghost"
|
<MalioButtonIcon
|
||||||
button-class="absolute top-3 right-3"
|
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
||||||
v-bind="{ ariaLabel: t('commercial.clients.form.accounting.removeRib') }"
|
icon="mdi:delete-outline"
|
||||||
@click="askRemoveRib(index)"
|
variant="ghost"
|
||||||
/>
|
button-class="p-0"
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
v-bind="{ ariaLabel: t('commercial.clients.form.accounting.removeRib') }"
|
||||||
|
@click="askRemoveRib(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="rib.label"
|
v-model="rib.label"
|
||||||
:label="t('commercial.clients.form.accounting.ribLabel')"
|
:label="t('commercial.clients.form.accounting.ribLabel')"
|
||||||
@@ -469,9 +479,6 @@ import { readHistoryTab } from '~/shared/utils/historyTab'
|
|||||||
const SIREN_MASK = '#########'
|
const SIREN_MASK = '#########'
|
||||||
const EMPLOYEES_MASK = '#######'
|
const EMPLOYEES_MASK = '#######'
|
||||||
|
|
||||||
// Codes de categorie interdits sur une adresse (RG-1.29, ERP-78).
|
|
||||||
const FORBIDDEN_ADDRESS_CATEGORY_CODES = ['DISTRIBUTEUR', 'COURTIER']
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -563,15 +570,17 @@ function mergeOptions<T extends { value: string }>(primary: T[], extra: T[]): T[
|
|||||||
return [...primary, ...extra.filter(o => !seen.has(o.value))]
|
return [...primary, ...extra.filter(o => !seen.has(o.value))]
|
||||||
}
|
}
|
||||||
|
|
||||||
const embedCategoryOptions = computed<CategoryOption[]>(() => {
|
// Categories du formulaire principal (type CLIENT) : referentiel UNION categories
|
||||||
const fromClient = categoryOptionsOf(client.value?.categories)
|
// embarquees du client (fallback si le referentiel n'est pas chargeable).
|
||||||
const fromAddresses = (client.value?.addresses ?? []).flatMap(a => categoryOptionsOf(a.categories))
|
const embedClientCategoryOptions = computed<CategoryOption[]>(() => categoryOptionsOf(client.value?.categories))
|
||||||
return mergeOptions(fromClient, fromAddresses)
|
const mainCategoryOptions = computed(() => mergeOptions(referentials.categories.value, embedClientCategoryOptions.value))
|
||||||
})
|
// Categories des blocs adresse (type ADRESSE) : referentiel dedie UNION categories
|
||||||
const mainCategoryOptions = computed(() => mergeOptions(referentials.categories.value, embedCategoryOptions.value))
|
// embarquees des adresses (fallback meme fonction qu'au-dessus).
|
||||||
// Categories autorisees sur une adresse : toutes SAUF DISTRIBUTEUR/COURTIER (RG-1.29).
|
const embedAddressCategoryOptions = computed<CategoryOption[]>(() =>
|
||||||
|
mergeOptions([], (client.value?.addresses ?? []).flatMap(a => categoryOptionsOf(a.categories))),
|
||||||
|
)
|
||||||
const addressCategoryOptions = computed(() =>
|
const addressCategoryOptions = computed(() =>
|
||||||
mainCategoryOptions.value.filter(c => !FORBIDDEN_ADDRESS_CATEGORY_CODES.includes(c.code)),
|
mergeOptions(referentials.addressCategories.value, embedAddressCategoryOptions.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
const embedSiteOptions = computed<RefOption[]>(() =>
|
const embedSiteOptions = computed<RefOption[]>(() =>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
||||||
<!-- Onglet Information -->
|
<!-- Onglet Information -->
|
||||||
<template #information>
|
<template #information>
|
||||||
<div class="mt-12 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)]">
|
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas
|
<!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas
|
||||||
sur les inputs (champ 40px centre dans un h-12 -> ~4px de
|
sur les inputs (champ 40px centre dans un h-12 -> ~4px de
|
||||||
coussin de chaque cote). -->
|
coussin de chaque cote). -->
|
||||||
@@ -156,6 +156,7 @@
|
|||||||
:key="contact.id ?? index"
|
:key="contact.id ?? index"
|
||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
:title="t('commercial.clients.form.contact.title', { n: index + 1 })"
|
:title="t('commercial.clients.form.contact.title', { n: index + 1 })"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
disabled
|
disabled
|
||||||
hide-empty
|
hide-empty
|
||||||
/>
|
/>
|
||||||
@@ -170,6 +171,7 @@
|
|||||||
:key="view.draft.id ?? index"
|
:key="view.draft.id ?? index"
|
||||||
:model-value="view.draft"
|
:model-value="view.draft"
|
||||||
:title="t('commercial.clients.form.address.title', { n: index + 1 })"
|
:title="t('commercial.clients.form.address.title', { n: index + 1 })"
|
||||||
|
:last="index === addressViews.length - 1"
|
||||||
:category-options="view.categoryOptions"
|
:category-options="view.categoryOptions"
|
||||||
:site-options="allSiteOptions"
|
:site-options="allSiteOptions"
|
||||||
:contact-options="contactOptions"
|
:contact-options="contactOptions"
|
||||||
@@ -183,8 +185,10 @@
|
|||||||
<!-- Onglet Comptabilite (present uniquement si accounting.view). -->
|
<!-- Onglet Comptabilite (present uniquement si accounting.view). -->
|
||||||
<template v-if="canAccountingView" #accounting>
|
<template v-if="canAccountingView" #accounting>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
<!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<div class="pb-[20px]" :class="{ 'border-b border-black': ribs.length > 0 }">
|
||||||
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.infoTitle') }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-if="isFilled(accounting.siren)"
|
v-if="isFilled(accounting.siren)"
|
||||||
:model-value="accounting.siren"
|
:model-value="accounting.siren"
|
||||||
@@ -239,13 +243,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blocs RIB (0..n), lecture seule. -->
|
<!-- Blocs RIB (0..n), lecture seule.
|
||||||
|
Titre « RIB N », filet de separation sauf sous le dernier. -->
|
||||||
<div
|
<div
|
||||||
v-for="(rib, index) in ribs"
|
v-for="(rib, index) in ribs"
|
||||||
:key="rib.id ?? index"
|
:key="rib.id ?? index"
|
||||||
class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
class="pb-[20px]"
|
||||||
|
:class="{ 'border-b border-black': index !== ribs.length - 1 }"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-if="isFilled(rib.label)"
|
v-if="isFilled(rib.label)"
|
||||||
:model-value="rib.label"
|
:model-value="rib.label"
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
|
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
|
||||||
<!-- Onglet Information -->
|
<!-- Onglet Information -->
|
||||||
<template #information>
|
<template #information>
|
||||||
<div class="mt-12 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)]">
|
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<!-- pt-1/pb-1 alignent le textarea (h-full) sur les inputs, dont
|
<!-- pt-1/pb-1 alignent le textarea (h-full) sur les inputs, dont
|
||||||
le champ de 40px est centre dans un conteneur h-12 (~4px de
|
le champ de 40px est centre dans un conteneur h-12 (~4px de
|
||||||
coussin en HAUT et en BAS). Sans pb-1, le textarea descend ~4px
|
coussin en HAUT et en BAS). Sans pb-1, le textarea descend ~4px
|
||||||
@@ -177,6 +177,7 @@
|
|||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
:title="t('commercial.clients.form.contact.title', { n: index + 1 })"
|
:title="t('commercial.clients.form.contact.title', { n: index + 1 })"
|
||||||
:removable="isRowRemovable(contacts, index)"
|
:removable="isRowRemovable(contacts, index)"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
:disabled="isValidated('contact')"
|
:disabled="isValidated('contact')"
|
||||||
:errors="contactErrors[index]"
|
:errors="contactErrors[index]"
|
||||||
@update:model-value="(v) => contacts[index] = v"
|
@update:model-value="(v) => contacts[index] = v"
|
||||||
@@ -209,6 +210,7 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
:model-value="address"
|
:model-value="address"
|
||||||
:title="t('commercial.clients.form.address.title', { n: index + 1 })"
|
:title="t('commercial.clients.form.address.title', { n: index + 1 })"
|
||||||
|
:last="index === addresses.length - 1"
|
||||||
:category-options="addressCategoryOptions"
|
:category-options="addressCategoryOptions"
|
||||||
:site-options="referentials.sites.value"
|
:site-options="referentials.sites.value"
|
||||||
:contact-options="contactOptions"
|
:contact-options="contactOptions"
|
||||||
@@ -242,8 +244,10 @@
|
|||||||
<!-- Onglet Comptabilite (present uniquement si accounting.view) -->
|
<!-- Onglet Comptabilite (present uniquement si accounting.view) -->
|
||||||
<template v-if="canAccountingView" #accounting>
|
<template v-if="canAccountingView" #accounting>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
<!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
|
||||||
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.infoTitle') }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="accounting.siren"
|
v-model="accounting.siren"
|
||||||
:label="t('commercial.clients.form.accounting.siren')"
|
:label="t('commercial.clients.form.accounting.siren')"
|
||||||
@@ -312,22 +316,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-1.13). -->
|
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-1.13).
|
||||||
|
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
|
||||||
<div
|
<div
|
||||||
v-for="(rib, index) in visibleRibs"
|
v-for="(rib, index) in visibleRibs"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
class="pb-[20px]"
|
||||||
|
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
|
||||||
>
|
>
|
||||||
<!-- ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). -->
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
|
||||||
<MalioButtonIcon
|
<div class="flex items-center justify-between">
|
||||||
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
|
||||||
icon="mdi:delete-outline"
|
<!-- ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). -->
|
||||||
variant="ghost"
|
<MalioButtonIcon
|
||||||
button-class="absolute top-3 right-3"
|
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
||||||
v-bind="{ ariaLabel: t('commercial.clients.form.accounting.removeRib') }"
|
icon="mdi:delete-outline"
|
||||||
@click="askRemoveRib(index)"
|
variant="ghost"
|
||||||
/>
|
button-class="p-0"
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
v-bind="{ ariaLabel: t('commercial.clients.form.accounting.removeRib') }"
|
||||||
|
@click="askRemoveRib(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="rib.label"
|
v-model="rib.label"
|
||||||
:label="t('commercial.clients.form.accounting.ribLabel')"
|
:label="t('commercial.clients.form.accounting.ribLabel')"
|
||||||
@@ -446,9 +456,6 @@ const SIREN_MASK = '#########'
|
|||||||
// Masque « nombre » du champ Nombre de salaries : chiffres uniquement (max 7).
|
// Masque « nombre » du champ Nombre de salaries : chiffres uniquement (max 7).
|
||||||
const EMPLOYEES_MASK = '#######'
|
const EMPLOYEES_MASK = '#######'
|
||||||
|
|
||||||
// Codes de categorie interdits sur une adresse (RG-1.29, ERP-78).
|
|
||||||
const FORBIDDEN_ADDRESS_CATEGORY_CODES = ['DISTRIBUTEUR', 'COURTIER']
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -806,10 +813,8 @@ async function submitContacts(): Promise<void> {
|
|||||||
const addresses = ref<AddressFormDraft[]>([emptyAddress()])
|
const addresses = ref<AddressFormDraft[]>([emptyAddress()])
|
||||||
const addressDegradedNotified = ref(false)
|
const addressDegradedNotified = ref(false)
|
||||||
|
|
||||||
// Categories autorisees sur une adresse : toutes SAUF DISTRIBUTEUR/COURTIER (RG-1.29).
|
// Categories autorisees sur une adresse : taxonomie dediee type ADRESSE.
|
||||||
const addressCategoryOptions = computed(() =>
|
const addressCategoryOptions = computed(() => referentials.addressCategories.value)
|
||||||
referentials.categories.value.filter(c => !FORBIDDEN_ADDRESS_CATEGORY_CODES.includes(c.code)),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Contacts deja crees, rattachables a une adresse (M2M, via leur IRI).
|
// Contacts deja crees, rattachables a une adresse (M2M, via leur IRI).
|
||||||
const contactOptions = computed<RefOption[]>(() =>
|
const contactOptions = computed<RefOption[]>(() =>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
<MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
||||||
<!-- Onglet Information -->
|
<!-- Onglet Information -->
|
||||||
<template #information>
|
<template #information>
|
||||||
<div class="mt-12 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)]">
|
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<!-- pt-1/pb-1 alignent le textarea (h-full) sur les inputs. -->
|
<!-- pt-1/pb-1 alignent le textarea (h-full) sur les inputs. -->
|
||||||
<MalioInputTextArea
|
<MalioInputTextArea
|
||||||
v-model="information.description"
|
v-model="information.description"
|
||||||
@@ -147,6 +147,7 @@
|
|||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
|
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
|
||||||
:removable="isRowRemovable(contacts, index)"
|
:removable="isRowRemovable(contacts, index)"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
:disabled="businessReadonly"
|
:disabled="businessReadonly"
|
||||||
:errors="contactErrors[index]"
|
:errors="contactErrors[index]"
|
||||||
@update:model-value="(v) => contacts[index] = v"
|
@update:model-value="(v) => contacts[index] = v"
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
:key="address.id ?? `new-${index}`"
|
:key="address.id ?? `new-${index}`"
|
||||||
:model-value="address"
|
:model-value="address"
|
||||||
:title="t('commercial.suppliers.form.address.title', { n: index + 1 })"
|
:title="t('commercial.suppliers.form.address.title', { n: index + 1 })"
|
||||||
:category-options="mainCategoryOptions"
|
:last="index === addresses.length - 1"
|
||||||
|
:category-options="addressCategoryOptions"
|
||||||
:site-options="siteOptions"
|
:site-options="siteOptions"
|
||||||
:contact-options="contactOptions"
|
:contact-options="contactOptions"
|
||||||
:country-options="countryOptions"
|
:country-options="countryOptions"
|
||||||
@@ -213,8 +215,10 @@
|
|||||||
editable uniquement si accounting.manage). -->
|
editable uniquement si accounting.manage). -->
|
||||||
<template v-if="canAccountingView" #accounting>
|
<template v-if="canAccountingView" #accounting>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
<!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
|
||||||
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.infoTitle') }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="accounting.siren"
|
v-model="accounting.siren"
|
||||||
:label="t('commercial.suppliers.form.accounting.siren')"
|
:label="t('commercial.suppliers.form.accounting.siren')"
|
||||||
@@ -283,21 +287,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-2.08). -->
|
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-2.08).
|
||||||
|
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
|
||||||
<div
|
<div
|
||||||
v-for="(rib, index) in visibleRibs"
|
v-for="(rib, index) in visibleRibs"
|
||||||
:key="rib.id ?? `new-${index}`"
|
:key="rib.id ?? `new-${index}`"
|
||||||
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
class="pb-[20px]"
|
||||||
|
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
|
||||||
>
|
>
|
||||||
<MalioButtonIcon
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
|
||||||
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
<div class="flex items-center justify-between">
|
||||||
icon="mdi:delete-outline"
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
|
||||||
variant="ghost"
|
<MalioButtonIcon
|
||||||
button-class="absolute top-3 right-3"
|
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
||||||
v-bind="{ ariaLabel: t('commercial.suppliers.form.accounting.removeRib') }"
|
icon="mdi:delete-outline"
|
||||||
@click="askRemoveRib(index)"
|
variant="ghost"
|
||||||
/>
|
button-class="p-0"
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
v-bind="{ ariaLabel: t('commercial.suppliers.form.accounting.removeRib') }"
|
||||||
|
@click="askRemoveRib(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="rib.label"
|
v-model="rib.label"
|
||||||
:label="t('commercial.suppliers.form.accounting.ribLabel')"
|
:label="t('commercial.suppliers.form.accounting.ribLabel')"
|
||||||
@@ -526,15 +536,18 @@ function mergeOptions<T extends { value: string }>(primary: T[], extra: T[]): T[
|
|||||||
return [...primary, ...extra.filter(o => !seen.has(o.value))]
|
return [...primary, ...extra.filter(o => !seen.has(o.value))]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Categories issues de l'embed (fournisseur + adresses), role-independantes.
|
// Categories du formulaire principal (type FOURNISSEUR) : referentiel UNION
|
||||||
const embedCategoryOptions = computed<CategoryOption[]>(() => {
|
// categories embarquees du fournisseur (fallback si referentiel non chargeable).
|
||||||
const fromSupplier = categoryOptionsOf(supplier.value?.categories)
|
const embedSupplierCategoryOptions = computed<CategoryOption[]>(() => categoryOptionsOf(supplier.value?.categories))
|
||||||
const fromAddresses = (supplier.value?.addresses ?? []).flatMap(a => categoryOptionsOf(a.categories))
|
const mainCategoryOptions = computed(() => mergeOptions(referentials.categories.value, embedSupplierCategoryOptions.value))
|
||||||
return mergeOptions(fromSupplier, fromAddresses)
|
// Categories des blocs adresse (type ADRESSE) : referentiel dedie UNION categories
|
||||||
})
|
// embarquees des adresses (meme logique de fallback).
|
||||||
// Toutes les categories de type FOURNISSEUR sont autorisees, sur le bloc principal
|
const embedAddressCategoryOptions = computed<CategoryOption[]>(() =>
|
||||||
// comme sur une adresse (pas de restriction Distributeur/Courtier comme au M1 — RG-2.10).
|
mergeOptions([], (supplier.value?.addresses ?? []).flatMap(a => categoryOptionsOf(a.categories))),
|
||||||
const mainCategoryOptions = computed(() => mergeOptions(referentials.categories.value, embedCategoryOptions.value))
|
)
|
||||||
|
const addressCategoryOptions = computed(() =>
|
||||||
|
mergeOptions(referentials.addressCategories.value, embedAddressCategoryOptions.value),
|
||||||
|
)
|
||||||
|
|
||||||
const embedSiteOptions = computed<RefOption[]>(() =>
|
const embedSiteOptions = computed<RefOption[]>(() =>
|
||||||
mergeOptions([], (supplier.value?.addresses ?? []).flatMap(a => siteOptionsOf(a.sites))),
|
mergeOptions([], (supplier.value?.addresses ?? []).flatMap(a => siteOptionsOf(a.sites))),
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
||||||
<!-- Onglet Information -->
|
<!-- Onglet Information -->
|
||||||
<template #information>
|
<template #information>
|
||||||
<div class="mt-12 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)]">
|
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas
|
<!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas
|
||||||
sur les inputs (champ 40px centre dans un h-12). -->
|
sur les inputs (champ 40px centre dans un h-12). -->
|
||||||
<MalioInputTextArea
|
<MalioInputTextArea
|
||||||
@@ -137,6 +137,7 @@
|
|||||||
:key="contact.id ?? index"
|
:key="contact.id ?? index"
|
||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
|
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
disabled
|
disabled
|
||||||
hide-empty
|
hide-empty
|
||||||
/>
|
/>
|
||||||
@@ -151,6 +152,7 @@
|
|||||||
:key="view.draft.id ?? index"
|
:key="view.draft.id ?? index"
|
||||||
:model-value="view.draft"
|
:model-value="view.draft"
|
||||||
:title="t('commercial.suppliers.form.address.title', { n: index + 1 })"
|
:title="t('commercial.suppliers.form.address.title', { n: index + 1 })"
|
||||||
|
:last="index === addressViews.length - 1"
|
||||||
:category-options="view.categoryOptions"
|
:category-options="view.categoryOptions"
|
||||||
:site-options="allSiteOptions"
|
:site-options="allSiteOptions"
|
||||||
:contact-options="contactOptions"
|
:contact-options="contactOptions"
|
||||||
@@ -164,8 +166,10 @@
|
|||||||
<!-- Onglet Comptabilite (present uniquement si accounting.view). -->
|
<!-- Onglet Comptabilite (present uniquement si accounting.view). -->
|
||||||
<template v-if="canAccountingView" #accounting>
|
<template v-if="canAccountingView" #accounting>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
<!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<div class="pb-[20px]" :class="{ 'border-b border-black': ribs.length > 0 }">
|
||||||
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.infoTitle') }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-if="isFilled(accounting.siren)"
|
v-if="isFilled(accounting.siren)"
|
||||||
:model-value="accounting.siren"
|
:model-value="accounting.siren"
|
||||||
@@ -220,13 +224,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blocs RIB (0..n), lecture seule. -->
|
<!-- Blocs RIB (0..n), lecture seule.
|
||||||
|
Titre « RIB N », filet de separation sauf sous le dernier. -->
|
||||||
<div
|
<div
|
||||||
v-for="(rib, index) in ribs"
|
v-for="(rib, index) in ribs"
|
||||||
:key="rib.id ?? index"
|
:key="rib.id ?? index"
|
||||||
class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
class="pb-[20px]"
|
||||||
|
:class="{ 'border-b border-black': index !== ribs.length - 1 }"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-if="isFilled(rib.label)"
|
v-if="isFilled(rib.label)"
|
||||||
:model-value="rib.label"
|
:model-value="rib.label"
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
|
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
|
||||||
<!-- Onglet Information -->
|
<!-- Onglet Information -->
|
||||||
<template #information>
|
<template #information>
|
||||||
<div class="mt-12 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)]">
|
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputTextArea
|
<MalioInputTextArea
|
||||||
v-model="information.description"
|
v-model="information.description"
|
||||||
:label="t('commercial.suppliers.form.information.description')"
|
:label="t('commercial.suppliers.form.information.description')"
|
||||||
@@ -145,6 +145,7 @@
|
|||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
|
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
|
||||||
:removable="isRowRemovable(contacts, index)"
|
:removable="isRowRemovable(contacts, index)"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
:disabled="isValidated('contacts')"
|
:disabled="isValidated('contacts')"
|
||||||
:errors="contactErrors[index]"
|
:errors="contactErrors[index]"
|
||||||
@update:model-value="(v) => contacts[index] = v"
|
@update:model-value="(v) => contacts[index] = v"
|
||||||
@@ -177,7 +178,8 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
:model-value="address"
|
:model-value="address"
|
||||||
:title="t('commercial.suppliers.form.address.title', { n: index + 1 })"
|
:title="t('commercial.suppliers.form.address.title', { n: index + 1 })"
|
||||||
:category-options="referentials.categories.value"
|
:last="index === addresses.length - 1"
|
||||||
|
:category-options="referentials.addressCategories.value"
|
||||||
:site-options="referentials.sites.value"
|
:site-options="referentials.sites.value"
|
||||||
:contact-options="contactOptions"
|
:contact-options="contactOptions"
|
||||||
:country-options="countryOptions"
|
:country-options="countryOptions"
|
||||||
@@ -210,8 +212,10 @@
|
|||||||
<!-- Onglet Comptabilite (present uniquement si accounting.view) -->
|
<!-- Onglet Comptabilite (present uniquement si accounting.view) -->
|
||||||
<template v-if="canAccountingView" #accounting>
|
<template v-if="canAccountingView" #accounting>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
<!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
|
||||||
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.infoTitle') }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="accounting.siren"
|
v-model="accounting.siren"
|
||||||
:label="t('commercial.suppliers.form.accounting.siren')"
|
:label="t('commercial.suppliers.form.accounting.siren')"
|
||||||
@@ -280,21 +284,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-2.08). -->
|
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-2.08).
|
||||||
|
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
|
||||||
<div
|
<div
|
||||||
v-for="(rib, index) in visibleRibs"
|
v-for="(rib, index) in visibleRibs"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
class="pb-[20px]"
|
||||||
|
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
|
||||||
>
|
>
|
||||||
<MalioButtonIcon
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
|
||||||
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
<div class="flex items-center justify-between">
|
||||||
icon="mdi:delete-outline"
|
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
|
||||||
variant="ghost"
|
<MalioButtonIcon
|
||||||
button-class="absolute top-3 right-3"
|
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
||||||
v-bind="{ ariaLabel: t('commercial.suppliers.form.accounting.removeRib') }"
|
icon="mdi:delete-outline"
|
||||||
@click="askRemoveRib(index)"
|
variant="ghost"
|
||||||
/>
|
button-class="p-0"
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
v-bind="{ ariaLabel: t('commercial.suppliers.form.accounting.removeRib') }"
|
||||||
|
@click="askRemoveRib(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="rib.label"
|
v-model="rib.label"
|
||||||
:label="t('commercial.suppliers.form.accounting.ribLabel')"
|
:label="t('commercial.suppliers.form.accounting.ribLabel')"
|
||||||
|
|||||||
@@ -1,131 +1,140 @@
|
|||||||
<template>
|
<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)]">
|
<!-- Bloc a plat (sans box-shadow) : un filet noir 1px le separe du suivant
|
||||||
<!-- Suppression : modal de confirmation cote parent. -->
|
(pas de bordure sous le dernier bloc). -->
|
||||||
<MalioButtonIcon
|
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
|
||||||
v-if="removable && !readonly && !disabled"
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
|
||||||
icon="mdi:delete-outline"
|
<div class="flex items-center justify-between">
|
||||||
variant="ghost"
|
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
|
||||||
button-class="absolute top-3 right-3"
|
<!-- Suppression : modal de confirmation cote parent. -->
|
||||||
v-bind="{ ariaLabel: t('technique.providers.form.address.remove') }"
|
<MalioButtonIcon
|
||||||
@click="$emit('remove')"
|
v-if="removable && !readonly && !disabled"
|
||||||
/>
|
icon="mdi:delete-outline"
|
||||||
|
variant="ghost"
|
||||||
<!-- Sites Starseed : multiselect a tags (>= 1 obligatoire, RG-3.05). -->
|
button-class="p-0"
|
||||||
<MalioSelectCheckbox
|
v-bind="{ ariaLabel: t('technique.providers.form.address.remove') }"
|
||||||
v-if="!hideEmpty || isFilled(model.siteIris)"
|
@click="$emit('remove')"
|
||||||
:model-value="model.siteIris"
|
|
||||||
:options="siteOptions"
|
|
||||||
:label="t('technique.providers.form.address.sites')"
|
|
||||||
:display-tag="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.sites"
|
|
||||||
@update:model-value="(v: (string | number)[]) => update('siteIris', v.map(String))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Contacts rattaches (M2M, facultatif) : alimente par l'onglet Contact. -->
|
|
||||||
<MalioSelectCheckbox
|
|
||||||
v-if="!hideEmpty || isFilled(model.contactIris)"
|
|
||||||
:model-value="model.contactIris"
|
|
||||||
:options="contactOptions"
|
|
||||||
:label="t('technique.providers.form.address.contacts')"
|
|
||||||
:display-tag="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
@update:model-value="(v: (string | number)[]) => update('contactIris', v.map(String))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MalioSelect
|
|
||||||
v-if="!hideEmpty || isFilled(model.country)"
|
|
||||||
:model-value="model.country"
|
|
||||||
:options="countryOptions"
|
|
||||||
:label="t('technique.providers.form.address.country')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
@update:model-value="(v: string | number | null) => update('country', String(v ?? 'France'))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MalioInputText
|
|
||||||
v-if="!hideEmpty || isFilled(model.postalCode)"
|
|
||||||
:model-value="model.postalCode"
|
|
||||||
:label="t('technique.providers.form.address.postalCode')"
|
|
||||||
:mask="POSTAL_CODE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.postalCode"
|
|
||||||
@update:model-value="onPostalCodeChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Ville : MalioSelect alimente par le code postal (BAN). Saisie libre si BAN indispo. -->
|
|
||||||
<MalioSelect
|
|
||||||
v-if="!degraded && (!hideEmpty || isFilled(model.city))"
|
|
||||||
:model-value="model.city"
|
|
||||||
:options="cityOptions"
|
|
||||||
:label="t('technique.providers.form.address.city')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
empty-option-label=""
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.city"
|
|
||||||
@update:model-value="onCityChange"
|
|
||||||
/>
|
|
||||||
<MalioInputText
|
|
||||||
v-else-if="degraded && (!hideEmpty || isFilled(model.city))"
|
|
||||||
:model-value="model.city"
|
|
||||||
:label="t('technique.providers.form.address.city')"
|
|
||||||
:mask="ADDRESS_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.city"
|
|
||||||
@update:model-value="(v: string) => update('city', v)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Adresse (BAN) sur 2 colonnes + Adresse complementaire. allow-create : le
|
|
||||||
texte saisi est conserve si la BAN ne propose rien (saisie manuelle). -->
|
|
||||||
<div v-if="!hideEmpty || isFilled(model.street)" class="col-span-2">
|
|
||||||
<MalioInputAutocomplete
|
|
||||||
v-if="!readonly && !disabled"
|
|
||||||
:model-value="model.street"
|
|
||||||
:options="addressOptions"
|
|
||||||
:loading="addressLoading"
|
|
||||||
:min-search-length="3"
|
|
||||||
:label="t('technique.providers.form.address.street')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.street"
|
|
||||||
:allow-create="true"
|
|
||||||
:no-results-text="t('technique.providers.form.address.streetNotFound')"
|
|
||||||
@update:model-value="(v: string | number | null) => update('street', v === null ? null : String(v))"
|
|
||||||
@search="onAddressSearch"
|
|
||||||
@select="onAddressSelect"
|
|
||||||
/>
|
|
||||||
<MalioInputText
|
|
||||||
v-else
|
|
||||||
:model-value="model.street"
|
|
||||||
:label="t('technique.providers.form.address.street')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.street"
|
|
||||||
@update:model-value="(v: string) => update('street', v)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!hideEmpty || isFilled(model.streetComplement)" class="col-span-1">
|
<!-- Grille 4 colonnes des champs de l'adresse. -->
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
|
<!-- Sites Starseed : multiselect a tags (>= 1 obligatoire, RG-3.05). -->
|
||||||
|
<MalioSelectCheckbox
|
||||||
|
v-if="!hideEmpty || isFilled(model.siteIris)"
|
||||||
|
:model-value="model.siteIris"
|
||||||
|
:options="siteOptions"
|
||||||
|
:label="t('technique.providers.form.address.sites')"
|
||||||
|
:display-tag="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.sites"
|
||||||
|
@update:model-value="(v: (string | number)[]) => update('siteIris', v.map(String))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Contacts rattaches (M2M, facultatif) : alimente par l'onglet Contact. -->
|
||||||
|
<MalioSelectCheckbox
|
||||||
|
v-if="!hideEmpty || isFilled(model.contactIris)"
|
||||||
|
:model-value="model.contactIris"
|
||||||
|
:options="contactOptions"
|
||||||
|
:label="t('technique.providers.form.address.contacts')"
|
||||||
|
:display-tag="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
@update:model-value="(v: (string | number)[]) => update('contactIris', v.map(String))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MalioSelect
|
||||||
|
v-if="!hideEmpty || isFilled(model.country)"
|
||||||
|
:model-value="model.country"
|
||||||
|
:options="countryOptions"
|
||||||
|
:label="t('technique.providers.form.address.country')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
@update:model-value="(v: string | number | null) => update('country', String(v ?? 'France'))"
|
||||||
|
/>
|
||||||
|
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
:model-value="model.streetComplement"
|
v-if="!hideEmpty || isFilled(model.postalCode)"
|
||||||
:label="t('technique.providers.form.address.streetComplement')"
|
:model-value="model.postalCode"
|
||||||
|
:label="t('technique.providers.form.address.postalCode')"
|
||||||
|
:mask="POSTAL_CODE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.postalCode"
|
||||||
|
@update:model-value="onPostalCodeChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Ville : MalioSelect alimente par le code postal (BAN). Saisie libre si BAN indispo. -->
|
||||||
|
<MalioSelect
|
||||||
|
v-if="!degraded && (!hideEmpty || isFilled(model.city))"
|
||||||
|
:model-value="model.city"
|
||||||
|
:options="cityOptions"
|
||||||
|
:label="t('technique.providers.form.address.city')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.city"
|
||||||
|
@update:model-value="onCityChange"
|
||||||
|
/>
|
||||||
|
<MalioInputText
|
||||||
|
v-else-if="degraded && (!hideEmpty || isFilled(model.city))"
|
||||||
|
:model-value="model.city"
|
||||||
|
:label="t('technique.providers.form.address.city')"
|
||||||
:mask="ADDRESS_MASK"
|
:mask="ADDRESS_MASK"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:error="errors?.streetComplement"
|
:required="!readonly && !disabled"
|
||||||
@update:model-value="(v: string) => update('streetComplement', v)"
|
:error="errors?.city"
|
||||||
|
@update:model-value="(v: string) => update('city', v)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Adresse (BAN) sur 2 colonnes + Adresse complementaire. allow-create : le
|
||||||
|
texte saisi est conserve si la BAN ne propose rien (saisie manuelle). -->
|
||||||
|
<div v-if="!hideEmpty || isFilled(model.street)" class="col-span-2">
|
||||||
|
<MalioInputAutocomplete
|
||||||
|
v-if="!readonly && !disabled"
|
||||||
|
:model-value="model.street"
|
||||||
|
:options="addressOptions"
|
||||||
|
:loading="addressLoading"
|
||||||
|
:min-search-length="3"
|
||||||
|
:label="t('technique.providers.form.address.street')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.street"
|
||||||
|
:allow-create="true"
|
||||||
|
:no-results-text="t('technique.providers.form.address.streetNotFound')"
|
||||||
|
@update:model-value="(v: string | number | null) => update('street', v === null ? null : String(v))"
|
||||||
|
@search="onAddressSearch"
|
||||||
|
@select="onAddressSelect"
|
||||||
|
/>
|
||||||
|
<MalioInputText
|
||||||
|
v-else
|
||||||
|
:model-value="model.street"
|
||||||
|
:label="t('technique.providers.form.address.street')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.street"
|
||||||
|
@update:model-value="(v: string) => update('street', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!hideEmpty || isFilled(model.streetComplement)" class="col-span-1">
|
||||||
|
<MalioInputText
|
||||||
|
:model-value="model.streetComplement"
|
||||||
|
:label="t('technique.providers.form.address.streetComplement')"
|
||||||
|
:mask="ADDRESS_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.streetComplement"
|
||||||
|
@update:model-value="(v: string) => update('streetComplement', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -143,6 +152,8 @@ const POSTAL_CODE_MASK = '#####'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** Brouillon de l'adresse (v-model). */
|
/** Brouillon de l'adresse (v-model). */
|
||||||
modelValue: ProviderAddressFormDraft
|
modelValue: ProviderAddressFormDraft
|
||||||
|
/** Titre du bloc (ex: « Adresse 1 »). */
|
||||||
|
title: string
|
||||||
/** Sites Starseed disponibles. */
|
/** Sites Starseed disponibles. */
|
||||||
siteOptions: RefOption[]
|
siteOptions: RefOption[]
|
||||||
/** Contacts deja saisis, rattachables a l'adresse. */
|
/** Contacts deja saisis, rattachables a l'adresse. */
|
||||||
@@ -150,6 +161,8 @@ const props = defineProps<{
|
|||||||
/** Pays disponibles (France par defaut). */
|
/** Pays disponibles (France par defaut). */
|
||||||
countryOptions: RefOption[]
|
countryOptions: RefOption[]
|
||||||
removable?: boolean
|
removable?: boolean
|
||||||
|
/** Dernier bloc de la liste : supprime le filet de separation bas. */
|
||||||
|
last?: boolean
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
|||||||
@@ -1,84 +1,93 @@
|
|||||||
<template>
|
<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)]">
|
<!-- Bloc a plat (sans box-shadow) : un filet noir 1px le separe du suivant
|
||||||
<!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si
|
(pas de bordure sous le dernier bloc). -->
|
||||||
non supprimable (1er bloc) ou en lecture seule. -->
|
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
|
||||||
<MalioButtonIcon
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
|
||||||
v-if="removable && !readonly && !disabled"
|
<div class="flex items-center justify-between">
|
||||||
icon="mdi:delete-outline"
|
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
|
||||||
variant="ghost"
|
<!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si
|
||||||
button-class="absolute top-3 right-3"
|
non supprimable (1er bloc) ou en lecture seule. -->
|
||||||
v-bind="{ ariaLabel: t('technique.providers.form.contact.remove') }"
|
<MalioButtonIcon
|
||||||
@click="$emit('remove')"
|
v-if="removable && !readonly && !disabled"
|
||||||
/>
|
icon="mdi:delete-outline"
|
||||||
|
variant="ghost"
|
||||||
<MalioInputText
|
button-class="p-0"
|
||||||
v-if="!hideEmpty || isFilled(model.lastName)"
|
v-bind="{ ariaLabel: t('technique.providers.form.contact.remove') }"
|
||||||
:model-value="model.lastName"
|
@click="$emit('remove')"
|
||||||
:label="t('technique.providers.form.contact.lastName')"
|
/>
|
||||||
:mask="PERSON_NAME_MASK"
|
</div>
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
<!-- Grille 4 colonnes des champs du contact. -->
|
||||||
:error="errors?.lastName"
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
@update:model-value="(v: string) => update('lastName', v)"
|
<MalioInputText
|
||||||
/>
|
v-if="!hideEmpty || isFilled(model.lastName)"
|
||||||
<MalioInputText
|
:model-value="model.lastName"
|
||||||
v-if="!hideEmpty || isFilled(model.firstName)"
|
:label="t('technique.providers.form.contact.lastName')"
|
||||||
:model-value="model.firstName"
|
:mask="PERSON_NAME_MASK"
|
||||||
:label="t('technique.providers.form.contact.firstName')"
|
:readonly="readonly"
|
||||||
:mask="PERSON_NAME_MASK"
|
:disabled="disabled"
|
||||||
:readonly="readonly"
|
:error="errors?.lastName"
|
||||||
:disabled="disabled"
|
@update:model-value="(v: string) => update('lastName', v)"
|
||||||
:error="errors?.firstName"
|
/>
|
||||||
@update:model-value="(v: string) => update('firstName', v)"
|
<MalioInputText
|
||||||
/>
|
v-if="!hideEmpty || isFilled(model.firstName)"
|
||||||
<!-- Fonction sur 2 colonnes : on wrappe car MalioInputText
|
:model-value="model.firstName"
|
||||||
(inheritAttrs:false) renvoie `class` sur l'input interne, pas sur la
|
:label="t('technique.providers.form.contact.firstName')"
|
||||||
cellule de grille. Le wrapper porte le col-span-2, le champ le remplit. -->
|
:mask="PERSON_NAME_MASK"
|
||||||
<div v-if="!hideEmpty || isFilled(model.jobTitle)" class="col-span-2">
|
:readonly="readonly"
|
||||||
<MalioInputText
|
:disabled="disabled"
|
||||||
:model-value="model.jobTitle"
|
:error="errors?.firstName"
|
||||||
:label="t('technique.providers.form.contact.jobTitle')"
|
@update:model-value="(v: string) => update('firstName', v)"
|
||||||
:mask="FREE_TEXT_MASK"
|
/>
|
||||||
:readonly="readonly"
|
<!-- Fonction sur 2 colonnes : on wrappe car MalioInputText
|
||||||
:disabled="disabled"
|
(inheritAttrs:false) renvoie `class` sur l'input interne, pas sur la
|
||||||
:error="errors?.jobTitle"
|
cellule de grille. Le wrapper porte le col-span-2, le champ le remplit. -->
|
||||||
@update:model-value="(v: string) => update('jobTitle', v)"
|
<div v-if="!hideEmpty || isFilled(model.jobTitle)" class="col-span-2">
|
||||||
|
<MalioInputText
|
||||||
|
:model-value="model.jobTitle"
|
||||||
|
:label="t('technique.providers.form.contact.jobTitle')"
|
||||||
|
:mask="FREE_TEXT_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.jobTitle"
|
||||||
|
@update:model-value="(v: string) => update('jobTitle', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<MalioInputEmail
|
||||||
|
v-if="!hideEmpty || isFilled(model.email)"
|
||||||
|
:model-value="model.email"
|
||||||
|
:label="t('technique.providers.form.contact.email')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:lowercase="true"
|
||||||
|
:error="errors?.email"
|
||||||
|
@update:model-value="(v: string) => update('email', v)"
|
||||||
|
/>
|
||||||
|
<MalioInputPhone
|
||||||
|
v-if="!hideEmpty || isFilled(model.phonePrimary)"
|
||||||
|
:model-value="model.phonePrimary"
|
||||||
|
:label="t('technique.providers.form.contact.phonePrimary')"
|
||||||
|
:mask="PHONE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.phonePrimary"
|
||||||
|
:addable="!model.hasSecondaryPhone && !readonly"
|
||||||
|
:add-button-label="t('technique.providers.form.contact.addPhone')"
|
||||||
|
@update:model-value="(v: string) => update('phonePrimary', v)"
|
||||||
|
@add="revealSecondaryPhone"
|
||||||
|
/>
|
||||||
|
<!-- 2e numero : revele a la demande (max 2 telephones par contact). -->
|
||||||
|
<MalioInputPhone
|
||||||
|
v-if="model.hasSecondaryPhone && (!hideEmpty || isFilled(model.phoneSecondary))"
|
||||||
|
:model-value="model.phoneSecondary"
|
||||||
|
:label="t('technique.providers.form.contact.phoneSecondary')"
|
||||||
|
:mask="PHONE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.phoneSecondary"
|
||||||
|
@update:model-value="(v: string) => update('phoneSecondary', v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MalioInputEmail
|
|
||||||
v-if="!hideEmpty || isFilled(model.email)"
|
|
||||||
:model-value="model.email"
|
|
||||||
:label="t('technique.providers.form.contact.email')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:lowercase="true"
|
|
||||||
:error="errors?.email"
|
|
||||||
@update:model-value="(v: string) => update('email', v)"
|
|
||||||
/>
|
|
||||||
<MalioInputPhone
|
|
||||||
v-if="!hideEmpty || isFilled(model.phonePrimary)"
|
|
||||||
:model-value="model.phonePrimary"
|
|
||||||
:label="t('technique.providers.form.contact.phonePrimary')"
|
|
||||||
:mask="PHONE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.phonePrimary"
|
|
||||||
:addable="!model.hasSecondaryPhone && !readonly"
|
|
||||||
:add-button-label="t('technique.providers.form.contact.addPhone')"
|
|
||||||
@update:model-value="(v: string) => update('phonePrimary', v)"
|
|
||||||
@add="revealSecondaryPhone"
|
|
||||||
/>
|
|
||||||
<!-- 2e numero : revele a la demande (max 2 telephones par contact). -->
|
|
||||||
<MalioInputPhone
|
|
||||||
v-if="model.hasSecondaryPhone && (!hideEmpty || isFilled(model.phoneSecondary))"
|
|
||||||
:model-value="model.phoneSecondary"
|
|
||||||
:label="t('technique.providers.form.contact.phoneSecondary')"
|
|
||||||
:mask="PHONE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.phoneSecondary"
|
|
||||||
@update:model-value="(v: string) => update('phoneSecondary', v)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -93,8 +102,12 @@ const PHONE_MASK = '## ## ## ## ##'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** Brouillon du contact (v-model). */
|
/** Brouillon du contact (v-model). */
|
||||||
modelValue: ProviderContactFormDraft
|
modelValue: ProviderContactFormDraft
|
||||||
|
/** Titre du bloc (ex: « Contact 1 »). */
|
||||||
|
title: string
|
||||||
/** Affiche l'icone de suppression (1er bloc non supprimable). */
|
/** Affiche l'icone de suppression (1er bloc non supprimable). */
|
||||||
removable?: boolean
|
removable?: boolean
|
||||||
|
/** Dernier bloc de la liste : supprime le filet de separation bas. */
|
||||||
|
last?: boolean
|
||||||
/** Bloc en lecture seule (onglet valide). */
|
/** Bloc en lecture seule (onglet valide). */
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ function mountBlock(overrides: Record<string, unknown> = {}, errors?: Record<str
|
|||||||
return mount(ProviderAddressBlock, {
|
return mount(ProviderAddressBlock, {
|
||||||
props: {
|
props: {
|
||||||
modelValue: { ...emptyProviderAddress(), ...overrides },
|
modelValue: { ...emptyProviderAddress(), ...overrides },
|
||||||
|
title: 'Adresse 1',
|
||||||
siteOptions: [],
|
siteOptions: [],
|
||||||
contactOptions: [],
|
contactOptions: [],
|
||||||
countryOptions: [],
|
countryOptions: [],
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ function mountBlock(errors?: Record<string, string>) {
|
|||||||
return mount(ProviderContactBlock, {
|
return mount(ProviderContactBlock, {
|
||||||
props: {
|
props: {
|
||||||
modelValue: emptyProviderContact(),
|
modelValue: emptyProviderContact(),
|
||||||
|
title: 'Contact 1',
|
||||||
...(errors ? { errors } : {}),
|
...(errors ? { errors } : {}),
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
|
|||||||
@@ -72,7 +72,9 @@
|
|||||||
v-for="(contact, index) in contacts"
|
v-for="(contact, index) in contacts"
|
||||||
:key="index"
|
:key="index"
|
||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
|
:title="t('technique.providers.form.contact.title', { n: index + 1 })"
|
||||||
:removable="isRowRemovable(contacts, index)"
|
:removable="isRowRemovable(contacts, index)"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
:disabled="businessReadonly"
|
:disabled="businessReadonly"
|
||||||
:errors="contactErrors[index]"
|
:errors="contactErrors[index]"
|
||||||
@update:model-value="(v) => contacts[index] = v"
|
@update:model-value="(v) => contacts[index] = v"
|
||||||
@@ -104,6 +106,8 @@
|
|||||||
v-for="(address, index) in addresses"
|
v-for="(address, index) in addresses"
|
||||||
:key="index"
|
:key="index"
|
||||||
:model-value="address"
|
:model-value="address"
|
||||||
|
:title="t('technique.providers.form.address.title', { n: index + 1 })"
|
||||||
|
:last="index === addresses.length - 1"
|
||||||
:site-options="referentials.sites.value"
|
:site-options="referentials.sites.value"
|
||||||
:contact-options="contactOptions"
|
:contact-options="contactOptions"
|
||||||
:country-options="countryOptions"
|
:country-options="countryOptions"
|
||||||
@@ -136,8 +140,10 @@
|
|||||||
<!-- Onglet Comptabilite (present si accounting.view ; editable si manage). -->
|
<!-- Onglet Comptabilite (present si accounting.view ; editable si manage). -->
|
||||||
<template v-if="canAccountingView" #accounting>
|
<template v-if="canAccountingView" #accounting>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
<!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
|
||||||
|
<h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.infoTitle') }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="accounting.siren"
|
v-model="accounting.siren"
|
||||||
:label="t('technique.providers.form.accounting.siren')"
|
:label="t('technique.providers.form.accounting.siren')"
|
||||||
@@ -206,21 +212,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-3.08). -->
|
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-3.08).
|
||||||
|
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
|
||||||
<div
|
<div
|
||||||
v-for="(rib, index) in visibleRibs"
|
v-for="(rib, index) in visibleRibs"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
class="pb-[20px]"
|
||||||
|
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
|
||||||
>
|
>
|
||||||
<MalioButtonIcon
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
|
||||||
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
<div class="flex items-center justify-between">
|
||||||
icon="mdi:delete-outline"
|
<h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
|
||||||
variant="ghost"
|
<MalioButtonIcon
|
||||||
button-class="absolute top-3 right-3"
|
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
||||||
v-bind="{ ariaLabel: t('technique.providers.form.accounting.removeRib') }"
|
icon="mdi:delete-outline"
|
||||||
@click="askRemoveRib(index)"
|
variant="ghost"
|
||||||
/>
|
button-class="p-0"
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
v-bind="{ ariaLabel: t('technique.providers.form.accounting.removeRib') }"
|
||||||
|
@click="askRemoveRib(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="rib.label"
|
v-model="rib.label"
|
||||||
:label="t('technique.providers.form.accounting.ribLabel')"
|
:label="t('technique.providers.form.accounting.ribLabel')"
|
||||||
|
|||||||
@@ -81,6 +81,8 @@
|
|||||||
v-for="(contact, index) in contacts"
|
v-for="(contact, index) in contacts"
|
||||||
:key="index"
|
:key="index"
|
||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
|
:title="t('technique.providers.form.contact.title', { n: index + 1 })"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
disabled
|
disabled
|
||||||
hide-empty
|
hide-empty
|
||||||
/>
|
/>
|
||||||
@@ -94,6 +96,8 @@
|
|||||||
v-for="(view, index) in addressViews"
|
v-for="(view, index) in addressViews"
|
||||||
:key="index"
|
:key="index"
|
||||||
:model-value="view.draft"
|
:model-value="view.draft"
|
||||||
|
:title="t('technique.providers.form.address.title', { n: index + 1 })"
|
||||||
|
:last="index === addressViews.length - 1"
|
||||||
:site-options="view.siteOptions"
|
:site-options="view.siteOptions"
|
||||||
:contact-options="contactOptions"
|
:contact-options="contactOptions"
|
||||||
:country-options="countryOptionsFor(view.draft.country)"
|
:country-options="countryOptionsFor(view.draft.country)"
|
||||||
@@ -108,8 +112,10 @@
|
|||||||
<!-- Onglet Comptabilite (present uniquement si accounting.view). -->
|
<!-- Onglet Comptabilite (present uniquement si accounting.view). -->
|
||||||
<template v-if="canAccountingView" #accounting>
|
<template v-if="canAccountingView" #accounting>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
<!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
|
||||||
|
<h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.infoTitle') }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText v-if="isFilled(accounting.siren)" :model-value="accounting.siren" :label="t('technique.providers.form.accounting.siren')" disabled />
|
<MalioInputText v-if="isFilled(accounting.siren)" :model-value="accounting.siren" :label="t('technique.providers.form.accounting.siren')" disabled />
|
||||||
<MalioInputText v-if="isFilled(accounting.accountNumber)" :model-value="accounting.accountNumber" :label="t('technique.providers.form.accounting.accountNumber')" disabled />
|
<MalioInputText v-if="isFilled(accounting.accountNumber)" :model-value="accounting.accountNumber" :label="t('technique.providers.form.accounting.accountNumber')" disabled />
|
||||||
<MalioSelect v-if="isFilled(accounting.tvaModeIri)" :model-value="accounting.tvaModeIri" :options="tvaModeOptions" :label="t('technique.providers.form.accounting.tvaMode')" disabled empty-option-label="" />
|
<MalioSelect v-if="isFilled(accounting.tvaModeIri)" :model-value="accounting.tvaModeIri" :options="tvaModeOptions" :label="t('technique.providers.form.accounting.tvaMode')" disabled empty-option-label="" />
|
||||||
@@ -120,13 +126,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blocs RIB (uniquement si type de reglement = LCR). -->
|
<!-- Blocs RIB (uniquement si type de reglement = LCR).
|
||||||
|
Titre « RIB N », filet de separation sauf sous le dernier. -->
|
||||||
<div
|
<div
|
||||||
v-for="(rib, index) in visibleRibs"
|
v-for="(rib, index) in visibleRibs"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
class="pb-[20px]"
|
||||||
|
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText v-if="isFilled(rib.label)" :model-value="rib.label" :label="t('technique.providers.form.accounting.ribLabel')" disabled />
|
<MalioInputText v-if="isFilled(rib.label)" :model-value="rib.label" :label="t('technique.providers.form.accounting.ribLabel')" disabled />
|
||||||
<MalioInputText v-if="isFilled(rib.bic)" :model-value="rib.bic" :label="t('technique.providers.form.accounting.ribBic')" disabled />
|
<MalioInputText v-if="isFilled(rib.bic)" :model-value="rib.bic" :label="t('technique.providers.form.accounting.ribBic')" disabled />
|
||||||
<MalioInputText v-if="isFilled(rib.iban)" :model-value="rib.iban" :label="t('technique.providers.form.accounting.ribIban')" disabled />
|
<MalioInputText v-if="isFilled(rib.iban)" :model-value="rib.iban" :label="t('technique.providers.form.accounting.ribIban')" disabled />
|
||||||
|
|||||||
@@ -73,7 +73,9 @@
|
|||||||
v-for="(contact, index) in contacts"
|
v-for="(contact, index) in contacts"
|
||||||
:key="index"
|
:key="index"
|
||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
|
:title="t('technique.providers.form.contact.title', { n: index + 1 })"
|
||||||
:removable="isRowRemovable(contacts, index)"
|
:removable="isRowRemovable(contacts, index)"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
:disabled="isValidated('contact')"
|
:disabled="isValidated('contact')"
|
||||||
:errors="contactErrors[index]"
|
:errors="contactErrors[index]"
|
||||||
@update:model-value="(v) => contacts[index] = v"
|
@update:model-value="(v) => contacts[index] = v"
|
||||||
@@ -108,6 +110,8 @@
|
|||||||
v-for="(address, index) in addresses"
|
v-for="(address, index) in addresses"
|
||||||
:key="index"
|
:key="index"
|
||||||
:model-value="address"
|
:model-value="address"
|
||||||
|
:title="t('technique.providers.form.address.title', { n: index + 1 })"
|
||||||
|
:last="index === addresses.length - 1"
|
||||||
:site-options="referentials.sites.value"
|
:site-options="referentials.sites.value"
|
||||||
:contact-options="contactOptions"
|
:contact-options="contactOptions"
|
||||||
:country-options="countryOptions"
|
:country-options="countryOptions"
|
||||||
@@ -139,8 +143,10 @@
|
|||||||
<!-- Onglet Comptabilite (present uniquement si accounting.view ; editable si manage). -->
|
<!-- Onglet Comptabilite (present uniquement si accounting.view ; editable si manage). -->
|
||||||
<template v-if="canAccountingView" #accounting>
|
<template v-if="canAccountingView" #accounting>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
<!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
<div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
|
||||||
|
<h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.infoTitle') }}</h2>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="accounting.siren"
|
v-model="accounting.siren"
|
||||||
:label="t('technique.providers.form.accounting.siren')"
|
:label="t('technique.providers.form.accounting.siren')"
|
||||||
@@ -210,21 +216,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-3.08). -->
|
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-3.08).
|
||||||
|
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
|
||||||
<div
|
<div
|
||||||
v-for="(rib, index) in visibleRibs"
|
v-for="(rib, index) in visibleRibs"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
class="pb-[20px]"
|
||||||
|
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
|
||||||
>
|
>
|
||||||
<MalioButtonIcon
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
|
||||||
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
<div class="flex items-center justify-between">
|
||||||
icon="mdi:delete-outline"
|
<h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
|
||||||
variant="ghost"
|
<MalioButtonIcon
|
||||||
button-class="absolute top-3 right-3"
|
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
|
||||||
v-bind="{ ariaLabel: t('technique.providers.form.accounting.removeRib') }"
|
icon="mdi:delete-outline"
|
||||||
@click="askRemoveRib(index)"
|
variant="ghost"
|
||||||
/>
|
button-class="p-0"
|
||||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
v-bind="{ ariaLabel: t('technique.providers.form.accounting.removeRib') }"
|
||||||
|
@click="askRemoveRib(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="rib.label"
|
v-model="rib.label"
|
||||||
:label="t('technique.providers.form.accounting.ribLabel')"
|
:label="t('technique.providers.form.accounting.ribLabel')"
|
||||||
|
|||||||
@@ -1,103 +1,113 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Adresse UNIQUE par transporteur (ERP-172) : un seul bloc, jamais supprimable. -->
|
<!-- Adresse UNIQUE par transporteur (ERP-172) : un seul bloc, jamais supprimable. -->
|
||||||
<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)]">
|
<!-- Bloc a plat (sans box-shadow) : un filet noir 1px le separe du suivant
|
||||||
<!-- Pays : prerempli « France » (RG-4.05). -->
|
(pas de bordure sous le dernier bloc). -->
|
||||||
<MalioSelect
|
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
|
||||||
v-if="!hideEmpty || isFilled(model.country)"
|
<!-- En-tete : titre du bloc, en noir (adresse unique, sans suppression). -->
|
||||||
:model-value="model.country"
|
<div class="flex items-center justify-between">
|
||||||
:options="countryOptions"
|
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
|
||||||
:label="t('transport.carriers.form.address.country')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.country"
|
|
||||||
@update:model-value="(v: string | number | null) => update('country', String(v ?? 'France'))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Code postal (RG-4.06) : declenche l'autocomplete ville (BAN). -->
|
|
||||||
<MalioInputText
|
|
||||||
v-if="!hideEmpty || isFilled(model.postalCode)"
|
|
||||||
:model-value="model.postalCode"
|
|
||||||
:label="t('transport.carriers.form.address.postalCode')"
|
|
||||||
:mask="POSTAL_CODE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.postalCode"
|
|
||||||
@update:model-value="onPostalCodeChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Ville : MalioSelect alimente par le code postal (BAN). Saisie libre si BAN indispo. -->
|
|
||||||
<MalioSelect
|
|
||||||
v-if="!degraded && (!hideEmpty || isFilled(model.city))"
|
|
||||||
:model-value="model.city"
|
|
||||||
:options="cityOptions"
|
|
||||||
:label="t('transport.carriers.form.address.city')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
empty-option-label=""
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.city"
|
|
||||||
@update:model-value="onCityChange"
|
|
||||||
/>
|
|
||||||
<MalioInputText
|
|
||||||
v-else-if="degraded && (!hideEmpty || isFilled(model.city))"
|
|
||||||
:model-value="model.city"
|
|
||||||
:label="t('transport.carriers.form.address.city')"
|
|
||||||
:mask="ADDRESS_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.city"
|
|
||||||
@update:model-value="(v: string) => update('city', v)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Filler : aligne le debut de la ligne suivante sur la grille. Inutile en
|
|
||||||
consultation masquee (la grille se recompose sans les champs vides). -->
|
|
||||||
<div v-if="!hideEmpty" aria-hidden="true" />
|
|
||||||
|
|
||||||
<!-- Adresse (BAN) sur 2 colonnes + Adresse complementaire. allow-create : le
|
|
||||||
texte saisi est conserve si la BAN ne propose rien (saisie manuelle). -->
|
|
||||||
<div v-if="!hideEmpty || isFilled(model.street)" class="col-span-2">
|
|
||||||
<MalioInputAutocomplete
|
|
||||||
v-if="!readonly && !disabled"
|
|
||||||
:model-value="model.street"
|
|
||||||
:options="addressOptions"
|
|
||||||
:loading="addressLoading"
|
|
||||||
:min-search-length="3"
|
|
||||||
:label="t('transport.carriers.form.address.street')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.street"
|
|
||||||
:allow-create="true"
|
|
||||||
:no-results-text="t('transport.carriers.form.address.streetNotFound')"
|
|
||||||
@update:model-value="(v: string | number | null) => update('street', v === null ? null : String(v))"
|
|
||||||
@search="onAddressSearch"
|
|
||||||
@select="onAddressSelect"
|
|
||||||
/>
|
|
||||||
<MalioInputText
|
|
||||||
v-else
|
|
||||||
:model-value="model.street"
|
|
||||||
:label="t('transport.carriers.form.address.street')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="!readonly && !disabled"
|
|
||||||
:error="errors?.street"
|
|
||||||
@update:model-value="(v: string) => update('street', v)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MalioInputText
|
<!-- Grille 4 colonnes des champs de l'adresse. -->
|
||||||
v-if="!hideEmpty || isFilled(model.streetComplement)"
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
:model-value="model.streetComplement"
|
<!-- Pays : prerempli « France » (RG-4.05). -->
|
||||||
:label="t('transport.carriers.form.address.streetComplement')"
|
<MalioSelect
|
||||||
:mask="ADDRESS_MASK"
|
v-if="!hideEmpty || isFilled(model.country)"
|
||||||
:readonly="readonly"
|
:model-value="model.country"
|
||||||
:disabled="disabled"
|
:options="countryOptions"
|
||||||
:error="errors?.streetComplement"
|
:label="t('transport.carriers.form.address.country')"
|
||||||
@update:model-value="(v: string) => update('streetComplement', v)"
|
:readonly="readonly"
|
||||||
/>
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.country"
|
||||||
|
@update:model-value="(v: string | number | null) => update('country', String(v ?? 'France'))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Code postal (RG-4.06) : declenche l'autocomplete ville (BAN). -->
|
||||||
|
<MalioInputText
|
||||||
|
v-if="!hideEmpty || isFilled(model.postalCode)"
|
||||||
|
:model-value="model.postalCode"
|
||||||
|
:label="t('transport.carriers.form.address.postalCode')"
|
||||||
|
:mask="POSTAL_CODE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.postalCode"
|
||||||
|
@update:model-value="onPostalCodeChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Ville : MalioSelect alimente par le code postal (BAN). Saisie libre si BAN indispo. -->
|
||||||
|
<MalioSelect
|
||||||
|
v-if="!degraded && (!hideEmpty || isFilled(model.city))"
|
||||||
|
:model-value="model.city"
|
||||||
|
:options="cityOptions"
|
||||||
|
:label="t('transport.carriers.form.address.city')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.city"
|
||||||
|
@update:model-value="onCityChange"
|
||||||
|
/>
|
||||||
|
<MalioInputText
|
||||||
|
v-else-if="degraded && (!hideEmpty || isFilled(model.city))"
|
||||||
|
:model-value="model.city"
|
||||||
|
:label="t('transport.carriers.form.address.city')"
|
||||||
|
:mask="ADDRESS_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.city"
|
||||||
|
@update:model-value="(v: string) => update('city', v)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Filler : aligne le debut de la ligne suivante sur la grille. Inutile en
|
||||||
|
consultation masquee (la grille se recompose sans les champs vides). -->
|
||||||
|
<div v-if="!hideEmpty" aria-hidden="true" />
|
||||||
|
|
||||||
|
<!-- Adresse (BAN) sur 2 colonnes + Adresse complementaire. allow-create : le
|
||||||
|
texte saisi est conserve si la BAN ne propose rien (saisie manuelle). -->
|
||||||
|
<div v-if="!hideEmpty || isFilled(model.street)" class="col-span-2">
|
||||||
|
<MalioInputAutocomplete
|
||||||
|
v-if="!readonly && !disabled"
|
||||||
|
:model-value="model.street"
|
||||||
|
:options="addressOptions"
|
||||||
|
:loading="addressLoading"
|
||||||
|
:min-search-length="3"
|
||||||
|
:label="t('transport.carriers.form.address.street')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.street"
|
||||||
|
:allow-create="true"
|
||||||
|
:no-results-text="t('transport.carriers.form.address.streetNotFound')"
|
||||||
|
@update:model-value="(v: string | number | null) => update('street', v === null ? null : String(v))"
|
||||||
|
@search="onAddressSearch"
|
||||||
|
@select="onAddressSelect"
|
||||||
|
/>
|
||||||
|
<MalioInputText
|
||||||
|
v-else
|
||||||
|
:model-value="model.street"
|
||||||
|
:label="t('transport.carriers.form.address.street')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="!readonly && !disabled"
|
||||||
|
:error="errors?.street"
|
||||||
|
@update:model-value="(v: string) => update('street', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MalioInputText
|
||||||
|
v-if="!hideEmpty || isFilled(model.streetComplement)"
|
||||||
|
:model-value="model.streetComplement"
|
||||||
|
:label="t('transport.carriers.form.address.streetComplement')"
|
||||||
|
:mask="ADDRESS_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.streetComplement"
|
||||||
|
@update:model-value="(v: string) => update('streetComplement', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -118,8 +128,12 @@ const POSTAL_CODE_MASK = '#####'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** Brouillon de l'adresse (v-model). */
|
/** Brouillon de l'adresse (v-model). */
|
||||||
modelValue: CarrierAddressFormDraft
|
modelValue: CarrierAddressFormDraft
|
||||||
|
/** Titre du bloc (ex: « Adresse 1 »). */
|
||||||
|
title: string
|
||||||
/** Pays disponibles (France par defaut). */
|
/** Pays disponibles (France par defaut). */
|
||||||
countryOptions: RefOption[]
|
countryOptions: RefOption[]
|
||||||
|
/** Dernier bloc de la liste : supprime le filet de separation bas. */
|
||||||
|
last?: boolean
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
|||||||
@@ -1,84 +1,93 @@
|
|||||||
<template>
|
<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)]">
|
<!-- Bloc a plat (sans box-shadow) : un filet noir 1px le separe du suivant
|
||||||
<!-- Suppression : ouvre une modal de confirmation côté parent. Masquée si
|
(pas de bordure sous le dernier bloc). -->
|
||||||
non supprimable (1er bloc) ou en lecture seule. -->
|
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
|
||||||
<MalioButtonIcon
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
|
||||||
v-if="removable && !readonly && !disabled"
|
<div class="flex items-center justify-between">
|
||||||
icon="mdi:delete-outline"
|
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
|
||||||
variant="ghost"
|
<!-- Suppression : ouvre une modal de confirmation côté parent. Masquée si
|
||||||
button-class="absolute top-3 right-3"
|
non supprimable (1er bloc) ou en lecture seule. -->
|
||||||
v-bind="{ ariaLabel: t('transport.carriers.form.contact.remove') }"
|
<MalioButtonIcon
|
||||||
@click="$emit('remove')"
|
v-if="removable && !readonly && !disabled"
|
||||||
/>
|
icon="mdi:delete-outline"
|
||||||
|
variant="ghost"
|
||||||
<MalioInputText
|
button-class="p-0"
|
||||||
v-if="!hideEmpty || isFilled(model.lastName)"
|
v-bind="{ ariaLabel: t('transport.carriers.form.contact.remove') }"
|
||||||
:model-value="model.lastName"
|
@click="$emit('remove')"
|
||||||
:label="t('transport.carriers.form.contact.lastName')"
|
/>
|
||||||
:mask="PERSON_NAME_MASK"
|
</div>
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
<!-- Grille 4 colonnes des champs du contact. -->
|
||||||
:error="errors?.lastName"
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
@update:model-value="(v: string) => update('lastName', v)"
|
<MalioInputText
|
||||||
/>
|
v-if="!hideEmpty || isFilled(model.lastName)"
|
||||||
<MalioInputText
|
:model-value="model.lastName"
|
||||||
v-if="!hideEmpty || isFilled(model.firstName)"
|
:label="t('transport.carriers.form.contact.lastName')"
|
||||||
:model-value="model.firstName"
|
:mask="PERSON_NAME_MASK"
|
||||||
:label="t('transport.carriers.form.contact.firstName')"
|
:readonly="readonly"
|
||||||
:mask="PERSON_NAME_MASK"
|
:disabled="disabled"
|
||||||
:readonly="readonly"
|
:error="errors?.lastName"
|
||||||
:disabled="disabled"
|
@update:model-value="(v: string) => update('lastName', v)"
|
||||||
:error="errors?.firstName"
|
/>
|
||||||
@update:model-value="(v: string) => update('firstName', v)"
|
<MalioInputText
|
||||||
/>
|
v-if="!hideEmpty || isFilled(model.firstName)"
|
||||||
<!-- Fonction sur 2 colonnes : on wrappe car MalioInputText (inheritAttrs:false)
|
:model-value="model.firstName"
|
||||||
renvoie `class` sur l'input interne, pas sur la cellule de grille. -->
|
:label="t('transport.carriers.form.contact.firstName')"
|
||||||
<div v-if="!hideEmpty || isFilled(model.jobTitle)" class="col-span-2">
|
:mask="PERSON_NAME_MASK"
|
||||||
<MalioInputText
|
:readonly="readonly"
|
||||||
:model-value="model.jobTitle"
|
:disabled="disabled"
|
||||||
:label="t('transport.carriers.form.contact.jobTitle')"
|
:error="errors?.firstName"
|
||||||
:mask="FREE_TEXT_MASK"
|
@update:model-value="(v: string) => update('firstName', v)"
|
||||||
:readonly="readonly"
|
/>
|
||||||
:disabled="disabled"
|
<!-- Fonction sur 2 colonnes : on wrappe car MalioInputText (inheritAttrs:false)
|
||||||
:error="errors?.jobTitle"
|
renvoie `class` sur l'input interne, pas sur la cellule de grille. -->
|
||||||
@update:model-value="(v: string) => update('jobTitle', v)"
|
<div v-if="!hideEmpty || isFilled(model.jobTitle)" class="col-span-2">
|
||||||
|
<MalioInputText
|
||||||
|
:model-value="model.jobTitle"
|
||||||
|
:label="t('transport.carriers.form.contact.jobTitle')"
|
||||||
|
:mask="FREE_TEXT_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.jobTitle"
|
||||||
|
@update:model-value="(v: string) => update('jobTitle', v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<MalioInputEmail
|
||||||
|
v-if="!hideEmpty || isFilled(model.email)"
|
||||||
|
:model-value="model.email"
|
||||||
|
:label="t('transport.carriers.form.contact.email')"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:lowercase="true"
|
||||||
|
:error="errors?.email"
|
||||||
|
@update:model-value="(v: string) => update('email', v)"
|
||||||
|
/>
|
||||||
|
<!-- Téléphone principal + bouton « + » révélant le 2e numéro (max 2). -->
|
||||||
|
<MalioInputPhone
|
||||||
|
v-if="!hideEmpty || isFilled(model.phonePrimary)"
|
||||||
|
:model-value="model.phonePrimary"
|
||||||
|
:label="t('transport.carriers.form.contact.phonePrimary')"
|
||||||
|
:mask="PHONE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.phonePrimary"
|
||||||
|
:addable="!model.hasSecondaryPhone && !readonly"
|
||||||
|
:add-button-label="t('transport.carriers.form.contact.addPhone')"
|
||||||
|
@update:model-value="(v: string) => update('phonePrimary', v)"
|
||||||
|
@add="revealSecondaryPhone"
|
||||||
|
/>
|
||||||
|
<!-- 2e numéro : révélé à la demande (max 2 téléphones — RG-4.08). -->
|
||||||
|
<MalioInputPhone
|
||||||
|
v-if="model.hasSecondaryPhone && (!hideEmpty || isFilled(model.phoneSecondary))"
|
||||||
|
:model-value="model.phoneSecondary"
|
||||||
|
:label="t('transport.carriers.form.contact.phoneSecondary')"
|
||||||
|
:mask="PHONE_MASK"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.phoneSecondary"
|
||||||
|
@update:model-value="(v: string) => update('phoneSecondary', v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MalioInputEmail
|
|
||||||
v-if="!hideEmpty || isFilled(model.email)"
|
|
||||||
:model-value="model.email"
|
|
||||||
:label="t('transport.carriers.form.contact.email')"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:lowercase="true"
|
|
||||||
:error="errors?.email"
|
|
||||||
@update:model-value="(v: string) => update('email', v)"
|
|
||||||
/>
|
|
||||||
<!-- Téléphone principal + bouton « + » révélant le 2e numéro (max 2). -->
|
|
||||||
<MalioInputPhone
|
|
||||||
v-if="!hideEmpty || isFilled(model.phonePrimary)"
|
|
||||||
:model-value="model.phonePrimary"
|
|
||||||
:label="t('transport.carriers.form.contact.phonePrimary')"
|
|
||||||
:mask="PHONE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.phonePrimary"
|
|
||||||
:addable="!model.hasSecondaryPhone && !readonly"
|
|
||||||
:add-button-label="t('transport.carriers.form.contact.addPhone')"
|
|
||||||
@update:model-value="(v: string) => update('phonePrimary', v)"
|
|
||||||
@add="revealSecondaryPhone"
|
|
||||||
/>
|
|
||||||
<!-- 2e numéro : révélé à la demande (max 2 téléphones — RG-4.08). -->
|
|
||||||
<MalioInputPhone
|
|
||||||
v-if="model.hasSecondaryPhone && (!hideEmpty || isFilled(model.phoneSecondary))"
|
|
||||||
:model-value="model.phoneSecondary"
|
|
||||||
:label="t('transport.carriers.form.contact.phoneSecondary')"
|
|
||||||
:mask="PHONE_MASK"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.phoneSecondary"
|
|
||||||
@update:model-value="(v: string) => update('phoneSecondary', v)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -93,8 +102,12 @@ const PHONE_MASK = '## ## ## ## ##'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** Brouillon du contact (v-model). */
|
/** Brouillon du contact (v-model). */
|
||||||
modelValue: CarrierContactFormDraft
|
modelValue: CarrierContactFormDraft
|
||||||
|
/** Titre du bloc (ex: « Contact 1 »). */
|
||||||
|
title: string
|
||||||
/** Affiche l'icône de suppression (1er bloc non supprimable). */
|
/** Affiche l'icône de suppression (1er bloc non supprimable). */
|
||||||
removable?: boolean
|
removable?: boolean
|
||||||
|
/** Dernier bloc de la liste : supprime le filet de separation bas. */
|
||||||
|
last?: boolean
|
||||||
/** Bloc en lecture seule (onglet validé). */
|
/** Bloc en lecture seule (onglet validé). */
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
/** Bloc desactive (champs grises, consultation — distinct de readonly). */
|
||||||
|
|||||||
@@ -1,190 +1,199 @@
|
|||||||
<template>
|
<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)]">
|
<!-- Bloc a plat (sans box-shadow) : un filet noir 1px le separe du suivant
|
||||||
<!-- Suppression : modal de confirmation côté parent. -->
|
(pas de bordure sous le dernier bloc), aligne sur les blocs contact / adresse. -->
|
||||||
<MalioButtonIcon
|
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
|
||||||
v-if="removable && !readonly && !disabled"
|
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
|
||||||
icon="mdi:delete-outline"
|
<div class="flex items-center justify-between">
|
||||||
variant="ghost"
|
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
|
||||||
button-class="absolute top-3 right-3"
|
<!-- Suppression : modal de confirmation côté parent. -->
|
||||||
v-bind="{ ariaLabel: t('transport.carriers.form.price.remove') }"
|
<MalioButtonIcon
|
||||||
@click="$emit('remove')"
|
v-if="removable && !readonly && !disabled"
|
||||||
/>
|
icon="mdi:delete-outline"
|
||||||
|
variant="ghost"
|
||||||
<!-- RG-4.09 : sens du prix (CLIENT / FOURNISSEUR) en colonne 1 / ligne 1, radios
|
button-class="p-0"
|
||||||
EN LIGNE (horizontaux), centrés sur la hauteur de champ (h-12) comme la
|
v-bind="{ ariaLabel: t('transport.carriers.form.price.remove') }"
|
||||||
case « Affréter ». Pas de label de groupe. -->
|
@click="$emit('remove')"
|
||||||
<div>
|
/>
|
||||||
<div class="flex h-12 items-center gap-6">
|
|
||||||
<MalioRadioButton
|
|
||||||
:model-value="model.direction"
|
|
||||||
:name="`price-direction-${uid}`"
|
|
||||||
value="CLIENT"
|
|
||||||
:label="t('transport.carriers.form.price.directionClient')"
|
|
||||||
:disabled="readonly || disabled"
|
|
||||||
group-class="mt-0"
|
|
||||||
@update:model-value="onDirectionChange"
|
|
||||||
/>
|
|
||||||
<MalioRadioButton
|
|
||||||
:model-value="model.direction"
|
|
||||||
:name="`price-direction-${uid}`"
|
|
||||||
value="FOURNISSEUR"
|
|
||||||
:label="t('transport.carriers.form.price.directionSupplier')"
|
|
||||||
:disabled="readonly || disabled"
|
|
||||||
group-class="mt-0"
|
|
||||||
@update:model-value="onDirectionChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p v-if="errors?.direction" class="ml-[2px] text-xs text-m-danger">{{ errors.direction }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Branche CLIENT (RG-4.10). -->
|
<!-- Grille 4 colonnes des champs du prix. -->
|
||||||
<template v-if="model.direction === 'CLIENT'">
|
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<MalioSelect
|
<!-- RG-4.09 : sens du prix (CLIENT / FOURNISSEUR) en colonne 1 / ligne 1, radios
|
||||||
:model-value="model.clientIri"
|
EN LIGNE (horizontaux), centrés sur la hauteur de champ (h-12) comme la
|
||||||
:options="clientOptions"
|
case « Affréter ». Pas de label de groupe. -->
|
||||||
:label="t('transport.carriers.form.price.client')"
|
|
||||||
empty-option-label=""
|
|
||||||
:required="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.client"
|
|
||||||
@update:model-value="onClientChange"
|
|
||||||
/>
|
|
||||||
<MalioSelect
|
|
||||||
:model-value="model.clientDeliveryAddressIri"
|
|
||||||
:options="clientAddressOptions"
|
|
||||||
:label="t('transport.carriers.form.price.clientDeliveryAddress')"
|
|
||||||
empty-option-label=""
|
|
||||||
:required="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.clientDeliveryAddress"
|
|
||||||
@update:model-value="(v: string | number | null) => update('clientDeliveryAddressIri', v === null ? null : String(v))"
|
|
||||||
/>
|
|
||||||
<MalioSelect
|
|
||||||
:model-value="model.departureSiteIri"
|
|
||||||
:options="siteOptions"
|
|
||||||
:label="t('transport.carriers.form.price.departureSite')"
|
|
||||||
empty-option-label=""
|
|
||||||
:required="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.departureSite"
|
|
||||||
@update:model-value="(v: string | number | null) => update('departureSiteIri', v === null ? null : String(v))"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Branche FOURNISSEUR (RG-4.11). -->
|
|
||||||
<template v-else-if="model.direction === 'FOURNISSEUR'">
|
|
||||||
<MalioSelect
|
|
||||||
:model-value="model.supplierIri"
|
|
||||||
:options="supplierOptions"
|
|
||||||
:label="t('transport.carriers.form.price.supplier')"
|
|
||||||
empty-option-label=""
|
|
||||||
:required="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.supplier"
|
|
||||||
@update:model-value="onSupplierChange"
|
|
||||||
/>
|
|
||||||
<MalioSelect
|
|
||||||
:model-value="model.supplierSupplyAddressIri"
|
|
||||||
:options="supplierAddressOptions"
|
|
||||||
:label="t('transport.carriers.form.price.supplierSupplyAddress')"
|
|
||||||
empty-option-label=""
|
|
||||||
:required="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.supplierSupplyAddress"
|
|
||||||
@update:model-value="(v: string | number | null) => update('supplierSupplyAddressIri', v === null ? null : String(v))"
|
|
||||||
/>
|
|
||||||
<MalioSelect
|
|
||||||
:model-value="model.deliverySiteIri"
|
|
||||||
:options="siteOptions"
|
|
||||||
:label="t('transport.carriers.form.price.deliverySite')"
|
|
||||||
empty-option-label=""
|
|
||||||
:required="true"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:error="errors?.deliverySite"
|
|
||||||
@update:model-value="(v: string | number | null) => update('deliverySiteIri', v === null ? null : String(v))"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Communs (visibles dès qu'un sens est choisi). -->
|
|
||||||
<template v-if="model.direction !== null">
|
|
||||||
<!-- Contenant : Benne / Fond mouvant (radios centrés h-12, pas de label). -->
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex h-12 items-center gap-4">
|
<div class="flex h-12 items-center gap-6">
|
||||||
<MalioRadioButton
|
<MalioRadioButton
|
||||||
:model-value="model.containerType"
|
:model-value="model.direction"
|
||||||
:name="`price-container-${uid}`"
|
:name="`price-direction-${uid}`"
|
||||||
value="BENNE"
|
value="CLIENT"
|
||||||
:label="t('transport.carriers.containerType.BENNE')"
|
:label="t('transport.carriers.form.price.directionClient')"
|
||||||
:disabled="readonly || disabled"
|
:disabled="readonly || disabled"
|
||||||
group-class="mt-0"
|
group-class="mt-0"
|
||||||
@update:model-value="(v: string | number | boolean | null) => update('containerType', v === null ? null : String(v))"
|
@update:model-value="onDirectionChange"
|
||||||
/>
|
/>
|
||||||
<MalioRadioButton
|
<MalioRadioButton
|
||||||
:model-value="model.containerType"
|
:model-value="model.direction"
|
||||||
:name="`price-container-${uid}`"
|
:name="`price-direction-${uid}`"
|
||||||
value="FOND_MOUVANT"
|
value="FOURNISSEUR"
|
||||||
:label="t('transport.carriers.containerType.FOND_MOUVANT')"
|
:label="t('transport.carriers.form.price.directionSupplier')"
|
||||||
:disabled="readonly || disabled"
|
:disabled="readonly || disabled"
|
||||||
group-class="mt-0"
|
group-class="mt-0"
|
||||||
@update:model-value="(v: string | number | boolean | null) => update('containerType', v === null ? null : String(v))"
|
@update:model-value="onDirectionChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="errors?.containerType" class="ml-[2px] text-xs text-m-danger">{{ errors.containerType }}</p>
|
<p v-if="errors?.direction" class="ml-[2px] text-xs text-m-danger">{{ errors.direction }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tarification : Forfait / Tonne (radios centrés h-12, pas de label). -->
|
<!-- Branche CLIENT (RG-4.10). -->
|
||||||
<div>
|
<template v-if="model.direction === 'CLIENT'">
|
||||||
<div class="flex h-12 items-center gap-4">
|
<MalioSelect
|
||||||
<MalioRadioButton
|
:model-value="model.clientIri"
|
||||||
:model-value="model.pricingUnit"
|
:options="clientOptions"
|
||||||
:name="`price-unit-${uid}`"
|
:label="t('transport.carriers.form.price.client')"
|
||||||
value="FORFAIT"
|
empty-option-label=""
|
||||||
:label="t('transport.carriers.form.price.pricingForfait')"
|
:required="true"
|
||||||
:disabled="readonly || disabled"
|
:readonly="readonly"
|
||||||
group-class="mt-0"
|
:disabled="disabled"
|
||||||
@update:model-value="(v: string | number | boolean | null) => update('pricingUnit', v === null ? null : String(v))"
|
:error="errors?.client"
|
||||||
/>
|
@update:model-value="onClientChange"
|
||||||
<MalioRadioButton
|
/>
|
||||||
:model-value="model.pricingUnit"
|
<MalioSelect
|
||||||
:name="`price-unit-${uid}`"
|
:model-value="model.clientDeliveryAddressIri"
|
||||||
value="TONNE"
|
:options="clientAddressOptions"
|
||||||
:label="t('transport.carriers.form.price.pricingTonne')"
|
:label="t('transport.carriers.form.price.clientDeliveryAddress')"
|
||||||
:disabled="readonly || disabled"
|
empty-option-label=""
|
||||||
group-class="mt-0"
|
:required="true"
|
||||||
@update:model-value="(v: string | number | boolean | null) => update('pricingUnit', v === null ? null : String(v))"
|
:readonly="readonly"
|
||||||
/>
|
:disabled="disabled"
|
||||||
|
:error="errors?.clientDeliveryAddress"
|
||||||
|
@update:model-value="(v: string | number | null) => update('clientDeliveryAddressIri', v === null ? null : String(v))"
|
||||||
|
/>
|
||||||
|
<MalioSelect
|
||||||
|
:model-value="model.departureSiteIri"
|
||||||
|
:options="siteOptions"
|
||||||
|
:label="t('transport.carriers.form.price.departureSite')"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.departureSite"
|
||||||
|
@update:model-value="(v: string | number | null) => update('departureSiteIri', v === null ? null : String(v))"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Branche FOURNISSEUR (RG-4.11). -->
|
||||||
|
<template v-else-if="model.direction === 'FOURNISSEUR'">
|
||||||
|
<MalioSelect
|
||||||
|
:model-value="model.supplierIri"
|
||||||
|
:options="supplierOptions"
|
||||||
|
:label="t('transport.carriers.form.price.supplier')"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.supplier"
|
||||||
|
@update:model-value="onSupplierChange"
|
||||||
|
/>
|
||||||
|
<MalioSelect
|
||||||
|
:model-value="model.supplierSupplyAddressIri"
|
||||||
|
:options="supplierAddressOptions"
|
||||||
|
:label="t('transport.carriers.form.price.supplierSupplyAddress')"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.supplierSupplyAddress"
|
||||||
|
@update:model-value="(v: string | number | null) => update('supplierSupplyAddressIri', v === null ? null : String(v))"
|
||||||
|
/>
|
||||||
|
<MalioSelect
|
||||||
|
:model-value="model.deliverySiteIri"
|
||||||
|
:options="siteOptions"
|
||||||
|
:label="t('transport.carriers.form.price.deliverySite')"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.deliverySite"
|
||||||
|
@update:model-value="(v: string | number | null) => update('deliverySiteIri', v === null ? null : String(v))"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Communs (visibles dès qu'un sens est choisi). -->
|
||||||
|
<template v-if="model.direction !== null">
|
||||||
|
<!-- Contenant : Benne / Fond mouvant (radios centrés h-12, pas de label). -->
|
||||||
|
<div>
|
||||||
|
<div class="flex h-12 items-center gap-4">
|
||||||
|
<MalioRadioButton
|
||||||
|
:model-value="model.containerType"
|
||||||
|
:name="`price-container-${uid}`"
|
||||||
|
value="BENNE"
|
||||||
|
:label="t('transport.carriers.containerType.BENNE')"
|
||||||
|
:disabled="readonly || disabled"
|
||||||
|
group-class="mt-0"
|
||||||
|
@update:model-value="(v: string | number | boolean | null) => update('containerType', v === null ? null : String(v))"
|
||||||
|
/>
|
||||||
|
<MalioRadioButton
|
||||||
|
:model-value="model.containerType"
|
||||||
|
:name="`price-container-${uid}`"
|
||||||
|
value="FOND_MOUVANT"
|
||||||
|
:label="t('transport.carriers.containerType.FOND_MOUVANT')"
|
||||||
|
:disabled="readonly || disabled"
|
||||||
|
group-class="mt-0"
|
||||||
|
@update:model-value="(v: string | number | boolean | null) => update('containerType', v === null ? null : String(v))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="errors?.containerType" class="ml-[2px] text-xs text-m-danger">{{ errors.containerType }}</p>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="errors?.pricingUnit" class="ml-[2px] text-xs text-m-danger">{{ errors.pricingUnit }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MalioInputAmount
|
<!-- Tarification : Forfait / Tonne (radios centrés h-12, pas de label). -->
|
||||||
:model-value="model.price"
|
<div>
|
||||||
:label="t('transport.carriers.form.price.price')"
|
<div class="flex h-12 items-center gap-4">
|
||||||
:required="true"
|
<MalioRadioButton
|
||||||
:readonly="readonly"
|
:model-value="model.pricingUnit"
|
||||||
:disabled="disabled"
|
:name="`price-unit-${uid}`"
|
||||||
:error="errors?.price"
|
value="FORFAIT"
|
||||||
@update:model-value="(v: string) => update('price', v)"
|
:label="t('transport.carriers.form.price.pricingForfait')"
|
||||||
/>
|
:disabled="readonly || disabled"
|
||||||
|
group-class="mt-0"
|
||||||
|
@update:model-value="(v: string | number | boolean | null) => update('pricingUnit', v === null ? null : String(v))"
|
||||||
|
/>
|
||||||
|
<MalioRadioButton
|
||||||
|
:model-value="model.pricingUnit"
|
||||||
|
:name="`price-unit-${uid}`"
|
||||||
|
value="TONNE"
|
||||||
|
:label="t('transport.carriers.form.price.pricingTonne')"
|
||||||
|
:disabled="readonly || disabled"
|
||||||
|
group-class="mt-0"
|
||||||
|
@update:model-value="(v: string | number | boolean | null) => update('pricingUnit', v === null ? null : String(v))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="errors?.pricingUnit" class="ml-[2px] text-xs text-m-danger">{{ errors.pricingUnit }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MalioSelect
|
<MalioInputAmount
|
||||||
:model-value="model.priceState"
|
:model-value="model.price"
|
||||||
:options="priceStateOptions"
|
:label="t('transport.carriers.form.price.price')"
|
||||||
:label="t('transport.carriers.form.price.priceState')"
|
:required="true"
|
||||||
empty-option-label=""
|
:readonly="readonly"
|
||||||
:required="true"
|
:disabled="disabled"
|
||||||
:readonly="readonly"
|
:error="errors?.price"
|
||||||
:disabled="disabled"
|
@update:model-value="(v: string) => update('price', v)"
|
||||||
:error="errors?.priceState"
|
/>
|
||||||
@update:model-value="(v: string | number | null) => update('priceState', v === null ? null : String(v))"
|
|
||||||
/>
|
<MalioSelect
|
||||||
</template>
|
:model-value="model.priceState"
|
||||||
|
:options="priceStateOptions"
|
||||||
|
:label="t('transport.carriers.form.price.priceState')"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="true"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:error="errors?.priceState"
|
||||||
|
@update:model-value="(v: string | number | null) => update('priceState', v === null ? null : String(v))"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -200,6 +209,10 @@ interface SelectOption {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** Brouillon du prix (v-model). */
|
/** Brouillon du prix (v-model). */
|
||||||
modelValue: CarrierPriceFormDraft
|
modelValue: CarrierPriceFormDraft
|
||||||
|
/** Titre du bloc (ex: « Prix 1 »). */
|
||||||
|
title: string
|
||||||
|
/** Dernier bloc de la liste : supprime le filet de separation bas. */
|
||||||
|
last?: boolean
|
||||||
/** Clients disponibles (IRI en value). */
|
/** Clients disponibles (IRI en value). */
|
||||||
clientOptions: SelectOption[]
|
clientOptions: SelectOption[]
|
||||||
/** Fournisseurs disponibles (IRI en value). */
|
/** Fournisseurs disponibles (IRI en value). */
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ function mountBlock(overrides: Record<string, unknown> = {}) {
|
|||||||
return mount(CarrierAddressBlock, {
|
return mount(CarrierAddressBlock, {
|
||||||
props: {
|
props: {
|
||||||
modelValue: { ...emptyCarrierAddress(), ...overrides },
|
modelValue: { ...emptyCarrierAddress(), ...overrides },
|
||||||
|
title: 'Adresse 1',
|
||||||
countryOptions: [{ value: 'France', label: 'France' }],
|
countryOptions: [{ value: 'France', label: 'France' }],
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
|
|||||||
@@ -143,6 +143,8 @@
|
|||||||
<!-- Adresse UNIQUE (ERP-172) : un seul bloc, sans ajouter/supprimer. -->
|
<!-- Adresse UNIQUE (ERP-172) : un seul bloc, sans ajouter/supprimer. -->
|
||||||
<CarrierAddressBlock
|
<CarrierAddressBlock
|
||||||
:model-value="address"
|
:model-value="address"
|
||||||
|
:title="t('transport.carriers.form.address.title')"
|
||||||
|
:last="true"
|
||||||
:country-options="countryOptions"
|
:country-options="countryOptions"
|
||||||
:errors="addressErrors"
|
:errors="addressErrors"
|
||||||
@update:model-value="(v) => address = v"
|
@update:model-value="(v) => address = v"
|
||||||
@@ -160,7 +162,9 @@
|
|||||||
v-for="(contact, index) in contacts"
|
v-for="(contact, index) in contacts"
|
||||||
:key="index"
|
:key="index"
|
||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
|
:title="t('transport.carriers.form.contact.title', { n: index + 1 })"
|
||||||
:removable="isRowRemovable(contacts, index)"
|
:removable="isRowRemovable(contacts, index)"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
:errors="contactErrors[index]"
|
:errors="contactErrors[index]"
|
||||||
@update:model-value="(v) => contacts[index] = v"
|
@update:model-value="(v) => contacts[index] = v"
|
||||||
@remove="askRemoveContact(index)"
|
@remove="askRemoveContact(index)"
|
||||||
@@ -178,10 +182,12 @@
|
|||||||
v-for="(price, index) in prices"
|
v-for="(price, index) in prices"
|
||||||
:key="index"
|
:key="index"
|
||||||
:model-value="price"
|
:model-value="price"
|
||||||
|
:title="t('transport.carriers.form.price.title', { n: index + 1 })"
|
||||||
:client-options="clientOptions"
|
:client-options="clientOptions"
|
||||||
:supplier-options="supplierOptions"
|
:supplier-options="supplierOptions"
|
||||||
:site-options="siteOptions"
|
:site-options="siteOptions"
|
||||||
removable
|
removable
|
||||||
|
:last="index === prices.length - 1"
|
||||||
:errors="priceErrors[index]"
|
:errors="priceErrors[index]"
|
||||||
@update:model-value="(v) => prices[index] = v"
|
@update:model-value="(v) => prices[index] = v"
|
||||||
@remove="askRemovePrice(index)"
|
@remove="askRemovePrice(index)"
|
||||||
|
|||||||
@@ -123,6 +123,8 @@
|
|||||||
<!-- Adresse UNIQUE (ERP-172). -->
|
<!-- Adresse UNIQUE (ERP-172). -->
|
||||||
<CarrierAddressBlock
|
<CarrierAddressBlock
|
||||||
:model-value="address"
|
:model-value="address"
|
||||||
|
:title="t('transport.carriers.form.address.title')"
|
||||||
|
:last="true"
|
||||||
:country-options="countryOptionsFor(address.country)"
|
:country-options="countryOptionsFor(address.country)"
|
||||||
disabled
|
disabled
|
||||||
hide-empty
|
hide-empty
|
||||||
@@ -136,6 +138,8 @@
|
|||||||
v-for="(contact, index) in contacts"
|
v-for="(contact, index) in contacts"
|
||||||
:key="index"
|
:key="index"
|
||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
|
:title="t('transport.carriers.form.contact.title', { n: index + 1 })"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
disabled
|
disabled
|
||||||
hide-empty
|
hide-empty
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -180,6 +180,8 @@
|
|||||||
<!-- Adresse UNIQUE (ERP-172) : un seul bloc, sans ajouter/supprimer. -->
|
<!-- Adresse UNIQUE (ERP-172) : un seul bloc, sans ajouter/supprimer. -->
|
||||||
<CarrierAddressBlock
|
<CarrierAddressBlock
|
||||||
:model-value="address"
|
:model-value="address"
|
||||||
|
:title="t('transport.carriers.form.address.title')"
|
||||||
|
:last="true"
|
||||||
:country-options="countryOptions"
|
:country-options="countryOptions"
|
||||||
:disabled="isQualimat || isValidated('addresses')"
|
:disabled="isQualimat || isValidated('addresses')"
|
||||||
:errors="addressErrors"
|
:errors="addressErrors"
|
||||||
@@ -207,7 +209,9 @@
|
|||||||
v-for="(contact, index) in contacts"
|
v-for="(contact, index) in contacts"
|
||||||
:key="index"
|
:key="index"
|
||||||
:model-value="contact"
|
:model-value="contact"
|
||||||
|
:title="t('transport.carriers.form.contact.title', { n: index + 1 })"
|
||||||
:removable="isRowRemovable(contacts, index)"
|
:removable="isRowRemovable(contacts, index)"
|
||||||
|
:last="index === contacts.length - 1"
|
||||||
:disabled="isValidated('contacts')"
|
:disabled="isValidated('contacts')"
|
||||||
:errors="contactErrors[index]"
|
:errors="contactErrors[index]"
|
||||||
@update:model-value="(v) => contacts[index] = v"
|
@update:model-value="(v) => contacts[index] = v"
|
||||||
@@ -240,11 +244,13 @@
|
|||||||
v-for="(price, index) in prices"
|
v-for="(price, index) in prices"
|
||||||
:key="index"
|
:key="index"
|
||||||
:model-value="price"
|
:model-value="price"
|
||||||
|
:title="t('transport.carriers.form.price.title', { n: index + 1 })"
|
||||||
:client-options="clientOptions"
|
:client-options="clientOptions"
|
||||||
:supplier-options="supplierOptions"
|
:supplier-options="supplierOptions"
|
||||||
:site-options="siteOptions"
|
:site-options="siteOptions"
|
||||||
:removable="!isValidated('prices')"
|
:removable="!isValidated('prices')"
|
||||||
:disabled="isValidated('prices')"
|
:disabled="isValidated('prices')"
|
||||||
|
:last="index === prices.length - 1"
|
||||||
:errors="priceErrors[index]"
|
:errors="priceErrors[index]"
|
||||||
@update:model-value="(v) => prices[index] = v"
|
@update:model-value="(v) => prices[index] = v"
|
||||||
@remove="askRemovePrice(index)"
|
@remove="askRemovePrice(index)"
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ArrayParameterType;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taxonomie ADRESSE (module Catalog) — categories du champ « Categorie » des blocs adresse.
|
||||||
|
*
|
||||||
|
* Contexte : jusqu'ici le multi-select « Categorie » des blocs adresse reutilisait
|
||||||
|
* la taxonomie CLIENT (M1, codes DISTRIBUTEUR/COURTIER blacklistes par RG-1.29) ou
|
||||||
|
* FOURNISSEUR (M2, RG-2.10). On introduit un type dedie ADRESSE : les blocs adresse
|
||||||
|
* client (ClientAddress) et fournisseur (SupplierAddress) ne referencent plus que
|
||||||
|
* des `Category` rattachees au type ADRESSE (validation whitelist par type).
|
||||||
|
*
|
||||||
|
* Cette migration :
|
||||||
|
* 1. cree le `category_type` ADRESSE (code ADRESSE, label « Adresse ») ;
|
||||||
|
* 2. seede 6 `Category` rattachees a ce type via la jonction ManyToMany
|
||||||
|
* `category_category_type` (modele courant depuis Version20260608120000 ;
|
||||||
|
* la colonne ManyToOne `category.category_type_id` n'existe plus).
|
||||||
|
*
|
||||||
|
* Aucune colonne creee/modifiee -> pas de `COMMENT ON COLUMN` (regle ABSOLUE n°12) :
|
||||||
|
* la migration ne fait que des INSERT de donnees de reference.
|
||||||
|
*
|
||||||
|
* Namespace racine `DoctrineMigrations` (regle ABSOLUE n°11) et NON modulaire :
|
||||||
|
* garantit l'ordre par timestamp avant les migrations modulaires sur base vide.
|
||||||
|
*
|
||||||
|
* Idempotence : `INSERT ... ON CONFLICT (code) DO NOTHING` pour le type,
|
||||||
|
* `INSERT ... SELECT ... WHERE NOT EXISTS` pour chaque categorie et chaque ligne
|
||||||
|
* de jonction (aligne sur le pattern PRESTATAIRE / Version20260612080000). En prod
|
||||||
|
* la table `category` est vide (aucune fixture metier) ; en dev/test le purger
|
||||||
|
* Doctrine vide `category` / `category_type` avant les fixtures qui reproduisent le
|
||||||
|
* meme etat final (CategoryTypeFixtures / CategoryFixtures etendus a ADRESSE).
|
||||||
|
*/
|
||||||
|
final class Version20260625100000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Categories de demonstration du type ADRESSE : nom => code stable. Le code est
|
||||||
|
* la cle metier (slug MAJUSCULE du nom, miroir du CategoryCodeGenerator) et reste
|
||||||
|
* unique parmi les actifs (uq_category_code). Le nom est unique GLOBALEMENT parmi
|
||||||
|
* les actifs (uq_category_name_active) : aucune collision avec les categories
|
||||||
|
* deja seedees (CLIENT / FOURNISSEUR / PRESTATAIRE).
|
||||||
|
*/
|
||||||
|
private const array ADDRESS_CATEGORIES = [
|
||||||
|
'Siège' => 'SIEGE',
|
||||||
|
'Contact issues' => 'CONTACT_ISSUES',
|
||||||
|
'Facturation' => 'FACTURATION',
|
||||||
|
'Livraison' => 'LIVRAISON',
|
||||||
|
'Approvisionnement' => 'APPROVISIONNEMENT',
|
||||||
|
'Méthaniseur' => 'METHANISEUR',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Taxonomie ADRESSE : cree le CategoryType ADRESSE + seed des categories adresse (Siege, Contact issues, Facturation, Livraison, Approvisionnement, Methaniseur).';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// 1. Type ADRESSE (idempotent via l'index unique uq_category_type_code).
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
INSERT INTO category_type (code, label) VALUES ('ADRESSE', 'Adresse')
|
||||||
|
ON CONFLICT (code) DO NOTHING
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
foreach (self::ADDRESS_CATEGORIES as $name => $code) {
|
||||||
|
// 2a. Categorie sous ADRESSE (si le code est libre parmi les actifs).
|
||||||
|
// created_at/updated_at NOT NULL -> NOW() ; le blame reste null
|
||||||
|
// (seed hors contexte HTTP, libelle « Systeme » cote front).
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
INSERT INTO category (name, code, created_at, updated_at)
|
||||||
|
SELECT :name, :code, NOW(), NOW()
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM category c WHERE c.code = :code AND c.deleted_at IS NULL
|
||||||
|
)
|
||||||
|
SQL, ['name' => $name, 'code' => $code]);
|
||||||
|
|
||||||
|
// 2b. Jonction M2M categorie <-> type ADRESSE (modele courant).
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
INSERT INTO category_category_type (category_id, category_type_id)
|
||||||
|
SELECT c.id, ct.id
|
||||||
|
FROM category c
|
||||||
|
CROSS JOIN category_type ct
|
||||||
|
WHERE c.code = :code AND c.deleted_at IS NULL
|
||||||
|
AND ct.code = 'ADRESSE'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM category_category_type cct
|
||||||
|
WHERE cct.category_id = c.id AND cct.category_type_id = ct.id
|
||||||
|
)
|
||||||
|
SQL, ['code' => $code]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// Best-effort : on retire d'abord les categories seedees (par code) — la FK
|
||||||
|
// category_category_type est ON DELETE CASCADE cote category, donc les lignes
|
||||||
|
// de jonction partent avec —, puis le type s'il n'est plus reference.
|
||||||
|
$this->addSql(
|
||||||
|
'DELETE FROM category WHERE code IN (:codes) '
|
||||||
|
.'AND id IN (SELECT category_id FROM category_category_type cct '
|
||||||
|
."JOIN category_type ct ON ct.id = cct.category_type_id WHERE ct.code = 'ADRESSE')",
|
||||||
|
['codes' => array_values(self::ADDRESS_CATEGORIES)],
|
||||||
|
['codes' => ArrayParameterType::STRING],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DELETE FROM category_type
|
||||||
|
WHERE code = 'ADRESSE'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM category_category_type cct WHERE cct.category_type_id = category_type.id
|
||||||
|
)
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,10 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|||||||
* a leur CategoryType. Le type CLIENT porte ~11 categories clients (refonte
|
* a leur CategoryType. Le type CLIENT porte ~11 categories clients (refonte
|
||||||
* taxonomie ERP-78) ; le type FOURNISSEUR porte les categories fournisseurs
|
* taxonomie ERP-78) ; le type FOURNISSEUR porte les categories fournisseurs
|
||||||
* (ERP-84 : Negociant, Cooperative...) ; le type PRESTATAIRE porte les categories
|
* (ERP-84 : Negociant, Cooperative...) ; le type PRESTATAIRE porte les categories
|
||||||
* prestataires (M3 1.1 : Maintenance industrielle, Nettoyage, Transport). Chaque
|
* prestataires (M3 1.1 : Maintenance industrielle, Nettoyage, Transport) ; le type
|
||||||
* categorie porte un `code` stable.
|
* ADRESSE porte les categories des blocs adresse (Siege, Contact issues,
|
||||||
|
* Facturation, Livraison, Approvisionnement, Methaniseur). Chaque categorie porte
|
||||||
|
* un `code` stable.
|
||||||
* Alimente le repertoire clients (ClientFixtures, module Commercial) avec des
|
* Alimente le repertoire clients (ClientFixtures, module Commercial) avec des
|
||||||
* donnees realistes couvrant RG-1.03 (codes DISTRIBUTEUR / COURTIER) et RG-1.29
|
* donnees realistes couvrant RG-1.03 (codes DISTRIBUTEUR / COURTIER) et RG-1.29
|
||||||
* (codes interdits sur adresse), et le multi-select Categorie fournisseur (M2).
|
* (codes interdits sur adresse), et le multi-select Categorie fournisseur (M2).
|
||||||
@@ -78,6 +80,14 @@ class CategoryFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
'Nettoyage' => 'NETTOYAGE',
|
'Nettoyage' => 'NETTOYAGE',
|
||||||
'Transport' => 'TRANSPORT',
|
'Transport' => 'TRANSPORT',
|
||||||
],
|
],
|
||||||
|
'ADRESSE' => [
|
||||||
|
'Siège' => 'SIEGE',
|
||||||
|
'Contact issues' => 'CONTACT_ISSUES',
|
||||||
|
'Facturation' => 'FACTURATION',
|
||||||
|
'Livraison' => 'LIVRAISON',
|
||||||
|
'Approvisionnement' => 'APPROVISIONNEMENT',
|
||||||
|
'Méthaniseur' => 'METHANISEUR',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ use Doctrine\Persistence\ObjectManager;
|
|||||||
* taxonomie distincte des prestataires (Maintenance industrielle, Nettoyage,
|
* taxonomie distincte des prestataires (Maintenance industrielle, Nettoyage,
|
||||||
* Transport). Mirroir de la migration Version20260612080000.
|
* Transport). Mirroir de la migration Version20260612080000.
|
||||||
*
|
*
|
||||||
|
* ADRESSE : ajout du type ADRESSE (code ADRESSE, label « Adresse »), taxonomie
|
||||||
|
* dediee au champ « Categorie » des blocs adresse (client + fournisseur). Mirroir
|
||||||
|
* de la migration Version20260625100000.
|
||||||
|
*
|
||||||
* Pourquoi une fixture EN PLUS du seed de la migration : `category_type` est une
|
* Pourquoi une fixture EN PLUS du seed de la migration : `category_type` est une
|
||||||
* entite managee par l ORM, donc le purger Doctrine la vide avant chaque
|
* entite managee par l ORM, donc le purger Doctrine la vide avant chaque
|
||||||
* `doctrine:fixtures:load`. Sans cette fixture, le type CLIENT seede par la
|
* `doctrine:fixtures:load`. Sans cette fixture, le type CLIENT seede par la
|
||||||
@@ -41,12 +45,14 @@ class CategoryTypeFixtures extends Fixture
|
|||||||
/**
|
/**
|
||||||
* Source unique des types : code technique => libelle FR. Doit rester aligne
|
* Source unique des types : code technique => libelle FR. Doit rester aligne
|
||||||
* sur le seed des migrations Version20260602100000 (CLIENT),
|
* sur le seed des migrations Version20260602100000 (CLIENT),
|
||||||
* Version20260605120000 (FOURNISSEUR) et Version20260612080000 (PRESTATAIRE).
|
* Version20260605120000 (FOURNISSEUR), Version20260612080000 (PRESTATAIRE) et
|
||||||
|
* Version20260625100000 (ADRESSE).
|
||||||
*/
|
*/
|
||||||
private const TYPES = [
|
private const TYPES = [
|
||||||
'CLIENT' => 'Client',
|
'CLIENT' => 'Client',
|
||||||
'FOURNISSEUR' => 'Fournisseur',
|
'FOURNISSEUR' => 'Fournisseur',
|
||||||
'PRESTATAIRE' => 'Prestataire',
|
'PRESTATAIRE' => 'Prestataire',
|
||||||
|
'ADRESSE' => 'Adresse',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|||||||
* - sites : SiteInterface (module Sites) via resolve_target_entities
|
* - sites : SiteInterface (module Sites) via resolve_target_entities
|
||||||
* - contacts : ClientContact (meme module)
|
* - contacts : ClientContact (meme module)
|
||||||
* - categories : CategoryInterface (module Catalog) via resolve_target_entities
|
* - categories : CategoryInterface (module Catalog) via resolve_target_entities
|
||||||
* — codes DISTRIBUTEUR/COURTIER interdits (RG-1.29, validateCategoryCodes, ERP-78)
|
* — type ADRESSE attendu (validateCategoryType)
|
||||||
*
|
*
|
||||||
* Audite (#[Auditable]) + Timestampable/Blamable.
|
* Audite (#[Auditable]) + Timestampable/Blamable.
|
||||||
*
|
*
|
||||||
@@ -96,11 +96,11 @@ class ClientAddress implements TimestampableInterface, BlamableInterface, Client
|
|||||||
use TimestampableBlamableTrait;
|
use TimestampableBlamableTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RG-1.29 (ERP-78) : ces codes de categorie decrivent une relation entre
|
* Seules les categories PORTANT ce type sont autorisees sur une adresse client.
|
||||||
* clients (distributeur / courtier) et n'ont pas de sens sur une adresse.
|
* S'appuie sur CategoryInterface::getCategoryTypeCodes() (multi-type — pas
|
||||||
* Toute autre categorie du type CLIENT est autorisee.
|
* d'import du module Catalog, regle ABSOLUE n°1).
|
||||||
*/
|
*/
|
||||||
private const array FORBIDDEN_CATEGORY_CODES = ['DISTRIBUTEUR', 'COURTIER'];
|
private const string REQUIRED_CATEGORY_TYPE_CODE = 'ADRESSE';
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
@@ -215,7 +215,7 @@ class ClientAddress implements TimestampableInterface, BlamableInterface, Client
|
|||||||
private Collection $contacts;
|
private Collection $contacts;
|
||||||
|
|
||||||
// Au moins une categorie est obligatoire sur une adresse (spec-front § Adresse).
|
// Au moins une categorie est obligatoire sur une adresse (spec-front § Adresse).
|
||||||
// RG-1.29 : categories de code DISTRIBUTEUR/COURTIER interdites (validateCategoryCodes).
|
// Categories de type ADRESSE uniquement (validateCategoryType).
|
||||||
/** @var Collection<int, CategoryInterface> */
|
/** @var Collection<int, CategoryInterface> */
|
||||||
#[ORM\ManyToMany(targetEntity: CategoryInterface::class)]
|
#[ORM\ManyToMany(targetEntity: CategoryInterface::class)]
|
||||||
#[ORM\JoinTable(name: 'client_address_category')]
|
#[ORM\JoinTable(name: 'client_address_category')]
|
||||||
@@ -335,20 +335,19 @@ class ClientAddress implements TimestampableInterface, BlamableInterface, Client
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RG-1.29 (ERP-78) : une adresse interdit les categories de code
|
* Toute categorie posee sur une adresse client doit etre de type ADRESSE ->
|
||||||
* DISTRIBUTEUR / COURTIER — elles decrivent une relation entre clients
|
* sinon 422 avec violation sur le champ `categories`. S'appuie sur
|
||||||
* (RG-1.03) et n'ont pas de sens sur une adresse physique -> 422 avec
|
* CategoryInterface::getCategoryTypeCodes() (multi-type — la categorie est
|
||||||
* violation sur le champ `categories`. Toute autre categorie (type unique
|
* acceptee des qu'elle PORTE le type ADRESSE ; pas d'import du module Catalog,
|
||||||
* CLIENT) est acceptee. S'appuie sur CategoryInterface::getCode() (pas
|
* regle ABSOLUE n°1).
|
||||||
* d'import du module Catalog — regle ABSOLUE n°1).
|
|
||||||
*/
|
*/
|
||||||
#[Assert\Callback]
|
#[Assert\Callback]
|
||||||
public function validateCategoryCodes(ExecutionContextInterface $context): void
|
public function validateCategoryType(ExecutionContextInterface $context): void
|
||||||
{
|
{
|
||||||
foreach ($this->categories as $category) {
|
foreach ($this->categories as $category) {
|
||||||
if ($category instanceof CategoryInterface
|
if ($category instanceof CategoryInterface
|
||||||
&& in_array($category->getCode(), self::FORBIDDEN_CATEGORY_CODES, true)) {
|
&& !in_array(self::REQUIRED_CATEGORY_TYPE_CODE, $category->getCategoryTypeCodes(), true)) {
|
||||||
$context->buildViolation('Type de catégorie non autorisé sur une adresse.')
|
$context->buildViolation('Type de catégorie non autorisé (ADRESSE attendu).')
|
||||||
->atPath('categories')
|
->atPath('categories')
|
||||||
->addViolation()
|
->addViolation()
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|||||||
* un site obligatoire (RG-2.06, Assert\Count). Site n'a pas de `code`.
|
* un site obligatoire (RG-2.06, Assert\Count). Site n'a pas de `code`.
|
||||||
* - contacts : SupplierContact (meme module).
|
* - contacts : SupplierContact (meme module).
|
||||||
* - categories : CategoryInterface (module Catalog) via resolve_target_entities —
|
* - categories : CategoryInterface (module Catalog) via resolve_target_entities —
|
||||||
* type FOURNISSEUR attendu (RG-2.10, Assert\Callback validateCategoryType).
|
* type ADRESSE attendu (Assert\Callback validateCategoryType).
|
||||||
*
|
*
|
||||||
* Embarquee sous `supplier.addresses` au detail (groupe supplier:item:read,
|
* Embarquee sous `supplier.addresses` au detail (groupe supplier:item:read,
|
||||||
* maillon (a)).
|
* maillon (a)).
|
||||||
@@ -110,11 +110,11 @@ class SupplierAddress implements TimestampableInterface, BlamableInterface, Supp
|
|||||||
public const array ADDRESS_TYPES = ['PROSPECT', 'DEPART', 'RENDU'];
|
public const array ADDRESS_TYPES = ['PROSPECT', 'DEPART', 'RENDU'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RG-2.10 : seules les categories PORTANT ce type sont autorisees sur une
|
* Seules les categories PORTANT ce type sont autorisees sur une adresse
|
||||||
* adresse fournisseur. S'appuie sur CategoryInterface::getCategoryTypeCodes()
|
* fournisseur. S'appuie sur CategoryInterface::getCategoryTypeCodes() (pas
|
||||||
* (pas d'import du module Catalog — regle ABSOLUE n°1).
|
* d'import du module Catalog — regle ABSOLUE n°1).
|
||||||
*/
|
*/
|
||||||
private const string REQUIRED_CATEGORY_TYPE_CODE = 'FOURNISSEUR';
|
private const string REQUIRED_CATEGORY_TYPE_CODE = 'ADRESSE';
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
@@ -208,8 +208,8 @@ class SupplierAddress implements TimestampableInterface, BlamableInterface, Supp
|
|||||||
#[Groups(['supplier:item:read', 'supplier:write:addresses'])]
|
#[Groups(['supplier:item:read', 'supplier:write:addresses'])]
|
||||||
private Collection $contacts;
|
private Collection $contacts;
|
||||||
|
|
||||||
// RG-2.10 : au moins une categorie de type FOURNISSEUR par adresse (le type est
|
// Au moins une categorie de type ADRESSE par adresse (le type est controle par
|
||||||
// controle par validateCategoryType ; le minimum par Assert\Count, miroir sites).
|
// validateCategoryType ; le minimum par Assert\Count, miroir sites).
|
||||||
/** @var Collection<int, CategoryInterface> */
|
/** @var Collection<int, CategoryInterface> */
|
||||||
#[ORM\ManyToMany(targetEntity: CategoryInterface::class)]
|
#[ORM\ManyToMany(targetEntity: CategoryInterface::class)]
|
||||||
#[ORM\JoinTable(name: 'supplier_address_category')]
|
#[ORM\JoinTable(name: 'supplier_address_category')]
|
||||||
@@ -227,12 +227,12 @@ class SupplierAddress implements TimestampableInterface, BlamableInterface, Supp
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RG-2.10 : toute categorie posee sur une adresse fournisseur doit etre de
|
* Toute categorie posee sur une adresse fournisseur doit etre de type ADRESSE
|
||||||
* type FOURNISSEUR -> sinon 422 avec violation sur le champ `categories`
|
* -> sinon 422 avec violation sur le champ `categories` (propertyPath aligne
|
||||||
* (propertyPath aligne ERP-101, message FR ERP-107). S'appuie sur
|
* ERP-101, message FR ERP-107). S'appuie sur
|
||||||
* CategoryInterface::getCategoryTypeCodes() (multi-type — la categorie est
|
* CategoryInterface::getCategoryTypeCodes() (multi-type — la categorie est
|
||||||
* acceptee des qu'elle PORTE le type FOURNISSEUR ; pas d'import du module
|
* acceptee des qu'elle PORTE le type ADRESSE ; pas d'import du module Catalog,
|
||||||
* Catalog, regle ABSOLUE n°1). Joue avant la base via la validation API Platform.
|
* regle ABSOLUE n°1). Joue avant la base via la validation API Platform.
|
||||||
*/
|
*/
|
||||||
#[Assert\Callback]
|
#[Assert\Callback]
|
||||||
public function validateCategoryType(ExecutionContextInterface $context): void
|
public function validateCategoryType(ExecutionContextInterface $context): void
|
||||||
@@ -240,7 +240,7 @@ class SupplierAddress implements TimestampableInterface, BlamableInterface, Supp
|
|||||||
foreach ($this->categories as $category) {
|
foreach ($this->categories as $category) {
|
||||||
if ($category instanceof CategoryInterface
|
if ($category instanceof CategoryInterface
|
||||||
&& !in_array(self::REQUIRED_CATEGORY_TYPE_CODE, $category->getCategoryTypeCodes(), true)) {
|
&& !in_array(self::REQUIRED_CATEGORY_TYPE_CODE, $category->getCategoryTypeCodes(), true)) {
|
||||||
$context->buildViolation('Type de catégorie non autorisé (FOURNISSEUR attendu).')
|
$context->buildViolation('Type de catégorie non autorisé (ADRESSE attendu).')
|
||||||
->atPath('categories')
|
->atPath('categories')
|
||||||
->addViolation()
|
->addViolation()
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -55,8 +55,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|||||||
* Audit / Blamable : persist hors contexte HTTP -> created_by / updated_by
|
* Audit / Blamable : persist hors contexte HTTP -> created_by / updated_by
|
||||||
* restent null (« Systeme » cote front), c'est attendu. Les donnees respectent
|
* restent null (« Systeme » cote front), c'est attendu. Les donnees respectent
|
||||||
* les CHECK BDD ET les validators applicatifs (exclusivite Prospect, billingEmail
|
* les CHECK BDD ET les validators applicatifs (exclusivite Prospect, billingEmail
|
||||||
* ssi facturation, aucune categorie de code DISTRIBUTEUR/COURTIER sur une adresse
|
* ssi facturation, categories de type ADRESSE sur les adresses).
|
||||||
* — RG-1.29, ERP-78).
|
|
||||||
*
|
*
|
||||||
* Depend de CategoryFixtures (categories), SitesFixtures (sites) et
|
* Depend de CategoryFixtures (categories), SitesFixtures (sites) et
|
||||||
* CommercialReferentialFixtures (referentiels comptables Bank / PaymentType).
|
* CommercialReferentialFixtures (referentiels comptables Bank / PaymentType).
|
||||||
@@ -116,7 +115,7 @@ class ClientFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
);
|
);
|
||||||
if ($gsoIsNew) {
|
if ($gsoIsNew) {
|
||||||
$this->addContact($gso, 'Paul', 'Garnier', 'Directeur commercial', '05 56 10 20 30', null, 'paul.garnier@distrib-gso.fr');
|
$this->addContact($gso, 'Paul', 'Garnier', 'Directeur commercial', '05 56 10 20 30', null, 'paul.garnier@distrib-gso.fr');
|
||||||
$this->addAddress($gso, ['Pommevic'], '82400', 'Pommevic', '1 Av. Jean Duquesne', isDelivery: true, categoryNames: ['Transport/Logistique']);
|
$this->addAddress($gso, ['Pommevic'], '82400', 'Pommevic', '1 Av. Jean Duquesne', isDelivery: true, categoryNames: ['Livraison']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Courtier reference par d'autres clients.
|
// Courtier reference par d'autres clients.
|
||||||
@@ -140,7 +139,7 @@ class ClientFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
$dubois->setPaymentType($this->paymentType($manager, 'VIREMENT'));
|
$dubois->setPaymentType($this->paymentType($manager, 'VIREMENT'));
|
||||||
$dubois->setBank($this->bank($manager, 'SG'));
|
$dubois->setBank($this->bank($manager, 'SG'));
|
||||||
$this->addContact($dubois, 'Jean', 'Dubois', 'Gérant', '05 49 00 00 01', null, 'jean.dubois@menuiserie-dubois.fr');
|
$this->addContact($dubois, 'Jean', 'Dubois', 'Gérant', '05 49 00 00 01', null, 'jean.dubois@menuiserie-dubois.fr');
|
||||||
$this->addAddress($dubois, ['Chatellerault'], '86100', 'Châtellerault', '12 rue de l\'Atelier', isDelivery: true, categoryNames: ['BTP']);
|
$this->addAddress($dubois, ['Chatellerault'], '86100', 'Châtellerault', '12 rue de l\'Atelier', isDelivery: true, categoryNames: ['Livraison']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Dependant d'un distributeur (RG-1.03) ===
|
// === Dependant d'un distributeur (RG-1.03) ===
|
||||||
@@ -176,7 +175,7 @@ class ClientFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
if ($isNew) {
|
if ($isNew) {
|
||||||
$transports->setPaymentType($this->paymentType($manager, 'LCR'));
|
$transports->setPaymentType($this->paymentType($manager, 'LCR'));
|
||||||
$this->addContact($transports, null, 'Bernard', 'Responsable exploitation', '05 56 12 13 14', null, 'expl@transports-rapides.fr');
|
$this->addContact($transports, null, 'Bernard', 'Responsable exploitation', '05 56 12 13 14', null, 'expl@transports-rapides.fr');
|
||||||
$this->addAddress($transports, ['Saint-Jean'], '17400', 'Fontenet', '2 zone industrielle', isDelivery: true, categoryNames: ['Transport/Logistique']);
|
$this->addAddress($transports, ['Saint-Jean'], '17400', 'Fontenet', '2 zone industrielle', isDelivery: true, categoryNames: ['Approvisionnement']);
|
||||||
$this->addRib($transports, 'Compte principal', 'BNPAFRPPXXX', 'FR1420041010050500013M02606', 0);
|
$this->addRib($transports, 'Compte principal', 'BNPAFRPPXXX', 'FR1420041010050500013M02606', 0);
|
||||||
$this->addRib($transports, 'Compte secondaire', 'SOGEFRPPXXX', 'FR7630006000011234567890189', 1);
|
$this->addRib($transports, 'Compte secondaire', 'SOGEFRPPXXX', 'FR7630006000011234567890189', 1);
|
||||||
}
|
}
|
||||||
@@ -192,9 +191,9 @@ class ClientFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
// Prospect : exclusif de livraison/facturation (sans billingEmail).
|
// Prospect : exclusif de livraison/facturation (sans billingEmail).
|
||||||
$this->addAddress($industries, ['Chatellerault'], '86100', 'Châtellerault', '1 avenue de la Prospection', isProspect: true, position: 0);
|
$this->addAddress($industries, ['Chatellerault'], '86100', 'Châtellerault', '1 avenue de la Prospection', isProspect: true, position: 0);
|
||||||
// Livraison.
|
// Livraison.
|
||||||
$this->addAddress($industries, ['Saint-Jean'], '17400', 'Fontenet', '4 rue de la Livraison', isDelivery: true, categoryNames: ['Industrie'], position: 1);
|
$this->addAddress($industries, ['Saint-Jean'], '17400', 'Fontenet', '4 rue de la Livraison', isDelivery: true, categoryNames: ['Livraison'], position: 1);
|
||||||
// Facturation : billingEmail obligatoire.
|
// Facturation : billingEmail obligatoire.
|
||||||
$this->addAddress($industries, ['Chatellerault'], '86100', 'Châtellerault', '7 boulevard des Factures', isBilling: true, billingEmail: 'Compta@Industries-Vertes.FR', position: 2);
|
$this->addAddress($industries, ['Chatellerault'], '86100', 'Châtellerault', '7 boulevard des Factures', isBilling: true, billingEmail: 'Compta@Industries-Vertes.FR', categoryNames: ['Facturation'], position: 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 3 contacts dont un avec telephone secondaire (RG-1.05/1.02) ===
|
// === 3 contacts dont un avec telephone secondaire (RG-1.05/1.02) ===
|
||||||
@@ -249,7 +248,7 @@ class ClientFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
$holding->setDirectorName('Antoine Lefèvre');
|
$holding->setDirectorName('Antoine Lefèvre');
|
||||||
$holding->setProfitAmount('1250000.00');
|
$holding->setProfitAmount('1250000.00');
|
||||||
$this->addContact($holding, 'Antoine', 'Lefèvre', 'PDG', '05 56 51 52 53', null, 'antoine.lefevre@holding-premium.fr');
|
$this->addContact($holding, 'Antoine', 'Lefèvre', 'PDG', '05 56 51 52 53', null, 'antoine.lefevre@holding-premium.fr');
|
||||||
$this->addAddress($holding, ['Pommevic'], '82400', 'Pommevic', '1 allée des Investisseurs', isDelivery: true, categoryNames: ['Industrie']);
|
$this->addAddress($holding, ['Pommevic'], '82400', 'Pommevic', '1 allée des Investisseurs', isDelivery: true, categoryNames: ['Siège']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Multi-categories M2M ===
|
// === Multi-categories M2M ===
|
||||||
@@ -260,7 +259,7 @@ class ClientFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
);
|
);
|
||||||
if ($isNew) {
|
if ($isNew) {
|
||||||
$this->addContact($conglo, 'Hélène', 'Faure', 'Directrice générale', '05 49 61 62 63', null, 'helene.faure@conglomerat-multi.fr');
|
$this->addContact($conglo, 'Hélène', 'Faure', 'Directrice générale', '05 49 61 62 63', null, 'helene.faure@conglomerat-multi.fr');
|
||||||
$this->addAddress($conglo, ['Chatellerault', 'Saint-Jean'], '86100', 'Châtellerault', '20 rue des Activités', isDelivery: true, categoryNames: ['BTP', 'Services']);
|
$this->addAddress($conglo, ['Chatellerault', 'Saint-Jean'], '86100', 'Châtellerault', '20 rue des Activités', isDelivery: true, categoryNames: ['Livraison', 'Approvisionnement']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Prospect seul ===
|
// === Prospect seul ===
|
||||||
@@ -282,7 +281,7 @@ class ClientFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
);
|
);
|
||||||
if ($isNew) {
|
if ($isNew) {
|
||||||
$this->addContact($association, null, 'Caron', 'Président', '05 49 81 82 83', null, 'president@asso-riverains.fr');
|
$this->addContact($association, null, 'Caron', 'Président', '05 49 81 82 83', null, 'president@asso-riverains.fr');
|
||||||
$this->addAddress($association, ['Saint-Jean'], '17400', 'Fontenet', '6 chemin du Village', isDelivery: true, categoryNames: ['Association']);
|
$this->addAddress($association, ['Saint-Jean'], '17400', 'Fontenet', '6 chemin du Village', isDelivery: true, categoryNames: ['Contact issues']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$manager->flush();
|
$manager->flush();
|
||||||
@@ -359,10 +358,10 @@ class ClientFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
/**
|
/**
|
||||||
* Ajoute une adresse au client (cascade persist via Client.addresses). Les
|
* Ajoute une adresse au client (cascade persist via Client.addresses). Les
|
||||||
* donnees respectent les validators : exclusivite Prospect, billingEmail ssi
|
* donnees respectent les validators : exclusivite Prospect, billingEmail ssi
|
||||||
* facturation, aucune categorie de code DISTRIBUTEUR/COURTIER (RG-1.29).
|
* facturation, categories de type ADRESSE uniquement.
|
||||||
*
|
*
|
||||||
* @param list<string> $siteNames au moins un site (RG-1.10)
|
* @param list<string> $siteNames au moins un site (RG-1.10)
|
||||||
* @param list<string> $categoryNames categories hors DISTRIBUTEUR/COURTIER (RG-1.29)
|
* @param list<string> $categoryNames categories de type ADRESSE (Siege, Livraison...)
|
||||||
*/
|
*/
|
||||||
private function addAddress(
|
private function addAddress(
|
||||||
Client $client,
|
Client $client,
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ interface CategoryInterface
|
|||||||
* entre environnements) ni importer la classe concrete Category (regle
|
* entre environnements) ni importer la classe concrete Category (regle
|
||||||
* ABSOLUE n°1). Pilote, cote M1 Commercial :
|
* ABSOLUE n°1). Pilote, cote M1 Commercial :
|
||||||
* - RG-1.03 : un distributor doit referencer un client portant la categorie
|
* - RG-1.03 : un distributor doit referencer un client portant la categorie
|
||||||
* de code DISTRIBUTEUR (resp. COURTIER pour broker) ;
|
* de code DISTRIBUTEUR (resp. COURTIER pour broker).
|
||||||
* - RG-1.29 : une adresse interdit les categories de code DISTRIBUTEUR /
|
|
||||||
* COURTIER (relations entre clients, pas des attributs d'adresse).
|
|
||||||
*/
|
*/
|
||||||
public function getCode(): ?string;
|
public function getCode(): ?string;
|
||||||
|
|
||||||
@@ -38,9 +36,10 @@ interface CategoryInterface
|
|||||||
* Codes des types de categorie rattaches (CategoryType::code), tableau vide
|
* Codes des types de categorie rattaches (CategoryType::code), tableau vide
|
||||||
* si aucun. Depuis le passage en ManyToMany, une categorie peut porter
|
* si aucun. Depuis le passage en ManyToMany, une categorie peut porter
|
||||||
* plusieurs types : un module tiers teste l'appartenance via
|
* plusieurs types : un module tiers teste l'appartenance via
|
||||||
* `in_array($code, $category->getCategoryTypeCodes(), true)`. Pilote, cote
|
* `in_array($code, $category->getCategoryTypeCodes(), true)`. Pilote la
|
||||||
* M2 Commercial, la RG-2.10 (une categorie de fournisseur doit etre de type
|
* RG-2.10 (une categorie de fournisseur doit etre de type FOURNISSEUR) et la
|
||||||
* FOURNISSEUR).
|
* validation des blocs adresse (categories de type ADRESSE uniquement, client
|
||||||
|
* comme fournisseur).
|
||||||
*
|
*
|
||||||
* @return list<string>
|
* @return list<string>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -271,9 +271,9 @@ final class ColumnCommentsCatalog
|
|||||||
],
|
],
|
||||||
|
|
||||||
'client_address_category' => [
|
'client_address_category' => [
|
||||||
'_table' => 'Jointure M2M client_address <-> category — categories d adresse (types SECTEUR/AUTRE uniquement, RG-1.29).',
|
'_table' => 'Jointure M2M client_address <-> category — categories d adresse de type ADRESSE uniquement.',
|
||||||
'client_address_id' => 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.',
|
'client_address_id' => 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.',
|
||||||
'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse (type SECTEUR ou AUTRE, RG-1.29).',
|
'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse de type ADRESSE.',
|
||||||
],
|
],
|
||||||
|
|
||||||
'client_rib' => [
|
'client_rib' => [
|
||||||
@@ -360,9 +360,9 @@ final class ColumnCommentsCatalog
|
|||||||
],
|
],
|
||||||
|
|
||||||
'supplier_address_category' => [
|
'supplier_address_category' => [
|
||||||
'_table' => 'Jointure M2M supplier_address <-> category — categories d adresse de type FOURNISSEUR (RG-2.10).',
|
'_table' => 'Jointure M2M supplier_address <-> category — categories d adresse de type ADRESSE.',
|
||||||
'supplier_address_id' => 'FK -> supplier_address.id, ON DELETE CASCADE — adresse concernee.',
|
'supplier_address_id' => 'FK -> supplier_address.id, ON DELETE CASCADE — adresse concernee.',
|
||||||
'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse de type FOURNISSEUR (RG-2.10).',
|
'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse de type ADRESSE.',
|
||||||
],
|
],
|
||||||
|
|
||||||
'supplier_rib' => [
|
'supplier_rib' => [
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Module\Catalog\Api;
|
||||||
|
|
||||||
|
use App\Module\Catalog\Domain\Entity\CategoryType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests du seed de la taxonomie ADRESSE cote API.
|
||||||
|
*
|
||||||
|
* Le multi-select « Categorie » des blocs adresse (client + fournisseur) consomme
|
||||||
|
* `GET /api/categories?typeCode=ADRESSE`. Ce test prouve que :
|
||||||
|
* - le filtre `?typeCode=ADRESSE` ne renvoie QUE les categories du type ADRESSE
|
||||||
|
* (aucune fuite de categorie d'un autre type) ;
|
||||||
|
* - chaque membre renvoye porte bien le type ADRESSE dans `categoryTypes`.
|
||||||
|
*
|
||||||
|
* NB : la base de test est purgee de toute categorie / type entre chaque test
|
||||||
|
* (cf. AbstractCatalogApiTestCase::cleanupCatalogTestData), donc le type et les
|
||||||
|
* categories ADRESSE sont materialises ici (et non lus depuis le seed de la
|
||||||
|
* migration / fixture, qui ne survit pas a la purge). On valide ainsi le contrat
|
||||||
|
* du filtre sur le code reel `ADRESSE`. La presence du seed apres un
|
||||||
|
* `make db-reset` reel est, elle, verifiee par l'idempotence des fixtures.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class CategoryAdresseSeedTest extends AbstractCatalogApiTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Categories de demonstration seedees par la migration / fixture ADRESSE.
|
||||||
|
*/
|
||||||
|
private const array ADDRESS_CATEGORIES = [
|
||||||
|
'Siège',
|
||||||
|
'Contact issues',
|
||||||
|
'Facturation',
|
||||||
|
'Livraison',
|
||||||
|
'Approvisionnement',
|
||||||
|
'Méthaniseur',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function testTypeCodeAdresseReturnsOnlyAddressCategories(): void
|
||||||
|
{
|
||||||
|
$addressType = $this->getOrCreateAdresseType();
|
||||||
|
foreach (self::ADDRESS_CATEGORIES as $name) {
|
||||||
|
$this->createCategory($name, $addressType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bruit : un type + une categorie d'un autre type ne doivent PAS fuiter.
|
||||||
|
$noiseType = $this->createCategoryType('TEST_CLIENT', 'Test Client');
|
||||||
|
$this->createCategory(self::TEST_CATEGORY_PREFIX.'noise', $noiseType);
|
||||||
|
|
||||||
|
$client = $this->createAdminClient();
|
||||||
|
$response = $client->request('GET', '/api/categories?typeCode=ADRESSE&pagination=false');
|
||||||
|
self::assertSame(200, $response->getStatusCode());
|
||||||
|
|
||||||
|
$members = $response->toArray()['member'];
|
||||||
|
$names = array_map(static fn (array $m): string => $m['name'], $members);
|
||||||
|
sort($names);
|
||||||
|
|
||||||
|
$expected = self::ADDRESS_CATEGORIES;
|
||||||
|
sort($expected);
|
||||||
|
self::assertSame(
|
||||||
|
$expected,
|
||||||
|
$names,
|
||||||
|
'Le filtre ?typeCode=ADRESSE doit ne renvoyer QUE les categories du type ADRESSE.',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Chaque categorie remontee doit PORTER le type ADRESSE.
|
||||||
|
foreach ($members as $member) {
|
||||||
|
self::assertContains('ADRESSE', array_column($member['categoryTypes'], 'code'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTypeCodeAdresseKeepsHydraPagination(): void
|
||||||
|
{
|
||||||
|
$addressType = $this->getOrCreateAdresseType();
|
||||||
|
$this->createCategory('Siège', $addressType);
|
||||||
|
|
||||||
|
$client = $this->createAdminClient();
|
||||||
|
$response = $client->request('GET', '/api/categories?typeCode=ADRESSE');
|
||||||
|
self::assertSame(200, $response->getStatusCode());
|
||||||
|
|
||||||
|
$data = $response->toArray();
|
||||||
|
self::assertArrayHasKey('totalItems', $data, 'Le filtre ne doit pas casser la pagination Hydra.');
|
||||||
|
self::assertArrayHasKey('member', $data);
|
||||||
|
|
||||||
|
foreach ($data['member'] as $member) {
|
||||||
|
self::assertContains('ADRESSE', array_column($member['categoryTypes'], 'code'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupere le type ADRESSE reel, ou le cree s'il est absent. Le code `ADRESSE`
|
||||||
|
* est seede par CategoryTypeFixtures (present en debut de suite), mais le
|
||||||
|
* cleanup purge tous les `category_type` entre les tests : selon l'ordre
|
||||||
|
* d'execution, le type peut donc exister ou non. Le get-or-create rend le test
|
||||||
|
* robuste sans dependre du seed ni le dupliquer.
|
||||||
|
*/
|
||||||
|
private function getOrCreateAdresseType(): CategoryType
|
||||||
|
{
|
||||||
|
$em = $this->getEm();
|
||||||
|
$existing = $em->getRepository(CategoryType::class)->findOneBy(['code' => 'ADRESSE']);
|
||||||
|
|
||||||
|
if ($existing instanceof CategoryType) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->createCategoryType('ADRESSE', 'Adresse');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,10 +36,10 @@ abstract class AbstractCommercialApiTestCase extends AbstractApiTestCase
|
|||||||
protected const string TEST_CATEGORY_PREFIX = 'test_cli_cat_';
|
protected const string TEST_CATEGORY_PREFIX = 'test_cli_cat_';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codes pilotant les RG (RG-1.03 distributor/broker, RG-1.29 adresse) : ils
|
* Codes pilotant les RG (RG-1.03 distributor/broker) : ils doivent matcher
|
||||||
* doivent matcher exactement, donc createCategory() les fetch-or-create par
|
* exactement, donc createCategory() les fetch-or-create par code. Les autres
|
||||||
* code. Les autres codes sont traites comme de simples libelles generiques et
|
* codes sont traites comme de simples libelles generiques et produisent une
|
||||||
* produisent une categorie a code UNIQUE (cf. createCategory).
|
* categorie a code UNIQUE (cf. createCategory).
|
||||||
*/
|
*/
|
||||||
private const array RG_EXACT_CODES = ['DISTRIBUTEUR', 'COURTIER'];
|
private const array RG_EXACT_CODES = ['DISTRIBUTEUR', 'COURTIER'];
|
||||||
|
|
||||||
@@ -75,6 +75,47 @@ abstract class AbstractCommercialApiTestCase extends AbstractApiTestCase
|
|||||||
return $type;
|
return $type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupere (ou cree) le type ADRESSE (categories des blocs adresse). Idempotent
|
||||||
|
* via l'unicite de category_type.code. Laisse en place au tearDown.
|
||||||
|
*/
|
||||||
|
protected function addressCategoryType(): CategoryType
|
||||||
|
{
|
||||||
|
$em = $this->getEm();
|
||||||
|
$existing = $em->getRepository(CategoryType::class)->findOneBy(['code' => 'ADRESSE']);
|
||||||
|
if (null !== $existing) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = new CategoryType();
|
||||||
|
$type->setCode('ADRESSE');
|
||||||
|
$type->setLabel('Adresse');
|
||||||
|
$em->persist($type);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cree une Category de test de type ADRESSE (autorisee sur un bloc adresse).
|
||||||
|
* Code UNIQUE (suffixe aleatoire) : les categories d'adresse ne pilotent aucune
|
||||||
|
* RG par code, deux appels produisent donc deux categories distinctes.
|
||||||
|
*/
|
||||||
|
protected function createAddressCategory(): Category
|
||||||
|
{
|
||||||
|
$em = $this->getEm();
|
||||||
|
$suffix = substr(bin2hex(random_bytes(4)), 0, 8);
|
||||||
|
|
||||||
|
$category = new Category();
|
||||||
|
$category->setName(self::TEST_CATEGORY_PREFIX.'adresse_'.$suffix);
|
||||||
|
$category->setCode('ADRESSE_'.strtoupper($suffix));
|
||||||
|
$category->addCategoryType($this->addressCategoryType());
|
||||||
|
$em->persist($category);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
return $category;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cree une Category de test sous le type unique CLIENT (ERP-78).
|
* Cree une Category de test sous le type unique CLIENT (ERP-78).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ abstract class AbstractSupplierApiTestCase extends AbstractCommercialApiTestCase
|
|||||||
* Seede un fournisseur COMPLET (sans passer par l'API — validations
|
* Seede un fournisseur COMPLET (sans passer par l'API — validations
|
||||||
* applicatives non rejouees mais CHECK BDD respectes) : onglet Information
|
* applicatives non rejouees mais CHECK BDD respectes) : onglet Information
|
||||||
* rempli, bloc comptable non nul (SIREN + refs), >= 1 RIB, >= 1 adresse
|
* rempli, bloc comptable non nul (SIREN + refs), >= 1 RIB, >= 1 adresse
|
||||||
* multi-sites (>= 2 sites, triageProvider=true) avec >= 1 categorie
|
* multi-sites (>= 2 sites, triageProvider=true) avec >= 1 categorie de type
|
||||||
* FOURNISSEUR, >= 1 contact, >= 1 categorie sur le fournisseur. Sert de socle
|
* ADRESSE, >= 1 contact, >= 1 categorie FOURNISSEUR sur le fournisseur. Sert de socle
|
||||||
* au contrat de serialisation et a la DoD (§ 4.0.bis).
|
* au contrat de serialisation et a la DoD (§ 4.0.bis).
|
||||||
*
|
*
|
||||||
* @param string $paymentTypeCode code du type de reglement a poser (defaut LCR,
|
* @param string $paymentTypeCode code du type de reglement a poser (defaut LCR,
|
||||||
@@ -202,7 +202,9 @@ abstract class AbstractSupplierApiTestCase extends AbstractCommercialApiTestCase
|
|||||||
foreach ($sites as $site) {
|
foreach ($sites as $site) {
|
||||||
$address->addSite($site);
|
$address->addSite($site);
|
||||||
}
|
}
|
||||||
$address->addCategory($this->supplierCategory('NEGOCIANT'));
|
// Categorie de bloc adresse : type ADRESSE (et non FOURNISSEUR — celui-ci
|
||||||
|
// reste sur le bloc principal du fournisseur).
|
||||||
|
$address->addCategory($this->createAddressCategory());
|
||||||
$address->addContact($contact);
|
$address->addContact($contact);
|
||||||
$supplier->addAddress($address);
|
$supplier->addAddress($address);
|
||||||
$em->persist($address);
|
$em->persist($address);
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ use App\Module\Sites\Domain\Entity\Site;
|
|||||||
* - RG-1.06 / RG-1.07 / RG-1.08 : exclusivite is_prospect vs
|
* - RG-1.06 / RG-1.07 / RG-1.08 : exclusivite is_prospect vs
|
||||||
* is_delivery / is_billing ;
|
* is_delivery / is_billing ;
|
||||||
* - RG-1.11 : billing_email obligatoire ssi is_billing ;
|
* - RG-1.11 : billing_email obligatoire ssi is_billing ;
|
||||||
* - RG-1.29 (ERP-78) : les categories de code DISTRIBUTEUR / COURTIER sont
|
* - categorie d'adresse : seules les categories de type ADRESSE sont acceptees
|
||||||
* interdites sur une adresse (-> 422) ; toute autre categorie est acceptee.
|
* (-> 422 sinon), au moins une est obligatoire.
|
||||||
*
|
*
|
||||||
* Depuis ERP-76, ces regles sont portees par des Assert\Callback sur l'entite
|
* Depuis ERP-76, ces regles sont portees par des Assert\Callback sur l'entite
|
||||||
* ClientAddress (mirror applicatif des CHECK Postgres) : la combinaison invalide
|
* ClientAddress (mirror applicatif des CHECK Postgres) : la combinaison invalide
|
||||||
@@ -170,7 +170,7 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
|
|||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedClient('Non Billing Empty Email');
|
$seed = $this->seedClient('Non Billing Empty Email');
|
||||||
$category = $this->createCategory('SECTEUR');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD],
|
'headers' => ['Content-Type' => self::LD],
|
||||||
@@ -197,7 +197,7 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
|
|||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedClient('Billing Two Emails');
|
$seed = $this->seedClient('Billing Two Emails');
|
||||||
$category = $this->createCategory('SECTEUR');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD],
|
'headers' => ['Content-Type' => self::LD],
|
||||||
@@ -225,7 +225,7 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
|
|||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedClient('Secondary Email Non Billing');
|
$seed = $this->seedClient('Secondary Email Non Billing');
|
||||||
$category = $this->createCategory('SECTEUR');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
$body = $client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
$body = $client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
||||||
@@ -246,15 +246,16 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RG-1.29 : poster une categorie de type DISTRIBUTEUR sur une adresse -> 422
|
* Une categorie qui n'est PAS de type ADRESSE (ici une categorie CLIENT) est
|
||||||
* avec violation sur le champ `categories`.
|
* refusee sur une adresse -> 422 avec violation sur le champ `categories`.
|
||||||
*/
|
*/
|
||||||
public function testAddressRejectsDistributorCategory(): void
|
public function testAddressRejectsNonAddressCategory(): void
|
||||||
{
|
{
|
||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedClient('Address Distributor Cat');
|
$seed = $this->seedClient('Address Non Address Cat');
|
||||||
$category = $this->createCategory('DISTRIBUTEUR');
|
// Categorie de type CLIENT (et non ADRESSE) -> doit etre refusee sur l'adresse.
|
||||||
|
$category = $this->createCategory('SECTEUR');
|
||||||
|
|
||||||
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD],
|
'headers' => ['Content-Type' => self::LD],
|
||||||
@@ -270,70 +271,20 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
|
|||||||
|
|
||||||
self::assertResponseStatusCodeSame(422);
|
self::assertResponseStatusCodeSame(422);
|
||||||
self::assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
'Type de catégorie non autorisé sur une adresse.',
|
'Type de catégorie non autorisé (ADRESSE attendu).',
|
||||||
(string) $client->getResponse()->getContent(false),
|
(string) $client->getResponse()->getContent(false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RG-1.29 : poster une categorie de type COURTIER sur une adresse -> 422.
|
* Une categorie de type ADRESSE est acceptee sur une adresse -> 201.
|
||||||
*/
|
*/
|
||||||
public function testAddressRejectsBrokerCategory(): void
|
public function testAddressAcceptsAddressCategory(): void
|
||||||
{
|
{
|
||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedClient('Address Broker Cat');
|
$seed = $this->seedClient('Address Address Cat');
|
||||||
$category = $this->createCategory('COURTIER');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
|
||||||
'headers' => ['Content-Type' => self::LD],
|
|
||||||
'json' => [
|
|
||||||
'isDelivery' => true,
|
|
||||||
'postalCode' => '86100',
|
|
||||||
'city' => 'Châtellerault',
|
|
||||||
'street' => '1 rue du Test',
|
|
||||||
'sites' => [$this->firstSiteIri()],
|
|
||||||
'categories' => ['/api/categories/'.$category->getId()],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
self::assertResponseStatusCodeSame(422);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RG-1.29 : une categorie de type SECTEUR est autorisee sur une adresse.
|
|
||||||
*/
|
|
||||||
public function testAddressAcceptsSectorCategory(): void
|
|
||||||
{
|
|
||||||
$this->skipIfSitesModuleDisabled();
|
|
||||||
$client = $this->createAdminClient();
|
|
||||||
$seed = $this->seedClient('Address Sector Cat');
|
|
||||||
$category = $this->createCategory('SECTEUR');
|
|
||||||
|
|
||||||
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
|
||||||
'headers' => ['Content-Type' => self::LD],
|
|
||||||
'json' => [
|
|
||||||
'isDelivery' => true,
|
|
||||||
'postalCode' => '86100',
|
|
||||||
'city' => 'Châtellerault',
|
|
||||||
'street' => '1 rue du Test',
|
|
||||||
'sites' => [$this->firstSiteIri()],
|
|
||||||
'categories' => ['/api/categories/'.$category->getId()],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
self::assertResponseStatusCodeSame(201);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RG-1.29 : une categorie de type AUTRE est autorisee sur une adresse.
|
|
||||||
*/
|
|
||||||
public function testAddressAcceptsOtherCategory(): void
|
|
||||||
{
|
|
||||||
$this->skipIfSitesModuleDisabled();
|
|
||||||
$client = $this->createAdminClient();
|
|
||||||
$seed = $this->seedClient('Address Other Cat');
|
|
||||||
$category = $this->createCategory('AUTRE');
|
|
||||||
|
|
||||||
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD],
|
'headers' => ['Content-Type' => self::LD],
|
||||||
@@ -385,7 +336,7 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
|
|||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedClient('Address No Type');
|
$seed = $this->seedClient('Address No Type');
|
||||||
$category = $this->createCategory('SECTEUR');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
$body = $client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
$body = $client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
||||||
@@ -413,7 +364,7 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
|
|||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedClient('Address Broker Type');
|
$seed = $this->seedClient('Address Broker Type');
|
||||||
$category = $this->createCategory('SECTEUR');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD],
|
'headers' => ['Content-Type' => self::LD],
|
||||||
@@ -435,7 +386,7 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
|
|||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedClient('Address Distributor Type');
|
$seed = $this->seedClient('Address Distributor Type');
|
||||||
$category = $this->createCategory('SECTEUR');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
$client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD],
|
'headers' => ['Content-Type' => self::LD],
|
||||||
@@ -462,7 +413,7 @@ final class ClientAddressTest extends AbstractCommercialApiTestCase
|
|||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedClient('Address Broker Mix');
|
$seed = $this->seedClient('Address Broker Mix');
|
||||||
$category = $this->createCategory('SECTEUR');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
$body = $client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
$body = $client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ final class ClientSubResourceApiTest extends AbstractCommercialApiTestCase
|
|||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedClient('Address Host');
|
$seed = $this->seedClient('Address Host');
|
||||||
$siteIri = $this->firstSiteIri();
|
$siteIri = $this->firstSiteIri();
|
||||||
$category = $this->createCategory('SECTEUR');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
$data = $client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
$data = $client->request('POST', '/api/clients/'.$seed->getId().'/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD],
|
'headers' => ['Content-Type' => self::LD],
|
||||||
@@ -276,7 +276,7 @@ final class ClientSubResourceApiTest extends AbstractCommercialApiTestCase
|
|||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedClient('Addr Multi');
|
$seed = $this->seedClient('Addr Multi');
|
||||||
$siteIri = $this->firstSiteIri();
|
$siteIri = $this->firstSiteIri();
|
||||||
$category = $this->createCategory('SECTEUR');
|
$category = $this->createAddressCategory();
|
||||||
$this->seedAddress($seed, 'Bordeaux');
|
$this->seedAddress($seed, 'Bordeaux');
|
||||||
$this->seedAddress($seed, 'Lyon');
|
$this->seedAddress($seed, 'Lyon');
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ final class ClientSubResourceApiTest extends AbstractCommercialApiTestCase
|
|||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$siteIri = $this->firstSiteIri();
|
$siteIri = $this->firstSiteIri();
|
||||||
$category = $this->createCategory('SECTEUR');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
$client->request('POST', '/api/clients/999999/addresses', [
|
$client->request('POST', '/api/clients/999999/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ final class SupplierSubResourceApiTest extends AbstractSupplierApiTestCase
|
|||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedSupplier('Address Host');
|
$seed = $this->seedSupplier('Address Host');
|
||||||
$category = $this->supplierCategory('NEGOCIANT');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
$data = $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
$data = $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
||||||
'headers' => ['Content-Type' => self::LD],
|
'headers' => ['Content-Type' => self::LD],
|
||||||
@@ -174,7 +174,7 @@ final class SupplierSubResourceApiTest extends AbstractSupplierApiTestCase
|
|||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedSupplier('Address Incoherent');
|
$seed = $this->seedSupplier('Address Incoherent');
|
||||||
$category = $this->supplierCategory('NEGOCIANT');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
// RG-2.05 : pas de controle strict de coherence CP/ville cote serveur.
|
// RG-2.05 : pas de controle strict de coherence CP/ville cote serveur.
|
||||||
$client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
$client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
||||||
@@ -222,7 +222,7 @@ final class SupplierSubResourceApiTest extends AbstractSupplierApiTestCase
|
|||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedSupplier('Address Types');
|
$seed = $this->seedSupplier('Address Types');
|
||||||
$siteIri = $this->firstSiteIri();
|
$siteIri = $this->firstSiteIri();
|
||||||
$category = $this->supplierCategory('NEGOCIANT');
|
$category = $this->createAddressCategory();
|
||||||
|
|
||||||
foreach (['PROSPECT', 'DEPART', 'RENDU'] as $type) {
|
foreach (['PROSPECT', 'DEPART', 'RENDU'] as $type) {
|
||||||
$client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
$client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
||||||
@@ -240,12 +240,12 @@ final class SupplierSubResourceApiTest extends AbstractSupplierApiTestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testPostAddressWithNonFournisseurCategoryReturns422(): void
|
public function testPostAddressWithNonAddressCategoryReturns422(): void
|
||||||
{
|
{
|
||||||
$this->skipIfSitesModuleDisabled();
|
$this->skipIfSitesModuleDisabled();
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$seed = $this->seedSupplier('Address Bad Cat');
|
$seed = $this->seedSupplier('Address Bad Cat');
|
||||||
// categorie de type CLIENT -> interdite sur une adresse fournisseur.
|
// categorie de type CLIENT (et non ADRESSE) -> interdite sur une adresse.
|
||||||
$clientTypedCategory = $this->createCategory('SECTEUR');
|
$clientTypedCategory = $this->createCategory('SECTEUR');
|
||||||
|
|
||||||
$response = $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
$response = $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
||||||
@@ -260,7 +260,7 @@ final class SupplierSubResourceApiTest extends AbstractSupplierApiTestCase
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// RG-2.10 -> 422 rattachee a categories.
|
// Categorie hors type ADRESSE -> 422 rattachee a categories.
|
||||||
self::assertResponseStatusCodeSame(422);
|
self::assertResponseStatusCodeSame(422);
|
||||||
self::assertArrayHasKey('categories', $this->violationsByPath($response->toArray(false)));
|
self::assertArrayHasKey('categories', $this->violationsByPath($response->toArray(false)));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user