fix(transport) : tableau prix consultation — cellule de groupe fusionnée (Fond Mouvant/Benne) + colonnes maquette (ERP-170)
This commit is contained in:
@@ -102,43 +102,52 @@
|
|||||||
<!-- Prix : tableau présentationnel regroupé par contenant + export. -->
|
<!-- Prix : tableau présentationnel regroupé par contenant + export. -->
|
||||||
<template #prices>
|
<template #prices>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
<table class="w-full border-collapse text-left text-sm">
|
<table class="w-full border-collapse border border-m-muted/40 text-left text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-black">
|
<tr class="border-b border-black bg-m-muted/10">
|
||||||
<th class="py-2 font-semibold">{{ t('transport.carriers.consultation.price.group') }}</th>
|
<th class="px-3 py-2"></th>
|
||||||
<th class="py-2 font-semibold">{{ t('transport.carriers.consultation.price.carrier') }}</th>
|
<th class="px-3 py-2 font-semibold">{{ t('transport.carriers.consultation.price.carrier') }}</th>
|
||||||
<th class="py-2 font-semibold">{{ t('transport.carriers.consultation.price.aproOrSite') }}</th>
|
<th class="px-3 py-2 font-semibold">{{ t('transport.carriers.consultation.price.aproOrSite') }}</th>
|
||||||
<th class="py-2 font-semibold">{{ t('transport.carriers.consultation.price.delivery') }}</th>
|
<th class="px-3 py-2 font-semibold">{{ t('transport.carriers.consultation.price.delivery') }}</th>
|
||||||
<th class="py-2 font-semibold">{{ t('transport.carriers.consultation.price.forfait') }}</th>
|
<th class="px-3 py-2 font-semibold">{{ t('transport.carriers.consultation.price.forfait') }}</th>
|
||||||
<th class="py-2 font-semibold">{{ t('transport.carriers.consultation.price.tonne') }}</th>
|
<th class="px-3 py-2 font-semibold">{{ t('transport.carriers.consultation.price.tonne') }}</th>
|
||||||
<th class="py-2 font-semibold">{{ t('transport.carriers.consultation.price.indexation') }}</th>
|
<th class="px-3 py-2 font-semibold">{{ t('transport.carriers.consultation.price.indexation') }}</th>
|
||||||
<th class="py-2 font-semibold">{{ t('transport.carriers.consultation.price.state') }}</th>
|
<th class="px-3 py-2 font-semibold">{{ t('transport.carriers.consultation.price.state') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<template v-for="(group, gi) in priceGroups" :key="group.label">
|
||||||
v-for="(row, index) in priceRows"
|
<tr
|
||||||
:key="index"
|
v-for="(row, i) in group.rows"
|
||||||
class="border-b border-m-muted/30"
|
:key="`${gi}-${i}`"
|
||||||
>
|
:class="i === group.rows.length - 1 ? 'border-b border-black' : 'border-b border-m-muted/30'"
|
||||||
<td class="py-2 font-medium">{{ row.group }}</td>
|
>
|
||||||
<td class="py-2">{{ headerTitle }}</td>
|
<!-- Cellule de groupe fusionnée (rowspan), centrée verticalement. -->
|
||||||
<td class="py-2">{{ row.aproOrSite }}</td>
|
<td
|
||||||
<td class="py-2">{{ row.delivery }}</td>
|
v-if="i === 0"
|
||||||
<td class="py-2">{{ row.forfait }}</td>
|
:rowspan="group.rows.length"
|
||||||
<td class="py-2">{{ row.tonne }}</td>
|
class="border-r border-black px-3 py-2 text-center align-middle font-medium"
|
||||||
<td class="py-2">{{ row.indexation }}</td>
|
>
|
||||||
<td class="py-2">{{ row.state }}</td>
|
{{ group.label }}
|
||||||
</tr>
|
</td>
|
||||||
<tr v-if="priceRows.length === 0">
|
<td class="px-3 py-2">{{ headerTitle }}</td>
|
||||||
<td colspan="8" class="py-4 text-center text-m-muted">
|
<td class="px-3 py-2">{{ row.apro }}</td>
|
||||||
|
<td class="px-3 py-2">{{ row.delivery }}</td>
|
||||||
|
<td class="px-3 py-2">{{ row.forfait }}</td>
|
||||||
|
<td class="px-3 py-2">{{ row.tonne }}</td>
|
||||||
|
<td class="px-3 py-2">{{ row.indexation }}</td>
|
||||||
|
<td class="px-3 py-2">{{ row.state }}</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<tr v-if="!hasPrices">
|
||||||
|
<td colspan="8" class="px-3 py-4 text-center text-m-muted">
|
||||||
{{ t('transport.carriers.consultation.price.empty') }}
|
{{ t('transport.carriers.consultation.price.empty') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div v-if="priceRows.length > 0" class="flex justify-center">
|
<div v-if="hasPrices" class="flex justify-center">
|
||||||
<MalioButton
|
<MalioButton
|
||||||
variant="primary"
|
variant="primary"
|
||||||
:label="t('transport.carriers.consultation.price.export')"
|
:label="t('transport.carriers.consultation.price.export')"
|
||||||
@@ -256,8 +265,7 @@ function countryOptionsFor(country: string): SelectOption[] {
|
|||||||
const PRICE_GROUP_ORDER = ['FOND_MOUVANT', 'BENNE'] as const
|
const PRICE_GROUP_ORDER = ['FOND_MOUVANT', 'BENNE'] as const
|
||||||
|
|
||||||
interface PriceRowView {
|
interface PriceRowView {
|
||||||
group: string
|
apro: string
|
||||||
aproOrSite: string
|
|
||||||
delivery: string
|
delivery: string
|
||||||
forfait: string
|
forfait: string
|
||||||
tonne: string
|
tonne: string
|
||||||
@@ -265,17 +273,41 @@ interface PriceRowView {
|
|||||||
state: string
|
state: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Construit une ligne d'affichage depuis un prix embarqué. */
|
/** Groupe de prix d'un même contenant (Fond Mouvant / Benne) — cellule fusionnée. */
|
||||||
|
interface PriceGroupView {
|
||||||
|
label: string
|
||||||
|
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 })} €`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit une ligne d'affichage depuis un prix embarqué (maquette Prix) :
|
||||||
|
* - « Adresse APRO » = adresse (voie) du client/fournisseur ;
|
||||||
|
* - « Adresse livraisons » = le site (86 / 17 / 82) ;
|
||||||
|
* - le prix tombe dans Forfait € OU Tonne € selon `pricingUnit`.
|
||||||
|
*/
|
||||||
function toPriceRow(price: CarrierPriceRead): PriceRowView {
|
function toPriceRow(price: CarrierPriceRead): PriceRowView {
|
||||||
const isClient = price.direction === 'CLIENT'
|
const isClient = price.direction === 'CLIENT'
|
||||||
return {
|
return {
|
||||||
group: price.containerType ? t(`transport.carriers.containerType.${price.containerType}`) : '',
|
apro: isClient ? labelOfRelation(price.clientDeliveryAddress) : labelOfRelation(price.supplierSupplyAddress),
|
||||||
// RG : prix Client → site de départ ; prix Fournisseur → adresse d'appro.
|
delivery: isClient ? labelOfRelation(price.departureSite) : labelOfRelation(price.deliverySite),
|
||||||
aproOrSite: isClient ? labelOfRelation(price.departureSite) : labelOfRelation(price.supplierSupplyAddress),
|
forfait: price.pricingUnit === 'FORFAIT' ? formatAmount(price.price) : '',
|
||||||
delivery: isClient ? labelOfRelation(price.clientDeliveryAddress) : labelOfRelation(price.deliverySite),
|
tonne: price.pricingUnit === 'TONNE' ? formatAmount(price.price) : '',
|
||||||
forfait: price.pricingUnit === 'FORFAIT' ? (price.price ?? '') : '',
|
// CarrierPrice n'a pas de taux d'indexation propre → on affiche celui du
|
||||||
tonne: price.pricingUnit === 'TONNE' ? (price.price ?? '') : '',
|
// transporteur (formulaire principal). À faire évoluer si un taux par prix
|
||||||
indexation: main.value.indexationRate || '',
|
// est requis (gap back).
|
||||||
|
indexation: main.value.indexationRate ? `${main.value.indexationRate} %` : '',
|
||||||
state: price.priceState ? t(`transport.carriers.form.price.state${stateSuffix(price.priceState)}`) : '',
|
state: price.priceState ? t(`transport.carriers.form.price.state${stateSuffix(price.priceState)}`) : '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,15 +318,20 @@ function stateSuffix(state: string): string {
|
|||||||
return map[state] ?? ''
|
return map[state] ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prix triés/regroupés par contenant (Fond Mouvant puis Benne).
|
// Prix regroupés par contenant (Fond Mouvant puis Benne) — une cellule fusionnée
|
||||||
const priceRows = computed<PriceRowView[]>(() => {
|
// par groupe (rowspan) à gauche, conformément à la maquette.
|
||||||
|
const priceGroups = computed<PriceGroupView[]>(() => {
|
||||||
const list = carrier.value?.prices ?? []
|
const list = carrier.value?.prices ?? []
|
||||||
return [...list]
|
return PRICE_GROUP_ORDER
|
||||||
.sort((a, b) => PRICE_GROUP_ORDER.indexOf((a.containerType ?? '') as 'FOND_MOUVANT')
|
.map(container => ({
|
||||||
- PRICE_GROUP_ORDER.indexOf((b.containerType ?? '') as 'FOND_MOUVANT'))
|
label: t(`transport.carriers.containerType.${container}`),
|
||||||
.map(toPriceRow)
|
rows: list.filter(p => p.containerType === container).map(toPriceRow),
|
||||||
|
}))
|
||||||
|
.filter(group => group.rows.length > 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hasPrices = computed(() => priceGroups.value.length > 0)
|
||||||
|
|
||||||
// ── Export XLSX des prix ─────────────────────────────────────────────────────
|
// ── Export XLSX des prix ─────────────────────────────────────────────────────
|
||||||
const exporting = ref(false)
|
const exporting = ref(false)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user