c76c447aa2
Auto Tag Develop / tag (push) Successful in 8s
Empilée sur ERP-144 (#106). ## Périmètre ERP-145 Écrans **Consultation** (lecture seule) et **Modification** (édition par onglet), peuplés depuis la **seule** réponse `GET /api/providers/{id}` (embed contacts/adresses/ribs + refs comptables — pas de N+1). ### Consultation — `pages/providers/[id]/index.vue` (`/providers/{id}`) - Ouverture par défaut sur **Contacts** ; tous champs readonly ; onglets **Contacts · Adresse · Rapports · Échanges · Comptabilité** (navigation libre). Rapports/Échanges = placeholders « À venir ». - Flèche retour → répertoire. Bouton **Modifier** (si `manage` OU `accounting.manage`). Bouton **Archiver** (Admin seul, `archive`) → modal → PATCH `{isArchived:true}` ; **Restaurer** si archivé. - Comptabilité visible seulement si `accounting.view` ; banque/RIB affichés selon le type de règlement (VIREMENT/LCR). ### Modification — `pages/providers/[id]/edit.vue` (`/providers/{id}/edit`) - Pré-rempli ; **bloc principal éditable** (Nom/Catégories/Sites, PATCH `provider:write:main` via `updateMain`) ; onglets Contact/Adresse/Comptabilité en **navigation libre**, PATCH partiel par onglet (réutilise `useProviderForm` en `editMode`). - Onglets sans permission `manage` / `accounting.manage` restent **readonly** (pas de bouton Valider / suppression). Accès réservé à `manage` OU `accounting.manage`. ### Composables / helpers - **`useProvider(id)`** : charge le détail (ld+json) + archive/restore (PATCH isArchived seul, puis rechargement). - **`useProviderForm`** étendu : `updateMain()` (PATCH principal en édition) + `editMode` (completeTab ne verrouille/avance plus). - **`providerDetail.ts`** : mapping embed → brouillons + options role-indépendantes (libellés depuis l'embed) + règles d'actions (Modifier/Archiver/Restaurer). ## Conformité - `useApi()` only ; `Malio*` only ; `usePermissions()` pour boutons/onglets ; aucun texte FR en dur ; pas d'import inter-module (règle ABSOLUE n°1). ## Vérifications - Vitest : 470/470 (16 nouveaux : mapping détail, actions par permission, updateMain + editMode). - ESLint : OK · `nuxi typecheck` : 0 erreur sur les fichiers source du ticket. - Golden path navigateur : **Consultation** (ACME) — bloc principal readonly + libellés catégories/sites résolus depuis l'embed, 5 onglets, Modifier+Archiver visibles (admin), Comptabilité readonly. **Modification** — bloc principal éditable pré-rempli (Site « 86 17 »), 3 onglets navigation libre, onglet Contact pré-rempli. Reviewed-on: #107 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
531 lines
23 KiB
Vue
531 lines
23 KiB
Vue
<template>
|
|
<div>
|
|
<!-- En-tete : retour vers le repertoire + titre. -->
|
|
<div class="flex items-center gap-3 pt-11">
|
|
<MalioButtonIcon
|
|
icon="mdi:arrow-left-bold"
|
|
icon-size="24"
|
|
variant="ghost"
|
|
v-bind="{ ariaLabel: t('technique.providers.form.back') }"
|
|
@click="goBack"
|
|
/>
|
|
<h1 class="text-[30px] font-semibold text-m-primary">{{ t('technique.providers.form.title') }}</h1>
|
|
</div>
|
|
|
|
<!-- ── Formulaire principal (pre-onglets) ─────────────────────────────
|
|
Sans validation de ce bloc, les onglets restent inaccessibles. Au
|
|
succes du POST, les champs passent en lecture seule et on bascule
|
|
automatiquement sur l'onglet Contact (PAS d'onglet Information au M3).
|
|
Selecteur de site present ici (RG-3.03, relation directe). -->
|
|
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
|
<MalioInputText
|
|
v-model="main.companyName"
|
|
:label="t('technique.providers.form.main.companyName')"
|
|
:required="true"
|
|
:readonly="mainLocked"
|
|
:error="mainErrors.errors.companyName"
|
|
/>
|
|
<MalioSelectCheckbox
|
|
:model-value="main.categoryIris"
|
|
:options="referentials.categories.value"
|
|
:label="t('technique.providers.form.main.categories')"
|
|
:display-tag="true"
|
|
:readonly="mainLocked"
|
|
:required="true"
|
|
:error="mainErrors.errors.categories"
|
|
@update:model-value="(v: (string | number)[]) => main.categoryIris = v.map(String)"
|
|
/>
|
|
<MalioSelectCheckbox
|
|
:model-value="main.siteIris"
|
|
:options="referentials.sites.value"
|
|
:label="t('technique.providers.form.main.sites')"
|
|
:display-tag="true"
|
|
:readonly="mainLocked"
|
|
:required="true"
|
|
:error="mainErrors.errors.sites"
|
|
@update:model-value="(v: (string | number)[]) => main.siteIris = v.map(String)"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="!mainLocked" class="mt-12 flex justify-center">
|
|
<MalioButton
|
|
variant="primary"
|
|
:label="t('technique.providers.form.submit')"
|
|
:disabled="mainSubmitting"
|
|
@click="submitMain"
|
|
/>
|
|
</div>
|
|
|
|
<!-- ── Onglets a validation incrementale ─────────────────────────────
|
|
Onglet Contact actif (ERP-142) ; Adresse / Comptabilite arrivent aux
|
|
tickets ERP-143 / 144 : placeholders « A venir » pour l'instant. -->
|
|
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
|
|
<!-- Onglet Contact : saisie multi-contacts (blocs ajoutables). -->
|
|
<template #contact>
|
|
<div class="mt-12 flex flex-col gap-6">
|
|
<ProviderContactBlock
|
|
v-for="(contact, index) in contacts"
|
|
:key="index"
|
|
:model-value="contact"
|
|
:removable="index > 0"
|
|
:readonly="isValidated('contact')"
|
|
:errors="contactErrors[index]"
|
|
@update:model-value="(v) => contacts[index] = v"
|
|
@remove="askRemoveContact(index)"
|
|
/>
|
|
<div v-if="!isValidated('contact')" class="flex justify-center gap-6">
|
|
<MalioButton
|
|
variant="secondary"
|
|
icon-name="mdi:add-bold"
|
|
icon-position="left"
|
|
:label="t('technique.providers.form.contact.add')"
|
|
:disabled="!canAddContact"
|
|
@click="addContact"
|
|
/>
|
|
<MalioButton
|
|
variant="primary"
|
|
:label="t('technique.providers.form.submit')"
|
|
:disabled="tabSubmitting || providerId === null"
|
|
@click="onSubmitContacts"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<!-- Onglet Adresse : saisie multi-adresses (blocs ajoutables). -->
|
|
<template #address>
|
|
<div class="mt-12 flex flex-col gap-6">
|
|
<ProviderAddressBlock
|
|
v-for="(address, index) in addresses"
|
|
:key="index"
|
|
:model-value="address"
|
|
:category-options="referentials.categories.value"
|
|
:site-options="referentials.sites.value"
|
|
:contact-options="contactOptions"
|
|
:country-options="countryOptions"
|
|
:removable="index > 0"
|
|
:readonly="isValidated('address')"
|
|
:errors="addressErrors[index]"
|
|
@update:model-value="(v) => addresses[index] = v"
|
|
@remove="askRemoveAddress(index)"
|
|
@degraded="onAddressDegraded"
|
|
/>
|
|
<div v-if="!isValidated('address')" class="flex justify-center gap-6">
|
|
<MalioButton
|
|
variant="secondary"
|
|
icon-name="mdi:add-bold"
|
|
icon-position="left"
|
|
:label="t('technique.providers.form.address.add')"
|
|
:disabled="!canAddAddress"
|
|
@click="addAddress"
|
|
/>
|
|
<MalioButton
|
|
variant="primary"
|
|
:label="t('technique.providers.form.submit')"
|
|
:disabled="tabSubmitting || providerId === null"
|
|
@click="onSubmitAddresses"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<!-- Onglet Comptabilite (present uniquement si accounting.view ; editable si manage). -->
|
|
<template v-if="canAccountingView" #accounting>
|
|
<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)]">
|
|
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
|
<MalioInputText
|
|
v-model="accounting.siren"
|
|
:label="t('technique.providers.form.accounting.siren')"
|
|
:mask="SIREN_MASK"
|
|
:readonly="accountingReadonly"
|
|
:required="true"
|
|
:error="accountingErrors.errors.siren"
|
|
/>
|
|
<MalioInputText
|
|
v-model="accounting.accountNumber"
|
|
:label="t('technique.providers.form.accounting.accountNumber')"
|
|
:readonly="accountingReadonly"
|
|
:required="true"
|
|
:error="accountingErrors.errors.accountNumber"
|
|
/>
|
|
<MalioSelect
|
|
:model-value="accounting.tvaModeIri"
|
|
:options="referentials.tvaModes.value"
|
|
:label="t('technique.providers.form.accounting.tvaMode')"
|
|
:readonly="accountingReadonly"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:error="accountingErrors.errors.tvaMode"
|
|
@update:model-value="(v: string | number | null) => accounting.tvaModeIri = v === null ? null : String(v)"
|
|
/>
|
|
<MalioInputText
|
|
v-model="accounting.nTva"
|
|
:label="t('technique.providers.form.accounting.nTva')"
|
|
:readonly="accountingReadonly"
|
|
:required="true"
|
|
:error="accountingErrors.errors.nTva"
|
|
/>
|
|
<MalioSelect
|
|
:model-value="accounting.paymentDelayIri"
|
|
:options="referentials.paymentDelays.value"
|
|
:label="t('technique.providers.form.accounting.paymentDelay')"
|
|
:readonly="accountingReadonly"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:error="accountingErrors.errors.paymentDelay"
|
|
@update:model-value="(v: string | number | null) => accounting.paymentDelayIri = v === null ? null : String(v)"
|
|
/>
|
|
<MalioSelect
|
|
:model-value="accounting.paymentTypeIri"
|
|
:options="referentials.paymentTypes.value"
|
|
:label="t('technique.providers.form.accounting.paymentType')"
|
|
:readonly="accountingReadonly"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:error="accountingErrors.errors.paymentType"
|
|
@update:model-value="onPaymentTypeChange"
|
|
/>
|
|
<!-- Banque : visible et obligatoire seulement si VIREMENT (RG-3.07). -->
|
|
<MalioSelect
|
|
v-if="isBankRequired"
|
|
:model-value="accounting.bankIri"
|
|
:options="referentials.banks.value"
|
|
:label="t('technique.providers.form.accounting.bank')"
|
|
:readonly="accountingReadonly"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:error="accountingErrors.errors.bank"
|
|
@update:model-value="(v: string | number | null) => accounting.bankIri = v === null ? null : String(v)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-3.08). -->
|
|
<div
|
|
v-for="(rib, index) in visibleRibs"
|
|
:key="index"
|
|
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
|
>
|
|
<MalioButtonIcon
|
|
v-if="!accountingReadonly && visibleRibs.length > 1"
|
|
icon="mdi:delete-outline"
|
|
variant="ghost"
|
|
button-class="absolute top-3 right-3"
|
|
v-bind="{ ariaLabel: t('technique.providers.form.accounting.removeRib') }"
|
|
@click="askRemoveRib(index)"
|
|
/>
|
|
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
|
<MalioInputText
|
|
v-model="rib.label"
|
|
:label="t('technique.providers.form.accounting.ribLabel')"
|
|
:readonly="accountingReadonly"
|
|
:required="true"
|
|
:error="ribErrors[index]?.label"
|
|
/>
|
|
<MalioInputText
|
|
v-model="rib.bic"
|
|
:label="t('technique.providers.form.accounting.ribBic')"
|
|
:readonly="accountingReadonly"
|
|
:required="true"
|
|
:error="ribErrors[index]?.bic"
|
|
/>
|
|
<MalioInputText
|
|
v-model="rib.iban"
|
|
:label="t('technique.providers.form.accounting.ribIban')"
|
|
:readonly="accountingReadonly"
|
|
:required="true"
|
|
:error="ribErrors[index]?.iban"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!accountingReadonly" class="flex justify-center gap-6">
|
|
<MalioButton
|
|
v-if="isRibRequired"
|
|
variant="secondary"
|
|
icon-name="mdi:add-bold"
|
|
icon-position="left"
|
|
:label="t('technique.providers.form.accounting.addRib')"
|
|
:disabled="!canAddRib"
|
|
@click="addRib"
|
|
/>
|
|
<MalioButton
|
|
variant="primary"
|
|
:label="t('technique.providers.form.submit')"
|
|
:disabled="tabSubmitting || providerId === null"
|
|
@click="onSubmitAccounting"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</MalioTabList>
|
|
|
|
<!-- Modal de confirmation generique (suppression d'un bloc contact). -->
|
|
<MalioModal v-model="confirmModal.open" modal-class="max-w-md">
|
|
<template #header>
|
|
<h2 class="text-[24px] font-bold">{{ t('technique.providers.form.confirmDelete.title') }}</h2>
|
|
</template>
|
|
<p>{{ confirmModal.message }}</p>
|
|
<template #footer>
|
|
<MalioButton
|
|
variant="secondary"
|
|
button-class="flex-1"
|
|
:label="t('technique.providers.form.confirmDelete.cancel')"
|
|
@click="confirmModal.open = false"
|
|
/>
|
|
<MalioButton
|
|
variant="danger"
|
|
button-class="flex-1"
|
|
:label="t('technique.providers.form.confirmDelete.confirm')"
|
|
@click="runConfirm"
|
|
/>
|
|
</template>
|
|
</MalioModal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
import { useProviderReferentials, type RefOption } from '~/modules/technique/composables/useProviderReferentials'
|
|
import { useProviderForm } from '~/modules/technique/composables/useProviderForm'
|
|
import {
|
|
isBankRequiredForPaymentType,
|
|
isRibRequiredForPaymentType,
|
|
} from '~/modules/technique/utils/forms/providerAccounting'
|
|
import { extractApiErrorMessage } from '~/shared/utils/api'
|
|
|
|
// Masque SIREN : 9 chiffres (la normalisation finale reste serveur).
|
|
const SIREN_MASK = '#########'
|
|
|
|
const { t } = useI18n()
|
|
const router = useRouter()
|
|
const toast = useToast()
|
|
const { can } = usePermissions()
|
|
|
|
useHead({ title: t('technique.providers.form.title') })
|
|
|
|
// Gating de la route : la creation est reservee a `manage` (POST /providers garde
|
|
// manage seul — Compta ne cree pas). Compta (accounting seul) et Usine sont
|
|
// rediriges vers le repertoire.
|
|
if (!can('technique.providers.manage')) {
|
|
await navigateTo('/providers')
|
|
}
|
|
|
|
const referentials = useProviderReferentials()
|
|
|
|
const {
|
|
main,
|
|
providerId,
|
|
mainLocked,
|
|
mainSubmitting,
|
|
mainErrors,
|
|
canAccountingView,
|
|
tabKeys,
|
|
activeTab,
|
|
unlockedIndex,
|
|
submitMain,
|
|
tabSubmitting,
|
|
isValidated,
|
|
contacts,
|
|
contactErrors,
|
|
canAddContact,
|
|
addContact,
|
|
removeContact,
|
|
submitContacts,
|
|
addresses,
|
|
addressErrors,
|
|
canAddAddress,
|
|
addAddress,
|
|
removeAddress,
|
|
submitAddresses,
|
|
accounting,
|
|
ribs,
|
|
accountingErrors,
|
|
ribErrors,
|
|
accountingReadonly,
|
|
setPaymentType,
|
|
canAddRib,
|
|
addRib,
|
|
removeRib,
|
|
submitAccounting,
|
|
} = useProviderForm()
|
|
|
|
/** Retour vers le repertoire prestataires (fleche d'en-tete). */
|
|
function goBack(): void {
|
|
router.push('/providers')
|
|
}
|
|
|
|
/**
|
|
* Message d'erreur a afficher dans un toast a partir d'une erreur d'API. Retourne
|
|
* TOUJOURS une chaine (le composant de toast plante sur `undefined`).
|
|
*/
|
|
function apiErrorMessage(error: unknown): string {
|
|
const data = (error as { response?: { _data?: unknown } })?.response?._data
|
|
return extractApiErrorMessage(data) || t('technique.providers.toast.error')
|
|
}
|
|
|
|
// Dernier onglet REMPLISSABLE par le role : tabKeys exclut deja la Comptabilite
|
|
// si l'user n'a pas accounting.view. Sa validation cloture l'ajout (redirection).
|
|
const lastFillableTab = computed(() => tabKeys.value[tabKeys.value.length - 1])
|
|
|
|
/**
|
|
* Apres validation d'un onglet (creation) : si c'est le dernier onglet du role,
|
|
* l'ajout est termine -> toast final + retour au repertoire (miroir M1/M2) ; sinon
|
|
* toast de mise a jour (l'onglet suivant a deja ete deverrouille par completeTab).
|
|
*/
|
|
function onTabSaved(key: string): void {
|
|
if (key === lastFillableTab.value) {
|
|
toast.success({ title: t('technique.providers.toast.addComplete') })
|
|
router.push('/providers')
|
|
return
|
|
}
|
|
toast.success({ title: t('technique.providers.toast.updateSuccess') })
|
|
}
|
|
|
|
// ── Onglet Contact ──────────────────────────────────────────────────────────
|
|
/** Valide l'onglet Contact ; redirige si c'est le dernier onglet du role. */
|
|
async function onSubmitContacts(): Promise<void> {
|
|
const ok = await submitContacts(error => toast.error({
|
|
title: t('technique.providers.toast.error'),
|
|
message: apiErrorMessage(error),
|
|
}))
|
|
if (ok) {
|
|
onTabSaved('contact')
|
|
}
|
|
}
|
|
|
|
function askRemoveContact(index: number): void {
|
|
askConfirm(t('technique.providers.form.confirmDelete.contact'), () => removeContact(index))
|
|
}
|
|
|
|
// ── Onglet Adresse ────────────────────────────────────────────────────────────
|
|
// Contacts deja persistes (IRI non nul), rattachables a une adresse (M2M). Le
|
|
// libelle reprend le nom complet, a defaut l'email.
|
|
const contactOptions = computed<RefOption[]>(() =>
|
|
contacts.value
|
|
.filter(c => c.iri !== null)
|
|
.map(c => ({
|
|
value: c.iri as string,
|
|
label: [c.firstName, c.lastName].filter(Boolean).join(' ') || (c.email ?? ''),
|
|
})),
|
|
)
|
|
|
|
// Pays : France garantie en tete meme si /countries echoue (resilience ERP-102),
|
|
// pour rester preselectionnable par defaut sur chaque adresse.
|
|
const countryOptions = computed<RefOption[]>(() => {
|
|
const list = referentials.countries.value
|
|
return list.some(c => c.value === 'France')
|
|
? list
|
|
: [{ value: 'France', label: 'France' }, ...list]
|
|
})
|
|
|
|
const addressDegradedNotified = ref(false)
|
|
|
|
/** Avertit une seule fois quand l'autocompletion d'adresse bascule en degrade (RG-3.06). */
|
|
function onAddressDegraded(): void {
|
|
if (addressDegradedNotified.value) {
|
|
return
|
|
}
|
|
addressDegradedNotified.value = true
|
|
toast.warning({
|
|
title: t('technique.providers.toast.error'),
|
|
message: t('technique.providers.form.address.degraded'),
|
|
})
|
|
}
|
|
|
|
/** Valide l'onglet Adresse ; redirige si c'est le dernier onglet du role. */
|
|
async function onSubmitAddresses(): Promise<void> {
|
|
const ok = await submitAddresses(error => toast.error({
|
|
title: t('technique.providers.toast.error'),
|
|
message: apiErrorMessage(error),
|
|
}))
|
|
if (ok) {
|
|
onTabSaved('address')
|
|
}
|
|
}
|
|
|
|
function askRemoveAddress(index: number): void {
|
|
askConfirm(t('technique.providers.form.confirmDelete.address'), () => removeAddress(index))
|
|
}
|
|
|
|
// ── Onglet Comptabilite ───────────────────────────────────────────────────────
|
|
// Code stable du type de reglement selectionne (pour RG-3.07 / RG-3.08).
|
|
const selectedPaymentTypeCode = computed(() =>
|
|
referentials.paymentTypes.value.find(p => p.value === accounting.paymentTypeIri)?.code ?? null,
|
|
)
|
|
const isBankRequired = computed(() => isBankRequiredForPaymentType(selectedPaymentTypeCode.value))
|
|
const isRibRequired = computed(() => isRibRequiredForPaymentType(selectedPaymentTypeCode.value))
|
|
|
|
// Les blocs RIB ne sont affiches que pour une LCR (RG-3.08).
|
|
const visibleRibs = computed(() => isRibRequired.value ? ribs.value : [])
|
|
|
|
/** Changement de type de reglement : propage les RG inter-champs (banque / RIB). */
|
|
function onPaymentTypeChange(value: string | number | null): void {
|
|
const iri = value === null ? null : String(value)
|
|
const code = referentials.paymentTypes.value.find(p => p.value === iri)?.code ?? null
|
|
setPaymentType(iri, isBankRequiredForPaymentType(code), isRibRequiredForPaymentType(code))
|
|
}
|
|
|
|
function askRemoveRib(index: number): void {
|
|
askConfirm(t('technique.providers.form.confirmDelete.rib'), () => removeRib(index))
|
|
}
|
|
|
|
/** Valide l'onglet Comptabilite ; redirige si c'est le dernier onglet du role. */
|
|
async function onSubmitAccounting(): Promise<void> {
|
|
const ok = await submitAccounting(
|
|
isBankRequired.value,
|
|
isRibRequired.value,
|
|
error => toast.error({
|
|
title: t('technique.providers.toast.error'),
|
|
message: apiErrorMessage(error),
|
|
}),
|
|
)
|
|
if (ok) {
|
|
onTabSaved('accounting')
|
|
}
|
|
}
|
|
|
|
// ── Modal de confirmation generique ─────────────────────────────────────────
|
|
const confirmModal = reactive({
|
|
open: false,
|
|
message: '',
|
|
action: null as null | (() => void),
|
|
})
|
|
|
|
function askConfirm(message: string, action: () => void): void {
|
|
confirmModal.message = message
|
|
confirmModal.action = action
|
|
confirmModal.open = true
|
|
}
|
|
|
|
function runConfirm(): void {
|
|
confirmModal.action?.()
|
|
confirmModal.action = null
|
|
confirmModal.open = false
|
|
}
|
|
|
|
// Icone (Iconify) affichee dans l'onglet, par cle.
|
|
const TAB_ICONS: Record<string, string> = {
|
|
contact: 'mdi:account-box-plus-outline',
|
|
address: 'mdi:map-marker-outline',
|
|
accounting: 'mdi:bank-circle-outline',
|
|
}
|
|
|
|
// Onglets desactives tant que le formulaire principal n'est pas valide
|
|
// (unlockedIndex = -1 au depart) ; deverrouillage progressif ensuite.
|
|
const tabs = computed(() => tabKeys.value.map((key, index) => ({
|
|
key,
|
|
label: t(`technique.providers.tab.${key}`),
|
|
icon: TAB_ICONS[key],
|
|
disabled: index > unlockedIndex.value,
|
|
})))
|
|
|
|
onMounted(() => {
|
|
// Echec du chargement des referentiels non bloquant : les selects restent vides.
|
|
referentials.loadMain().catch(() => {})
|
|
// Referentiels comptables charges uniquement si l'onglet est accessible.
|
|
if (canAccountingView.value) {
|
|
referentials.loadAccounting().catch(() => {})
|
|
}
|
|
})
|
|
</script>
|