Compare commits

..

4 Commits

Author SHA1 Message Date
gitea-actions 2e50a760c6 chore: bump version to v0.1.150
Auto Tag Develop / tag (push) Successful in 8s
Build & Push Docker Image / build (push) Successful in 47s
2026-06-24 17:14:00 +00:00
tristan 49e5e5548e feat(front) : refonte à plat des blocs Information (commercial) et Prix (transporteur) (#146)
Auto Tag Develop / tag (push) Successful in 11s
Complète la refonte **ERP-196** (blocs de formulaire à plat : sans box-shadow, titre noir, filet noir 1px) qui avait oublié deux blocs.

## Blocs concernés
- **Bloc « Information »** (Client + Fournisseur, écrans consultation / édition / création — 6 fichiers) : suppression du fond blanc, du box-shadow et du padding latéral → grille à plat pleine largeur. Pas de titre ajouté (le bloc est seul dans son onglet « Information », comme le bloc du haut du ticket de pesée).
- **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 la prop `last`). Câblage `title`/`last` dans les écrans Ajouter / Modifier + clé i18n `carriers.form.price.title`.

## Hors périmètre
La table de **consultation** des prix (lecture seule, avec export) n'est pas un bloc de formulaire et garde sa présentation actuelle.

## Vérifications
- Vitest : suite complète verte (667/667).
- ESLint : clean sur l'ensemble du projet.
- Aucune modif back.

> Pas de numéro de ticket fourni — branche nommée descriptivement, à renommer/rattacher si besoin.

