fix(transport) : tableau prix consultation — cellule de groupe fusionnée (Fond Mouvant/Benne) + colonnes maquette (ERP-170)

This commit is contained in:
2026-06-17 11:41:56 +02:00
parent e612eae391
commit 7adf3a511a
@@ -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)