833d992ebb
- retrait de la regle « prenom OU nom » sur le bloc Contact : garde CarrierContactProcessor::validateName supprimee, CHECK chk_carrier_contact_name droppe (migration Version20260619120000), commentaires SQL/catalogue alignes - front : gating « + Nouveau contact » sur bloc non vide (au lieu de « nomme »), onglet Contact vide finalisable sans creer de contact - Prix accessible des la validation des Adresses (Contacts optionnel ne bloque plus) - consultation <-> edition : on retombe sur le meme onglet via ?tab=
545 lines
26 KiB
Vue
545 lines
26 KiB
Vue
<template>
|
|
<div>
|
|
<!-- En-tête : retour répertoire + nom + 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('transport.carriers.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('transport.carriers.action.edit')"
|
|
@click="goEdit"
|
|
/>
|
|
<MalioButton
|
|
v-if="showArchive"
|
|
variant="danger"
|
|
icon-name="mdi:archive-arrow-down-outline"
|
|
icon-position="left"
|
|
:label="t('transport.carriers.action.archive')"
|
|
@click="askToggleArchive"
|
|
/>
|
|
<MalioButton
|
|
v-if="showRestore"
|
|
variant="secondary"
|
|
icon-name="mdi:archive-arrow-up-outline"
|
|
icon-position="left"
|
|
:label="t('transport.carriers.action.restore')"
|
|
@click="askToggleArchive"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<p v-if="loading" class="mt-12 text-center text-black/60">{{ t('transport.carriers.consultation.loading') }}</p>
|
|
<p v-else-if="error" class="mt-12 text-center text-m-danger">{{ t('transport.carriers.consultation.notFound') }}</p>
|
|
|
|
<template v-else-if="carrier">
|
|
<!-- ── Bloc principal (lecture seule) — même disposition que l'ajout ── -->
|
|
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
|
<MalioInputText :model-value="main.name" :label="t('transport.carriers.form.main.name')" disabled />
|
|
|
|
<!-- Cas LIOT : le champ immatriculations occupe les colonnes restantes
|
|
de la ligne (3 en xl, 2 sinon), comme à l'ajout / la modification. -->
|
|
<div v-if="isLiot" class="col-span-2 xl:col-span-3">
|
|
<MalioInputText
|
|
:model-value="main.liotPlates"
|
|
:label="t('transport.carriers.form.main.liotPlates')"
|
|
disabled
|
|
/>
|
|
</div>
|
|
|
|
<!-- Cas standard : certification + décharge (col 3 réservée) + affrètement (col 4). -->
|
|
<template v-if="!isLiot">
|
|
<MalioInputText
|
|
:model-value="certificationLabel"
|
|
:label="t('transport.carriers.form.main.certificationType')"
|
|
disabled
|
|
/>
|
|
|
|
<!-- Colonne 3 réservée à la décharge (si AUTRE), sinon vide (xl). -->
|
|
<MalioInputText
|
|
v-if="main.certificationType === 'AUTRE'"
|
|
:model-value="dischargeLabel"
|
|
:label="t('transport.carriers.form.main.discharge')"
|
|
disabled
|
|
/>
|
|
<div v-else class="hidden xl:block"></div>
|
|
|
|
<!-- Affréter : colonne 4, centré (h-12) comme à l'ajout. -->
|
|
<div class="flex h-12 items-center">
|
|
<MalioCheckbox
|
|
id="carrier-view-chartered"
|
|
:label="t('transport.carriers.form.main.isChartered')"
|
|
:model-value="main.isChartered"
|
|
disabled
|
|
:reserve-message-space="false"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Champs d'affrètement (ligne 2) si affrété. -->
|
|
<template v-if="main.isChartered">
|
|
<MalioInputText :model-value="indexationDisplay" :label="t('transport.carriers.form.main.indexationRate')" disabled />
|
|
<!-- Contenant : radios désactivés (lecture seule), aligné sur l'ajout / la modif. -->
|
|
<div>
|
|
<div class="flex h-12 items-center gap-4">
|
|
<MalioRadioButton
|
|
:model-value="main.containerType"
|
|
name="carrier-view-container"
|
|
value="BENNE"
|
|
:label="t('transport.carriers.containerType.BENNE')"
|
|
disabled
|
|
group-class="mt-0"
|
|
/>
|
|
<MalioRadioButton
|
|
:model-value="main.containerType"
|
|
name="carrier-view-container"
|
|
value="FOND_MOUVANT"
|
|
:label="t('transport.carriers.containerType.FOND_MOUVANT')"
|
|
disabled
|
|
group-class="mt-0"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<MalioInputText :model-value="main.volumeM3" :label="t('transport.carriers.form.main.volumeM3')" disabled />
|
|
</template>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- ── Onglets (Adresses · Contacts · Prix) — ouvre sur Adresses ──── -->
|
|
<!-- ERP-193 : barre masquee s'il ne reste aucun onglet non vide. -->
|
|
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" class="mt-[60px]">
|
|
<template #addresses>
|
|
<div class="mt-12 flex flex-col gap-6">
|
|
<!-- Adresse UNIQUE (ERP-172). -->
|
|
<CarrierAddressBlock
|
|
:model-value="address"
|
|
:country-options="countryOptionsFor(address.country)"
|
|
disabled
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<template #contacts>
|
|
<div class="mt-12 flex flex-col gap-6">
|
|
<CarrierContactBlock
|
|
v-for="(contact, index) in contacts"
|
|
:key="index"
|
|
:model-value="contact"
|
|
disabled
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Prix : tableau présentationnel regroupé par contenant + export. -->
|
|
<template #prices>
|
|
<div class="mt-12 flex flex-col gap-6">
|
|
<!-- Police / bordures / radius alignés sur MalioDataTable (header
|
|
16px, corps 14px). 1re colonne « Contenant » : libellé du
|
|
groupe (Fond Mouvant / Benne) fusionné en rowspan ; séparateur
|
|
épais entre les deux groupes. -->
|
|
<table class="w-full table-fixed border-separate border-spacing-0 overflow-hidden rounded-malio border border-black text-left text-black">
|
|
<!-- Répartition (table-fixed) : « Transport » étroit (libellé
|
|
court Benne / Fond mouvant) ; Fournisseurs/Clients et
|
|
Adresse livraisons larges ; Forfait / Tonne / Indexation
|
|
/ État réduits. -->
|
|
<colgroup>
|
|
<col class="w-[120px]" />
|
|
<col class="w-[20%]" />
|
|
<col class="w-[24%]" />
|
|
<col class="w-[11%]" />
|
|
<col class="w-[9%]" />
|
|
<col class="w-[9%]" />
|
|
<col class="w-[9%]" />
|
|
<col class="w-[9%]" />
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<!-- En-tête centré pour matcher les cellules fusionnées Benne / Fond mouvant. -->
|
|
<th class="border-b border-r border-black bg-m-surface px-3 py-3 text-center align-middle text-[16px] font-semibold">{{ t('transport.carriers.consultation.price.group') }}</th>
|
|
<th class="border-b border-black bg-m-surface px-3 py-3 align-middle text-[16px] font-semibold">{{ t('transport.carriers.consultation.price.carrier') }}</th>
|
|
<th class="border-b border-black bg-m-surface px-3 py-3 align-middle text-[16px] font-semibold">{{ t('transport.carriers.consultation.price.delivery') }}</th>
|
|
<th class="border-b border-black bg-m-surface px-3 py-3 align-middle text-[16px] font-semibold">{{ t('transport.carriers.consultation.price.aproOrSite') }}</th>
|
|
<th class="border-b border-black bg-m-surface px-3 py-3 align-middle text-[16px] font-semibold">{{ t('transport.carriers.consultation.price.forfait') }}</th>
|
|
<th class="border-b border-black bg-m-surface px-3 py-3 align-middle text-[16px] font-semibold">{{ t('transport.carriers.consultation.price.tonne') }}</th>
|
|
<th class="border-b border-black bg-m-surface px-3 py-3 align-middle text-[16px] font-semibold">{{ t('transport.carriers.consultation.price.indexation') }}</th>
|
|
<th class="border-b border-black bg-m-surface px-3 py-3 align-middle text-[16px] font-semibold">{{ t('transport.carriers.consultation.price.state') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template v-for="(group, gi) in priceGroups" :key="gi">
|
|
<tr
|
|
v-for="(row, i) in group.rows"
|
|
:key="`${gi}-${i}`"
|
|
>
|
|
<!-- Contenant par ligne (plus de cellule fusionnée) ; séparateur
|
|
à droite, comme l'ancienne colonne de groupe. -->
|
|
<td class="border-r border-black px-3 py-4 text-center align-middle text-[14px] font-medium" :class="dataBorder(gi, i)">{{ row.transport }}</td>
|
|
<td class="px-3 py-4 text-[14px]" :class="dataBorder(gi, i)">{{ row.party }}</td>
|
|
<td class="px-3 py-4 text-[14px]" :class="dataBorder(gi, i)">{{ row.delivery }}</td>
|
|
<td class="px-3 py-4 text-[14px]" :class="dataBorder(gi, i)">{{ row.apro }}</td>
|
|
<td class="px-3 py-4 text-[14px]" :class="dataBorder(gi, i)">{{ row.forfait }}</td>
|
|
<td class="px-3 py-4 text-[14px]" :class="dataBorder(gi, i)">{{ row.tonne }}</td>
|
|
<td class="px-3 py-4 text-[14px]" :class="dataBorder(gi, i)">{{ row.indexation }}</td>
|
|
<td class="px-3 py-4 text-[14px]" :class="dataBorder(gi, i)">{{ row.state }}</td>
|
|
</tr>
|
|
</template>
|
|
<tr v-if="!hasPrices">
|
|
<td colspan="8" class="px-3 py-4 text-center text-[14px] text-m-muted">
|
|
{{ t('transport.carriers.consultation.price.empty') }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div v-if="hasPrices" class="flex justify-center">
|
|
<MalioButton
|
|
variant="primary"
|
|
:label="t('transport.carriers.consultation.price.export')"
|
|
:disabled="exporting"
|
|
@click="exportPrices"
|
|
/>
|
|
</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('transport.carriers.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 CarrierAddressBlock from '~/modules/transport/components/CarrierAddressBlock.vue'
|
|
import CarrierContactBlock from '~/modules/transport/components/CarrierContactBlock.vue'
|
|
import { useCarrier } from '~/modules/transport/composables/useCarrier'
|
|
import {
|
|
canEditCarrier,
|
|
carrierConsultationVisibleTabs,
|
|
labelOfRelation,
|
|
mapAddressToDraft,
|
|
mapContactToDraft,
|
|
mapMainToDraft,
|
|
showArchiveAction,
|
|
showRestoreAction,
|
|
type CarrierPriceRead,
|
|
type Relation,
|
|
} from '~/modules/transport/utils/forms/carrierMappers'
|
|
import { extractApiErrorMessage } from '~/shared/utils/api'
|
|
|
|
interface SelectOption {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
const { t } = useI18n()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const toast = useToast()
|
|
const api = useApi()
|
|
const { can } = usePermissions()
|
|
|
|
const carrierId = route.params.id as string
|
|
const { carrier, loading, error, load, archive, restore } = useCarrier(carrierId)
|
|
|
|
const isArchived = computed(() => carrier.value?.isArchived ?? false)
|
|
const canEdit = computed(() => canEditCarrier(can))
|
|
const showArchive = computed(() => showArchiveAction(can, isArchived.value))
|
|
const showRestore = computed(() => showRestoreAction(can, isArchived.value))
|
|
|
|
const headerTitle = computed(() => carrier.value?.name || t('transport.carriers.consultation.title'))
|
|
useHead({ title: t('transport.carriers.consultation.title') })
|
|
|
|
// ── Bloc principal mappé (lecture seule) ─────────────────────────────────────
|
|
const main = computed(() => mapMainToDraft(carrier.value ?? { id: 0, '@id': '' }))
|
|
const isLiot = computed(() => main.value.name.trim().toUpperCase() === 'LIOT')
|
|
const certificationLabel = computed(() => main.value.certificationType
|
|
? t(`transport.carriers.certification.${main.value.certificationType}`)
|
|
: '')
|
|
// Indexation affichée avec le « % » (comme l'icône du champ amount de l'ajout).
|
|
const indexationDisplay = computed(() => main.value.indexationRate ? `${main.value.indexationRate} %` : '')
|
|
// Décharge : nom du fichier embarqué si présent (sinon vide ; la colonne reste réservée).
|
|
const dischargeLabel = computed(() => {
|
|
const doc = carrier.value?.dischargeDocument
|
|
if (doc && typeof doc !== 'string') {
|
|
const meta = doc as Record<string, unknown>
|
|
return String(meta.originalFilename ?? meta.name ?? '')
|
|
}
|
|
return ''
|
|
})
|
|
|
|
// ── Onglets : Adresses · Contacts · Prix (ouvre sur Adresses, pas de Qualimat) ──
|
|
// ERP-193 (retour metier) : on masque tout onglet de donnees vide.
|
|
const TAB_ICONS: Record<string, string> = {
|
|
addresses: 'mdi:map-marker-outline',
|
|
contacts: 'mdi:account-box-plus-outline',
|
|
prices: 'mdi:payment',
|
|
}
|
|
const visibleTabKeys = computed(() => carrierConsultationVisibleTabs(carrier.value))
|
|
const tabs = computed(() => visibleTabKeys.value.map(key => ({
|
|
key,
|
|
label: t(`transport.carriers.tab.${key}`),
|
|
icon: TAB_ICONS[key],
|
|
})))
|
|
|
|
// Onglet initial : vide tant que le transporteur n'est pas charge, puis premier
|
|
// onglet visible. Un watcher recale si l'onglet courant disparait. ERP-193 : on
|
|
// honore l'onglet demande via `?tab=` (navigation depuis l'edition) s'il est
|
|
// visible, pour retomber sur le meme onglet en passant edition <-> consultation.
|
|
const activeTab = ref('')
|
|
let requestedTab = typeof route.query.tab === 'string' ? route.query.tab : ''
|
|
watch(visibleTabKeys, (keys) => {
|
|
if (keys.length === 0) {
|
|
activeTab.value = ''
|
|
return
|
|
}
|
|
if (!keys.includes(activeTab.value)) {
|
|
activeTab.value = requestedTab && keys.includes(requestedTab) ? requestedTab : keys[0]
|
|
requestedTab = ''
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// Adresse UNIQUE (ERP-172) : un seul bloc en lecture seule (vide si pas d'adresse).
|
|
const address = computed(() => carrier.value?.address
|
|
? mapAddressToDraft(carrier.value.address)
|
|
: mapAddressToDraft({ id: 0, '@id': '' }))
|
|
const contacts = computed(() => {
|
|
const list = (carrier.value?.contacts ?? []).map(mapContactToDraft)
|
|
return list.length > 0 ? list : [mapContactToDraft({ id: 0, '@id': '' })]
|
|
})
|
|
|
|
/** Pays : une seule option (valeur courante), suffisant pour l'affichage readonly. */
|
|
function countryOptionsFor(country: string): SelectOption[] {
|
|
return country ? [{ value: country, label: country }] : []
|
|
}
|
|
|
|
// ── Tableau Prix consultation (regroupé par ADRESSE DE LIVRAISON) ─────────────
|
|
// Rang d'affichage des contenants au sein d'une même adresse (Fond mouvant puis Benne).
|
|
const CONTAINER_RANK: Record<string, number> = { FOND_MOUVANT: 0, BENNE: 1 }
|
|
|
|
interface PriceRowView {
|
|
/** Contenant (libellé affiché : Fond mouvant / Benne). */
|
|
transport: string
|
|
/** Contenant brut (FOND_MOUVANT / BENNE) — tri interne du groupe. */
|
|
transportType: string
|
|
/** Fournisseur ou client lié au prix (raison sociale). */
|
|
party: string
|
|
apro: string
|
|
delivery: string
|
|
forfait: string
|
|
tonne: string
|
|
indexation: string
|
|
state: string
|
|
}
|
|
|
|
/** Groupe de prix d'une même adresse de livraison (lignes consécutives). */
|
|
interface PriceGroupView {
|
|
rows: PriceRowView[]
|
|
}
|
|
|
|
/** Formate un montant décimal en « 1 000,00 € » (chaîne vide si absent). */
|
|
function formatAmount(value: string | null | undefined): string {
|
|
if (!value) {
|
|
return ''
|
|
}
|
|
const n = Number(value)
|
|
if (Number.isNaN(n)) {
|
|
return ''
|
|
}
|
|
return `${n.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`
|
|
}
|
|
|
|
/** Code du site = département (2 premiers chiffres du code postal, ex: 86 / 17 / 82). */
|
|
function siteCode(relation: Relation): string {
|
|
if (!relation || typeof relation === 'string') {
|
|
return ''
|
|
}
|
|
const postalCode = relation.postalCode as string | undefined
|
|
return postalCode ? postalCode.slice(0, 2) : ''
|
|
}
|
|
|
|
/**
|
|
* Construit une ligne d'affichage depuis un prix embarqué (maquette Prix) :
|
|
* - « Transport » = le contenant (Fond mouvant / Benne) ;
|
|
* - « Fournisseurs / Clients » = le fournisseur OU le client lié (raison sociale) ;
|
|
* - « Adresse sites » = le CODE du site (département, ex: 86 / 17 / 82) ;
|
|
* - « Adresse livraisons » = l'adresse (voie) du client/fournisseur ;
|
|
* - le prix tombe dans Forfait € OU Tonne € selon `pricingUnit`.
|
|
*/
|
|
function toPriceRow(price: CarrierPriceRead): PriceRowView {
|
|
const isClient = price.direction === 'CLIENT'
|
|
const containerType = price.containerType ?? ''
|
|
return {
|
|
transportType: containerType,
|
|
transport: containerType ? t(`transport.carriers.containerType.${containerType}`) : '',
|
|
party: labelOfRelation(isClient ? price.client : price.supplier),
|
|
apro: isClient ? siteCode(price.departureSite) : siteCode(price.deliverySite),
|
|
delivery: isClient ? labelOfRelation(price.clientDeliveryAddress) : labelOfRelation(price.supplierSupplyAddress),
|
|
forfait: price.pricingUnit === 'FORFAIT' ? formatAmount(price.price) : '',
|
|
tonne: price.pricingUnit === 'TONNE' ? formatAmount(price.price) : '',
|
|
// CarrierPrice n'a pas de taux d'indexation propre → on affiche celui du
|
|
// transporteur (formulaire principal). À faire évoluer si un taux par prix
|
|
// est requis (gap back).
|
|
indexation: main.value.indexationRate ? `${main.value.indexationRate} %` : '',
|
|
state: price.priceState ? t(`transport.carriers.form.price.state${stateSuffix(price.priceState)}`) : '',
|
|
}
|
|
}
|
|
|
|
/** EN_COURS → EnCours, VALIDE → Valide, NON_VALIDE → NonValide (clés i18n existantes). */
|
|
function stateSuffix(state: string): string {
|
|
const map: Record<string, string> = { EN_COURS: 'EnCours', VALIDE: 'Valide', NON_VALIDE: 'NonValide' }
|
|
return map[state] ?? ''
|
|
}
|
|
|
|
// Prix regroupés par ADRESSE DE LIVRAISON : les lignes d'une même adresse sont
|
|
// consécutives (triées par contenant Fond mouvant → Benne), les groupes triés
|
|
// alphabétiquement par adresse. Un séparateur épais sépare deux adresses.
|
|
const priceGroups = computed<PriceGroupView[]>(() => {
|
|
const rows = (carrier.value?.prices ?? []).map(toPriceRow)
|
|
const byDelivery = new Map<string, PriceRowView[]>()
|
|
for (const row of rows) {
|
|
const list = byDelivery.get(row.delivery)
|
|
if (list) {
|
|
list.push(row)
|
|
} else {
|
|
byDelivery.set(row.delivery, [row])
|
|
}
|
|
}
|
|
return [...byDelivery.entries()]
|
|
.sort(([a], [b]) => a.localeCompare(b, 'fr'))
|
|
.map(([, groupRows]) => ({
|
|
rows: groupRows
|
|
.slice()
|
|
.sort((x, y) => (CONTAINER_RANK[x.transportType] ?? 99) - (CONTAINER_RANK[y.transportType] ?? 99)),
|
|
}))
|
|
})
|
|
|
|
const hasPrices = computed(() => priceGroups.value.length > 0)
|
|
|
|
/**
|
|
* Bordure basse d'une cellule de données :
|
|
* - ligne interne d'un groupe d'adresse (même adresse de livraison) → fine grise ;
|
|
* - dernière ligne d'un groupe NON final → épaisse noire (sépare deux adresses) ;
|
|
* - dernière ligne du DERNIER groupe → aucune (le cadre du tableau s'en charge,
|
|
* évite la double bordure tout en bas).
|
|
*/
|
|
function dataBorder(gi: number, i: number): string {
|
|
const group = priceGroups.value[gi]
|
|
const isLastRow = i === group.rows.length - 1
|
|
const isLastGroup = gi === priceGroups.value.length - 1
|
|
// Couleur de bordure SIDE-SPECIFIC (border-b-*) : un `border-{color}` global
|
|
// ecraserait la couleur du bord droit noir de la colonne Transport.
|
|
if (!isLastRow) {
|
|
return 'border-b border-b-m-muted/30'
|
|
}
|
|
return isLastGroup ? '' : 'border-b-2 border-b-black'
|
|
}
|
|
|
|
// ── Export XLSX des prix ─────────────────────────────────────────────────────
|
|
const exporting = ref(false)
|
|
|
|
async function exportPrices(): Promise<void> {
|
|
if (exporting.value) return
|
|
exporting.value = true
|
|
try {
|
|
const blob = await api.get<Blob>(`/carriers/${carrierId}/prices/export.xlsx`, {}, {
|
|
responseType: 'blob',
|
|
toast: false,
|
|
} as unknown as Parameters<typeof api.get>[2])
|
|
triggerDownload(blob, `transporteur-${carrierId}-prix.xlsx`)
|
|
}
|
|
catch {
|
|
toast.error({ title: t('transport.carriers.toast.error'), message: t('transport.carriers.toast.exportError') })
|
|
}
|
|
finally {
|
|
exporting.value = false
|
|
}
|
|
}
|
|
|
|
function triggerDownload(blob: Blob, filename: string): void {
|
|
const url = URL.createObjectURL(blob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = filename
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
link.remove()
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
|
|
// ── Navigation / archivage ───────────────────────────────────────────────────
|
|
function goBack(): void {
|
|
router.push('/carriers')
|
|
}
|
|
|
|
function goEdit(): void {
|
|
// ERP-193 : on transmet l'onglet courant pour retomber dessus en edition.
|
|
router.push({ path: `/carriers/${carrierId}/edit`, query: activeTab.value ? { tab: activeTab.value } : {} })
|
|
}
|
|
|
|
const confirmArchive = reactive({ open: false, title: '', message: '', confirmLabel: '' })
|
|
|
|
function askToggleArchive(): void {
|
|
const archiving = !isArchived.value
|
|
confirmArchive.title = archiving ? t('transport.carriers.action.archive') : t('transport.carriers.action.restore')
|
|
confirmArchive.message = archiving
|
|
? t('transport.carriers.consultation.confirmArchive.message')
|
|
: t('transport.carriers.consultation.confirmRestore.message')
|
|
confirmArchive.confirmLabel = archiving ? t('transport.carriers.action.archive') : t('transport.carriers.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('transport.carriers.toast.archiveSuccess')
|
|
: t('transport.carriers.toast.restoreSuccess'),
|
|
})
|
|
}
|
|
catch (err) {
|
|
// Surface le message back (ex. 409 « homonyme actif » à la restauration),
|
|
// propagé exprès par useCarrier ; fallback générique sinon.
|
|
const data = (err as { response?: { _data?: unknown } })?.response?._data
|
|
toast.error({
|
|
title: t('transport.carriers.toast.error'),
|
|
message: extractApiErrorMessage(data) || undefined,
|
|
})
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|