feat(front) : refonte à plat des blocs Information (commercial) et Prix (transporteur)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 48s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 5m22s

Complète la refonte ERP-196 (blocs à plat sans box-shadow, titre noir, filet noir
1px) qui avait oublié deux blocs :
- bloc « Information » des écrans Client et Fournisseur (consultation / édition /
  création) : suppression du fond blanc, du box-shadow et du padding latéral → grille
  à plat pleine largeur.
- bloc « Prix » du transporteur (CarrierPriceBlock) : aligné sur les blocs
  contact/adresse — à plat, en-tête « Prix N » en noir + poubelle (button-class p-0),
  filet noir 1px entre blocs (sauf le dernier via prop last). Câblage title/last dans
  les écrans Ajouter/Modifier + i18n price.title.
This commit is contained in:
2026-06-24 18:25:32 +02:00
parent fd430bc123
commit eeaf56a1f7
10 changed files with 193 additions and 175 deletions
+1
View File
@@ -663,6 +663,7 @@
"confirm": "Supprimer" "confirm": "Supprimer"
}, },
"price": { "price": {
"title": "Prix {n}",
"direction": "Sens", "direction": "Sens",
"directionClient": "Client", "directionClient": "Client",
"directionSupplier": "Fournisseur", "directionSupplier": "Fournisseur",
@@ -93,7 +93,7 @@
<MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]"> <MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
<!-- Onglet Information --> <!-- Onglet Information -->
<template #information> <template #information>
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"> <div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
<!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas <!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas
sur les inputs (champ 40px centre dans un h-12 -> ~4px de sur les inputs (champ 40px centre dans un h-12 -> ~4px de
coussin de chaque cote). --> coussin de chaque cote). -->
@@ -96,7 +96,7 @@
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]"> <MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
<!-- Onglet Information --> <!-- Onglet Information -->
<template #information> <template #information>
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"> <div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
<!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas <!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas
sur les inputs (champ 40px centre dans un h-12 -> ~4px de sur les inputs (champ 40px centre dans un h-12 -> ~4px de
coussin de chaque cote). --> coussin de chaque cote). -->
@@ -87,7 +87,7 @@
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]"> <MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
<!-- Onglet Information --> <!-- Onglet Information -->
<template #information> <template #information>
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"> <div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
<!-- pt-1/pb-1 alignent le textarea (h-full) sur les inputs, dont <!-- pt-1/pb-1 alignent le textarea (h-full) sur les inputs, dont
le champ de 40px est centre dans un conteneur h-12 (~4px de le champ de 40px est centre dans un conteneur h-12 (~4px de
coussin en HAUT et en BAS). Sans pb-1, le textarea descend ~4px coussin en HAUT et en BAS). Sans pb-1, le textarea descend ~4px
@@ -56,7 +56,7 @@
<MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]"> <MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
<!-- Onglet Information --> <!-- Onglet Information -->
<template #information> <template #information>
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"> <div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
<!-- pt-1/pb-1 alignent le textarea (h-full) sur les inputs. --> <!-- pt-1/pb-1 alignent le textarea (h-full) sur les inputs. -->
<MalioInputTextArea <MalioInputTextArea
v-model="information.description" v-model="information.description"
@@ -71,7 +71,7 @@
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]"> <MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
<!-- Onglet Information --> <!-- Onglet Information -->
<template #information> <template #information>
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"> <div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
<!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas <!-- pt-1/pb-1 alignent le textarea (h-full) en haut ET en bas
sur les inputs (champ 40px centre dans un h-12). --> sur les inputs (champ 40px centre dans un h-12). -->
<MalioInputTextArea <MalioInputTextArea
@@ -51,7 +51,7 @@
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]"> <MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
<!-- Onglet Information --> <!-- Onglet Information -->
<template #information> <template #information>
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"> <div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputTextArea <MalioInputTextArea
v-model="information.description" v-model="information.description"
:label="t('commercial.suppliers.form.information.description')" :label="t('commercial.suppliers.form.information.description')"
@@ -1,190 +1,199 @@
<template> <template>
<div class="relative grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"> <!-- Bloc a plat (sans box-shadow) : un filet noir 1px le separe du suivant
<!-- Suppression : modal de confirmation côté parent. --> (pas de bordure sous le dernier bloc), aligne sur les blocs contact / adresse. -->
<MalioButtonIcon <div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
v-if="removable && !readonly && !disabled" <!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
icon="mdi:delete-outline" <div class="flex items-center justify-between">
variant="ghost" <h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
button-class="absolute top-3 right-3" <!-- Suppression : modal de confirmation côté parent. -->
v-bind="{ ariaLabel: t('transport.carriers.form.price.remove') }" <MalioButtonIcon
@click="$emit('remove')" v-if="removable && !readonly && !disabled"
/> icon="mdi:delete-outline"
variant="ghost"
<!-- RG-4.09 : sens du prix (CLIENT / FOURNISSEUR) en colonne 1 / ligne 1, radios button-class="p-0"
EN LIGNE (horizontaux), centrés sur la hauteur de champ (h-12) comme la v-bind="{ ariaLabel: t('transport.carriers.form.price.remove') }"
case « Affréter ». Pas de label de groupe. --> @click="$emit('remove')"
<div> />
<div class="flex h-12 items-center gap-6">
<MalioRadioButton
:model-value="model.direction"
:name="`price-direction-${uid}`"
value="CLIENT"
:label="t('transport.carriers.form.price.directionClient')"
:disabled="readonly || disabled"
group-class="mt-0"
@update:model-value="onDirectionChange"
/>
<MalioRadioButton
:model-value="model.direction"
:name="`price-direction-${uid}`"
value="FOURNISSEUR"
:label="t('transport.carriers.form.price.directionSupplier')"
:disabled="readonly || disabled"
group-class="mt-0"
@update:model-value="onDirectionChange"
/>
</div>
<p v-if="errors?.direction" class="ml-[2px] text-xs text-m-danger">{{ errors.direction }}</p>
</div> </div>
<!-- Branche CLIENT (RG-4.10). --> <!-- Grille 4 colonnes des champs du prix. -->
<template v-if="model.direction === 'CLIENT'"> <div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioSelect <!-- RG-4.09 : sens du prix (CLIENT / FOURNISSEUR) en colonne 1 / ligne 1, radios
:model-value="model.clientIri" EN LIGNE (horizontaux), centrés sur la hauteur de champ (h-12) comme la
:options="clientOptions" case « Affréter ». Pas de label de groupe. -->
:label="t('transport.carriers.form.price.client')"
empty-option-label=""
:required="true"
:readonly="readonly"
:disabled="disabled"
:error="errors?.client"
@update:model-value="onClientChange"
/>
<MalioSelect
:model-value="model.clientDeliveryAddressIri"
:options="clientAddressOptions"
:label="t('transport.carriers.form.price.clientDeliveryAddress')"
empty-option-label=""
:required="true"
:readonly="readonly"
:disabled="disabled"
:error="errors?.clientDeliveryAddress"
@update:model-value="(v: string | number | null) => update('clientDeliveryAddressIri', v === null ? null : String(v))"
/>
<MalioSelect
:model-value="model.departureSiteIri"
:options="siteOptions"
:label="t('transport.carriers.form.price.departureSite')"
empty-option-label=""
:required="true"
:readonly="readonly"
:disabled="disabled"
:error="errors?.departureSite"
@update:model-value="(v: string | number | null) => update('departureSiteIri', v === null ? null : String(v))"
/>
</template>
<!-- Branche FOURNISSEUR (RG-4.11). -->
<template v-else-if="model.direction === 'FOURNISSEUR'">
<MalioSelect
:model-value="model.supplierIri"
:options="supplierOptions"
:label="t('transport.carriers.form.price.supplier')"
empty-option-label=""
:required="true"
:readonly="readonly"
:disabled="disabled"
:error="errors?.supplier"
@update:model-value="onSupplierChange"
/>
<MalioSelect
:model-value="model.supplierSupplyAddressIri"
:options="supplierAddressOptions"
:label="t('transport.carriers.form.price.supplierSupplyAddress')"
empty-option-label=""
:required="true"
:readonly="readonly"
:disabled="disabled"
:error="errors?.supplierSupplyAddress"
@update:model-value="(v: string | number | null) => update('supplierSupplyAddressIri', v === null ? null : String(v))"
/>
<MalioSelect
:model-value="model.deliverySiteIri"
:options="siteOptions"
:label="t('transport.carriers.form.price.deliverySite')"
empty-option-label=""
:required="true"
:readonly="readonly"
:disabled="disabled"
:error="errors?.deliverySite"
@update:model-value="(v: string | number | null) => update('deliverySiteIri', v === null ? null : String(v))"
/>
</template>
<!-- Communs (visibles dès qu'un sens est choisi). -->
<template v-if="model.direction !== null">
<!-- Contenant : Benne / Fond mouvant (radios centrés h-12, pas de label). -->
<div> <div>
<div class="flex h-12 items-center gap-4"> <div class="flex h-12 items-center gap-6">
<MalioRadioButton <MalioRadioButton
:model-value="model.containerType" :model-value="model.direction"
:name="`price-container-${uid}`" :name="`price-direction-${uid}`"
value="BENNE" value="CLIENT"
:label="t('transport.carriers.containerType.BENNE')" :label="t('transport.carriers.form.price.directionClient')"
:disabled="readonly || disabled" :disabled="readonly || disabled"
group-class="mt-0" group-class="mt-0"
@update:model-value="(v: string | number | boolean | null) => update('containerType', v === null ? null : String(v))" @update:model-value="onDirectionChange"
/> />
<MalioRadioButton <MalioRadioButton
:model-value="model.containerType" :model-value="model.direction"
:name="`price-container-${uid}`" :name="`price-direction-${uid}`"
value="FOND_MOUVANT" value="FOURNISSEUR"
:label="t('transport.carriers.containerType.FOND_MOUVANT')" :label="t('transport.carriers.form.price.directionSupplier')"
:disabled="readonly || disabled" :disabled="readonly || disabled"
group-class="mt-0" group-class="mt-0"
@update:model-value="(v: string | number | boolean | null) => update('containerType', v === null ? null : String(v))" @update:model-value="onDirectionChange"
/> />
</div> </div>
<p v-if="errors?.containerType" class="ml-[2px] text-xs text-m-danger">{{ errors.containerType }}</p> <p v-if="errors?.direction" class="ml-[2px] text-xs text-m-danger">{{ errors.direction }}</p>
</div> </div>
<!-- Tarification : Forfait / Tonne (radios centrés h-12, pas de label). --> <!-- Branche CLIENT (RG-4.10). -->
<div> <template v-if="model.direction === 'CLIENT'">
<div class="flex h-12 items-center gap-4"> <MalioSelect
<MalioRadioButton :model-value="model.clientIri"
:model-value="model.pricingUnit" :options="clientOptions"
:name="`price-unit-${uid}`" :label="t('transport.carriers.form.price.client')"
value="FORFAIT" empty-option-label=""
:label="t('transport.carriers.form.price.pricingForfait')" :required="true"
:disabled="readonly || disabled" :readonly="readonly"
group-class="mt-0" :disabled="disabled"
@update:model-value="(v: string | number | boolean | null) => update('pricingUnit', v === null ? null : String(v))" :error="errors?.client"
/> @update:model-value="onClientChange"
<MalioRadioButton />
:model-value="model.pricingUnit" <MalioSelect
:name="`price-unit-${uid}`" :model-value="model.clientDeliveryAddressIri"
value="TONNE" :options="clientAddressOptions"
:label="t('transport.carriers.form.price.pricingTonne')" :label="t('transport.carriers.form.price.clientDeliveryAddress')"
:disabled="readonly || disabled" empty-option-label=""
group-class="mt-0" :required="true"
@update:model-value="(v: string | number | boolean | null) => update('pricingUnit', v === null ? null : String(v))" :readonly="readonly"
/> :disabled="disabled"
:error="errors?.clientDeliveryAddress"
@update:model-value="(v: string | number | null) => update('clientDeliveryAddressIri', v === null ? null : String(v))"
/>
<MalioSelect
:model-value="model.departureSiteIri"
:options="siteOptions"
:label="t('transport.carriers.form.price.departureSite')"
empty-option-label=""
:required="true"
:readonly="readonly"
:disabled="disabled"
:error="errors?.departureSite"
@update:model-value="(v: string | number | null) => update('departureSiteIri', v === null ? null : String(v))"
/>
</template>
<!-- Branche FOURNISSEUR (RG-4.11). -->
<template v-else-if="model.direction === 'FOURNISSEUR'">
<MalioSelect
:model-value="model.supplierIri"
:options="supplierOptions"
:label="t('transport.carriers.form.price.supplier')"
empty-option-label=""
:required="true"
:readonly="readonly"
:disabled="disabled"
:error="errors?.supplier"
@update:model-value="onSupplierChange"
/>
<MalioSelect
:model-value="model.supplierSupplyAddressIri"
:options="supplierAddressOptions"
:label="t('transport.carriers.form.price.supplierSupplyAddress')"
empty-option-label=""
:required="true"
:readonly="readonly"
:disabled="disabled"
:error="errors?.supplierSupplyAddress"
@update:model-value="(v: string | number | null) => update('supplierSupplyAddressIri', v === null ? null : String(v))"
/>
<MalioSelect
:model-value="model.deliverySiteIri"
:options="siteOptions"
:label="t('transport.carriers.form.price.deliverySite')"
empty-option-label=""
:required="true"
:readonly="readonly"
:disabled="disabled"
:error="errors?.deliverySite"
@update:model-value="(v: string | number | null) => update('deliverySiteIri', v === null ? null : String(v))"
/>
</template>
<!-- Communs (visibles dès qu'un sens est choisi). -->
<template v-if="model.direction !== null">
<!-- Contenant : Benne / Fond mouvant (radios centrés h-12, pas de label). -->
<div>
<div class="flex h-12 items-center gap-4">
<MalioRadioButton
:model-value="model.containerType"
:name="`price-container-${uid}`"
value="BENNE"
:label="t('transport.carriers.containerType.BENNE')"
:disabled="readonly || disabled"
group-class="mt-0"
@update:model-value="(v: string | number | boolean | null) => update('containerType', v === null ? null : String(v))"
/>
<MalioRadioButton
:model-value="model.containerType"
:name="`price-container-${uid}`"
value="FOND_MOUVANT"
:label="t('transport.carriers.containerType.FOND_MOUVANT')"
:disabled="readonly || disabled"
group-class="mt-0"
@update:model-value="(v: string | number | boolean | null) => update('containerType', v === null ? null : String(v))"
/>
</div>
<p v-if="errors?.containerType" class="ml-[2px] text-xs text-m-danger">{{ errors.containerType }}</p>
</div> </div>
<p v-if="errors?.pricingUnit" class="ml-[2px] text-xs text-m-danger">{{ errors.pricingUnit }}</p>
</div>
<MalioInputAmount <!-- Tarification : Forfait / Tonne (radios centrés h-12, pas de label). -->
:model-value="model.price" <div>
:label="t('transport.carriers.form.price.price')" <div class="flex h-12 items-center gap-4">
:required="true" <MalioRadioButton
:readonly="readonly" :model-value="model.pricingUnit"
:disabled="disabled" :name="`price-unit-${uid}`"
:error="errors?.price" value="FORFAIT"
@update:model-value="(v: string) => update('price', v)" :label="t('transport.carriers.form.price.pricingForfait')"
/> :disabled="readonly || disabled"
group-class="mt-0"
@update:model-value="(v: string | number | boolean | null) => update('pricingUnit', v === null ? null : String(v))"
/>
<MalioRadioButton
:model-value="model.pricingUnit"
:name="`price-unit-${uid}`"
value="TONNE"
:label="t('transport.carriers.form.price.pricingTonne')"
:disabled="readonly || disabled"
group-class="mt-0"
@update:model-value="(v: string | number | boolean | null) => update('pricingUnit', v === null ? null : String(v))"
/>
</div>
<p v-if="errors?.pricingUnit" class="ml-[2px] text-xs text-m-danger">{{ errors.pricingUnit }}</p>
</div>
<MalioSelect <MalioInputAmount
:model-value="model.priceState" :model-value="model.price"
:options="priceStateOptions" :label="t('transport.carriers.form.price.price')"
:label="t('transport.carriers.form.price.priceState')" :required="true"
empty-option-label="" :readonly="readonly"
:required="true" :disabled="disabled"
:readonly="readonly" :error="errors?.price"
:disabled="disabled" @update:model-value="(v: string) => update('price', v)"
:error="errors?.priceState" />
@update:model-value="(v: string | number | null) => update('priceState', v === null ? null : String(v))"
/> <MalioSelect
</template> :model-value="model.priceState"
:options="priceStateOptions"
:label="t('transport.carriers.form.price.priceState')"
empty-option-label=""
:required="true"
:readonly="readonly"
:disabled="disabled"
:error="errors?.priceState"
@update:model-value="(v: string | number | null) => update('priceState', v === null ? null : String(v))"
/>
</template>
</div>
</div> </div>
</template> </template>
@@ -200,6 +209,10 @@ interface SelectOption {
const props = defineProps<{ const props = defineProps<{
/** Brouillon du prix (v-model). */ /** Brouillon du prix (v-model). */
modelValue: CarrierPriceFormDraft modelValue: CarrierPriceFormDraft
/** Titre du bloc (ex: « Prix 1 »). */
title: string
/** Dernier bloc de la liste : supprime le filet de separation bas. */
last?: boolean
/** Clients disponibles (IRI en value). */ /** Clients disponibles (IRI en value). */
clientOptions: SelectOption[] clientOptions: SelectOption[]
/** Fournisseurs disponibles (IRI en value). */ /** Fournisseurs disponibles (IRI en value). */
@@ -182,10 +182,12 @@
v-for="(price, index) in prices" v-for="(price, index) in prices"
:key="index" :key="index"
:model-value="price" :model-value="price"
:title="t('transport.carriers.form.price.title', { n: index + 1 })"
:client-options="clientOptions" :client-options="clientOptions"
:supplier-options="supplierOptions" :supplier-options="supplierOptions"
:site-options="siteOptions" :site-options="siteOptions"
removable removable
:last="index === prices.length - 1"
:errors="priceErrors[index]" :errors="priceErrors[index]"
@update:model-value="(v) => prices[index] = v" @update:model-value="(v) => prices[index] = v"
@remove="askRemovePrice(index)" @remove="askRemovePrice(index)"
@@ -244,11 +244,13 @@
v-for="(price, index) in prices" v-for="(price, index) in prices"
:key="index" :key="index"
:model-value="price" :model-value="price"
:title="t('transport.carriers.form.price.title', { n: index + 1 })"
:client-options="clientOptions" :client-options="clientOptions"
:supplier-options="supplierOptions" :supplier-options="supplierOptions"
:site-options="siteOptions" :site-options="siteOptions"
:removable="!isValidated('prices')" :removable="!isValidated('prices')"
:disabled="isValidated('prices')" :disabled="isValidated('prices')"
:last="index === prices.length - 1"
:errors="priceErrors[index]" :errors="priceErrors[index]"
@update:model-value="(v) => prices[index] = v" @update:model-value="(v) => prices[index] = v"
@remove="askRemovePrice(index)" @remove="askRemovePrice(index)"