Reviewed-on: #146
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-24 17:13:48 +00:00
gitea-actions fd430bc123 chore: bump version to v0.1.149
Auto Tag Develop / tag (push) Successful in 10s
Build & Push Docker Image / build (push) Successful in 3m9s
2026-06-24 16:05:04 +00:00
tristan a6b48b1dd1 feat : ERP-196 — refonte des blocs de formulaire (contact, adresse, compta) (#145)
Auto Tag Develop / tag (push) Successful in 11s
## ERP-196 — Refonte des blocs de formulaire

Refonte visuelle des blocs répétables des formulaires (clients, fournisseurs, prestataires, transporteurs), alignée sur les blocs « ticket de pesée » : à plat (sans box-shadow), titre de bloc en noir, séparation par filet noir 1px.

###  Blocs Contact
- Box-shadow / fond blanc / padding latéral retirés
- En-tête `flex justify-between` : titre noir (« Contact 1 »…) à gauche, poubelle `button-class="p-0"` à droite
- 4 colonnes, filet `border-b border-black` entre blocs (pas sous le dernier, prop `last`)
- i18n `contact.title` ajouté pour transporteurs / prestataires
- 9 pages câblées (new / edit / consultation des 4 répertoires)

###  Blocs Adresse
- Même traitement (à plat, titre noir, filet sauf dernier)
- i18n `address.title` pour transporteurs / prestataires
- Transporteur : adresse unique → titre « Adresse » (sans numéro)
- 12 pages câblées

###  Bloc Comptabilité
- Bloc **infos** : titre « Informations » + filet bas (uniquement si des RIB suivent)
- Blocs **RIB** : titre « RIB 1 / RIB 2… » + poubelle `p-0`, filet sauf le dernier
- i18n `accounting.infoTitle` (3 modules) + `accounting.ribTitle` (fournisseurs / prestataires)
- 9 pages câblées (clients / fournisseurs / prestataires)

### Vérifications
- Vitest : 44/44 (specs contact + adresse)
- Eslint : clean sur l'ensemble des composants et pages modifiés

### Commits
- `feat : refonte des blocs contact (ERP-196)`
- `feat : refonte des blocs adresse (ERP-196)`
- `feat : refonte du bloc comptabilité (ERP-196)`

Reviewed-on: #145
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-24 16:04:52 +00:00
26 changed files with 1375 additions and 1150 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.1.148' app.version: '0.1.150'
+10
View File
@@ -183,6 +183,7 @@
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre." "degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
}, },
"accounting": { "accounting": {
"infoTitle": "Informations",
"siren": "SIREN", "siren": "SIREN",
"accountNumber": "Numéro de compte", "accountNumber": "Numéro de compte",
"tvaMode": "Mode de TVA", "tvaMode": "Mode de TVA",
@@ -190,6 +191,7 @@
"paymentDelay": "Délai de règlement", "paymentDelay": "Délai de règlement",
"paymentType": "Type de règlement", "paymentType": "Type de règlement",
"bank": "Banque", "bank": "Banque",
"ribTitle": "RIB {n}",
"ribLabel": "Libellé", "ribLabel": "Libellé",
"ribBic": "BIC", "ribBic": "BIC",
"ribIban": "IBAN", "ribIban": "IBAN",
@@ -350,6 +352,7 @@
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre." "degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
}, },
"accounting": { "accounting": {
"infoTitle": "Informations",
"siren": "SIREN", "siren": "SIREN",
"accountNumber": "Numéro de compte", "accountNumber": "Numéro de compte",
"tvaMode": "Mode de TVA", "tvaMode": "Mode de TVA",
@@ -441,6 +444,7 @@
"categoryRequired": "Sélectionnez au moins une catégorie." "categoryRequired": "Sélectionnez au moins une catégorie."
}, },
"contact": { "contact": {
"title": "Contact {n}",
"lastName": "Nom", "lastName": "Nom",
"firstName": "Prénom", "firstName": "Prénom",
"jobTitle": "Fonction", "jobTitle": "Fonction",
@@ -452,6 +456,7 @@
"add": "Nouveau contact" "add": "Nouveau contact"
}, },
"address": { "address": {
"title": "Adresse {n}",
"sites": "Sites", "sites": "Sites",
"contacts": "Contact(s) rattaché(s)", "contacts": "Contact(s) rattaché(s)",
"country": "Pays", "country": "Pays",
@@ -465,6 +470,7 @@
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre." "degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
}, },
"accounting": { "accounting": {
"infoTitle": "Informations",
"siren": "SIREN", "siren": "SIREN",
"accountNumber": "Numéro de compte", "accountNumber": "Numéro de compte",
"tvaMode": "Mode de TVA", "tvaMode": "Mode de TVA",
@@ -472,6 +478,7 @@
"paymentDelay": "Délai de règlement", "paymentDelay": "Délai de règlement",
"paymentType": "Type de règlement", "paymentType": "Type de règlement",
"bank": "Banque", "bank": "Banque",
"ribTitle": "RIB {n}",
"ribLabel": "Libellé", "ribLabel": "Libellé",
"ribBic": "BIC", "ribBic": "BIC",
"ribIban": "IBAN", "ribIban": "IBAN",
@@ -628,6 +635,7 @@
"uploadFailed": "Le téléversement de la décharge a échoué." "uploadFailed": "Le téléversement de la décharge a échoué."
}, },
"address": { "address": {
"title": "Adresse",
"country": "Pays", "country": "Pays",
"postalCode": "Code postal", "postalCode": "Code postal",
"city": "Ville", "city": "Ville",
@@ -637,6 +645,7 @@
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre." "degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
}, },
"contact": { "contact": {
"title": "Contact {n}",
"lastName": "Nom", "lastName": "Nom",
"firstName": "Prénom", "firstName": "Prénom",
"jobTitle": "Fonction", "jobTitle": "Fonction",
@@ -654,6 +663,7 @@
"confirm": "Supprimer" "confirm": "Supprimer"
}, },
"price": { "price": {
"title": "Prix {n}",
"direction": "Sens", "direction": "Sens",
"directionClient": "Client", "directionClient": "Client",
"directionSupplier": "Fournisseur", "directionSupplier": "Fournisseur",
@@ -1,15 +1,23 @@
<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
(pas de bordure sous le dernier bloc). -->
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
<!-- ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). --> <!-- ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). -->
<MalioButtonIcon <MalioButtonIcon
v-if="removable && !readonly && !disabled" v-if="removable && !readonly && !disabled"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('commercial.clients.form.address.remove') }" v-bind="{ ariaLabel: t('commercial.clients.form.address.remove') }"
@click="$emit('remove')" @click="$emit('remove')"
/> />
</div>
<!-- Grille 4 colonnes des champs de l'adresse. -->
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<!-- Usage de l'adresse : Select unique (plus simple pour l'utilisateur) <!-- Usage de l'adresse : Select unique (plus simple pour l'utilisateur)
remplacant les 3 cases. Les options encodent les combinaisons valides remplacant les 3 cases. Les options encodent les combinaisons valides
(exclusivite Prospect, RG-1.06/07/08) ; le back recoit toujours les (exclusivite Prospect, RG-1.06/07/08) ; le back recoit toujours les
@@ -197,7 +205,7 @@
@update:model-value="(v: string) => update('streetComplement', v)" @update:model-value="(v: string) => update('streetComplement', v)"
/> />
</div> </div>
</div>
</div> </div>
</template> </template>
@@ -230,6 +238,8 @@ const props = defineProps<{
/** Pays disponibles (France par defaut). */ /** Pays disponibles (France par defaut). */
countryOptions: RefOption[] countryOptions: RefOption[]
removable?: boolean removable?: boolean
/** Dernier bloc de la liste : supprime le filet de separation bas. */
last?: boolean
readonly?: boolean readonly?: boolean
/** Bloc desactive (champs grises, consultation — distinct de readonly). */ /** Bloc desactive (champs grises, consultation — distinct de readonly). */
disabled?: boolean disabled?: boolean
@@ -1,5 +1,10 @@
<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
(pas de bordure sous le dernier bloc). -->
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
<!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si <!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si
non supprimable (1er bloc obligatoire RG-1.14) ou en lecture seule. non supprimable (1er bloc obligatoire RG-1.14) ou en lecture seule.
ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). --> ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). -->
@@ -7,11 +12,14 @@
v-if="removable && !readonly && !disabled" v-if="removable && !readonly && !disabled"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('commercial.clients.form.contact.remove') }" v-bind="{ ariaLabel: t('commercial.clients.form.contact.remove') }"
@click="$emit('remove')" @click="$emit('remove')"
/> />
</div>
<!-- Grille 4 colonnes des champs du contact. -->
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-if="!hideEmpty || isFilled(model.lastName)" v-if="!hideEmpty || isFilled(model.lastName)"
:model-value="model.lastName" :model-value="model.lastName"
@@ -80,6 +88,7 @@
@update:model-value="(v: string) => update('phoneSecondary', v)" @update:model-value="(v: string) => update('phoneSecondary', v)"
/> />
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -98,6 +107,8 @@ const props = defineProps<{
title: string title: string
/** Affiche l'icone de suppression (1er bloc non supprimable, RG-1.14). */ /** Affiche l'icone de suppression (1er bloc non supprimable, RG-1.14). */
removable?: boolean removable?: boolean
/** Dernier bloc de la liste : supprime le filet de separation bas. */
last?: boolean
/** Bloc en lecture seule (onglet valide). */ /** Bloc en lecture seule (onglet valide). */
readonly?: boolean readonly?: boolean
/** Bloc desactive (champs grises, consultation — distinct de readonly). */ /** Bloc desactive (champs grises, consultation — distinct de readonly). */
@@ -1,15 +1,23 @@
<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
(pas de bordure sous le dernier bloc). -->
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
<!-- Suppression : modal de confirmation cote parent. --> <!-- Suppression : modal de confirmation cote parent. -->
<MalioButtonIcon <MalioButtonIcon
v-if="removable && !readonly && !disabled" v-if="removable && !readonly && !disabled"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('commercial.suppliers.form.address.remove') }" v-bind="{ ariaLabel: t('commercial.suppliers.form.address.remove') }"
@click="$emit('remove')" @click="$emit('remove')"
/> />
</div>
<!-- Grille 4 colonnes des champs de l'adresse. -->
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<!-- Type d'adresse : Prospect / Depart / Rendu (RG-2.09). Select en attendant <!-- Type d'adresse : Prospect / Depart / Rendu (RG-2.09). Select en attendant
l'arbitrage metier (radio vs select) ; l'erreur 422 (propertyPath l'arbitrage metier (radio vs select) ; l'erreur 422 (propertyPath
`addressType`) s'affiche via la prop native :error de MalioSelect. --> `addressType`) s'affiche via la prop native :error de MalioSelect. -->
@@ -185,6 +193,7 @@
@update:model-value="(v: boolean) => update('triageProvider', v)" @update:model-value="(v: boolean) => update('triageProvider', v)"
/> />
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -210,6 +219,8 @@ const props = defineProps<{
/** Pays disponibles (France par defaut). */ /** Pays disponibles (France par defaut). */
countryOptions: RefOption[] countryOptions: RefOption[]
removable?: boolean removable?: boolean
/** Dernier bloc de la liste : supprime le filet de separation bas. */
last?: boolean
readonly?: boolean readonly?: boolean
/** Bloc desactive (champs grises, consultation — distinct de readonly). */ /** Bloc desactive (champs grises, consultation — distinct de readonly). */
disabled?: boolean disabled?: boolean
@@ -1,16 +1,24 @@
<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
(pas de bordure sous le dernier bloc). -->
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
<!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si <!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si
non supprimable (1er bloc, RG-2.13) ou en lecture seule. --> non supprimable (1er bloc, RG-2.13) ou en lecture seule. -->
<MalioButtonIcon <MalioButtonIcon
v-if="removable && !readonly && !disabled" v-if="removable && !readonly && !disabled"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('commercial.suppliers.form.contact.remove') }" v-bind="{ ariaLabel: t('commercial.suppliers.form.contact.remove') }"
@click="$emit('remove')" @click="$emit('remove')"
/> />
</div>
<!-- Grille 4 colonnes des champs du contact. -->
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-if="!hideEmpty || isFilled(model.lastName)" v-if="!hideEmpty || isFilled(model.lastName)"
:model-value="model.lastName" :model-value="model.lastName"
@@ -79,6 +87,7 @@
@update:model-value="(v: string) => update('phoneSecondary', v)" @update:model-value="(v: string) => update('phoneSecondary', v)"
/> />
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -96,6 +105,8 @@ const props = defineProps<{
title: string title: string
/** Affiche l'icone de suppression (1er bloc non supprimable, RG-2.13). */ /** Affiche l'icone de suppression (1er bloc non supprimable, RG-2.13). */
removable?: boolean removable?: boolean
/** Dernier bloc de la liste : supprime le filet de separation bas. */
last?: boolean
/** Bloc en lecture seule (onglet valide). */ /** Bloc en lecture seule (onglet valide). */
readonly?: boolean readonly?: boolean
/** Bloc desactive (champs grises, consultation — distinct de readonly). */ /** Bloc desactive (champs grises, consultation — distinct de readonly). */
@@ -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). -->
@@ -178,6 +178,7 @@
:model-value="contact" :model-value="contact"
:title="t('commercial.clients.form.contact.title', { n: index + 1 })" :title="t('commercial.clients.form.contact.title', { n: index + 1 })"
:removable="isRowRemovable(contacts, index)" :removable="isRowRemovable(contacts, index)"
:last="index === contacts.length - 1"
:disabled="businessReadonly" :disabled="businessReadonly"
:errors="contactErrors[index]" :errors="contactErrors[index]"
@update:model-value="(v) => contacts[index] = v" @update:model-value="(v) => contacts[index] = v"
@@ -210,6 +211,7 @@
:key="address.id ?? `new-${index}`" :key="address.id ?? `new-${index}`"
:model-value="address" :model-value="address"
:title="t('commercial.clients.form.address.title', { n: index + 1 })" :title="t('commercial.clients.form.address.title', { n: index + 1 })"
:last="index === addresses.length - 1"
:category-options="addressCategoryOptions" :category-options="addressCategoryOptions"
:site-options="siteOptions" :site-options="siteOptions"
:contact-options="contactOptions" :contact-options="contactOptions"
@@ -244,8 +246,10 @@
editable uniquement si accounting.manage). --> editable uniquement si accounting.manage). -->
<template v-if="canAccountingView" #accounting> <template v-if="canAccountingView" #accounting>
<div class="mt-12 flex flex-col gap-6"> <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)]"> <!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.infoTitle') }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="accounting.siren" v-model="accounting.siren"
:label="t('commercial.clients.form.accounting.siren')" :label="t('commercial.clients.form.accounting.siren')"
@@ -314,21 +318,27 @@
</div> </div>
</div> </div>
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-1.13). --> <!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-1.13).
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
<div <div
v-for="(rib, index) in visibleRibs" v-for="(rib, index) in visibleRibs"
:key="rib.id ?? `new-${index}`" :key="rib.id ?? `new-${index}`"
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]" class="pb-[20px]"
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
> >
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
<MalioButtonIcon <MalioButtonIcon
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)" v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('commercial.clients.form.accounting.removeRib') }" v-bind="{ ariaLabel: t('commercial.clients.form.accounting.removeRib') }"
@click="askRemoveRib(index)" @click="askRemoveRib(index)"
/> />
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> </div>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="rib.label" v-model="rib.label"
:label="t('commercial.clients.form.accounting.ribLabel')" :label="t('commercial.clients.form.accounting.ribLabel')"
@@ -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). -->
@@ -156,6 +156,7 @@
:key="contact.id ?? index" :key="contact.id ?? index"
:model-value="contact" :model-value="contact"
:title="t('commercial.clients.form.contact.title', { n: index + 1 })" :title="t('commercial.clients.form.contact.title', { n: index + 1 })"
:last="index === contacts.length - 1"
disabled disabled
hide-empty hide-empty
/> />
@@ -170,6 +171,7 @@
:key="view.draft.id ?? index" :key="view.draft.id ?? index"
:model-value="view.draft" :model-value="view.draft"
:title="t('commercial.clients.form.address.title', { n: index + 1 })" :title="t('commercial.clients.form.address.title', { n: index + 1 })"
:last="index === addressViews.length - 1"
:category-options="view.categoryOptions" :category-options="view.categoryOptions"
:site-options="allSiteOptions" :site-options="allSiteOptions"
:contact-options="contactOptions" :contact-options="contactOptions"
@@ -183,8 +185,10 @@
<!-- Onglet Comptabilite (present uniquement si accounting.view). --> <!-- Onglet Comptabilite (present uniquement si accounting.view). -->
<template v-if="canAccountingView" #accounting> <template v-if="canAccountingView" #accounting>
<div class="mt-12 flex flex-col gap-6"> <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)]"> <!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <div class="pb-[20px]" :class="{ 'border-b border-black': ribs.length > 0 }">
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.infoTitle') }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-if="isFilled(accounting.siren)" v-if="isFilled(accounting.siren)"
:model-value="accounting.siren" :model-value="accounting.siren"
@@ -239,13 +243,16 @@
</div> </div>
</div> </div>
<!-- Blocs RIB (0..n), lecture seule. --> <!-- Blocs RIB (0..n), lecture seule.
Titre « RIB N », filet de separation sauf sous le dernier. -->
<div <div
v-for="(rib, index) in ribs" v-for="(rib, index) in ribs"
:key="rib.id ?? index" :key="rib.id ?? index"
class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]" class="pb-[20px]"
:class="{ 'border-b border-black': index !== ribs.length - 1 }"
> >
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-if="isFilled(rib.label)" v-if="isFilled(rib.label)"
:model-value="rib.label" :model-value="rib.label"
@@ -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
@@ -177,6 +177,7 @@
:model-value="contact" :model-value="contact"
:title="t('commercial.clients.form.contact.title', { n: index + 1 })" :title="t('commercial.clients.form.contact.title', { n: index + 1 })"
:removable="isRowRemovable(contacts, index)" :removable="isRowRemovable(contacts, index)"
:last="index === contacts.length - 1"
:disabled="isValidated('contact')" :disabled="isValidated('contact')"
:errors="contactErrors[index]" :errors="contactErrors[index]"
@update:model-value="(v) => contacts[index] = v" @update:model-value="(v) => contacts[index] = v"
@@ -209,6 +210,7 @@
:key="index" :key="index"
:model-value="address" :model-value="address"
:title="t('commercial.clients.form.address.title', { n: index + 1 })" :title="t('commercial.clients.form.address.title', { n: index + 1 })"
:last="index === addresses.length - 1"
:category-options="addressCategoryOptions" :category-options="addressCategoryOptions"
:site-options="referentials.sites.value" :site-options="referentials.sites.value"
:contact-options="contactOptions" :contact-options="contactOptions"
@@ -242,8 +244,10 @@
<!-- Onglet Comptabilite (present uniquement si accounting.view) --> <!-- Onglet Comptabilite (present uniquement si accounting.view) -->
<template v-if="canAccountingView" #accounting> <template v-if="canAccountingView" #accounting>
<div class="mt-12 flex flex-col gap-6"> <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)]"> <!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.infoTitle') }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="accounting.siren" v-model="accounting.siren"
:label="t('commercial.clients.form.accounting.siren')" :label="t('commercial.clients.form.accounting.siren')"
@@ -312,22 +316,28 @@
</div> </div>
</div> </div>
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-1.13). --> <!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-1.13).
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
<div <div
v-for="(rib, index) in visibleRibs" v-for="(rib, index) in visibleRibs"
:key="index" :key="index"
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]" class="pb-[20px]"
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
> >
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.clients.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
<!-- ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). --> <!-- ariaLabel via v-bind objet (prop camelCase ; aria-* serait un attribut HTML). -->
<MalioButtonIcon <MalioButtonIcon
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)" v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('commercial.clients.form.accounting.removeRib') }" v-bind="{ ariaLabel: t('commercial.clients.form.accounting.removeRib') }"
@click="askRemoveRib(index)" @click="askRemoveRib(index)"
/> />
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> </div>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="rib.label" v-model="rib.label"
:label="t('commercial.clients.form.accounting.ribLabel')" :label="t('commercial.clients.form.accounting.ribLabel')"
@@ -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"
@@ -147,6 +147,7 @@
:model-value="contact" :model-value="contact"
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })" :title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
:removable="isRowRemovable(contacts, index)" :removable="isRowRemovable(contacts, index)"
:last="index === contacts.length - 1"
:disabled="businessReadonly" :disabled="businessReadonly"
:errors="contactErrors[index]" :errors="contactErrors[index]"
@update:model-value="(v) => contacts[index] = v" @update:model-value="(v) => contacts[index] = v"
@@ -179,6 +180,7 @@
:key="address.id ?? `new-${index}`" :key="address.id ?? `new-${index}`"
:model-value="address" :model-value="address"
:title="t('commercial.suppliers.form.address.title', { n: index + 1 })" :title="t('commercial.suppliers.form.address.title', { n: index + 1 })"
:last="index === addresses.length - 1"
:category-options="mainCategoryOptions" :category-options="mainCategoryOptions"
:site-options="siteOptions" :site-options="siteOptions"
:contact-options="contactOptions" :contact-options="contactOptions"
@@ -213,8 +215,10 @@
editable uniquement si accounting.manage). --> editable uniquement si accounting.manage). -->
<template v-if="canAccountingView" #accounting> <template v-if="canAccountingView" #accounting>
<div class="mt-12 flex flex-col gap-6"> <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)]"> <!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.infoTitle') }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="accounting.siren" v-model="accounting.siren"
:label="t('commercial.suppliers.form.accounting.siren')" :label="t('commercial.suppliers.form.accounting.siren')"
@@ -283,21 +287,27 @@
</div> </div>
</div> </div>
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-2.08). --> <!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-2.08).
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
<div <div
v-for="(rib, index) in visibleRibs" v-for="(rib, index) in visibleRibs"
:key="rib.id ?? `new-${index}`" :key="rib.id ?? `new-${index}`"
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]" class="pb-[20px]"
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
> >
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
<MalioButtonIcon <MalioButtonIcon
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)" v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('commercial.suppliers.form.accounting.removeRib') }" v-bind="{ ariaLabel: t('commercial.suppliers.form.accounting.removeRib') }"
@click="askRemoveRib(index)" @click="askRemoveRib(index)"
/> />
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> </div>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="rib.label" v-model="rib.label"
:label="t('commercial.suppliers.form.accounting.ribLabel')" :label="t('commercial.suppliers.form.accounting.ribLabel')"
@@ -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
@@ -137,6 +137,7 @@
:key="contact.id ?? index" :key="contact.id ?? index"
:model-value="contact" :model-value="contact"
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })" :title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
:last="index === contacts.length - 1"
disabled disabled
hide-empty hide-empty
/> />
@@ -151,6 +152,7 @@
:key="view.draft.id ?? index" :key="view.draft.id ?? index"
:model-value="view.draft" :model-value="view.draft"
:title="t('commercial.suppliers.form.address.title', { n: index + 1 })" :title="t('commercial.suppliers.form.address.title', { n: index + 1 })"
:last="index === addressViews.length - 1"
:category-options="view.categoryOptions" :category-options="view.categoryOptions"
:site-options="allSiteOptions" :site-options="allSiteOptions"
:contact-options="contactOptions" :contact-options="contactOptions"
@@ -164,8 +166,10 @@
<!-- Onglet Comptabilite (present uniquement si accounting.view). --> <!-- Onglet Comptabilite (present uniquement si accounting.view). -->
<template v-if="canAccountingView" #accounting> <template v-if="canAccountingView" #accounting>
<div class="mt-12 flex flex-col gap-6"> <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)]"> <!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <div class="pb-[20px]" :class="{ 'border-b border-black': ribs.length > 0 }">
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.infoTitle') }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-if="isFilled(accounting.siren)" v-if="isFilled(accounting.siren)"
:model-value="accounting.siren" :model-value="accounting.siren"
@@ -220,13 +224,16 @@
</div> </div>
</div> </div>
<!-- Blocs RIB (0..n), lecture seule. --> <!-- Blocs RIB (0..n), lecture seule.
Titre « RIB N », filet de separation sauf sous le dernier. -->
<div <div
v-for="(rib, index) in ribs" v-for="(rib, index) in ribs"
:key="rib.id ?? index" :key="rib.id ?? index"
class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]" class="pb-[20px]"
:class="{ 'border-b border-black': index !== ribs.length - 1 }"
> >
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-if="isFilled(rib.label)" v-if="isFilled(rib.label)"
:model-value="rib.label" :model-value="rib.label"
@@ -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')"
@@ -145,6 +145,7 @@
:model-value="contact" :model-value="contact"
:title="t('commercial.suppliers.form.contact.title', { n: index + 1 })" :title="t('commercial.suppliers.form.contact.title', { n: index + 1 })"
:removable="isRowRemovable(contacts, index)" :removable="isRowRemovable(contacts, index)"
:last="index === contacts.length - 1"
:disabled="isValidated('contacts')" :disabled="isValidated('contacts')"
:errors="contactErrors[index]" :errors="contactErrors[index]"
@update:model-value="(v) => contacts[index] = v" @update:model-value="(v) => contacts[index] = v"
@@ -177,6 +178,7 @@
:key="index" :key="index"
:model-value="address" :model-value="address"
:title="t('commercial.suppliers.form.address.title', { n: index + 1 })" :title="t('commercial.suppliers.form.address.title', { n: index + 1 })"
:last="index === addresses.length - 1"
:category-options="referentials.categories.value" :category-options="referentials.categories.value"
:site-options="referentials.sites.value" :site-options="referentials.sites.value"
:contact-options="contactOptions" :contact-options="contactOptions"
@@ -210,8 +212,10 @@
<!-- Onglet Comptabilite (present uniquement si accounting.view) --> <!-- Onglet Comptabilite (present uniquement si accounting.view) -->
<template v-if="canAccountingView" #accounting> <template v-if="canAccountingView" #accounting>
<div class="mt-12 flex flex-col gap-6"> <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)]"> <!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.infoTitle') }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="accounting.siren" v-model="accounting.siren"
:label="t('commercial.suppliers.form.accounting.siren')" :label="t('commercial.suppliers.form.accounting.siren')"
@@ -280,21 +284,27 @@
</div> </div>
</div> </div>
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-2.08). --> <!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-2.08).
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
<div <div
v-for="(rib, index) in visibleRibs" v-for="(rib, index) in visibleRibs"
:key="index" :key="index"
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]" class="pb-[20px]"
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
> >
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ t('commercial.suppliers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
<MalioButtonIcon <MalioButtonIcon
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)" v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('commercial.suppliers.form.accounting.removeRib') }" v-bind="{ ariaLabel: t('commercial.suppliers.form.accounting.removeRib') }"
@click="askRemoveRib(index)" @click="askRemoveRib(index)"
/> />
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> </div>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="rib.label" v-model="rib.label"
:label="t('commercial.suppliers.form.accounting.ribLabel')" :label="t('commercial.suppliers.form.accounting.ribLabel')"
@@ -1,15 +1,23 @@
<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
(pas de bordure sous le dernier bloc). -->
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
<!-- Suppression : modal de confirmation cote parent. --> <!-- Suppression : modal de confirmation cote parent. -->
<MalioButtonIcon <MalioButtonIcon
v-if="removable && !readonly && !disabled" v-if="removable && !readonly && !disabled"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('technique.providers.form.address.remove') }" v-bind="{ ariaLabel: t('technique.providers.form.address.remove') }"
@click="$emit('remove')" @click="$emit('remove')"
/> />
</div>
<!-- Grille 4 colonnes des champs de l'adresse. -->
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<!-- Sites Starseed : multiselect a tags (>= 1 obligatoire, RG-3.05). --> <!-- Sites Starseed : multiselect a tags (>= 1 obligatoire, RG-3.05). -->
<MalioSelectCheckbox <MalioSelectCheckbox
v-if="!hideEmpty || isFilled(model.siteIris)" v-if="!hideEmpty || isFilled(model.siteIris)"
@@ -128,6 +136,7 @@
/> />
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -143,6 +152,8 @@ const POSTAL_CODE_MASK = '#####'
const props = defineProps<{ const props = defineProps<{
/** Brouillon de l'adresse (v-model). */ /** Brouillon de l'adresse (v-model). */
modelValue: ProviderAddressFormDraft modelValue: ProviderAddressFormDraft
/** Titre du bloc (ex: « Adresse 1 »). */
title: string
/** Sites Starseed disponibles. */ /** Sites Starseed disponibles. */
siteOptions: RefOption[] siteOptions: RefOption[]
/** Contacts deja saisis, rattachables a l'adresse. */ /** Contacts deja saisis, rattachables a l'adresse. */
@@ -150,6 +161,8 @@ const props = defineProps<{
/** Pays disponibles (France par defaut). */ /** Pays disponibles (France par defaut). */
countryOptions: RefOption[] countryOptions: RefOption[]
removable?: boolean removable?: boolean
/** Dernier bloc de la liste : supprime le filet de separation bas. */
last?: boolean
readonly?: boolean readonly?: boolean
/** Bloc desactive (champs grises, consultation — distinct de readonly). */ /** Bloc desactive (champs grises, consultation — distinct de readonly). */
disabled?: boolean disabled?: boolean
@@ -1,16 +1,24 @@
<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
(pas de bordure sous le dernier bloc). -->
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
<!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si <!-- Suppression : ouvre une modal de confirmation cote parent. Masquee si
non supprimable (1er bloc) ou en lecture seule. --> non supprimable (1er bloc) ou en lecture seule. -->
<MalioButtonIcon <MalioButtonIcon
v-if="removable && !readonly && !disabled" v-if="removable && !readonly && !disabled"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('technique.providers.form.contact.remove') }" v-bind="{ ariaLabel: t('technique.providers.form.contact.remove') }"
@click="$emit('remove')" @click="$emit('remove')"
/> />
</div>
<!-- Grille 4 colonnes des champs du contact. -->
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-if="!hideEmpty || isFilled(model.lastName)" v-if="!hideEmpty || isFilled(model.lastName)"
:model-value="model.lastName" :model-value="model.lastName"
@@ -80,6 +88,7 @@
@update:model-value="(v: string) => update('phoneSecondary', v)" @update:model-value="(v: string) => update('phoneSecondary', v)"
/> />
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -93,8 +102,12 @@ const PHONE_MASK = '## ## ## ## ##'
const props = defineProps<{ const props = defineProps<{
/** Brouillon du contact (v-model). */ /** Brouillon du contact (v-model). */
modelValue: ProviderContactFormDraft modelValue: ProviderContactFormDraft
/** Titre du bloc (ex: « Contact 1 »). */
title: string
/** Affiche l'icone de suppression (1er bloc non supprimable). */ /** Affiche l'icone de suppression (1er bloc non supprimable). */
removable?: boolean removable?: boolean
/** Dernier bloc de la liste : supprime le filet de separation bas. */
last?: boolean
/** Bloc en lecture seule (onglet valide). */ /** Bloc en lecture seule (onglet valide). */
readonly?: boolean readonly?: boolean
/** Bloc desactive (champs grises, consultation — distinct de readonly). */ /** Bloc desactive (champs grises, consultation — distinct de readonly). */
@@ -46,6 +46,7 @@ function mountBlock(overrides: Record<string, unknown> = {}, errors?: Record<str
return mount(ProviderAddressBlock, { return mount(ProviderAddressBlock, {
props: { props: {
modelValue: { ...emptyProviderAddress(), ...overrides }, modelValue: { ...emptyProviderAddress(), ...overrides },
title: 'Adresse 1',
siteOptions: [], siteOptions: [],
contactOptions: [], contactOptions: [],
countryOptions: [], countryOptions: [],
@@ -29,6 +29,7 @@ function mountBlock(errors?: Record<string, string>) {
return mount(ProviderContactBlock, { return mount(ProviderContactBlock, {
props: { props: {
modelValue: emptyProviderContact(), modelValue: emptyProviderContact(),
title: 'Contact 1',
...(errors ? { errors } : {}), ...(errors ? { errors } : {}),
}, },
global: { global: {
@@ -72,7 +72,9 @@
v-for="(contact, index) in contacts" v-for="(contact, index) in contacts"
:key="index" :key="index"
:model-value="contact" :model-value="contact"
:title="t('technique.providers.form.contact.title', { n: index + 1 })"
:removable="isRowRemovable(contacts, index)" :removable="isRowRemovable(contacts, index)"
:last="index === contacts.length - 1"
:disabled="businessReadonly" :disabled="businessReadonly"
:errors="contactErrors[index]" :errors="contactErrors[index]"
@update:model-value="(v) => contacts[index] = v" @update:model-value="(v) => contacts[index] = v"
@@ -104,6 +106,8 @@
v-for="(address, index) in addresses" v-for="(address, index) in addresses"
:key="index" :key="index"
:model-value="address" :model-value="address"
:title="t('technique.providers.form.address.title', { n: index + 1 })"
:last="index === addresses.length - 1"
:site-options="referentials.sites.value" :site-options="referentials.sites.value"
:contact-options="contactOptions" :contact-options="contactOptions"
:country-options="countryOptions" :country-options="countryOptions"
@@ -136,8 +140,10 @@
<!-- Onglet Comptabilite (present si accounting.view ; editable si manage). --> <!-- Onglet Comptabilite (present si accounting.view ; editable si manage). -->
<template v-if="canAccountingView" #accounting> <template v-if="canAccountingView" #accounting>
<div class="mt-12 flex flex-col gap-6"> <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)]"> <!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
<h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.infoTitle') }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="accounting.siren" v-model="accounting.siren"
:label="t('technique.providers.form.accounting.siren')" :label="t('technique.providers.form.accounting.siren')"
@@ -206,21 +212,27 @@
</div> </div>
</div> </div>
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-3.08). --> <!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-3.08).
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
<div <div
v-for="(rib, index) in visibleRibs" v-for="(rib, index) in visibleRibs"
:key="index" :key="index"
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]" class="pb-[20px]"
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
> >
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
<MalioButtonIcon <MalioButtonIcon
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)" v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('technique.providers.form.accounting.removeRib') }" v-bind="{ ariaLabel: t('technique.providers.form.accounting.removeRib') }"
@click="askRemoveRib(index)" @click="askRemoveRib(index)"
/> />
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> </div>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="rib.label" v-model="rib.label"
:label="t('technique.providers.form.accounting.ribLabel')" :label="t('technique.providers.form.accounting.ribLabel')"
@@ -81,6 +81,8 @@
v-for="(contact, index) in contacts" v-for="(contact, index) in contacts"
:key="index" :key="index"
:model-value="contact" :model-value="contact"
:title="t('technique.providers.form.contact.title', { n: index + 1 })"
:last="index === contacts.length - 1"
disabled disabled
hide-empty hide-empty
/> />
@@ -94,6 +96,8 @@
v-for="(view, index) in addressViews" v-for="(view, index) in addressViews"
:key="index" :key="index"
:model-value="view.draft" :model-value="view.draft"
:title="t('technique.providers.form.address.title', { n: index + 1 })"
:last="index === addressViews.length - 1"
:site-options="view.siteOptions" :site-options="view.siteOptions"
:contact-options="contactOptions" :contact-options="contactOptions"
:country-options="countryOptionsFor(view.draft.country)" :country-options="countryOptionsFor(view.draft.country)"
@@ -108,8 +112,10 @@
<!-- Onglet Comptabilite (present uniquement si accounting.view). --> <!-- Onglet Comptabilite (present uniquement si accounting.view). -->
<template v-if="canAccountingView" #accounting> <template v-if="canAccountingView" #accounting>
<div class="mt-12 flex flex-col gap-6"> <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)]"> <!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
<h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.infoTitle') }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText v-if="isFilled(accounting.siren)" :model-value="accounting.siren" :label="t('technique.providers.form.accounting.siren')" disabled /> <MalioInputText v-if="isFilled(accounting.siren)" :model-value="accounting.siren" :label="t('technique.providers.form.accounting.siren')" disabled />
<MalioInputText v-if="isFilled(accounting.accountNumber)" :model-value="accounting.accountNumber" :label="t('technique.providers.form.accounting.accountNumber')" disabled /> <MalioInputText v-if="isFilled(accounting.accountNumber)" :model-value="accounting.accountNumber" :label="t('technique.providers.form.accounting.accountNumber')" disabled />
<MalioSelect v-if="isFilled(accounting.tvaModeIri)" :model-value="accounting.tvaModeIri" :options="tvaModeOptions" :label="t('technique.providers.form.accounting.tvaMode')" disabled empty-option-label="" /> <MalioSelect v-if="isFilled(accounting.tvaModeIri)" :model-value="accounting.tvaModeIri" :options="tvaModeOptions" :label="t('technique.providers.form.accounting.tvaMode')" disabled empty-option-label="" />
@@ -120,13 +126,16 @@
</div> </div>
</div> </div>
<!-- Blocs RIB (uniquement si type de reglement = LCR). --> <!-- Blocs RIB (uniquement si type de reglement = LCR).
Titre « RIB N », filet de separation sauf sous le dernier. -->
<div <div
v-for="(rib, index) in visibleRibs" v-for="(rib, index) in visibleRibs"
:key="index" :key="index"
class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]" class="pb-[20px]"
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
> >
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText v-if="isFilled(rib.label)" :model-value="rib.label" :label="t('technique.providers.form.accounting.ribLabel')" disabled /> <MalioInputText v-if="isFilled(rib.label)" :model-value="rib.label" :label="t('technique.providers.form.accounting.ribLabel')" disabled />
<MalioInputText v-if="isFilled(rib.bic)" :model-value="rib.bic" :label="t('technique.providers.form.accounting.ribBic')" disabled /> <MalioInputText v-if="isFilled(rib.bic)" :model-value="rib.bic" :label="t('technique.providers.form.accounting.ribBic')" disabled />
<MalioInputText v-if="isFilled(rib.iban)" :model-value="rib.iban" :label="t('technique.providers.form.accounting.ribIban')" disabled /> <MalioInputText v-if="isFilled(rib.iban)" :model-value="rib.iban" :label="t('technique.providers.form.accounting.ribIban')" disabled />
@@ -73,7 +73,9 @@
v-for="(contact, index) in contacts" v-for="(contact, index) in contacts"
:key="index" :key="index"
:model-value="contact" :model-value="contact"
:title="t('technique.providers.form.contact.title', { n: index + 1 })"
:removable="isRowRemovable(contacts, index)" :removable="isRowRemovable(contacts, index)"
:last="index === contacts.length - 1"
:disabled="isValidated('contact')" :disabled="isValidated('contact')"
:errors="contactErrors[index]" :errors="contactErrors[index]"
@update:model-value="(v) => contacts[index] = v" @update:model-value="(v) => contacts[index] = v"
@@ -108,6 +110,8 @@
v-for="(address, index) in addresses" v-for="(address, index) in addresses"
:key="index" :key="index"
:model-value="address" :model-value="address"
:title="t('technique.providers.form.address.title', { n: index + 1 })"
:last="index === addresses.length - 1"
:site-options="referentials.sites.value" :site-options="referentials.sites.value"
:contact-options="contactOptions" :contact-options="contactOptions"
:country-options="countryOptions" :country-options="countryOptions"
@@ -139,8 +143,10 @@
<!-- Onglet Comptabilite (present uniquement si accounting.view ; editable si manage). --> <!-- Onglet Comptabilite (present uniquement si accounting.view ; editable si manage). -->
<template v-if="canAccountingView" #accounting> <template v-if="canAccountingView" #accounting>
<div class="mt-12 flex flex-col gap-6"> <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)]"> <!-- Bloc infos comptables : titre + filet bas (filet uniquement s'il y a des RIB en dessous). -->
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> <div class="pb-[20px]" :class="{ 'border-b border-black': visibleRibs.length > 0 }">
<h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.infoTitle') }}</h2>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="accounting.siren" v-model="accounting.siren"
:label="t('technique.providers.form.accounting.siren')" :label="t('technique.providers.form.accounting.siren')"
@@ -210,21 +216,27 @@
</div> </div>
</div> </div>
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-3.08). --> <!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-3.08).
Titre « RIB N » + poubelle, filet de separation sauf sous le dernier. -->
<div <div
v-for="(rib, index) in visibleRibs" v-for="(rib, index) in visibleRibs"
:key="index" :key="index"
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]" class="pb-[20px]"
:class="{ 'border-b border-black': index !== visibleRibs.length - 1 }"
> >
<!-- En-tete : titre du bloc (noir) a gauche, poubelle a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ t('technique.providers.form.accounting.ribTitle', { n: index + 1 }) }}</h2>
<MalioButtonIcon <MalioButtonIcon
v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)" v-if="!accountingReadonly && isRowRemovable(visibleRibs, index)"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('technique.providers.form.accounting.removeRib') }" v-bind="{ ariaLabel: t('technique.providers.form.accounting.removeRib') }"
@click="askRemoveRib(index)" @click="askRemoveRib(index)"
/> />
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4"> </div>
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-model="rib.label" v-model="rib.label"
:label="t('technique.providers.form.accounting.ribLabel')" :label="t('technique.providers.form.accounting.ribLabel')"
@@ -1,6 +1,15 @@
<template> <template>
<!-- Adresse UNIQUE par transporteur (ERP-172) : un seul bloc, jamais supprimable. --> <!-- Adresse UNIQUE par transporteur (ERP-172) : un seul bloc, jamais supprimable. -->
<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
(pas de bordure sous le dernier bloc). -->
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
<!-- En-tete : titre du bloc, en noir (adresse unique, sans suppression). -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
</div>
<!-- Grille 4 colonnes des champs de l'adresse. -->
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<!-- Pays : prerempli « France » (RG-4.05). --> <!-- Pays : prerempli « France » (RG-4.05). -->
<MalioSelect <MalioSelect
v-if="!hideEmpty || isFilled(model.country)" v-if="!hideEmpty || isFilled(model.country)"
@@ -99,6 +108,7 @@
@update:model-value="(v: string) => update('streetComplement', v)" @update:model-value="(v: string) => update('streetComplement', v)"
/> />
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -118,8 +128,12 @@ const POSTAL_CODE_MASK = '#####'
const props = defineProps<{ const props = defineProps<{
/** Brouillon de l'adresse (v-model). */ /** Brouillon de l'adresse (v-model). */
modelValue: CarrierAddressFormDraft modelValue: CarrierAddressFormDraft
/** Titre du bloc (ex: « Adresse 1 »). */
title: string
/** Pays disponibles (France par defaut). */ /** Pays disponibles (France par defaut). */
countryOptions: RefOption[] countryOptions: RefOption[]
/** Dernier bloc de la liste : supprime le filet de separation bas. */
last?: boolean
readonly?: boolean readonly?: boolean
/** Bloc desactive (champs grises, consultation — distinct de readonly). */ /** Bloc desactive (champs grises, consultation — distinct de readonly). */
disabled?: boolean disabled?: boolean
@@ -1,16 +1,24 @@
<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
(pas de bordure sous le dernier bloc). -->
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
<!-- Suppression : ouvre une modal de confirmation côté parent. Masquée si <!-- Suppression : ouvre une modal de confirmation côté parent. Masquée si
non supprimable (1er bloc) ou en lecture seule. --> non supprimable (1er bloc) ou en lecture seule. -->
<MalioButtonIcon <MalioButtonIcon
v-if="removable && !readonly && !disabled" v-if="removable && !readonly && !disabled"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('transport.carriers.form.contact.remove') }" v-bind="{ ariaLabel: t('transport.carriers.form.contact.remove') }"
@click="$emit('remove')" @click="$emit('remove')"
/> />
</div>
<!-- Grille 4 colonnes des champs du contact. -->
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
v-if="!hideEmpty || isFilled(model.lastName)" v-if="!hideEmpty || isFilled(model.lastName)"
:model-value="model.lastName" :model-value="model.lastName"
@@ -80,6 +88,7 @@
@update:model-value="(v: string) => update('phoneSecondary', v)" @update:model-value="(v: string) => update('phoneSecondary', v)"
/> />
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -93,8 +102,12 @@ const PHONE_MASK = '## ## ## ## ##'
const props = defineProps<{ const props = defineProps<{
/** Brouillon du contact (v-model). */ /** Brouillon du contact (v-model). */
modelValue: CarrierContactFormDraft modelValue: CarrierContactFormDraft
/** Titre du bloc (ex: « Contact 1 »). */
title: string
/** Affiche l'icône de suppression (1er bloc non supprimable). */ /** Affiche l'icône de suppression (1er bloc non supprimable). */
removable?: boolean removable?: boolean
/** Dernier bloc de la liste : supprime le filet de separation bas. */
last?: boolean
/** Bloc en lecture seule (onglet validé). */ /** Bloc en lecture seule (onglet validé). */
readonly?: boolean readonly?: boolean
/** Bloc desactive (champs grises, consultation — distinct de readonly). */ /** Bloc desactive (champs grises, consultation — distinct de readonly). */
@@ -1,15 +1,23 @@
<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
(pas de bordure sous le dernier bloc), aligne sur les blocs contact / adresse. -->
<div class="pb-[20px]" :class="{ 'border-b border-black': !last }">
<!-- En-tete : titre du bloc (noir) a gauche, poubelle de suppression a droite. -->
<div class="flex items-center justify-between">
<h2 class="text-[20px] font-semibold text-black">{{ title }}</h2>
<!-- Suppression : modal de confirmation côté parent. --> <!-- Suppression : modal de confirmation côté parent. -->
<MalioButtonIcon <MalioButtonIcon
v-if="removable && !readonly && !disabled" v-if="removable && !readonly && !disabled"
icon="mdi:delete-outline" icon="mdi:delete-outline"
variant="ghost" variant="ghost"
button-class="absolute top-3 right-3" button-class="p-0"
v-bind="{ ariaLabel: t('transport.carriers.form.price.remove') }" v-bind="{ ariaLabel: t('transport.carriers.form.price.remove') }"
@click="$emit('remove')" @click="$emit('remove')"
/> />
</div>
<!-- Grille 4 colonnes des champs du prix. -->
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<!-- RG-4.09 : sens du prix (CLIENT / FOURNISSEUR) en colonne 1 / ligne 1, radios <!-- RG-4.09 : sens du prix (CLIENT / FOURNISSEUR) en colonne 1 / ligne 1, radios
EN LIGNE (horizontaux), centrés sur la hauteur de champ (h-12) comme la EN LIGNE (horizontaux), centrés sur la hauteur de champ (h-12) comme la
case « Affréter ». Pas de label de groupe. --> case « Affréter ». Pas de label de groupe. -->
@@ -186,6 +194,7 @@
/> />
</template> </template>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -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). */
@@ -56,6 +56,7 @@ function mountBlock(overrides: Record<string, unknown> = {}) {
return mount(CarrierAddressBlock, { return mount(CarrierAddressBlock, {
props: { props: {
modelValue: { ...emptyCarrierAddress(), ...overrides }, modelValue: { ...emptyCarrierAddress(), ...overrides },
title: 'Adresse 1',
countryOptions: [{ value: 'France', label: 'France' }], countryOptions: [{ value: 'France', label: 'France' }],
}, },
global: { global: {
@@ -143,6 +143,8 @@
<!-- Adresse UNIQUE (ERP-172) : un seul bloc, sans ajouter/supprimer. --> <!-- Adresse UNIQUE (ERP-172) : un seul bloc, sans ajouter/supprimer. -->
<CarrierAddressBlock <CarrierAddressBlock
:model-value="address" :model-value="address"
:title="t('transport.carriers.form.address.title')"
:last="true"
:country-options="countryOptions" :country-options="countryOptions"
:errors="addressErrors" :errors="addressErrors"
@update:model-value="(v) => address = v" @update:model-value="(v) => address = v"
@@ -160,7 +162,9 @@
v-for="(contact, index) in contacts" v-for="(contact, index) in contacts"
:key="index" :key="index"
:model-value="contact" :model-value="contact"
:title="t('transport.carriers.form.contact.title', { n: index + 1 })"
:removable="isRowRemovable(contacts, index)" :removable="isRowRemovable(contacts, index)"
:last="index === contacts.length - 1"
:errors="contactErrors[index]" :errors="contactErrors[index]"
@update:model-value="(v) => contacts[index] = v" @update:model-value="(v) => contacts[index] = v"
@remove="askRemoveContact(index)" @remove="askRemoveContact(index)"
@@ -178,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)"
@@ -123,6 +123,8 @@
<!-- Adresse UNIQUE (ERP-172). --> <!-- Adresse UNIQUE (ERP-172). -->
<CarrierAddressBlock <CarrierAddressBlock
:model-value="address" :model-value="address"
:title="t('transport.carriers.form.address.title')"
:last="true"
:country-options="countryOptionsFor(address.country)" :country-options="countryOptionsFor(address.country)"
disabled disabled
hide-empty hide-empty
@@ -136,6 +138,8 @@
v-for="(contact, index) in contacts" v-for="(contact, index) in contacts"
:key="index" :key="index"
:model-value="contact" :model-value="contact"
:title="t('transport.carriers.form.contact.title', { n: index + 1 })"
:last="index === contacts.length - 1"
disabled disabled
hide-empty hide-empty
/> />
@@ -180,6 +180,8 @@
<!-- Adresse UNIQUE (ERP-172) : un seul bloc, sans ajouter/supprimer. --> <!-- Adresse UNIQUE (ERP-172) : un seul bloc, sans ajouter/supprimer. -->
<CarrierAddressBlock <CarrierAddressBlock
:model-value="address" :model-value="address"
:title="t('transport.carriers.form.address.title')"
:last="true"
:country-options="countryOptions" :country-options="countryOptions"
:disabled="isQualimat || isValidated('addresses')" :disabled="isQualimat || isValidated('addresses')"
:errors="addressErrors" :errors="addressErrors"
@@ -207,7 +209,9 @@
v-for="(contact, index) in contacts" v-for="(contact, index) in contacts"
:key="index" :key="index"
:model-value="contact" :model-value="contact"
:title="t('transport.carriers.form.contact.title', { n: index + 1 })"
:removable="isRowRemovable(contacts, index)" :removable="isRowRemovable(contacts, index)"
:last="index === contacts.length - 1"
:disabled="isValidated('contacts')" :disabled="isValidated('contacts')"
:errors="contactErrors[index]" :errors="contactErrors[index]"
@update:model-value="(v) => contacts[index] = v" @update:model-value="(v) => contacts[index] = v"
@@ -240,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)"