596 lines
25 KiB
Vue
596 lines
25 KiB
Vue
<template>
|
|
<div>
|
|
<!-- En-tete : retour vers le repertoire + titre. -->
|
|
<div class="flex items-center gap-3 pt-11">
|
|
<MalioButtonIcon
|
|
icon="mdi:arrow-left-bold"
|
|
icon-size="24"
|
|
variant="ghost"
|
|
v-bind="{ ariaLabel: t('transport.carriers.form.back') }"
|
|
@click="goBack"
|
|
/>
|
|
<h1 class="text-[30px] font-semibold text-m-primary">{{ t('transport.carriers.form.title') }}</h1>
|
|
</div>
|
|
|
|
<!-- ── Formulaire principal (pre-onglets) ─────────────────────────────
|
|
Champs conditionnels (ERP-166) : cas LIOT (nom == LIOT) → seul
|
|
« immatriculations » ; certification AUTRE → champ Decharge ; Affreter
|
|
coche → indexation / contenant / volume. La certification est en lecture
|
|
seule pour un transporteur QUALIMAT (saisie assistee, onglet Qualimat). -->
|
|
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
|
<MalioInputText
|
|
v-model="main.name"
|
|
:label="t('transport.carriers.form.main.name')"
|
|
:required="true"
|
|
:readonly="mainLocked"
|
|
:error="mainErrors.errors.name"
|
|
/>
|
|
|
|
<!-- Cas LIOT : seul le champ immatriculations est pertinent. -->
|
|
<MalioInputText
|
|
v-if="isLiot"
|
|
v-model="main.liotPlates"
|
|
:label="t('transport.carriers.form.main.liotPlates')"
|
|
:hint="t('transport.carriers.form.main.liotPlatesHint')"
|
|
:required="true"
|
|
:readonly="mainLocked"
|
|
:error="mainErrors.errors.liotPlates"
|
|
/>
|
|
|
|
<!-- Cas standard : certification + affretement + champs conditionnels. -->
|
|
<template v-if="!isLiot">
|
|
<MalioSelect
|
|
:model-value="main.certificationType"
|
|
:options="certificationOptions"
|
|
:label="t('transport.carriers.form.main.certificationType')"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:readonly="certificationReadonly"
|
|
:error="mainErrors.errors.certificationType"
|
|
@update:model-value="(v: string | number | null) => main.certificationType = v === null ? null : String(v)"
|
|
/>
|
|
|
|
<!-- Colonne 3 RÉSERVÉE à la Décharge (RG-4.02 : visible et obligatoire
|
|
si certification AUTRE). Si elle n'apparaît pas, on garde la colonne
|
|
vide (xl) pour qu'« Affréter » reste en colonne 4 de la ligne 1.
|
|
Upload DIFFÉRÉ (ERP-171) : le fichier choisi est mis en attente
|
|
et envoyé seulement à la validation du formulaire. -->
|
|
<MalioInputUpload
|
|
v-if="showDischarge"
|
|
:model-value="dischargeFileName"
|
|
:label="t('transport.carriers.form.main.discharge')"
|
|
accept="application/pdf,image/*"
|
|
:required="true"
|
|
:readonly="mainLocked || dischargeUploading"
|
|
:clearable="true"
|
|
:error="mainErrors.errors.dischargeDocument"
|
|
@update:model-value="(v: string) => dischargeFileName = v"
|
|
@file-selected="selectDischarge"
|
|
@clear="onClearDischarge"
|
|
/>
|
|
<div v-else class="hidden xl:block"></div>
|
|
|
|
<!-- « Affréter » : toujours en colonne 4 de la ligne 1 (colonne 3
|
|
réservée à la décharge ci-dessus). Wrapper h-12 + centrage vertical
|
|
pour aligner la case sur la ligne de champ des inputs/selects. -->
|
|
<div class="flex h-12 items-center">
|
|
<MalioCheckbox
|
|
id="carrier-is-chartered"
|
|
:label="t('transport.carriers.form.main.isChartered')"
|
|
:model-value="main.isChartered"
|
|
:readonly="mainLocked"
|
|
:reserve-message-space="false"
|
|
@update:model-value="(val: boolean) => main.isChartered = val"
|
|
/>
|
|
</div>
|
|
|
|
<!-- RG-4.03 : champs d'affretement (ligne 2) visibles + obligatoires si
|
|
« Affreter ». La ligne 1 étant pleine (4 colonnes), ils démarrent
|
|
naturellement en colonne 1 de la ligne 2. -->
|
|
<template v-if="showCharteredFields">
|
|
<!-- Indexation : montant en % (icône à droite), plafonné à 100. La
|
|
:key force le ré-affichage du champ contrôlé quand on plafonne
|
|
(sinon le modelValue inchangé n'est pas re-synchronisé par Vue). -->
|
|
<MalioInputAmount
|
|
:key="indexationKey"
|
|
:model-value="main.indexationRate"
|
|
:label="t('transport.carriers.form.main.indexationRate')"
|
|
icon-name="mdi:percent"
|
|
icon-position="right"
|
|
:required="true"
|
|
:readonly="mainLocked"
|
|
:error="mainErrors.errors.indexationRate"
|
|
@update:model-value="onIndexationInput"
|
|
/>
|
|
|
|
<!-- Contenant : Benne / Fond mouvant en radios, centrés (h-12) comme
|
|
à l'onglet Prix (Benne par défaut). -->
|
|
<div>
|
|
<div class="flex h-12 items-center gap-4">
|
|
<MalioRadioButton
|
|
:model-value="main.containerType"
|
|
name="carrier-main-container"
|
|
value="BENNE"
|
|
:label="t('transport.carriers.containerType.BENNE')"
|
|
:disabled="mainLocked"
|
|
group-class="mt-0"
|
|
@update:model-value="(v: string | number | boolean | null) => main.containerType = v === null ? null : String(v)"
|
|
/>
|
|
<MalioRadioButton
|
|
:model-value="main.containerType"
|
|
name="carrier-main-container"
|
|
value="FOND_MOUVANT"
|
|
:label="t('transport.carriers.containerType.FOND_MOUVANT')"
|
|
:disabled="mainLocked"
|
|
group-class="mt-0"
|
|
@update:model-value="(v: string | number | boolean | null) => main.containerType = v === null ? null : String(v)"
|
|
/>
|
|
</div>
|
|
<p v-if="mainErrors.errors.containerType" class="ml-[2px] text-xs text-m-danger">{{ mainErrors.errors.containerType }}</p>
|
|
</div>
|
|
|
|
<!-- Volume m³ : champ texte restreint aux nombres à décimales (point). -->
|
|
<MalioInputText
|
|
:model-value="main.volumeM3"
|
|
:label="t('transport.carriers.form.main.volumeM3')"
|
|
:required="true"
|
|
:readonly="mainLocked"
|
|
:error="mainErrors.errors.volumeM3"
|
|
@update:model-value="(v: string) => main.volumeM3 = sanitizeDecimal(v)"
|
|
/>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="!mainLocked" class="mt-12 flex justify-center">
|
|
<MalioButton
|
|
variant="primary"
|
|
:label="t('transport.carriers.form.submit')"
|
|
:disabled="mainSubmitting"
|
|
@click="onSubmitMain"
|
|
/>
|
|
</div>
|
|
|
|
<!-- ── Onglets a validation incrementale ─────────────────────────────
|
|
Barre Qualimat · Adresses · Contacts · Prix. Onglet Qualimat = saisie
|
|
assistee (table de selection) ; Adresses / Contacts / Prix arrivent aux
|
|
tickets suivants (placeholders « A venir »). -->
|
|
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
|
|
<!-- Onglet Qualimat : saisie assistée (recherche par nom). Composant
|
|
mutualisé avec l'écran de modification (ERP-172). -->
|
|
<template #qualimat>
|
|
<CarrierQualimatTab
|
|
:search-name="main.name"
|
|
:selected-iri="main.qualimatCarrierIri"
|
|
@integrate="onIntegrateQualimat"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Onglet Adresses (ERP-167) : un bloc par adresse + BAN. RG-4.05
|
|
préremplissage si QUALIMAT ; RG-4.07 pas de Valider si QUALIMAT. -->
|
|
<template #addresses>
|
|
<div class="mt-12 flex flex-col gap-6">
|
|
<!-- Adresse UNIQUE (ERP-172) : un seul bloc, sans ajouter/supprimer. -->
|
|
<CarrierAddressBlock
|
|
:model-value="address"
|
|
:country-options="countryOptions"
|
|
:removable="false"
|
|
:readonly="isQualimat || isValidated('addresses')"
|
|
:errors="addressErrors"
|
|
@update:model-value="(v) => address = v"
|
|
@degraded="onAddressDegraded"
|
|
/>
|
|
<!-- RG-4.07 : pas de bouton Valider pour un transporteur QUALIMAT
|
|
(adresse copiée et persistée automatiquement). -->
|
|
<div v-if="!isQualimat && !isValidated('addresses')" class="flex justify-center gap-6">
|
|
<MalioButton
|
|
variant="primary"
|
|
:label="t('transport.carriers.form.submit')"
|
|
:disabled="tabSubmitting || carrierId === null"
|
|
@click="onSubmitAddresses"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Onglet Contacts (ERP-168) : un bloc par contact (RG-4.08 ≥ 1 champ,
|
|
max 2 téléphones). Erreurs 422 par ligne. -->
|
|
<template #contacts>
|
|
<div class="mt-12 flex flex-col gap-6">
|
|
<CarrierContactBlock
|
|
v-for="(contact, index) in contacts"
|
|
:key="index"
|
|
:model-value="contact"
|
|
:removable="isRowRemovable(contacts, index)"
|
|
:readonly="isValidated('contacts')"
|
|
:errors="contactErrors[index]"
|
|
@update:model-value="(v) => contacts[index] = v"
|
|
@remove="askRemoveContact(index)"
|
|
/>
|
|
<div v-if="!isValidated('contacts')" class="flex justify-center gap-6">
|
|
<MalioButton
|
|
variant="secondary"
|
|
icon-name="mdi:add-bold"
|
|
icon-position="left"
|
|
:label="t('transport.carriers.form.contact.add')"
|
|
:disabled="!canAddContact"
|
|
@click="addContact"
|
|
/>
|
|
<MalioButton
|
|
variant="primary"
|
|
:label="t('transport.carriers.form.submit')"
|
|
:disabled="tabSubmitting || carrierId === null"
|
|
@click="onSubmitContacts"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Onglet Prix (ERP-169) : blocs multiples, branche CLIENT/FOURNISSEUR
|
|
(RG-4.09→4.11). Démarre vide ; l'utilisateur ajoute via « + Nouveau prix ». -->
|
|
<template #prices>
|
|
<div class="mt-12 flex flex-col gap-6">
|
|
<CarrierPriceBlock
|
|
v-for="(price, index) in prices"
|
|
:key="index"
|
|
:model-value="price"
|
|
:client-options="clientOptions"
|
|
:supplier-options="supplierOptions"
|
|
:site-options="siteOptions"
|
|
:removable="!isValidated('prices')"
|
|
:readonly="isValidated('prices')"
|
|
:errors="priceErrors[index]"
|
|
@update:model-value="(v) => prices[index] = v"
|
|
@remove="askRemovePrice(index)"
|
|
/>
|
|
<div v-if="!isValidated('prices')" class="flex justify-center gap-6">
|
|
<MalioButton
|
|
variant="secondary"
|
|
icon-name="mdi:add-bold"
|
|
icon-position="left"
|
|
:label="t('transport.carriers.form.price.add')"
|
|
:disabled="!canAddPrice"
|
|
@click="addPrice"
|
|
/>
|
|
<MalioButton
|
|
variant="primary"
|
|
:label="t('transport.carriers.form.submit')"
|
|
:disabled="tabSubmitting || carrierId === null"
|
|
@click="onSubmitPrices"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Plus d'onglet placeholder : tous les onglets ont leur contenu. -->
|
|
<template
|
|
v-for="key in placeholderTabs"
|
|
:key="key"
|
|
#[key]
|
|
>
|
|
<div class="mt-12 flex justify-center text-m-muted">
|
|
{{ t('transport.carriers.form.comingSoon') }}
|
|
</div>
|
|
</template>
|
|
</MalioTabList>
|
|
|
|
<!-- Modal de confirmation de suppression (bloc contact / prix). -->
|
|
<MalioModal v-model="deleteConfirm.open" modal-class="max-w-md">
|
|
<template #header>
|
|
<h2 class="text-[24px] font-bold">{{ t('transport.carriers.form.confirmDelete.title') }}</h2>
|
|
</template>
|
|
<p>{{ t('transport.carriers.form.confirmDelete.message') }}</p>
|
|
<template #footer>
|
|
<MalioButton
|
|
variant="secondary"
|
|
button-class="flex-1"
|
|
:label="t('transport.carriers.form.confirmDelete.cancel')"
|
|
@click="deleteConfirm.open = false"
|
|
/>
|
|
<MalioButton
|
|
variant="danger"
|
|
button-class="flex-1"
|
|
:label="t('transport.carriers.form.confirmDelete.confirm')"
|
|
@click="runDeleteConfirm"
|
|
/>
|
|
</template>
|
|
</MalioModal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
import { extractApiErrorMessage } from '~/shared/utils/api'
|
|
import { isRowRemovable } from '~/shared/utils/collectionRow'
|
|
import CarrierAddressBlock from '~/modules/transport/components/CarrierAddressBlock.vue'
|
|
import CarrierContactBlock from '~/modules/transport/components/CarrierContactBlock.vue'
|
|
import CarrierPriceBlock from '~/modules/transport/components/CarrierPriceBlock.vue'
|
|
import CarrierQualimatTab from '~/modules/transport/components/CarrierQualimatTab.vue'
|
|
import { useCarrierForm } from '~/modules/transport/composables/useCarrierForm'
|
|
import type { QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
|
|
import { clampPercent, sanitizeDecimal } from '~/modules/transport/utils/forms/numberInput'
|
|
|
|
interface SelectOption {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
const { t } = useI18n()
|
|
const api = useApi()
|
|
const router = useRouter()
|
|
const toast = useToast()
|
|
const { can } = usePermissions()
|
|
|
|
useHead({ title: t('transport.carriers.form.title') })
|
|
|
|
// Gating de la route : la creation est reservee a `manage` (Admin / Bureau).
|
|
// Commerciale (consultation seule), Compta et Usine sont rediriges vers le repertoire.
|
|
if (!can('transport.carriers.manage')) {
|
|
await navigateTo('/carriers')
|
|
}
|
|
|
|
const {
|
|
main,
|
|
carrierId,
|
|
mainLocked,
|
|
mainSubmitting,
|
|
tabSubmitting,
|
|
mainErrors,
|
|
dischargeUploading,
|
|
selectDischarge,
|
|
clearDischarge,
|
|
isLiot,
|
|
isQualimat,
|
|
certificationReadonly,
|
|
showCharteredFields,
|
|
showDischarge,
|
|
tabKeys,
|
|
activeTab,
|
|
unlockedIndex,
|
|
isValidated,
|
|
address,
|
|
addressErrors,
|
|
submitAddress,
|
|
contacts,
|
|
contactErrors,
|
|
canAddContact,
|
|
addContact,
|
|
removeContact,
|
|
submitContacts,
|
|
prices,
|
|
priceErrors,
|
|
canAddPrice,
|
|
addPrice,
|
|
removePrice,
|
|
submitPrices,
|
|
submitMain,
|
|
applyQualimatSelection,
|
|
} = useCarrierForm()
|
|
|
|
// Nom de fichier affiché dans le champ Décharge (alimenté à la sélection).
|
|
const dischargeFileName = ref('')
|
|
|
|
/** Vidage du champ Décharge : oublie le fichier en attente / l'IRI + le nom affiché. */
|
|
function onClearDischarge(): void {
|
|
clearDischarge()
|
|
dischargeFileName.value = ''
|
|
}
|
|
|
|
// Certifications selectionnables manuellement (spec § Formulaire principal) :
|
|
// GMP+ / OVOCOM / Compte-propre / Autre. QUALIMAT n'y figure PAS — il est posé par
|
|
// la saisie assistee (onglet Qualimat). On l'ajoute cependant aux options QUAND il
|
|
// est deja selectionne (transporteur QUALIMAT integre), uniquement pour AFFICHER
|
|
// son libelle dans le select en lecture seule.
|
|
const SELECTABLE_CERTIFICATIONS = ['GMP_PLUS', 'OVOCOM', 'COMPTE_PROPRE', 'AUTRE'] as const
|
|
|
|
const certificationOptions = computed<SelectOption[]>(() => {
|
|
const codes: string[] = [...SELECTABLE_CERTIFICATIONS]
|
|
if (main.certificationType === 'QUALIMAT') {
|
|
codes.unshift('QUALIMAT')
|
|
}
|
|
return codes.map(code => ({
|
|
value: code,
|
|
label: t(`transport.carriers.certification.${code}`),
|
|
}))
|
|
})
|
|
|
|
// Icone (Iconify) affichee dans chaque onglet, par cle.
|
|
const TAB_ICONS: Record<string, string> = {
|
|
qualimat: 'mdi:truck-fast-outline',
|
|
addresses: 'mdi:map-marker-outline',
|
|
contacts: 'mdi:account-box-plus-outline',
|
|
prices: 'mdi:payment',
|
|
}
|
|
|
|
// Onglets desactives tant que le formulaire principal n'est pas valide
|
|
// (unlockedIndex = -1 au depart) ; deverrouillage progressif ensuite.
|
|
const tabs = computed(() => tabKeys.value.map((key, index) => ({
|
|
key,
|
|
label: t(`transport.carriers.tab.${key}`),
|
|
icon: TAB_ICONS[key],
|
|
disabled: index > unlockedIndex.value,
|
|
})))
|
|
|
|
// Tous les onglets ont désormais leur contenu (qualimat / addresses / contacts / prices).
|
|
const placeholderTabs = computed(() => tabKeys.value.filter(
|
|
key => key !== 'qualimat' && key !== 'addresses' && key !== 'contacts' && key !== 'prices',
|
|
))
|
|
|
|
// ── Référentiels de l'onglet Prix (clients / fournisseurs / sites) ───────────
|
|
const clientOptions = ref<SelectOption[]>([])
|
|
const supplierOptions = ref<SelectOption[]>([])
|
|
const siteOptions = ref<SelectOption[]>([])
|
|
|
|
/** Charge un référentiel paginé (?pagination=false) et mappe en options { IRI, libellé }. */
|
|
async function loadOptions(
|
|
url: string,
|
|
target: typeof clientOptions,
|
|
labelOf: (m: Record<string, unknown>) => string,
|
|
): Promise<void> {
|
|
try {
|
|
const data = await api.get<{ member?: Record<string, unknown>[] }>(
|
|
url,
|
|
{ pagination: 'false' },
|
|
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
|
)
|
|
target.value = (data.member ?? []).map(m => ({ value: String(m['@id']), label: labelOf(m) }))
|
|
}
|
|
catch {
|
|
target.value = []
|
|
}
|
|
}
|
|
|
|
/** Charge les référentiels de l'onglet Prix (non bloquant : selects vides si échec). */
|
|
function loadPriceReferentials(): void {
|
|
void loadOptions('/clients', clientOptions, m => String(m.companyName ?? m['@id']))
|
|
void loadOptions('/suppliers', supplierOptions, m => String(m.companyName ?? m['@id']))
|
|
void loadOptions('/sites', siteOptions, m => String(m.name ?? m['@id']))
|
|
}
|
|
|
|
// ── Onglet Adresses (ERP-167) ────────────────────────────────────────────────
|
|
// Pays : France garantie en tete meme si /countries echoue (resilience), pour
|
|
// rester preselectionnable par defaut sur chaque adresse (RG-4.05).
|
|
const countryOptions = ref<SelectOption[]>([{ value: 'France', label: 'France' }])
|
|
|
|
/** Charge le referentiel pays (/api/countries) ; conserve France par defaut si echec. */
|
|
async function loadCountries(): Promise<void> {
|
|
try {
|
|
const data = await api.get<{ member?: { name: string }[] }>(
|
|
'/countries',
|
|
{ pagination: 'false' },
|
|
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
|
)
|
|
const list = (data.member ?? []).map(c => ({ value: c.name, label: c.name }))
|
|
countryOptions.value = list.some(c => c.value === 'France')
|
|
? list
|
|
: [{ value: 'France', label: 'France' }, ...list]
|
|
}
|
|
catch {
|
|
// Reste sur le fallback France (non bloquant).
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadCountries().catch(() => {})
|
|
loadPriceReferentials()
|
|
})
|
|
|
|
// Avertissement unique quand l'autocompletion d'adresse bascule en degrade.
|
|
const addressDegradedNotified = ref(false)
|
|
|
|
function onAddressDegraded(): void {
|
|
if (addressDegradedNotified.value) {
|
|
return
|
|
}
|
|
addressDegradedNotified.value = true
|
|
toast.warning({
|
|
title: t('transport.carriers.toast.error'),
|
|
message: t('transport.carriers.form.address.degraded'),
|
|
})
|
|
}
|
|
|
|
/** Message d'erreur affichable (toast) extrait d'une erreur API — jamais undefined. */
|
|
function apiErrorMessage(error: unknown): string {
|
|
const data = (error as { response?: { _data?: unknown } })?.response?._data
|
|
return extractApiErrorMessage(data) || t('transport.carriers.toast.error')
|
|
}
|
|
|
|
/** Valide l'onglet Adresses (POST/PATCH par ligne ; avance gere par le composable). */
|
|
async function onSubmitAddresses(): Promise<void> {
|
|
const ok = await submitAddress(error => toast.error({
|
|
title: t('transport.carriers.toast.error'),
|
|
message: apiErrorMessage(error),
|
|
}))
|
|
if (ok) {
|
|
toast.success({ title: t('transport.carriers.toast.addressSaved') })
|
|
}
|
|
}
|
|
|
|
// Modal de confirmation de suppression (générique : bloc contact OU prix).
|
|
const deleteConfirm = reactive({ open: false, action: null as null | (() => void) })
|
|
|
|
/** Valide l'onglet Contacts (POST/PATCH par ligne ; avance gérée par le composable). */
|
|
async function onSubmitContacts(): Promise<void> {
|
|
const ok = await submitContacts(error => toast.error({
|
|
title: t('transport.carriers.toast.error'),
|
|
message: apiErrorMessage(error),
|
|
}))
|
|
if (ok) {
|
|
toast.success({ title: t('transport.carriers.toast.contactSaved') })
|
|
}
|
|
}
|
|
|
|
function askRemoveContact(index: number): void {
|
|
deleteConfirm.action = () => { void removeContact(index) }
|
|
deleteConfirm.open = true
|
|
}
|
|
|
|
/**
|
|
* Valide l'onglet Prix = DERNIER onglet du flux de création. Au succès, l'ajout est
|
|
* terminé : toast final + retour au répertoire transporteurs (aligné sur M1/M2/M3).
|
|
*/
|
|
async function onSubmitPrices(): Promise<void> {
|
|
const ok = await submitPrices(error => toast.error({
|
|
title: t('transport.carriers.toast.error'),
|
|
message: apiErrorMessage(error),
|
|
}))
|
|
if (ok) {
|
|
toast.success({ title: t('transport.carriers.toast.priceSaved') })
|
|
await navigateTo('/carriers')
|
|
}
|
|
}
|
|
|
|
function askRemovePrice(index: number): void {
|
|
deleteConfirm.action = () => { void removePrice(index) }
|
|
deleteConfirm.open = true
|
|
}
|
|
|
|
function runDeleteConfirm(): void {
|
|
deleteConfirm.action?.()
|
|
deleteConfirm.action = null
|
|
deleteConfirm.open = false
|
|
}
|
|
|
|
/** Intégration d'une ligne QUALIMAT (émise par CarrierQualimatTab) : copie + PATCH
|
|
* (cf. useCarrierForm.applyQualimatSelection). */
|
|
async function onIntegrateQualimat(row: QualimatCarrierRow): Promise<void> {
|
|
const ok = await applyQualimatSelection(row)
|
|
if (ok) {
|
|
toast.success({ title: t('transport.carriers.toast.integrateSuccess') })
|
|
}
|
|
}
|
|
|
|
// Indexation plafonnée à 100 % : la clé force le ré-affichage du MalioInputAmount
|
|
// (contrôlé) quand le plafonnement laisse le modelValue inchangé.
|
|
const indexationKey = ref(0)
|
|
|
|
/** Saisie de l'indexation : plafonne à 100 et re-synchronise le champ si plafonné. */
|
|
function onIndexationInput(value: string): void {
|
|
const clamped = clampPercent(value)
|
|
main.indexationRate = clamped
|
|
if (clamped !== value) {
|
|
indexationKey.value += 1
|
|
}
|
|
}
|
|
|
|
/** Retour vers le repertoire transporteurs (fleche d'en-tete). */
|
|
function goBack(): void {
|
|
router.push('/carriers')
|
|
}
|
|
|
|
/**
|
|
* Valide le formulaire principal (POST /carriers ; bascule geree par le composable).
|
|
* RG-4.07 : pour un transporteur QUALIMAT, l'adresse copiee est persistee
|
|
* automatiquement (pas de bouton Valider dans l'onglet Adresses).
|
|
*/
|
|
async function onSubmitMain(): Promise<void> {
|
|
const ok = await submitMain()
|
|
if (ok && isQualimat.value) {
|
|
await submitAddress(error => toast.error({
|
|
title: t('transport.carriers.toast.error'),
|
|
message: apiErrorMessage(error),
|
|
}))
|
|
}
|
|
}
|
|
</script>
|