fix : retours métier ERP-193 (4 répertoires) (#139)
Auto Tag Develop / tag (push) Successful in 11s
Auto Tag Develop / tag (push) Successful in 11s
Lot de retours métier **ERP-193** (« Fix tous les retours starseed »), transverse aux 4 répertoires (clients, fournisseurs, prestataires, transporteurs).
## Contenu
- **Pagination** : défaut à 25 items/page sur les 4 répertoires.
- **Libellé** : colonne « Dernière activité » → « Dernière modification ».
- **Consultation** : masquage des onglets vides (coquilles « à venir » + onglets de données sans donnée).
- **Chiffre d'affaires** : plafonné à 999 999 999 999,99 (clamp front + `Assert\LessThanOrEqual` back).
- **Date de création** : interdiction des dates futures (`:max` MalioDate + `Assert\LessThanOrEqual('today')` back).
- **Caractères spéciaux** : blocage des caractères parasites (`²³§~#|…`) dans les champs texte via une allow-list par profil (nom de personne / texte libre / adresse / code alphanumérique) — filtrage front à la frappe + `Assert\Regex` back autoritaire. Email/IBAN/BIC/TVA conservent leurs validateurs de format.
- **UI** : champs en consultation et onglets validés grisés (`readonly` → `disabled`).
- **UI** : boutons « Archiver » en rouge (variant `danger`).
## Tests
- Back : nouveaux tests RG (plafond CA, dates futures, caractères spéciaux) + garde-fou contraintes — suite complète verte (813 tests).
- Front : nouveaux tests unitaires (sanitizers, helpers date/montant) — 615 tests verts, eslint clean.
---------
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #139
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #139.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
icon="mdi:arrow-left-bold"
|
||||
icon-size="24"
|
||||
variant="ghost"
|
||||
:title="t('commercial.clients.edit.back')"
|
||||
v-bind="{ ariaLabel: t('commercial.clients.edit.back') }"
|
||||
@click="goBack"
|
||||
/>
|
||||
@@ -25,9 +26,10 @@
|
||||
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<MalioInputText
|
||||
v-model="main.companyName"
|
||||
:mask="FREE_TEXT_MASK"
|
||||
:label="t('commercial.clients.form.main.companyName')"
|
||||
:required="true"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="mainErrors.errors.companyName"
|
||||
/>
|
||||
<MalioSelectCheckbox
|
||||
@@ -35,7 +37,7 @@
|
||||
:options="mainCategoryOptions"
|
||||
:label="t('commercial.clients.form.main.categories')"
|
||||
:display-tag="true"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:required="true"
|
||||
:error="mainErrors.errors.categories"
|
||||
@update:model-value="(v: (string | number)[]) => main.categoryIris = v.map(String)"
|
||||
@@ -46,7 +48,7 @@
|
||||
:options="relationOptions"
|
||||
:label="t('commercial.clients.form.main.relation')"
|
||||
:empty-option-label="t('commercial.clients.form.main.relationNone')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
@update:model-value="onRelationChange"
|
||||
/>
|
||||
<MalioSelect
|
||||
@@ -54,7 +56,7 @@
|
||||
:model-value="main.brokerIri"
|
||||
:options="brokerOptions"
|
||||
:label="t('commercial.clients.form.main.brokerName')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:required="true"
|
||||
:error="mainErrors.errors.broker"
|
||||
@update:model-value="(v: string | number | null) => main.brokerIri = v === null ? null : String(v)"
|
||||
@@ -64,7 +66,7 @@
|
||||
:model-value="main.distributorIri"
|
||||
:options="distributorOptions"
|
||||
:label="t('commercial.clients.form.main.distributorName')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:required="true"
|
||||
:error="mainErrors.errors.distributor"
|
||||
@update:model-value="(v: string | number | null) => main.distributorIri = v === null ? null : String(v)"
|
||||
@@ -74,7 +76,7 @@
|
||||
v-model="main.triageService"
|
||||
:label="t('commercial.clients.form.main.triageService')"
|
||||
group-class="self-center"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -101,20 +103,24 @@
|
||||
resize="none"
|
||||
group-class="row-span-2 pt-1 pb-1"
|
||||
text-input="h-full text-lg"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.description"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="information.competitors"
|
||||
:mask="FREE_TEXT_MASK"
|
||||
:label="t('commercial.clients.form.information.competitors')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.competitors"
|
||||
/>
|
||||
<!-- Date de creation jamais dans le futur (ERP-193) : :max plafonne
|
||||
le calendrier a aujourd'hui et invalide une saisie future. -->
|
||||
<MalioDate
|
||||
v-model="information.foundedAt"
|
||||
:label="t('commercial.clients.form.information.foundedAt')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:editable="true"
|
||||
:max="maxFoundedAt"
|
||||
:error="informationErrors.errors.foundedAt"
|
||||
@update:raw-value="(v: string) => information.foundedAtRaw = v"
|
||||
/>
|
||||
@@ -122,25 +128,30 @@
|
||||
v-model="information.employeesCount"
|
||||
:label="t('commercial.clients.form.information.employeesCount')"
|
||||
:mask="EMPLOYEES_MASK"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.employeesCount"
|
||||
/>
|
||||
<!-- CA plafonne a 999 999 999 999,99 (ERP-193) : clamp a la saisie,
|
||||
:key force le re-affichage quand on plafonne (modelValue inchange). -->
|
||||
<MalioInputAmount
|
||||
v-model="information.revenueAmount"
|
||||
:key="revenueAmountKey"
|
||||
:model-value="information.revenueAmount"
|
||||
:label="t('commercial.clients.form.information.revenueAmount')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.revenueAmount"
|
||||
@update:model-value="onRevenueAmountInput"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="information.directorName"
|
||||
:mask="PERSON_NAME_MASK"
|
||||
:label="t('commercial.clients.form.information.directorName')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.directorName"
|
||||
/>
|
||||
<MalioInputAmount
|
||||
v-model="information.profitAmount"
|
||||
:label="t('commercial.clients.form.information.profitAmount')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.profitAmount"
|
||||
/>
|
||||
</div>
|
||||
@@ -167,7 +178,7 @@
|
||||
:model-value="contact"
|
||||
:title="t('commercial.clients.form.contact.title', { n: index + 1 })"
|
||||
:removable="isRowRemovable(contacts, index)"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:errors="contactErrors[index]"
|
||||
@update:model-value="(v) => contacts[index] = v"
|
||||
@remove="askRemoveContact(index)"
|
||||
@@ -204,7 +215,7 @@
|
||||
:contact-options="contactOptions"
|
||||
:country-options="countryOptions"
|
||||
:removable="isRowRemovable(addresses, index)"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:errors="addressErrors[index]"
|
||||
@update:model-value="(v) => addresses[index] = v"
|
||||
@remove="askRemoveAddress(index)"
|
||||
@@ -239,14 +250,15 @@
|
||||
v-model="accounting.siren"
|
||||
:label="t('commercial.clients.form.accounting.siren')"
|
||||
:mask="SIREN_MASK"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.siren"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="accounting.accountNumber"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
:label="t('commercial.clients.form.accounting.accountNumber')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.accountNumber"
|
||||
/>
|
||||
@@ -254,7 +266,7 @@
|
||||
:model-value="accounting.tvaModeIri"
|
||||
:options="tvaModeOptions"
|
||||
:label="t('commercial.clients.form.accounting.tvaMode')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.tvaMode"
|
||||
@@ -262,8 +274,9 @@
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="accounting.nTva"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
:label="t('commercial.clients.form.accounting.nTva')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.nTva"
|
||||
/>
|
||||
@@ -271,7 +284,7 @@
|
||||
:model-value="accounting.paymentDelayIri"
|
||||
:options="paymentDelayOptions"
|
||||
:label="t('commercial.clients.form.accounting.paymentDelay')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.paymentDelay"
|
||||
@@ -281,7 +294,7 @@
|
||||
:model-value="accounting.paymentTypeIri"
|
||||
:options="paymentTypeOptions"
|
||||
:label="t('commercial.clients.form.accounting.paymentType')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.paymentType"
|
||||
@@ -292,7 +305,7 @@
|
||||
:model-value="accounting.bankIri"
|
||||
:options="bankOptions"
|
||||
:label="t('commercial.clients.form.accounting.bank')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.bank"
|
||||
@@ -319,21 +332,23 @@
|
||||
<MalioInputText
|
||||
v-model="rib.label"
|
||||
:label="t('commercial.clients.form.accounting.ribLabel')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.label"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="rib.bic"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
:label="t('commercial.clients.form.accounting.ribBic')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.bic"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="rib.iban"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
:label="t('commercial.clients.form.accounting.ribIban')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.iban"
|
||||
/>
|
||||
@@ -423,6 +438,9 @@ import {
|
||||
type InformationFormDraft,
|
||||
type MainFormDraft,
|
||||
} from '~/modules/commercial/utils/forms/clientEdit'
|
||||
import { clampRevenueAmount } from '~/modules/commercial/utils/forms/amountInput'
|
||||
import { todayIso } from '~/shared/utils/date'
|
||||
import { CODE_ALNUM_MASK, FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize'
|
||||
import {
|
||||
buildClientFormTabKeys,
|
||||
isAddressValid,
|
||||
@@ -491,6 +509,22 @@ const headerTitle = computed(() => client.value?.companyName ?? t('commercial.cl
|
||||
const main = reactive<MainFormDraft>(mapMainDraft({} as ClientDetail))
|
||||
const information = reactive<InformationFormDraft>(mapInformationDraft({} as ClientDetail))
|
||||
const accounting = reactive<AccountingFormDraft>(mapAccountingFormDraft({} as ClientDetail))
|
||||
|
||||
// Borne haute de la date de creation : aujourd'hui (ERP-193, pas de date future).
|
||||
const maxFoundedAt = todayIso()
|
||||
|
||||
// CA plafonne a 999 999 999 999,99 (ERP-193). La :key force le re-affichage du
|
||||
// champ controle quand le plafonnement laisse le modelValue inchange.
|
||||
const revenueAmountKey = ref(0)
|
||||
|
||||
/** Saisie du CA : plafonne au maximum metier et re-synchronise le champ si plafonne. */
|
||||
function onRevenueAmountInput(value: string | null): void {
|
||||
const clamped = clampRevenueAmount(value)
|
||||
information.revenueAmount = clamped ?? null
|
||||
if (clamped !== value) {
|
||||
revenueAmountKey.value += 1
|
||||
}
|
||||
}
|
||||
const contacts = ref<ContactFormDraft[]>([])
|
||||
const addresses = ref<AddressFormDraft[]>([])
|
||||
const ribs = ref<RibFormDraft[]>([])
|
||||
@@ -668,6 +702,11 @@ function showError(e: unknown, opts: { duplicateCompany?: boolean } = {}): void
|
||||
})
|
||||
}
|
||||
|
||||
/** Toast de succès après suppression serveur confirmée d'un bloc (contact / adresse / RIB). */
|
||||
function notifyRemovalSuccess(): void {
|
||||
toast.success({ title: t('success.title'), message: t('success.deleted') })
|
||||
}
|
||||
|
||||
// ── Erreurs de validation par champ (ERP-101) ───────────────────────────────
|
||||
// Etat d'erreurs factorise avec l'ecran de creation (cf. useClientFormErrors) :
|
||||
// un `useFormErrors` par groupe scalaire + un tableau d'erreurs par ligne pour
|
||||
@@ -767,6 +806,7 @@ function askRemoveContact(index: number): void {
|
||||
deleteRow: url => api.delete(url, {}, { toast: false }),
|
||||
makeEmpty: emptyContact,
|
||||
onError: showError,
|
||||
onSuccess: notifyRemovalSuccess,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -844,6 +884,7 @@ function askRemoveAddress(index: number): void {
|
||||
deleteRow: url => api.delete(url, {}, { toast: false }),
|
||||
makeEmpty: emptyAddress,
|
||||
onError: showError,
|
||||
onSuccess: notifyRemovalSuccess,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -944,6 +985,7 @@ function askRemoveRib(index: number): void {
|
||||
deleteRow: url => api.delete(url, {}, { toast: false }),
|
||||
makeEmpty: emptyRib,
|
||||
onError: showError,
|
||||
onSuccess: notifyRemovalSuccess,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
icon="mdi:arrow-left-bold"
|
||||
icon-size="24"
|
||||
variant="ghost"
|
||||
:title="t('commercial.clients.consultation.back')"
|
||||
v-bind="{ ariaLabel: t('commercial.clients.consultation.back') }"
|
||||
@click="goBack"
|
||||
/>
|
||||
@@ -23,7 +24,7 @@
|
||||
/>
|
||||
<MalioButton
|
||||
v-if="showArchive"
|
||||
variant="secondary"
|
||||
variant="danger"
|
||||
icon-name="mdi:archive-arrow-down-outline"
|
||||
icon-position="left"
|
||||
:label="t('commercial.clients.action.archive')"
|
||||
@@ -48,43 +49,51 @@
|
||||
<!-- ── Formulaire principal (lecture seule) ──────────────────────── -->
|
||||
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<MalioInputText
|
||||
v-if="isFilled(client.companyName)"
|
||||
:model-value="client.companyName"
|
||||
:label="t('commercial.clients.form.main.companyName')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioSelectCheckbox
|
||||
v-if="isFilled(categoryIris)"
|
||||
:model-value="categoryIris"
|
||||
:options="mainCategoryOptions"
|
||||
:label="t('commercial.clients.form.main.categories')"
|
||||
:display-tag="true"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<!-- Relation toujours affichee (vide = « Aucun »), comme en edition. -->
|
||||
<!-- Relation : masquee en consultation si aucune (ERP-193) ; en edition
|
||||
elle reste toujours visible (vide = « Aucun »). -->
|
||||
<MalioSelect
|
||||
v-if="isFilled(relation.type)"
|
||||
:model-value="relation.type"
|
||||
:options="relationOptions"
|
||||
:label="t('commercial.clients.form.main.relation')"
|
||||
:empty-option-label="t('commercial.clients.form.main.relationNone')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<!-- Nom du distributeur/courtier : conditionnel (libelle type-dependant,
|
||||
aucune valeur sans relation — meme comportement qu'en edition). -->
|
||||
<MalioInputText
|
||||
v-if="relation.type"
|
||||
v-if="relation.type && isFilled(relation.name)"
|
||||
:model-value="relation.name"
|
||||
:label="relation.type === 'distributeur' ? t('commercial.clients.form.main.distributorName') : t('commercial.clients.form.main.brokerName')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<!-- Service de triage : case a cocher masquee si non cochee (ERP-193). -->
|
||||
<MalioCheckbox
|
||||
v-if="isFilled(client.triageService === true)"
|
||||
:model-value="client.triageService === true"
|
||||
:label="t('commercial.clients.form.main.triageService')"
|
||||
group-class="self-center"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ── Onglets (navigation libre, tout en lecture seule) ─────────── -->
|
||||
<MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
||||
<!-- ERP-193 : on n'affiche la barre que s'il reste au moins un onglet
|
||||
non vide (sinon seul le bloc principal est visible). -->
|
||||
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
||||
<!-- Onglet 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)]">
|
||||
@@ -92,42 +101,49 @@
|
||||
sur les inputs (champ 40px centre dans un h-12 -> ~4px de
|
||||
coussin de chaque cote). -->
|
||||
<MalioInputTextArea
|
||||
v-if="isFilled(information.description)"
|
||||
:model-value="information.description"
|
||||
:label="t('commercial.clients.form.information.description')"
|
||||
resize="none"
|
||||
group-class="row-span-2 pt-1 pb-1"
|
||||
text-input="h-full text-lg"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(information.competitors)"
|
||||
:model-value="information.competitors"
|
||||
:label="t('commercial.clients.form.information.competitors')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioDate
|
||||
v-if="isFilled(information.foundedAt)"
|
||||
:model-value="information.foundedAt"
|
||||
:label="t('commercial.clients.form.information.foundedAt')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(information.employeesCount)"
|
||||
:model-value="information.employeesCount"
|
||||
:label="t('commercial.clients.form.information.employeesCount')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputAmount
|
||||
v-if="isFilled(information.revenueAmount)"
|
||||
:model-value="information.revenueAmount"
|
||||
:label="t('commercial.clients.form.information.revenueAmount')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(information.directorName)"
|
||||
:model-value="information.directorName"
|
||||
:label="t('commercial.clients.form.information.directorName')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputAmount
|
||||
v-if="isFilled(information.profitAmount)"
|
||||
:model-value="information.profitAmount"
|
||||
:label="t('commercial.clients.form.information.profitAmount')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -140,7 +156,8 @@
|
||||
:key="contact.id ?? index"
|
||||
:model-value="contact"
|
||||
:title="t('commercial.clients.form.contact.title', { n: index + 1 })"
|
||||
readonly
|
||||
disabled
|
||||
hide-empty
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -157,7 +174,8 @@
|
||||
:site-options="allSiteOptions"
|
||||
:contact-options="contactOptions"
|
||||
:country-options="countryOptions"
|
||||
readonly
|
||||
disabled
|
||||
hide-empty
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -168,41 +186,47 @@
|
||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<MalioInputText
|
||||
v-if="isFilled(accounting.siren)"
|
||||
:model-value="accounting.siren"
|
||||
:label="t('commercial.clients.form.accounting.siren')"
|
||||
:mask="SIREN_MASK"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(accounting.accountNumber)"
|
||||
:model-value="accounting.accountNumber"
|
||||
:label="t('commercial.clients.form.accounting.accountNumber')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioSelect
|
||||
v-if="isFilled(accounting.tvaModeIri)"
|
||||
:model-value="accounting.tvaModeIri"
|
||||
:options="tvaModeOptions"
|
||||
:label="t('commercial.clients.form.accounting.tvaMode')"
|
||||
empty-option-label=""
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(accounting.nTva)"
|
||||
:model-value="accounting.nTva"
|
||||
:label="t('commercial.clients.form.accounting.nTva')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioSelect
|
||||
v-if="isFilled(accounting.paymentDelayIri)"
|
||||
:model-value="accounting.paymentDelayIri"
|
||||
:options="paymentDelayOptions"
|
||||
:label="t('commercial.clients.form.accounting.paymentDelay')"
|
||||
empty-option-label=""
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioSelect
|
||||
v-if="isFilled(accounting.paymentTypeIri)"
|
||||
:model-value="accounting.paymentTypeIri"
|
||||
:options="paymentTypeOptions"
|
||||
:label="t('commercial.clients.form.accounting.paymentType')"
|
||||
empty-option-label=""
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioSelect
|
||||
v-if="accounting.bankIri"
|
||||
@@ -210,7 +234,7 @@
|
||||
:options="bankOptions"
|
||||
:label="t('commercial.clients.form.accounting.bank')"
|
||||
empty-option-label=""
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,30 +247,30 @@
|
||||
>
|
||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<MalioInputText
|
||||
v-if="isFilled(rib.label)"
|
||||
:model-value="rib.label"
|
||||
:label="t('commercial.clients.form.accounting.ribLabel')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(rib.bic)"
|
||||
:model-value="rib.bic"
|
||||
:label="t('commercial.clients.form.accounting.ribBic')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(rib.iban)"
|
||||
:model-value="rib.iban"
|
||||
:label="t('commercial.clients.form.accounting.ribIban')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Onglets non encore implementes : frame vide (navigation libre). -->
|
||||
<template #transport><ComingSoonPlaceholder /></template>
|
||||
<template #statistics><ComingSoonPlaceholder /></template>
|
||||
<template #reports><ComingSoonPlaceholder /></template>
|
||||
<template #exchanges><ComingSoonPlaceholder /></template>
|
||||
<!-- ERP-193 : les onglets « a venir » (Transport / Statistiques /
|
||||
Rapports / Echanges) ne sont plus rendus en consultation
|
||||
(masquage des onglets vides) — slots supprimes. -->
|
||||
</MalioTabList>
|
||||
</template>
|
||||
|
||||
@@ -278,13 +302,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useClient } from '~/modules/commercial/composables/useClient'
|
||||
import { buildClientFormTabKeys, isRibRequiredForPaymentType } from '~/modules/commercial/utils/forms/clientFormRules'
|
||||
import { isRibRequiredForPaymentType } from '~/modules/commercial/utils/forms/clientFormRules'
|
||||
import { readHistoryTab } from '~/shared/utils/historyTab'
|
||||
import {
|
||||
canEditClient,
|
||||
categoryOptionsOf,
|
||||
clientConsultationVisibleTabs,
|
||||
contactOptionsOf,
|
||||
mapAccountingDraft,
|
||||
mapAddressView,
|
||||
@@ -299,6 +324,7 @@ import {
|
||||
type SelectOption,
|
||||
} from '~/modules/commercial/utils/forms/clientConsultation'
|
||||
import { emptyAddress, emptyContact } from '~/modules/commercial/types/clientForm'
|
||||
import { isFilled } from '~/shared/utils/consultationDisplay'
|
||||
|
||||
// Masque d'affichage (purement visuel, la donnee reste celle du serveur).
|
||||
const SIREN_MASK = '#########'
|
||||
@@ -412,9 +438,11 @@ const paymentTypeOptions = computed(() => referentialOptionOf(client.value?.paym
|
||||
const bankOptions = computed(() => referentialOptionOf(client.value?.bank))
|
||||
|
||||
// ── Onglets : navigation LIBRE (pas de sequence forcee en consultation) ────
|
||||
// 4 onglets actifs (Information, Contact, Adresse, + Comptabilite si droit) et
|
||||
// 4 coquilles (Transport, Statistiques, Rapports, Echanges).
|
||||
const tabKeys = computed(() => buildClientFormTabKeys(canAccountingView.value, { includeEditOnlyTabs: true }))
|
||||
// ERP-193 (retour metier) : on masque les coquilles non implementees ET tout
|
||||
// onglet de donnees vide. La liste depend donc du payload charge.
|
||||
const visibleTabKeys = computed(() => clientConsultationVisibleTabs(client.value, {
|
||||
canAccountingView: canAccountingView.value,
|
||||
}))
|
||||
|
||||
const TAB_ICONS: Record<string, string> = {
|
||||
information: 'mdi:account-outline',
|
||||
@@ -427,14 +455,26 @@ const TAB_ICONS: Record<string, string> = {
|
||||
exchanges: 'mdi:account-group-outline',
|
||||
}
|
||||
|
||||
const tabs = computed(() => tabKeys.value.map(key => ({
|
||||
const tabs = computed(() => visibleTabKeys.value.map(key => ({
|
||||
key,
|
||||
label: t(`commercial.clients.tab.${key}`),
|
||||
icon: TAB_ICONS[key],
|
||||
})))
|
||||
|
||||
// Onglet initial : repris de l'edition au retour (history.state), sinon Information.
|
||||
const activeTab = ref(readHistoryTab(tabKeys.value) ?? 'information')
|
||||
// Onglet initial : vide tant que le client n'est pas charge. Des que la liste
|
||||
// des onglets visibles est connue, on cale sur l'onglet repris de l'edition
|
||||
// (history.state) s'il est encore visible, sinon le premier onglet visible.
|
||||
// Un watcher recale aussi si l'onglet courant disparait (ex: changement de droit).
|
||||
const activeTab = ref('')
|
||||
watch(visibleTabKeys, (keys) => {
|
||||
if (keys.length === 0) {
|
||||
activeTab.value = ''
|
||||
return
|
||||
}
|
||||
if (!keys.includes(activeTab.value)) {
|
||||
activeTab.value = readHistoryTab(keys) ?? keys[0]
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// ── Navigation ─────────────────────────────────────────────────────────────
|
||||
function goBack(): void {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
@update:page="goToPage"
|
||||
@update:per-page="setItemsPerPage"
|
||||
>
|
||||
<!-- Categories : codes stables separes par une virgule (ERP-78). -->
|
||||
<!-- Categories : libelles (name) separes par une virgule (ERP-193). -->
|
||||
<template #cell-categories="{ item }">
|
||||
{{ formatCategories(item) }}
|
||||
</template>
|
||||
@@ -209,10 +209,10 @@ const columns = [
|
||||
{ key: 'lastActivity', label: t('commercial.clients.column.lastActivity') },
|
||||
]
|
||||
|
||||
/** Codes des categories du client, separes par une virgule (ERP-78). */
|
||||
/** Libelles (name) des categories du client, separes par une virgule (ERP-193). */
|
||||
function formatCategories(item: Record<string, unknown>): string {
|
||||
const categories = (item.categories as Client['categories']) ?? []
|
||||
return categories.map(c => c.code).join(', ')
|
||||
return categories.map(c => c.name).join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
icon="mdi:arrow-left-bold"
|
||||
icon-size="24"
|
||||
variant="ghost"
|
||||
:title="t('commercial.clients.form.back')"
|
||||
v-bind="{ ariaLabel: t('commercial.clients.form.back') }"
|
||||
@click="goBack"
|
||||
/>
|
||||
@@ -19,9 +20,10 @@
|
||||
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<MalioInputText
|
||||
v-model="main.companyName"
|
||||
:mask="FREE_TEXT_MASK"
|
||||
:label="t('commercial.clients.form.main.companyName')"
|
||||
:required="true"
|
||||
:readonly="mainLocked"
|
||||
:disabled="mainLocked"
|
||||
:error="mainErrors.errors.companyName"
|
||||
/>
|
||||
<MalioSelectCheckbox
|
||||
@@ -29,7 +31,7 @@
|
||||
:options="referentials.categories.value"
|
||||
:label="t('commercial.clients.form.main.categories')"
|
||||
:display-tag="true"
|
||||
:readonly="mainLocked"
|
||||
:disabled="mainLocked"
|
||||
:required="true"
|
||||
:error="mainErrors.errors.categories"
|
||||
@update:model-value="(v: (string | number)[]) => main.categoryIris = v.map(String)"
|
||||
@@ -40,7 +42,7 @@
|
||||
:options="relationOptions"
|
||||
:label="t('commercial.clients.form.main.relation')"
|
||||
:empty-option-label="t('commercial.clients.form.main.relationNone')"
|
||||
:readonly="mainLocked"
|
||||
:disabled="mainLocked"
|
||||
@update:model-value="onRelationChange"
|
||||
/>
|
||||
<MalioSelect
|
||||
@@ -48,7 +50,7 @@
|
||||
:model-value="main.brokerIri"
|
||||
:options="referentials.brokers.value"
|
||||
:label="t('commercial.clients.form.main.brokerName')"
|
||||
:readonly="mainLocked"
|
||||
:disabled="mainLocked"
|
||||
:required="true"
|
||||
:error="mainErrors.errors.broker"
|
||||
@update:model-value="(v: string | number | null) => main.brokerIri = v === null ? null : String(v)"
|
||||
@@ -58,7 +60,7 @@
|
||||
:model-value="main.distributorIri"
|
||||
:options="referentials.distributors.value"
|
||||
:label="t('commercial.clients.form.main.distributorName')"
|
||||
:readonly="mainLocked"
|
||||
:disabled="mainLocked"
|
||||
:required="true"
|
||||
:error="mainErrors.errors.distributor"
|
||||
@update:model-value="(v: string | number | null) => main.distributorIri = v === null ? null : String(v)"
|
||||
@@ -68,7 +70,7 @@
|
||||
v-model="main.triageService"
|
||||
:label="t('commercial.clients.form.main.triageService')"
|
||||
group-class="self-center"
|
||||
:readonly="mainLocked"
|
||||
:disabled="mainLocked"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -96,20 +98,24 @@
|
||||
resize="none"
|
||||
group-class="row-span-2 pt-1 pb-1"
|
||||
text-input="h-full text-lg"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.description"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="information.competitors"
|
||||
:mask="FREE_TEXT_MASK"
|
||||
:label="t('commercial.clients.form.information.competitors')"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.competitors"
|
||||
/>
|
||||
<!-- Date de creation jamais dans le futur (ERP-193) : :max plafonne
|
||||
le calendrier a aujourd'hui et invalide une saisie future. -->
|
||||
<MalioDate
|
||||
v-model="information.foundedAt"
|
||||
:label="t('commercial.clients.form.information.foundedAt')"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:editable="true"
|
||||
:max="maxFoundedAt"
|
||||
:error="informationErrors.errors.foundedAt"
|
||||
@update:raw-value="(v: string) => information.foundedAtRaw = v"
|
||||
/>
|
||||
@@ -117,37 +123,42 @@
|
||||
v-model="information.employeesCount"
|
||||
:label="t('commercial.clients.form.information.employeesCount')"
|
||||
:mask="EMPLOYEES_MASK"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.employeesCount"
|
||||
/>
|
||||
<!-- CA plafonne a 999 999 999 999,99 (ERP-193) : clamp a la saisie,
|
||||
:key force le re-affichage quand on plafonne (modelValue inchange). -->
|
||||
<MalioInputAmount
|
||||
v-model="information.revenueAmount"
|
||||
:key="revenueAmountKey"
|
||||
:model-value="information.revenueAmount"
|
||||
:label="t('commercial.clients.form.information.revenueAmount')"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.revenueAmount"
|
||||
@update:model-value="onRevenueAmountInput"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="information.directorName"
|
||||
:mask="PERSON_NAME_MASK"
|
||||
:label="t('commercial.clients.form.information.directorName')"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.directorName"
|
||||
/>
|
||||
<MalioInputAmount
|
||||
v-model="information.profitAmount"
|
||||
:label="t('commercial.clients.form.information.profitAmount')"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.profitAmount"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isValidated('information')" class="mt-12 flex justify-center">
|
||||
<!-- Desactive tant que le client n'est pas cree (evite un PATCH
|
||||
avant le POST si clic trop tot, Information etant l'onglet
|
||||
actif par defaut). Onglet facultatif : un enregistrement a
|
||||
vide reste possible, c'est le back qui valide. -->
|
||||
<!-- Masque tant que le client n'est pas cree : Information etant
|
||||
l'onglet actif par defaut, son Valider ne doit pas apparaitre a
|
||||
cote de celui du formulaire principal (ERP-193). Onglet facultatif :
|
||||
un enregistrement a vide reste possible, c'est le back qui valide. -->
|
||||
<div v-if="!isValidated('information') && clientId !== null" class="mt-12 flex justify-center">
|
||||
<MalioButton
|
||||
variant="primary"
|
||||
:label="t('commercial.clients.form.submit')"
|
||||
:disabled="tabSubmitting || clientId === null"
|
||||
:disabled="tabSubmitting"
|
||||
@click="submitInformation"
|
||||
/>
|
||||
</div>
|
||||
@@ -166,7 +177,7 @@
|
||||
:model-value="contact"
|
||||
:title="t('commercial.clients.form.contact.title', { n: index + 1 })"
|
||||
:removable="isRowRemovable(contacts, index)"
|
||||
:readonly="isValidated('contact')"
|
||||
:disabled="isValidated('contact')"
|
||||
:errors="contactErrors[index]"
|
||||
@update:model-value="(v) => contacts[index] = v"
|
||||
@remove="askRemoveContact(index)"
|
||||
@@ -203,7 +214,7 @@
|
||||
:contact-options="contactOptions"
|
||||
:country-options="countryOptions"
|
||||
:removable="isRowRemovable(addresses, index)"
|
||||
:readonly="isValidated('address')"
|
||||
:disabled="isValidated('address')"
|
||||
:errors="addressErrors[index]"
|
||||
@update:model-value="(v) => addresses[index] = v"
|
||||
@remove="askRemoveAddress(index)"
|
||||
@@ -237,14 +248,15 @@
|
||||
v-model="accounting.siren"
|
||||
:label="t('commercial.clients.form.accounting.siren')"
|
||||
:mask="SIREN_MASK"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.siren"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="accounting.accountNumber"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
:label="t('commercial.clients.form.accounting.accountNumber')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.accountNumber"
|
||||
/>
|
||||
@@ -252,7 +264,7 @@
|
||||
:model-value="accounting.tvaModeIri"
|
||||
:options="referentials.tvaModes.value"
|
||||
:label="t('commercial.clients.form.accounting.tvaMode')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.tvaMode"
|
||||
@@ -260,8 +272,9 @@
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="accounting.nTva"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
:label="t('commercial.clients.form.accounting.nTva')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.nTva"
|
||||
/>
|
||||
@@ -269,7 +282,7 @@
|
||||
:model-value="accounting.paymentDelayIri"
|
||||
:options="referentials.paymentDelays.value"
|
||||
:label="t('commercial.clients.form.accounting.paymentDelay')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.paymentDelay"
|
||||
@@ -279,7 +292,7 @@
|
||||
:model-value="accounting.paymentTypeIri"
|
||||
:options="referentials.paymentTypes.value"
|
||||
:label="t('commercial.clients.form.accounting.paymentType')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.paymentType"
|
||||
@@ -290,7 +303,7 @@
|
||||
:model-value="accounting.bankIri"
|
||||
:options="referentials.banks.value"
|
||||
:label="t('commercial.clients.form.accounting.bank')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.bank"
|
||||
@@ -318,21 +331,23 @@
|
||||
<MalioInputText
|
||||
v-model="rib.label"
|
||||
:label="t('commercial.clients.form.accounting.ribLabel')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.label"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="rib.bic"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
:label="t('commercial.clients.form.accounting.ribBic')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.bic"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="rib.iban"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
:label="t('commercial.clients.form.accounting.ribIban')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.iban"
|
||||
/>
|
||||
@@ -407,6 +422,9 @@ import {
|
||||
lastFillableTabKey,
|
||||
showsRelationAndTriageFields,
|
||||
} from '~/modules/commercial/utils/forms/clientFormRules'
|
||||
import { clampRevenueAmount } from '~/modules/commercial/utils/forms/amountInput'
|
||||
import { todayIso } from '~/shared/utils/date'
|
||||
import { CODE_ALNUM_MASK, FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize'
|
||||
import {
|
||||
buildAddressPayload,
|
||||
buildMainPayload,
|
||||
@@ -665,6 +683,22 @@ const information = reactive({
|
||||
directorName: null as string | null,
|
||||
})
|
||||
|
||||
// Borne haute de la date de creation : aujourd'hui (ERP-193, pas de date future).
|
||||
const maxFoundedAt = todayIso()
|
||||
|
||||
// CA plafonne a 999 999 999 999,99 (ERP-193). La :key force le re-affichage du
|
||||
// champ controle quand le plafonnement laisse le modelValue inchange.
|
||||
const revenueAmountKey = ref(0)
|
||||
|
||||
/** Saisie du CA : plafonne au maximum metier et re-synchronise le champ si plafonne. */
|
||||
function onRevenueAmountInput(value: string | null): void {
|
||||
const clamped = clampRevenueAmount(value)
|
||||
information.revenueAmount = clamped ?? null
|
||||
if (clamped !== value) {
|
||||
revenueAmountKey.value += 1
|
||||
}
|
||||
}
|
||||
|
||||
/** PATCH /clients/{id} — mode strict : uniquement les champs du groupe information. */
|
||||
async function submitInformation(): Promise<void> {
|
||||
if (clientId.value === null || tabSubmitting.value) return
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
icon="mdi:arrow-left-bold"
|
||||
icon-size="24"
|
||||
variant="ghost"
|
||||
:title="t('commercial.suppliers.edit.back')"
|
||||
v-bind="{ ariaLabel: t('commercial.suppliers.edit.back') }"
|
||||
@click="goBack"
|
||||
/>
|
||||
@@ -26,15 +27,16 @@
|
||||
v-model="main.companyName"
|
||||
:label="t('commercial.suppliers.form.main.companyName')"
|
||||
:required="true"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="mainErrors.errors.companyName"
|
||||
:mask="FREE_TEXT_MASK"
|
||||
/>
|
||||
<MalioSelectCheckbox
|
||||
:model-value="main.categoryIris"
|
||||
:options="mainCategoryOptions"
|
||||
:label="t('commercial.suppliers.form.main.categories')"
|
||||
:display-tag="true"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:required="true"
|
||||
:error="mainErrors.errors.categories"
|
||||
@update:model-value="(v: (string | number)[]) => main.categoryIris = v.map(String)"
|
||||
@@ -62,20 +64,24 @@
|
||||
resize="none"
|
||||
group-class="row-span-2 pt-1 pb-1"
|
||||
text-input="h-full text-lg"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.description"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="information.competitors"
|
||||
:label="t('commercial.suppliers.form.information.competitors')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.competitors"
|
||||
:mask="FREE_TEXT_MASK"
|
||||
/>
|
||||
<!-- Date de creation jamais dans le futur (ERP-193) : :max plafonne
|
||||
le calendrier a aujourd'hui et invalide une saisie future. -->
|
||||
<MalioDate
|
||||
v-model="information.foundedAt"
|
||||
:label="t('commercial.suppliers.form.information.foundedAt')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:editable="true"
|
||||
:max="maxFoundedAt"
|
||||
:error="informationErrors.errors.foundedAt"
|
||||
@update:raw-value="(v: string) => information.foundedAtRaw = v"
|
||||
/>
|
||||
@@ -83,25 +89,30 @@
|
||||
v-model="information.employeesCount"
|
||||
:label="t('commercial.suppliers.form.information.employeesCount')"
|
||||
:mask="EMPLOYEES_MASK"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.employeesCount"
|
||||
/>
|
||||
<!-- CA plafonne a 999 999 999 999,99 (ERP-193) : clamp a la saisie,
|
||||
:key force le re-affichage quand on plafonne (modelValue inchange). -->
|
||||
<MalioInputAmount
|
||||
v-model="information.revenueAmount"
|
||||
:key="revenueAmountKey"
|
||||
:model-value="information.revenueAmount"
|
||||
:label="t('commercial.suppliers.form.information.revenueAmount')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.revenueAmount"
|
||||
@update:model-value="onRevenueAmountInput"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="information.directorName"
|
||||
:label="t('commercial.suppliers.form.information.directorName')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.directorName"
|
||||
:mask="PERSON_NAME_MASK"
|
||||
/>
|
||||
<MalioInputAmount
|
||||
v-model="information.profitAmount"
|
||||
:label="t('commercial.suppliers.form.information.profitAmount')"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.profitAmount"
|
||||
/>
|
||||
<!-- Volume previsionnel : specifique fournisseur (entier). -->
|
||||
@@ -109,7 +120,7 @@
|
||||
v-model="information.volumeForecast"
|
||||
:label="t('commercial.suppliers.form.information.volumeForecast')"
|
||||
:mask="VOLUME_FORECAST_MASK"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:error="informationErrors.errors.volumeForecast"
|
||||
/>
|
||||
</div>
|
||||
@@ -136,7 +147,7 @@
|
||||
:model-value="contact"
|
||||
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
|
||||
:removable="isRowRemovable(contacts, index)"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:errors="contactErrors[index]"
|
||||
@update:model-value="(v) => contacts[index] = v"
|
||||
@remove="askRemoveContact(index)"
|
||||
@@ -173,7 +184,7 @@
|
||||
:contact-options="contactOptions"
|
||||
:country-options="countryOptions"
|
||||
:removable="isRowRemovable(addresses, index)"
|
||||
:readonly="businessReadonly"
|
||||
:disabled="businessReadonly"
|
||||
:errors="addressErrors[index]"
|
||||
@update:model-value="(v) => addresses[index] = v"
|
||||
@remove="askRemoveAddress(index)"
|
||||
@@ -208,22 +219,23 @@
|
||||
v-model="accounting.siren"
|
||||
:label="t('commercial.suppliers.form.accounting.siren')"
|
||||
:mask="SIREN_MASK"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.siren"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="accounting.accountNumber"
|
||||
:label="t('commercial.suppliers.form.accounting.accountNumber')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.accountNumber"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
/>
|
||||
<MalioSelect
|
||||
:model-value="accounting.tvaModeIri"
|
||||
:options="tvaModeOptions"
|
||||
:label="t('commercial.suppliers.form.accounting.tvaMode')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.tvaMode"
|
||||
@@ -232,15 +244,16 @@
|
||||
<MalioInputText
|
||||
v-model="accounting.nTva"
|
||||
:label="t('commercial.suppliers.form.accounting.nTva')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.nTva"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
/>
|
||||
<MalioSelect
|
||||
:model-value="accounting.paymentDelayIri"
|
||||
:options="paymentDelayOptions"
|
||||
:label="t('commercial.suppliers.form.accounting.paymentDelay')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.paymentDelay"
|
||||
@@ -250,7 +263,7 @@
|
||||
:model-value="accounting.paymentTypeIri"
|
||||
:options="paymentTypeOptions"
|
||||
:label="t('commercial.suppliers.form.accounting.paymentType')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.paymentType"
|
||||
@@ -261,7 +274,7 @@
|
||||
:model-value="accounting.bankIri"
|
||||
:options="bankOptions"
|
||||
:label="t('commercial.suppliers.form.accounting.bank')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.bank"
|
||||
@@ -288,23 +301,25 @@
|
||||
<MalioInputText
|
||||
v-model="rib.label"
|
||||
:label="t('commercial.suppliers.form.accounting.ribLabel')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.label"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="rib.bic"
|
||||
:label="t('commercial.suppliers.form.accounting.ribBic')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.bic"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="rib.iban"
|
||||
:label="t('commercial.suppliers.form.accounting.ribIban')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.iban"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -392,6 +407,8 @@ import {
|
||||
type MainFormDraft,
|
||||
type SupplierEditAbilities,
|
||||
} from '~/modules/commercial/utils/forms/supplierEdit'
|
||||
import { clampRevenueAmount } from '~/modules/commercial/utils/forms/amountInput'
|
||||
import { todayIso } from '~/shared/utils/date'
|
||||
import {
|
||||
buildSupplierFormTabKeys,
|
||||
isAddressValid,
|
||||
@@ -412,6 +429,7 @@ import {
|
||||
} from '~/modules/commercial/types/supplierForm'
|
||||
import { extractApiErrorMessage } from '~/shared/utils/api'
|
||||
import { isRowRemovable, removeCollectionRow } from '~/shared/utils/collectionRow'
|
||||
import { CODE_ALNUM_MASK, FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize'
|
||||
import { readHistoryTab } from '~/shared/utils/historyTab'
|
||||
|
||||
// Masques de saisie (la normalisation finale reste serveur).
|
||||
@@ -457,6 +475,22 @@ const headerTitle = computed(() => supplier.value?.companyName ?? t('commercial.
|
||||
const main = reactive<MainFormDraft>(mapMainDraft({} as SupplierDetail))
|
||||
const information = reactive<InformationFormDraft>(mapInformationDraft({} as SupplierDetail))
|
||||
const accounting = reactive<AccountingFormDraft>(mapAccountingFormDraft({} as SupplierDetail))
|
||||
|
||||
// Borne haute de la date de creation : aujourd'hui (ERP-193, pas de date future).
|
||||
const maxFoundedAt = todayIso()
|
||||
|
||||
// CA plafonne a 999 999 999 999,99 (ERP-193). La :key force le re-affichage du
|
||||
// champ controle quand le plafonnement laisse le modelValue inchange.
|
||||
const revenueAmountKey = ref(0)
|
||||
|
||||
/** Saisie du CA : plafonne au maximum metier et re-synchronise le champ si plafonne. */
|
||||
function onRevenueAmountInput(value: string | null): void {
|
||||
const clamped = clampRevenueAmount(value)
|
||||
information.revenueAmount = clamped ?? null
|
||||
if (clamped !== value) {
|
||||
revenueAmountKey.value += 1
|
||||
}
|
||||
}
|
||||
const contacts = ref<SupplierContactFormDraft[]>([])
|
||||
const addresses = ref<SupplierAddressFormDraft[]>([])
|
||||
const ribs = ref<SupplierRibFormDraft[]>([])
|
||||
@@ -583,6 +617,11 @@ function showError(e: unknown): void {
|
||||
toast.error({ title: t('commercial.suppliers.toast.error'), message: apiErrorMessage(e) })
|
||||
}
|
||||
|
||||
/** Toast de succès après suppression serveur confirmée d'un bloc (contact / adresse / RIB). */
|
||||
function notifyRemovalSuccess(): void {
|
||||
toast.success({ title: t('success.title'), message: t('success.deleted') })
|
||||
}
|
||||
|
||||
// ── Erreurs de validation par champ (ERP-101) ───────────────────────────────
|
||||
const {
|
||||
mainErrors,
|
||||
@@ -666,6 +705,7 @@ function askRemoveContact(index: number): void {
|
||||
deleteRow: url => api.delete(url, {}, { toast: false }),
|
||||
makeEmpty: emptyContact,
|
||||
onError: showError,
|
||||
onSuccess: notifyRemovalSuccess,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -734,6 +774,7 @@ function askRemoveAddress(index: number): void {
|
||||
deleteRow: url => api.delete(url, {}, { toast: false }),
|
||||
makeEmpty: emptyAddress,
|
||||
onError: showError,
|
||||
onSuccess: notifyRemovalSuccess,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -833,6 +874,7 @@ function askRemoveRib(index: number): void {
|
||||
deleteRow: url => api.delete(url, {}, { toast: false }),
|
||||
makeEmpty: emptyRib,
|
||||
onError: showError,
|
||||
onSuccess: notifyRemovalSuccess,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
icon="mdi:arrow-left-bold"
|
||||
icon-size="24"
|
||||
variant="ghost"
|
||||
:title="t('commercial.suppliers.consultation.back')"
|
||||
v-bind="{ ariaLabel: t('commercial.suppliers.consultation.back') }"
|
||||
@click="goBack"
|
||||
/>
|
||||
@@ -23,7 +24,7 @@
|
||||
/>
|
||||
<MalioButton
|
||||
v-if="showArchive"
|
||||
variant="secondary"
|
||||
variant="danger"
|
||||
icon-name="mdi:archive-arrow-down-outline"
|
||||
icon-position="left"
|
||||
:label="t('commercial.suppliers.action.archive')"
|
||||
@@ -48,69 +49,82 @@
|
||||
<!-- ── Formulaire principal (lecture seule) ──────────────────────── -->
|
||||
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<MalioInputText
|
||||
v-if="isFilled(supplier.companyName)"
|
||||
:model-value="supplier.companyName"
|
||||
:label="t('commercial.suppliers.form.main.companyName')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioSelectCheckbox
|
||||
v-if="isFilled(categoryIris)"
|
||||
:model-value="categoryIris"
|
||||
:options="mainCategoryOptions"
|
||||
:label="t('commercial.suppliers.form.main.categories')"
|
||||
:display-tag="true"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ── Onglets (navigation libre, tout en lecture seule) ─────────── -->
|
||||
<MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
||||
<!-- Masque la barre d'onglets (et sa bordure) quand aucun onglet n'est
|
||||
visible : seul le formulaire principal est rempli (aligné sur le
|
||||
client). -->
|
||||
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
||||
<!-- Onglet 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)]">
|
||||
<!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas
|
||||
sur les inputs (champ 40px centre dans un h-12). -->
|
||||
<MalioInputTextArea
|
||||
v-if="isFilled(information.description)"
|
||||
:model-value="information.description"
|
||||
:label="t('commercial.suppliers.form.information.description')"
|
||||
resize="none"
|
||||
group-class="row-span-2 pt-1 pb-1"
|
||||
text-input="h-full text-lg"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(information.competitors)"
|
||||
:model-value="information.competitors"
|
||||
:label="t('commercial.suppliers.form.information.competitors')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioDate
|
||||
v-if="isFilled(information.foundedAt)"
|
||||
:model-value="information.foundedAt"
|
||||
:label="t('commercial.suppliers.form.information.foundedAt')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(information.employeesCount)"
|
||||
:model-value="information.employeesCount"
|
||||
:label="t('commercial.suppliers.form.information.employeesCount')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputAmount
|
||||
v-if="isFilled(information.revenueAmount)"
|
||||
:model-value="information.revenueAmount"
|
||||
:label="t('commercial.suppliers.form.information.revenueAmount')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(information.directorName)"
|
||||
:model-value="information.directorName"
|
||||
:label="t('commercial.suppliers.form.information.directorName')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputAmount
|
||||
v-if="isFilled(information.profitAmount)"
|
||||
:model-value="information.profitAmount"
|
||||
:label="t('commercial.suppliers.form.information.profitAmount')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<!-- Volume previsionnel : specifique fournisseur (entier). -->
|
||||
<MalioInputText
|
||||
v-if="isFilled(information.volumeForecast)"
|
||||
:model-value="information.volumeForecast"
|
||||
:label="t('commercial.suppliers.form.information.volumeForecast')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -123,7 +137,8 @@
|
||||
:key="contact.id ?? index"
|
||||
:model-value="contact"
|
||||
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
|
||||
readonly
|
||||
disabled
|
||||
hide-empty
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -140,7 +155,8 @@
|
||||
:site-options="allSiteOptions"
|
||||
:contact-options="contactOptions"
|
||||
:country-options="countryOptions"
|
||||
readonly
|
||||
disabled
|
||||
hide-empty
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -151,41 +167,47 @@
|
||||
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<MalioInputText
|
||||
v-if="isFilled(accounting.siren)"
|
||||
:model-value="accounting.siren"
|
||||
:label="t('commercial.suppliers.form.accounting.siren')"
|
||||
:mask="SIREN_MASK"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(accounting.accountNumber)"
|
||||
:model-value="accounting.accountNumber"
|
||||
:label="t('commercial.suppliers.form.accounting.accountNumber')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioSelect
|
||||
v-if="isFilled(accounting.tvaModeIri)"
|
||||
:model-value="accounting.tvaModeIri"
|
||||
:options="tvaModeOptions"
|
||||
:label="t('commercial.suppliers.form.accounting.tvaMode')"
|
||||
empty-option-label=""
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(accounting.nTva)"
|
||||
:model-value="accounting.nTva"
|
||||
:label="t('commercial.suppliers.form.accounting.nTva')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioSelect
|
||||
v-if="isFilled(accounting.paymentDelayIri)"
|
||||
:model-value="accounting.paymentDelayIri"
|
||||
:options="paymentDelayOptions"
|
||||
:label="t('commercial.suppliers.form.accounting.paymentDelay')"
|
||||
empty-option-label=""
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioSelect
|
||||
v-if="isFilled(accounting.paymentTypeIri)"
|
||||
:model-value="accounting.paymentTypeIri"
|
||||
:options="paymentTypeOptions"
|
||||
:label="t('commercial.suppliers.form.accounting.paymentType')"
|
||||
empty-option-label=""
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioSelect
|
||||
v-if="accounting.bankIri"
|
||||
@@ -193,7 +215,7 @@
|
||||
:options="bankOptions"
|
||||
:label="t('commercial.suppliers.form.accounting.bank')"
|
||||
empty-option-label=""
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,30 +228,30 @@
|
||||
>
|
||||
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<MalioInputText
|
||||
v-if="isFilled(rib.label)"
|
||||
:model-value="rib.label"
|
||||
:label="t('commercial.suppliers.form.accounting.ribLabel')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(rib.bic)"
|
||||
:model-value="rib.bic"
|
||||
:label="t('commercial.suppliers.form.accounting.ribBic')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
<MalioInputText
|
||||
v-if="isFilled(rib.iban)"
|
||||
:model-value="rib.iban"
|
||||
:label="t('commercial.suppliers.form.accounting.ribIban')"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Onglets non encore implementes : frame vide (navigation libre). -->
|
||||
<template #transport><ComingSoonPlaceholder /></template>
|
||||
<template #statistics><ComingSoonPlaceholder /></template>
|
||||
<template #reports><ComingSoonPlaceholder /></template>
|
||||
<template #exchanges><ComingSoonPlaceholder /></template>
|
||||
<!-- ERP-193 : les onglets « a venir » (Transport / Statistiques /
|
||||
Rapports / Echanges) ne sont plus rendus en consultation
|
||||
(masquage des onglets vides) — slots supprimes. -->
|
||||
</MalioTabList>
|
||||
</template>
|
||||
|
||||
@@ -261,9 +283,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useSupplier } from '~/modules/commercial/composables/useSupplier'
|
||||
import { buildSupplierFormTabKeys, isRibRequiredForPaymentType } from '~/modules/commercial/utils/forms/supplierFormRules'
|
||||
import { isRibRequiredForPaymentType } from '~/modules/commercial/utils/forms/supplierFormRules'
|
||||
import { readHistoryTab } from '~/shared/utils/historyTab'
|
||||
import {
|
||||
canEditSupplier,
|
||||
@@ -278,10 +300,12 @@ import {
|
||||
referentialOptionOf,
|
||||
showArchiveAction,
|
||||
showRestoreAction,
|
||||
supplierConsultationVisibleTabs,
|
||||
type SelectOption,
|
||||
type SupplierDetail,
|
||||
} from '~/modules/commercial/utils/forms/supplierConsultation'
|
||||
import { emptyContact } from '~/modules/commercial/types/supplierForm'
|
||||
import { isFilled } from '~/shared/utils/consultationDisplay'
|
||||
|
||||
// Masque d'affichage (purement visuel, la donnee reste celle du serveur).
|
||||
const SIREN_MASK = '#########'
|
||||
@@ -387,9 +411,11 @@ const paymentTypeOptions = computed(() => referentialOptionOf(supplier.value?.pa
|
||||
const bankOptions = computed(() => referentialOptionOf(supplier.value?.bank))
|
||||
|
||||
// ── Onglets : navigation LIBRE (pas de sequence forcee en consultation) ────
|
||||
// 3 onglets actifs (Information, Contacts, Adresses, + Comptabilite si droit) et
|
||||
// 4 coquilles (Transport, Statistiques, Rapports, Echanges).
|
||||
const tabKeys = computed(() => buildSupplierFormTabKeys(canAccountingView.value, { includeEditOnlyTabs: true }))
|
||||
// ERP-193 (retour metier) : on masque les coquilles non implementees ET tout
|
||||
// onglet de donnees vide. La liste depend donc du payload charge.
|
||||
const visibleTabKeys = computed(() => supplierConsultationVisibleTabs(supplier.value, {
|
||||
canAccountingView: canAccountingView.value,
|
||||
}))
|
||||
|
||||
const TAB_ICONS: Record<string, string> = {
|
||||
information: 'mdi:account-outline',
|
||||
@@ -402,14 +428,25 @@ const TAB_ICONS: Record<string, string> = {
|
||||
exchanges: 'mdi:account-group-outline',
|
||||
}
|
||||
|
||||
const tabs = computed(() => tabKeys.value.map(key => ({
|
||||
const tabs = computed(() => visibleTabKeys.value.map(key => ({
|
||||
key,
|
||||
label: t(`commercial.suppliers.tab.${key}`),
|
||||
icon: TAB_ICONS[key],
|
||||
})))
|
||||
|
||||
// Onglet initial : repris de l'edition au retour (history.state), sinon Information.
|
||||
const activeTab = ref(readHistoryTab(tabKeys.value) ?? 'information')
|
||||
// Onglet initial : vide tant que le fournisseur n'est pas charge. Des que la
|
||||
// liste des onglets visibles est connue, on cale sur l'onglet repris de
|
||||
// l'edition (history.state) s'il est encore visible, sinon le premier visible.
|
||||
const activeTab = ref('')
|
||||
watch(visibleTabKeys, (keys) => {
|
||||
if (keys.length === 0) {
|
||||
activeTab.value = ''
|
||||
return
|
||||
}
|
||||
if (!keys.includes(activeTab.value)) {
|
||||
activeTab.value = readHistoryTab(keys) ?? keys[0]
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// ── Navigation ─────────────────────────────────────────────────────────────
|
||||
function goBack(): void {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
@update:page="goToPage"
|
||||
@update:per-page="setItemsPerPage"
|
||||
>
|
||||
<!-- Categories : libelles (name) separes par une virgule (spec M2). -->
|
||||
<!-- Categories : libelles (name) separes par une virgule, aligne sur le client (ERP-193). -->
|
||||
<template #cell-categories="{ item }">
|
||||
{{ formatCategories(item) }}
|
||||
</template>
|
||||
@@ -209,7 +209,7 @@ const columns = [
|
||||
{ key: 'lastActivity', label: t('commercial.suppliers.column.lastActivity') },
|
||||
]
|
||||
|
||||
/** Libelles des categories du fournisseur, separes par une virgule (spec M2 : name). */
|
||||
/** Libelles (name) des categories du fournisseur, separes par une virgule (aligne sur le client, ERP-193). */
|
||||
function formatCategories(item: Record<string, unknown>): string {
|
||||
const categories = (item.categories as Supplier['categories']) ?? []
|
||||
return categories.map(c => c.name).join(', ')
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
icon="mdi:arrow-left-bold"
|
||||
icon-size="24"
|
||||
variant="ghost"
|
||||
:title="t('commercial.suppliers.form.back')"
|
||||
v-bind="{ ariaLabel: t('commercial.suppliers.form.back') }"
|
||||
@click="goBack"
|
||||
/>
|
||||
@@ -21,15 +22,16 @@
|
||||
v-model="main.companyName"
|
||||
:label="t('commercial.suppliers.form.main.companyName')"
|
||||
:required="true"
|
||||
:readonly="mainLocked"
|
||||
:disabled="mainLocked"
|
||||
:error="mainErrors.errors.companyName"
|
||||
:mask="FREE_TEXT_MASK"
|
||||
/>
|
||||
<MalioSelectCheckbox
|
||||
:model-value="main.categoryIris"
|
||||
:options="referentials.categories.value"
|
||||
:label="t('commercial.suppliers.form.main.categories')"
|
||||
:display-tag="true"
|
||||
:readonly="mainLocked"
|
||||
:disabled="mainLocked"
|
||||
:required="true"
|
||||
:error="mainErrors.errors.categories"
|
||||
@update:model-value="(v: (string | number)[]) => main.categoryIris = v.map(String)"
|
||||
@@ -56,20 +58,24 @@
|
||||
resize="none"
|
||||
group-class="row-span-2 pt-1 pb-1"
|
||||
text-input="h-full text-lg"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.description"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="information.competitors"
|
||||
:label="t('commercial.suppliers.form.information.competitors')"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.competitors"
|
||||
:mask="FREE_TEXT_MASK"
|
||||
/>
|
||||
<!-- Date de creation jamais dans le futur (ERP-193) : :max plafonne
|
||||
le calendrier a aujourd'hui et invalide une saisie future. -->
|
||||
<MalioDate
|
||||
v-model="information.foundedAt"
|
||||
:label="t('commercial.suppliers.form.information.foundedAt')"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:editable="true"
|
||||
:max="maxFoundedAt"
|
||||
:error="informationErrors.errors.foundedAt"
|
||||
@update:raw-value="(v: string) => information.foundedAtRaw = v"
|
||||
/>
|
||||
@@ -77,25 +83,30 @@
|
||||
v-model="information.employeesCount"
|
||||
:label="t('commercial.suppliers.form.information.employeesCount')"
|
||||
:mask="EMPLOYEES_MASK"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.employeesCount"
|
||||
/>
|
||||
<!-- CA plafonne a 999 999 999 999,99 (ERP-193) : clamp a la saisie,
|
||||
:key force le re-affichage quand on plafonne (modelValue inchange). -->
|
||||
<MalioInputAmount
|
||||
v-model="information.revenueAmount"
|
||||
:key="revenueAmountKey"
|
||||
:model-value="information.revenueAmount"
|
||||
:label="t('commercial.suppliers.form.information.revenueAmount')"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.revenueAmount"
|
||||
@update:model-value="onRevenueAmountInput"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="information.directorName"
|
||||
:label="t('commercial.suppliers.form.information.directorName')"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.directorName"
|
||||
:mask="PERSON_NAME_MASK"
|
||||
/>
|
||||
<MalioInputAmount
|
||||
v-model="information.profitAmount"
|
||||
:label="t('commercial.suppliers.form.information.profitAmount')"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.profitAmount"
|
||||
/>
|
||||
<!-- Volume previsionnel : specifique fournisseur. Champ texte
|
||||
@@ -104,15 +115,18 @@
|
||||
v-model="information.volumeForecast"
|
||||
:label="t('commercial.suppliers.form.information.volumeForecast')"
|
||||
:mask="VOLUME_FORECAST_MASK"
|
||||
:readonly="isValidated('information')"
|
||||
:disabled="isValidated('information')"
|
||||
:error="informationErrors.errors.volumeForecast"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isValidated('information')" class="mt-12 flex justify-center">
|
||||
<!-- Masque tant que le fournisseur n'est pas cree : Information etant
|
||||
l'onglet actif par defaut, son Valider ne doit pas apparaitre a cote
|
||||
de celui du formulaire principal (ERP-193). -->
|
||||
<div v-if="!isValidated('information') && supplierId !== null" class="mt-12 flex justify-center">
|
||||
<MalioButton
|
||||
variant="primary"
|
||||
:label="t('commercial.suppliers.form.submit')"
|
||||
:disabled="tabSubmitting || supplierId === null"
|
||||
:disabled="tabSubmitting"
|
||||
@click="submitInformation"
|
||||
/>
|
||||
</div>
|
||||
@@ -131,7 +145,7 @@
|
||||
:model-value="contact"
|
||||
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
|
||||
:removable="isRowRemovable(contacts, index)"
|
||||
:readonly="isValidated('contacts')"
|
||||
:disabled="isValidated('contacts')"
|
||||
:errors="contactErrors[index]"
|
||||
@update:model-value="(v) => contacts[index] = v"
|
||||
@remove="askRemoveContact(index)"
|
||||
@@ -168,7 +182,7 @@
|
||||
:contact-options="contactOptions"
|
||||
:country-options="countryOptions"
|
||||
:removable="isRowRemovable(addresses, index)"
|
||||
:readonly="isValidated('addresses')"
|
||||
:disabled="isValidated('addresses')"
|
||||
:errors="addressErrors[index]"
|
||||
@update:model-value="(v) => addresses[index] = v"
|
||||
@remove="askRemoveAddress(index)"
|
||||
@@ -202,22 +216,23 @@
|
||||
v-model="accounting.siren"
|
||||
:label="t('commercial.suppliers.form.accounting.siren')"
|
||||
:mask="SIREN_MASK"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.siren"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="accounting.accountNumber"
|
||||
:label="t('commercial.suppliers.form.accounting.accountNumber')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.accountNumber"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
/>
|
||||
<MalioSelect
|
||||
:model-value="accounting.tvaModeIri"
|
||||
:options="referentials.tvaModes.value"
|
||||
:label="t('commercial.suppliers.form.accounting.tvaMode')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.tvaMode"
|
||||
@@ -226,15 +241,16 @@
|
||||
<MalioInputText
|
||||
v-model="accounting.nTva"
|
||||
:label="t('commercial.suppliers.form.accounting.nTva')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.nTva"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
/>
|
||||
<MalioSelect
|
||||
:model-value="accounting.paymentDelayIri"
|
||||
:options="referentials.paymentDelays.value"
|
||||
:label="t('commercial.suppliers.form.accounting.paymentDelay')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.paymentDelay"
|
||||
@@ -244,7 +260,7 @@
|
||||
:model-value="accounting.paymentTypeIri"
|
||||
:options="referentials.paymentTypes.value"
|
||||
:label="t('commercial.suppliers.form.accounting.paymentType')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.paymentType"
|
||||
@@ -255,7 +271,7 @@
|
||||
:model-value="accounting.bankIri"
|
||||
:options="referentials.banks.value"
|
||||
:label="t('commercial.suppliers.form.accounting.bank')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="accountingErrors.errors.bank"
|
||||
@@ -282,23 +298,25 @@
|
||||
<MalioInputText
|
||||
v-model="rib.label"
|
||||
:label="t('commercial.suppliers.form.accounting.ribLabel')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.label"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="rib.bic"
|
||||
:label="t('commercial.suppliers.form.accounting.ribBic')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.bic"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="rib.iban"
|
||||
:label="t('commercial.suppliers.form.accounting.ribIban')"
|
||||
:readonly="accountingReadonly"
|
||||
:disabled="accountingReadonly"
|
||||
:required="isRibRequired"
|
||||
:error="ribErrors[index]?.iban"
|
||||
:mask="CODE_ALNUM_MASK"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -375,6 +393,8 @@ import {
|
||||
buildMainPayload,
|
||||
buildRibPayload,
|
||||
} from '~/modules/commercial/utils/forms/supplierEdit'
|
||||
import { clampRevenueAmount } from '~/modules/commercial/utils/forms/amountInput'
|
||||
import { todayIso } from '~/shared/utils/date'
|
||||
import {
|
||||
emptyAddress,
|
||||
emptyContact,
|
||||
@@ -385,6 +405,7 @@ import {
|
||||
} from '~/modules/commercial/types/supplierForm'
|
||||
import { extractApiErrorMessage } from '~/shared/utils/api'
|
||||
import { isRowRemovable } from '~/shared/utils/collectionRow'
|
||||
import { CODE_ALNUM_MASK, FREE_TEXT_MASK, PERSON_NAME_MASK } from '~/shared/utils/textSanitize'
|
||||
|
||||
// Masques de saisie (la normalisation finale reste serveur).
|
||||
const SIREN_MASK = '#########'
|
||||
@@ -564,6 +585,22 @@ const information = reactive({
|
||||
volumeForecast: null as string | null,
|
||||
})
|
||||
|
||||
// Borne haute de la date de creation : aujourd'hui (ERP-193, pas de date future).
|
||||
const maxFoundedAt = todayIso()
|
||||
|
||||
// CA plafonne a 999 999 999 999,99 (ERP-193). La :key force le re-affichage du
|
||||
// champ controle quand le plafonnement laisse le modelValue inchange.
|
||||
const revenueAmountKey = ref(0)
|
||||
|
||||
/** Saisie du CA : plafonne au maximum metier et re-synchronise le champ si plafonne. */
|
||||
function onRevenueAmountInput(value: string | null): void {
|
||||
const clamped = clampRevenueAmount(value)
|
||||
information.revenueAmount = clamped ?? null
|
||||
if (clamped !== value) {
|
||||
revenueAmountKey.value += 1
|
||||
}
|
||||
}
|
||||
|
||||
/** PATCH /suppliers/{id} — mode strict : uniquement les champs du groupe information. */
|
||||
async function submitInformation(): Promise<void> {
|
||||
if (supplierId.value === null || tabSubmitting.value) return
|
||||
|
||||
Reference in New Issue
Block a user