feat(transport) : saisie assistée QUALIMAT + champs conditionnels (ERP-166)
This commit is contained in:
@@ -13,11 +13,10 @@
|
||||
</div>
|
||||
|
||||
<!-- ── Formulaire principal (pre-onglets) ─────────────────────────────
|
||||
Sans validation de ce bloc, les onglets restent inaccessibles. Au
|
||||
succes du POST, les champs passent en lecture seule et on bascule
|
||||
automatiquement sur l'onglet Qualimat. Les champs conditionnels
|
||||
(indexation / benne / volume si affrete, decharge si AUTRE, cas LIOT)
|
||||
et la saisie assistee QUALIMAT arrivent a ERP-166. -->
|
||||
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"
|
||||
@@ -26,29 +25,104 @@
|
||||
:readonly="mainLocked"
|
||||
:error="mainErrors.errors.name"
|
||||
/>
|
||||
<MalioSelect
|
||||
:model-value="main.certificationType"
|
||||
:options="certificationOptions"
|
||||
:label="t('transport.carriers.form.main.certificationType')"
|
||||
empty-option-label=""
|
||||
|
||||
<!-- 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.certificationType"
|
||||
@update:model-value="(v: string | number | null) => main.certificationType = v === null ? null : String(v)"
|
||||
:error="mainErrors.errors.liotPlates"
|
||||
/>
|
||||
<!-- Wrapper h-12 + centrage vertical : aligne la case a cocher sur la
|
||||
ligne de champ des inputs/selects (qui posent un h-12 items-center
|
||||
en interne). reserve-message-space=false pour un centrage exact. -->
|
||||
<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"
|
||||
|
||||
<!-- 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)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Wrapper h-12 + centrage vertical : aligne la case sur la ligne
|
||||
de champ des inputs/selects (qui posent un h-12 en interne). -->
|
||||
<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.02 : Decharge visible et obligatoire si certification AUTRE.
|
||||
L'upload reel (File → IRI via useUpload) arrive a ERP-171. -->
|
||||
<!-- TODO ERP-171 : brancher useUpload pour resoudre le File en IRI
|
||||
(main.dischargeDocumentIri). Le champ est deja visible/obligatoire. -->
|
||||
<MalioInputUpload
|
||||
v-if="showDischarge"
|
||||
:label="t('transport.carriers.form.main.discharge')"
|
||||
accept="application/pdf,image/*"
|
||||
:required="true"
|
||||
:readonly="mainLocked"
|
||||
:error="mainErrors.errors.dischargeDocument"
|
||||
/>
|
||||
|
||||
<!-- RG-4.03 : champs d'affretement visibles + obligatoires si « Affreter ». -->
|
||||
<template v-if="showCharteredFields">
|
||||
<MalioInputNumber
|
||||
v-model="main.indexationRate"
|
||||
:label="t('transport.carriers.form.main.indexationRate')"
|
||||
:required="true"
|
||||
:readonly="mainLocked"
|
||||
:error="mainErrors.errors.indexationRate"
|
||||
/>
|
||||
|
||||
<!-- Contenant : radio Benne / Fond mouvant (RG-4.03). -->
|
||||
<div class="flex flex-col justify-center">
|
||||
<span class="mb-1 text-sm font-medium text-m-muted">
|
||||
{{ t('transport.carriers.form.main.containerType') }}<span class="text-m-danger"> *</span>
|
||||
</span>
|
||||
<div class="flex gap-4">
|
||||
<MalioRadioButton
|
||||
v-model="main.containerType"
|
||||
name="container-type"
|
||||
value="BENNE"
|
||||
:label="t('transport.carriers.containerType.BENNE')"
|
||||
:disabled="mainLocked"
|
||||
group-class="mt-0"
|
||||
/>
|
||||
<MalioRadioButton
|
||||
v-model="main.containerType"
|
||||
name="container-type"
|
||||
value="FOND_MOUVANT"
|
||||
:label="t('transport.carriers.containerType.FOND_MOUVANT')"
|
||||
:disabled="mainLocked"
|
||||
group-class="mt-0"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="mainErrors.errors.containerType" class="mt-1 ml-[2px] text-xs text-m-danger">
|
||||
{{ mainErrors.errors.containerType }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<MalioInputNumber
|
||||
v-model="main.volumeM3"
|
||||
:label="t('transport.carriers.form.main.volumeM3')"
|
||||
:required="true"
|
||||
:readonly="mainLocked"
|
||||
:error="mainErrors.errors.volumeM3"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="!mainLocked" class="mt-12 flex justify-center">
|
||||
@@ -61,13 +135,76 @@
|
||||
</div>
|
||||
|
||||
<!-- ── Onglets a validation incrementale ─────────────────────────────
|
||||
Barre Qualimat · Adresses · Contacts · Prix. Onglets verrouilles tant
|
||||
que le formulaire principal n'est pas valide (unlockedIndex = -1) puis
|
||||
deverrouilles progressivement. Le contenu de chaque onglet arrive aux
|
||||
tickets suivants (ERP-166+) : placeholders « A venir » pour l'instant. -->
|
||||
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 : recherche + table de selection (RG-4.01 / 4.04). -->
|
||||
<template #qualimat>
|
||||
<div class="mt-12 flex flex-col gap-6">
|
||||
<MalioInputText
|
||||
v-model="qualimatTerm"
|
||||
icon-name="mdi:magnify"
|
||||
:label="t('transport.carriers.form.qualimat.search')"
|
||||
/>
|
||||
|
||||
<table class="w-full border-collapse text-left">
|
||||
<thead>
|
||||
<tr class="border-b border-black">
|
||||
<th class="w-12 py-2"></th>
|
||||
<th class="py-2 font-semibold">{{ t('transport.carriers.form.qualimat.columns.name') }}</th>
|
||||
<th class="py-2 font-semibold">{{ t('transport.carriers.form.qualimat.columns.address') }}</th>
|
||||
<th class="py-2 font-semibold">{{ t('transport.carriers.form.qualimat.columns.validityDate') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in qualimatResults"
|
||||
:key="row.id"
|
||||
class="cursor-pointer border-b border-m-muted/30 hover:bg-m-muted/10"
|
||||
@click="askIntegrate(row)"
|
||||
>
|
||||
<td class="py-2">
|
||||
<MalioRadioButton
|
||||
:model-value="main.qualimatCarrierIri"
|
||||
name="qualimat-row"
|
||||
:value="row['@id']"
|
||||
group-class="mt-0"
|
||||
/>
|
||||
</td>
|
||||
<td class="py-2">{{ row.name }}</td>
|
||||
<td class="py-2">{{ formatQualimatAddress(row) }}</td>
|
||||
<td class="py-2">
|
||||
<span
|
||||
v-if="row.validityDate"
|
||||
:class="isExpired(row.validityDate) ? 'inline-block rounded px-2 py-0.5 bg-m-danger text-white' : ''"
|
||||
>
|
||||
{{ formatDateFr(row.validityDate) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="qualimatResults.length === 0">
|
||||
<td colspan="4" class="py-4 text-center text-m-muted">
|
||||
{{ t('transport.carriers.form.qualimat.empty') }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-if="!isValidated('qualimat')" class="flex justify-center">
|
||||
<MalioButton
|
||||
variant="primary"
|
||||
:label="t('transport.carriers.form.qualimat.continue')"
|
||||
:disabled="carrierId === null"
|
||||
@click="onContinueQualimat"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Adresses / Contacts / Prix : contenu aux tickets suivants. -->
|
||||
<template
|
||||
v-for="key in tabKeys"
|
||||
v-for="key in placeholderTabs"
|
||||
:key="key"
|
||||
#[key]
|
||||
>
|
||||
@@ -76,12 +213,35 @@
|
||||
</div>
|
||||
</template>
|
||||
</MalioTabList>
|
||||
|
||||
<!-- Modal de confirmation d'integration QUALIMAT (RG-4.01). -->
|
||||
<MalioModal v-model="confirmOpen" modal-class="max-w-md">
|
||||
<template #header>
|
||||
<h2 class="text-[24px] font-bold">{{ t('transport.carriers.form.qualimat.confirm.title') }}</h2>
|
||||
</template>
|
||||
<p>{{ t('transport.carriers.form.qualimat.confirm.message') }}</p>
|
||||
<template #footer>
|
||||
<MalioButton
|
||||
variant="secondary"
|
||||
button-class="flex-1"
|
||||
:label="t('transport.carriers.form.qualimat.confirm.cancel')"
|
||||
@click="confirmOpen = false"
|
||||
/>
|
||||
<MalioButton
|
||||
variant="primary"
|
||||
button-class="flex-1"
|
||||
:label="t('transport.carriers.form.qualimat.confirm.confirm')"
|
||||
@click="confirmIntegrate"
|
||||
/>
|
||||
</template>
|
||||
</MalioModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useCarrierForm } from '~/modules/transport/composables/useCarrierForm'
|
||||
import { useQualimatSearch, type QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
|
||||
|
||||
interface SelectOption {
|
||||
value: string
|
||||
@@ -90,6 +250,7 @@ interface SelectOption {
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const { can } = usePermissions()
|
||||
|
||||
useHead({ title: t('transport.carriers.form.title') })
|
||||
@@ -102,18 +263,27 @@ if (!can('transport.carriers.manage')) {
|
||||
|
||||
const {
|
||||
main,
|
||||
carrierId,
|
||||
mainLocked,
|
||||
mainSubmitting,
|
||||
mainErrors,
|
||||
isLiot,
|
||||
certificationReadonly,
|
||||
showCharteredFields,
|
||||
showDischarge,
|
||||
tabKeys,
|
||||
activeTab,
|
||||
unlockedIndex,
|
||||
isValidated,
|
||||
submitMain,
|
||||
applyQualimatSelection,
|
||||
completeTab,
|
||||
} = useCarrierForm()
|
||||
|
||||
const { results: qualimatResults, fetchNow: qualimatFetch, search: qualimatSearchDebounced } = useQualimatSearch()
|
||||
|
||||
// Certifications selectionnables manuellement (spec § Formulaire principal).
|
||||
// QUALIMAT n'est PAS dans cette liste : il est pose par la saisie assistee QUALIMAT
|
||||
// (ERP-166), pas choisi a la main.
|
||||
// QUALIMAT n'y figure PAS : il est pose par la saisie assistee (onglet Qualimat).
|
||||
const SELECTABLE_CERTIFICATIONS = ['GMP_PLUS', 'OVOCOM', 'COMPTE_PROPRE', 'AUTRE'] as const
|
||||
|
||||
const certificationOptions = computed<SelectOption[]>(() =>
|
||||
@@ -140,12 +310,87 @@ const tabs = computed(() => tabKeys.value.map((key, index) => ({
|
||||
disabled: index > unlockedIndex.value,
|
||||
})))
|
||||
|
||||
// Onglets dont le contenu arrive aux tickets suivants (tout sauf Qualimat).
|
||||
const placeholderTabs = computed(() => tabKeys.value.filter(key => key !== 'qualimat'))
|
||||
|
||||
// ── Saisie assistee QUALIMAT (onglet Qualimat) ───────────────────────────────
|
||||
const qualimatTerm = ref('')
|
||||
const qualimatLoaded = ref(false)
|
||||
const confirmOpen = ref(false)
|
||||
const pendingRow = ref<QualimatCarrierRow | null>(null)
|
||||
|
||||
// Recherche debouncee a chaque frappe.
|
||||
watch(qualimatTerm, term => qualimatSearchDebounced(term))
|
||||
|
||||
// Premier chargement (liste active complete) quand l'onglet Qualimat devient actif.
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'qualimat' && !qualimatLoaded.value) {
|
||||
qualimatLoaded.value = true
|
||||
qualimatFetch('').catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
/** Adresse QUALIMAT condensee pour la colonne « Adresse » (voie · CP · ville). */
|
||||
function formatQualimatAddress(row: QualimatCarrierRow): string {
|
||||
return [row.address, row.postalCode, row.city].filter(Boolean).join(' · ')
|
||||
}
|
||||
|
||||
/** RG-4.04 : un agrement est perime si sa date de validite est < aujourd'hui. */
|
||||
function isExpired(value: string): boolean {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return false
|
||||
}
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
return date.getTime() < today.getTime()
|
||||
}
|
||||
|
||||
/** Format court francais JJ-MM-AAAA (chaine vide si date absente / invalide). */
|
||||
function formatDateFr(value: string | null | undefined): string {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return ''
|
||||
}
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
return `${day}-${month}-${date.getFullYear()}`
|
||||
}
|
||||
|
||||
/** Clic sur une ligne QUALIMAT → modal de confirmation d'integration. */
|
||||
function askIntegrate(row: QualimatCarrierRow): void {
|
||||
pendingRow.value = row
|
||||
confirmOpen.value = true
|
||||
}
|
||||
|
||||
/** Confirme l'integration : copie + PATCH (cf. useCarrierForm.applyQualimatSelection). */
|
||||
async function confirmIntegrate(): Promise<void> {
|
||||
const row = pendingRow.value
|
||||
confirmOpen.value = false
|
||||
if (row === null) {
|
||||
return
|
||||
}
|
||||
const ok = await applyQualimatSelection(row)
|
||||
if (ok) {
|
||||
toast.success({ title: t('transport.carriers.toast.integrateSuccess') })
|
||||
}
|
||||
}
|
||||
|
||||
/** « Continuer » : valide l'onglet Qualimat et avance a l'onglet Adresses. */
|
||||
function onContinueQualimat(): void {
|
||||
completeTab('qualimat')
|
||||
}
|
||||
|
||||
/** Retour vers le repertoire transporteurs (fleche d'en-tete). */
|
||||
function goBack(): void {
|
||||
router.push('/carriers')
|
||||
}
|
||||
|
||||
/** Valide le formulaire principal (POST /carriers ; bascule gerée par le composable). */
|
||||
/** Valide le formulaire principal (POST /carriers ; bascule geree par le composable). */
|
||||
async function onSubmitMain(): Promise<void> {
|
||||
await submitMain()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user