324 lines
15 KiB
Vue
324 lines
15 KiB
Vue
<template>
|
|
<div>
|
|
<!-- En-tete : retour repertoire + nom du prestataire + actions. -->
|
|
<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.consultation.back') }"
|
|
@click="goBack"
|
|
/>
|
|
<h1 class="text-[30px] font-semibold text-m-primary">{{ headerTitle }}</h1>
|
|
|
|
<div class="ml-auto flex items-center gap-12">
|
|
<MalioButton
|
|
v-if="canEdit"
|
|
variant="secondary"
|
|
icon-name="mdi:pencil-outline"
|
|
icon-position="left"
|
|
:label="t('technique.providers.action.edit')"
|
|
@click="goEdit"
|
|
/>
|
|
<MalioButton
|
|
v-if="showArchive"
|
|
variant="danger"
|
|
icon-name="mdi:archive-arrow-down-outline"
|
|
icon-position="left"
|
|
:label="t('technique.providers.action.archive')"
|
|
@click="askToggleArchive"
|
|
/>
|
|
<MalioButton
|
|
v-if="showRestore"
|
|
variant="secondary"
|
|
icon-name="mdi:archive-arrow-up-outline"
|
|
icon-position="left"
|
|
:label="t('technique.providers.action.restore')"
|
|
@click="askToggleArchive"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Etats de chargement / introuvable. -->
|
|
<p v-if="loading" class="mt-12 text-center text-black/60">{{ t('technique.providers.consultation.loading') }}</p>
|
|
<p v-else-if="error" class="mt-12 text-center text-m-danger">{{ t('technique.providers.consultation.notFound') }}</p>
|
|
|
|
<template v-else-if="provider">
|
|
<!-- ── Bloc principal (lecture seule) ─────────────────────────────── -->
|
|
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
|
<MalioInputText
|
|
:model-value="provider.companyName"
|
|
:label="t('technique.providers.form.main.companyName')"
|
|
disabled
|
|
/>
|
|
<MalioSelectCheckbox
|
|
:model-value="mainCategoryIris"
|
|
:options="mainCategoryOptions"
|
|
:label="t('technique.providers.form.main.categories')"
|
|
:display-tag="true"
|
|
disabled
|
|
/>
|
|
<MalioSelectCheckbox
|
|
:model-value="mainSiteIris"
|
|
:options="mainSiteOptions"
|
|
:label="t('technique.providers.form.main.sites')"
|
|
:display-tag="true"
|
|
disabled
|
|
/>
|
|
</div>
|
|
|
|
<!-- ── Onglets (navigation libre, tout en lecture seule) ──────────── -->
|
|
<!-- ERP-193 : barre masquee s'il ne reste aucun onglet non vide. -->
|
|
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
|
<!-- Onglet Contacts -->
|
|
<template #contacts>
|
|
<div class="mt-12 flex flex-col gap-6">
|
|
<ProviderContactBlock
|
|
v-for="(contact, index) in contacts"
|
|
:key="index"
|
|
:model-value="contact"
|
|
disabled
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Onglet Adresse -->
|
|
<template #address>
|
|
<div class="mt-12 flex flex-col gap-6">
|
|
<ProviderAddressBlock
|
|
v-for="(view, index) in addressViews"
|
|
:key="index"
|
|
:model-value="view.draft"
|
|
:category-options="view.categoryOptions"
|
|
:site-options="view.siteOptions"
|
|
:contact-options="contactOptions"
|
|
:country-options="countryOptionsFor(view.draft.country)"
|
|
disabled
|
|
/>
|
|
</div>
|
|
</template>
|
|
<!-- ERP-193 : les onglets « a venir » (Rapports / Echanges) ne sont
|
|
plus rendus en consultation (masquage des onglets vides). -->
|
|
|
|
<!-- Onglet Comptabilite (present uniquement si accounting.view). -->
|
|
<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 :model-value="accounting.siren" :label="t('technique.providers.form.accounting.siren')" disabled />
|
|
<MalioInputText :model-value="accounting.accountNumber" :label="t('technique.providers.form.accounting.accountNumber')" disabled />
|
|
<MalioSelect :model-value="accounting.tvaModeIri" :options="tvaModeOptions" :label="t('technique.providers.form.accounting.tvaMode')" disabled empty-option-label="" />
|
|
<MalioInputText :model-value="accounting.nTva" :label="t('technique.providers.form.accounting.nTva')" disabled />
|
|
<MalioSelect :model-value="accounting.paymentDelayIri" :options="paymentDelayOptions" :label="t('technique.providers.form.accounting.paymentDelay')" disabled empty-option-label="" />
|
|
<MalioSelect :model-value="accounting.paymentTypeIri" :options="paymentTypeOptions" :label="t('technique.providers.form.accounting.paymentType')" disabled empty-option-label="" />
|
|
<MalioSelect v-if="isBankRequired" :model-value="accounting.bankIri" :options="bankOptions" :label="t('technique.providers.form.accounting.bank')" disabled empty-option-label="" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Blocs RIB (uniquement si type de reglement = LCR). -->
|
|
<div
|
|
v-for="(rib, index) in visibleRibs"
|
|
:key="index"
|
|
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 :model-value="rib.label" :label="t('technique.providers.form.accounting.ribLabel')" disabled />
|
|
<MalioInputText :model-value="rib.bic" :label="t('technique.providers.form.accounting.ribBic')" disabled />
|
|
<MalioInputText :model-value="rib.iban" :label="t('technique.providers.form.accounting.ribIban')" disabled />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</MalioTabList>
|
|
</template>
|
|
|
|
<!-- Modal de confirmation archivage / restauration. -->
|
|
<MalioModal v-model="confirmArchive.open" modal-class="max-w-md">
|
|
<template #header>
|
|
<h2 class="text-[24px] font-bold">{{ confirmArchive.title }}</h2>
|
|
</template>
|
|
<p>{{ confirmArchive.message }}</p>
|
|
<template #footer>
|
|
<MalioButton
|
|
variant="secondary"
|
|
button-class="flex-1"
|
|
:label="t('technique.providers.form.confirmDelete.cancel')"
|
|
@click="confirmArchive.open = false"
|
|
/>
|
|
<MalioButton
|
|
variant="danger"
|
|
button-class="flex-1"
|
|
:label="confirmArchive.confirmLabel"
|
|
@click="runToggleArchive"
|
|
/>
|
|
</template>
|
|
</MalioModal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
import { useProvider } from '~/modules/technique/composables/useProvider'
|
|
import {
|
|
canEditProvider,
|
|
categoryOptionsOf,
|
|
contactOptionsOf,
|
|
irisOf,
|
|
mapAccountingDraft,
|
|
mapAddressToDraft,
|
|
mapContactToDraft,
|
|
mapRibToDraft,
|
|
paymentTypeCodeOf,
|
|
providerConsultationVisibleTabs,
|
|
referentialOptionOf,
|
|
showArchiveAction,
|
|
showRestoreAction,
|
|
siteOptionsOf,
|
|
} from '~/modules/technique/utils/forms/providerDetail'
|
|
import { isBankRequiredForPaymentType, isRibRequiredForPaymentType } from '~/modules/technique/utils/forms/providerAccounting'
|
|
import { emptyProviderAddress, emptyProviderContact } from '~/modules/technique/types/providerForm'
|
|
|
|
const { t } = useI18n()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const toast = useToast()
|
|
const { can, canAny } = usePermissions()
|
|
|
|
const providerId = route.params.id as string
|
|
const { provider, loading, error, load, archive, restore } = useProvider(providerId)
|
|
|
|
const canAccountingView = computed(() => can('technique.providers.accounting.view'))
|
|
const canEdit = computed(() => canEditProvider(canAny))
|
|
const isArchived = computed(() => provider.value?.isArchived ?? false)
|
|
const showArchive = computed(() => showArchiveAction(can, isArchived.value))
|
|
const showRestore = computed(() => showRestoreAction(can, isArchived.value))
|
|
|
|
const headerTitle = computed(() => provider.value?.companyName || t('technique.providers.consultation.title'))
|
|
useHead({ title: t('technique.providers.consultation.title') })
|
|
|
|
// ── Onglets (ordre spec : Contacts · Adresse · Rapports · Échanges · Comptabilité) ──
|
|
const TAB_ICONS: Record<string, string> = {
|
|
contacts: 'mdi:account-box-plus-outline',
|
|
address: 'mdi:map-marker-outline',
|
|
reports: 'mdi:file-chart-outline',
|
|
exchanges: 'mdi:swap-horizontal',
|
|
accounting: 'mdi:bank-circle-outline',
|
|
}
|
|
// ERP-193 (retour metier) : on masque les coquilles (Rapports / Echanges) ET
|
|
// tout onglet de donnees vide. La liste depend donc du payload charge.
|
|
const visibleTabKeys = computed(() => providerConsultationVisibleTabs(provider.value, {
|
|
canAccountingView: canAccountingView.value,
|
|
}))
|
|
const tabs = computed(() => visibleTabKeys.value.map(
|
|
key => ({ key, label: t(`technique.providers.tab.${key}`), icon: TAB_ICONS[key] }),
|
|
))
|
|
|
|
// Onglet initial : vide tant que le prestataire n'est pas charge, puis premier
|
|
// onglet visible. Un watcher recale si l'onglet courant disparait.
|
|
const activeTab = ref('')
|
|
watch(visibleTabKeys, (keys) => {
|
|
if (keys.length === 0) {
|
|
activeTab.value = ''
|
|
return
|
|
}
|
|
if (!keys.includes(activeTab.value)) {
|
|
activeTab.value = keys[0]
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// ── Donnees mappees depuis la SEULE reponse detail ─────────────────────────────
|
|
const mainCategoryIris = computed(() => irisOf(provider.value?.categories))
|
|
const mainSiteIris = computed(() => irisOf(provider.value?.sites))
|
|
const mainCategoryOptions = computed(() => categoryOptionsOf(provider.value?.categories))
|
|
const mainSiteOptions = computed(() => siteOptionsOf(provider.value?.sites))
|
|
|
|
// Au moins un bloc affiche meme sans donnee (bloc vide en lecture seule, comme
|
|
// l'onglet Comptabilite et les autres modules — pas de message « Aucun … »).
|
|
const contacts = computed(() => {
|
|
const list = (provider.value?.contacts ?? []).map(mapContactToDraft)
|
|
return list.length > 0 ? list : [emptyProviderContact()]
|
|
})
|
|
// Contacts rattachables (pour resoudre les libelles des contacts lies aux adresses).
|
|
const contactOptions = computed(() => contactOptionsOf(provider.value?.contacts))
|
|
|
|
// Vue par adresse : brouillon + options propres a l'adresse (sites/categories embarques).
|
|
const addressViews = computed(() => {
|
|
const views = (provider.value?.addresses ?? []).map(address => ({
|
|
draft: mapAddressToDraft(address),
|
|
siteOptions: siteOptionsOf(address.sites),
|
|
categoryOptions: categoryOptionsOf(address.categories),
|
|
}))
|
|
return views.length > 0
|
|
? views
|
|
: [{ draft: emptyProviderAddress(), siteOptions: [], categoryOptions: [] }]
|
|
})
|
|
|
|
/** Pays : une seule option (la valeur courante), suffisant pour l'affichage readonly. */
|
|
function countryOptionsFor(country: string): { value: string, label: string }[] {
|
|
return country ? [{ value: country, label: country }] : []
|
|
}
|
|
|
|
// ── Comptabilite (presente uniquement si accounting.view) ──────────────────────
|
|
const accounting = computed(() => mapAccountingDraft(provider.value ?? { id: 0, '@id': '' }))
|
|
const paymentTypeCode = computed(() => paymentTypeCodeOf(provider.value?.paymentType))
|
|
const isBankRequired = computed(() => isBankRequiredForPaymentType(paymentTypeCode.value))
|
|
const isRibRequired = computed(() => isRibRequiredForPaymentType(paymentTypeCode.value))
|
|
const visibleRibs = computed(() => isRibRequired.value ? (provider.value?.ribs ?? []).map(mapRibToDraft) : [])
|
|
|
|
// Options « une entree » construites depuis l'embed (libelles role-independants).
|
|
const tvaModeOptions = computed(() => referentialOptionOf(provider.value?.tvaMode))
|
|
const paymentDelayOptions = computed(() => referentialOptionOf(provider.value?.paymentDelay))
|
|
const paymentTypeOptions = computed(() => referentialOptionOf(provider.value?.paymentType))
|
|
const bankOptions = computed(() => referentialOptionOf(provider.value?.bank))
|
|
|
|
// ── Navigation / actions ───────────────────────────────────────────────────────
|
|
function goBack(): void {
|
|
router.push('/providers')
|
|
}
|
|
|
|
function goEdit(): void {
|
|
router.push(`/providers/${providerId}/edit`)
|
|
}
|
|
|
|
// ── Archivage / restauration ───────────────────────────────────────────────────
|
|
const confirmArchive = reactive({
|
|
open: false,
|
|
title: '',
|
|
message: '',
|
|
confirmLabel: '',
|
|
})
|
|
|
|
function askToggleArchive(): void {
|
|
const archiving = !isArchived.value
|
|
confirmArchive.title = archiving
|
|
? t('technique.providers.action.archive')
|
|
: t('technique.providers.action.restore')
|
|
confirmArchive.message = archiving
|
|
? t('technique.providers.consultation.confirmArchive')
|
|
: t('technique.providers.consultation.confirmRestore')
|
|
confirmArchive.confirmLabel = archiving
|
|
? t('technique.providers.action.archive')
|
|
: t('technique.providers.action.restore')
|
|
confirmArchive.open = true
|
|
}
|
|
|
|
async function runToggleArchive(): Promise<void> {
|
|
const archiving = !isArchived.value
|
|
confirmArchive.open = false
|
|
try {
|
|
await (archiving ? archive() : restore())
|
|
toast.success({
|
|
title: archiving
|
|
? t('technique.providers.toast.archiveSuccess')
|
|
: t('technique.providers.toast.restoreSuccess'),
|
|
})
|
|
}
|
|
catch {
|
|
// 409 a la restauration (homonyme actif) ou autre : toast generique.
|
|
toast.error({ title: t('technique.providers.toast.error') })
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|