feat(transport) : modif — onglet Qualimat (actualisation) + certification éditable (déliage Qualimat) (ERP-172)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m14s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Has been cancelled

This commit is contained in:
2026-06-17 17:40:05 +02:00
parent e76bd1dd63
commit 76fb01c063
5 changed files with 333 additions and 190 deletions
@@ -0,0 +1,201 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { debounce } from '~/shared/utils/debounce'
import { useQualimatSearch, type QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
/**
* Onglet « Qualimat » — saisie assistée par recherche dans le référentiel QUALIMAT
* (RG-4.01 / RG-4.04). Datatable paginé filtré par le NOM (`searchName`), sélection
* d'une ligne → modal de confirmation → `integrate`. Mutualisé entre l'écran
* d'AJOUT (ERP-166) et l'écran de MODIFICATION (ERP-172, « actualiser le
* transporteur »). La persistance (copie nom / certification / FK) est portée par
* le parent via `useCarrierForm.applyQualimatSelection`.
*/
const props = defineProps<{
/** Terme de recherche (nom du transporteur saisi dans le formulaire principal). */
searchName: string
/** IRI QUALIMAT actuellement lié (coche le radio de la ligne correspondante). */
selectedIri: string | null
}>()
const emit = defineEmits<{
(event: 'integrate', row: QualimatCarrierRow): void
}>()
const { t } = useI18n()
const {
items: qualimatItems,
totalItems: qualimatTotal,
currentPage: qualimatPage,
itemsPerPage: qualimatPerPage,
itemsPerPageOptions: qualimatPerPageOptions,
goToPage: qualimatGoToPage,
setItemsPerPage: qualimatSetPerPage,
setFilters: qualimatSetFilters,
} = useQualimatSearch()
// Colonnes du datatable de sélection QUALIMAT (radio / Nom / Adresse / Validité).
const qualimatColumns = [
{ key: 'select', label: '' },
{ key: 'name', label: t('transport.carriers.form.qualimat.columns.name') },
{ key: 'address', label: t('transport.carriers.form.qualimat.columns.address') },
{ key: 'validityDate', label: t('transport.carriers.form.qualimat.columns.validityDate') },
]
// Le datatable n'affiche QUE des résultats de recherche : vide tant que le Nom n'est
// pas saisi (pas de liste complète par défaut).
const hasQualimatSearch = computed(() => props.searchName.trim() !== '')
const qualimatRows = computed(() => {
if (!hasQualimatSearch.value) {
return []
}
return qualimatItems.value.map(row => ({
id: row.id,
iri: row['@id'],
name: row.name,
address: formatQualimatAddress(row),
validityDate: row.validityDate,
}))
})
const qualimatTotalDisplay = computed(() => (hasQualimatSearch.value ? qualimatTotal.value : 0))
const qualimatEmptyMessage = computed(() => hasQualimatSearch.value
? t('transport.carriers.form.qualimat.empty')
: t('transport.carriers.form.qualimat.searchHint'))
// Re-filtrage debouncé sur le nom ; aucune recherche tant que le Nom est vide.
const filterQualimatByName = debounce((term: string) => {
if (term.trim() === '') {
return
}
void qualimatSetFilters({ search: term })
}, 300)
watch(() => props.searchName, term => filterQualimatByName(term), { immediate: true })
/** Adresse QUALIMAT condensée 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 agrément est périmé si sa date de validité 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 français JJ-MM-AAAA (chaîne 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()}`
}
// ── Confirmation d'intégration ───────────────────────────────────────────────
const confirmOpen = ref(false)
const pendingRow = ref<QualimatCarrierRow | null>(null)
/** Clic sur une ligne → retrouve la ligne QUALIMAT source + ouvre la modal. */
function onQualimatRowClick(item: Record<string, unknown>): void {
const row = qualimatItems.value.find(r => r.id === item.id)
if (row) {
pendingRow.value = row
confirmOpen.value = true
}
}
/** Confirme l'intégration : délègue la persistance au parent via `integrate`. */
function confirmIntegrate(): void {
const row = pendingRow.value
confirmOpen.value = false
if (row !== null) {
emit('integrate', row)
}
}
</script>
<template>
<div class="mt-12 flex flex-col gap-6">
<!-- table-fixed : 1re colonne (radio) étroite, les 3 autres à parts égales. -->
<MalioDataTable
class="qualimat-table"
table-class="table-fixed"
:columns="qualimatColumns"
:items="qualimatRows"
:total-items="qualimatTotalDisplay"
:page="qualimatPage"
:per-page="qualimatPerPage"
:per-page-options="qualimatPerPageOptions"
row-clickable
:empty-message="qualimatEmptyMessage"
@row-click="onQualimatRowClick"
@update:page="qualimatGoToPage"
@update:per-page="qualimatSetPerPage"
>
<!-- Radio reflétant la ligne QUALIMAT intégrée (lecture). -->
<template #cell-select="{ item }">
<MalioRadioButton
:model-value="selectedIri"
name="qualimat-row"
:value="item.iri"
group-class="mt-0"
/>
</template>
<!-- Date de validité : fond rouge si périmée (RG-4.04). -->
<template #cell-validityDate="{ item }">
<span
v-if="item.validityDate"
:class="isExpired(item.validityDate as string) ? 'inline-block rounded px-2 py-0.5 bg-m-danger text-white' : ''"
>
{{ formatDateFr(item.validityDate as string) }}
</span>
</template>
</MalioDataTable>
<!-- Modal de confirmation d'intégration QUALIMAT. -->
<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>
<style scoped>
/* Datatable QUALIMAT en table-fixed : la colonne radio (1re) reste étroite,
les 3 autres (nom / adresse / validité) se partagent l'espace à parts égales. */
.qualimat-table :deep(th:first-child),
.qualimat-table :deep(td:first-child) {
width: 56px;
}
</style>
@@ -975,3 +975,73 @@ describe('useCarrierForm — édition (ERP-170)', () => {
expect(form.main.name).toBe('TRANSPORTS ACME')
})
})
describe('useCarrierForm — modification : Qualimat + certification (ERP-172)', () => {
const QUALIMAT_ROW = {
'@id': '/api/qualimat_carriers/42',
id: '42',
name: 'TRANSPORTS QUALIMAT',
address: '1 rue du Port',
postalCode: '86000',
city: 'Poitiers',
validityDate: '2027-01-15',
status: 'VALIDE',
}
beforeEach(() => {
mockPost.mockReset()
mockPatch.mockReset()
})
it('setCertification : quitter QUALIMAT délie la FK qualimatCarrier', () => {
const form = useCarrierForm()
form.main.qualimatCarrierIri = '/api/qualimat_carriers/42'
form.main.certificationType = 'QUALIMAT'
form.setCertification('GMP_PLUS')
expect(form.main.certificationType).toBe('GMP_PLUS')
expect(form.main.qualimatCarrierIri).toBeNull()
})
it('certificationReadonly : éditable en modification même pour un QUALIMAT', () => {
const form = useCarrierForm()
form.prefillFrom({
'@id': '/api/carriers/7', id: 7, name: 'ACME', certificationType: 'QUALIMAT',
qualimatCarrier: { '@id': '/api/qualimat_carriers/42' },
})
expect(form.isQualimat.value).toBe(true)
expect(form.certificationReadonly.value).toBe(false)
})
it('buildMainPayload : en modification, délie le Qualimat (qualimatCarrier: null) sans lien', () => {
const form = useCarrierForm()
form.prefillFrom({
'@id': '/api/carriers/7', id: 7, name: 'ACME', certificationType: 'QUALIMAT',
qualimatCarrier: { '@id': '/api/qualimat_carriers/42' },
})
form.setCertification('GMP_PLUS')
expect(form.buildMainPayload()).toMatchObject({ certificationType: 'GMP_PLUS', qualimatCarrier: null })
})
it('applyQualimatSelection : en modification, conserve l\'adresse existante (PATCH nom/certif/FK)', async () => {
mockPatch.mockResolvedValueOnce({})
const form = useCarrierForm()
form.prefillFrom({
'@id': '/api/carriers/7', id: 7, name: 'OLD', certificationType: 'GMP_PLUS',
address: { '@id': '/api/carrier_addresses/3', id: 3, city: 'Poitiers', street: 'rue A' },
})
const addressBefore = { ...form.address.value }
const ok = await form.applyQualimatSelection(QUALIMAT_ROW)
expect(ok).toBe(true)
// Décision « conserver » (ERP-172) : l'adresse n'est pas réécrite en modification.
expect(form.address.value).toEqual(addressBefore)
// Nom + certification + FK actualisés via PATCH.
expect(form.main.name).toBe('TRANSPORTS QUALIMAT')
expect(form.main.certificationType).toBe('QUALIMAT')
expect(form.main.qualimatCarrierIri).toBe('/api/qualimat_carriers/42')
})
})
@@ -91,8 +91,10 @@ export function useCarrierForm() {
// Transporteur QUALIMAT : la FK est posée → certification figée à « QUALIMAT ».
const isQualimat = computed(() => main.qualimatCarrierIri !== null)
// Certification masquée en cas LIOT ; lecture seule si QUALIMAT (ou bloc verrouillé).
// En MODIFICATION (ERP-172) : éditable même pour un QUALIMAT (le métier doit pouvoir
// changer la certification) — la sortie de QUALIMAT délie le référentiel.
const showCertification = computed(() => !isLiot.value)
const certificationReadonly = computed(() => isQualimat.value || mainLocked.value)
const certificationReadonly = computed(() => (isQualimat.value && !editMode.value) || mainLocked.value)
// RG-4.03 : champs d'affrètement (indexation / contenant / volume) visibles et
// obligatoires si « Affréter » coché — masqués en cas LIOT.
const showCharteredFields = computed(() => main.isChartered && !isLiot.value)
@@ -211,6 +213,19 @@ export function useCarrierForm() {
}
}
/**
* Change la certification (sélecteur). Quitter « QUALIMAT » délie le référentiel
* (FK qualimatCarrier vidée — ERP-172) : un transporteur n'est QUALIMAT que tant
* que sa certification l'est. La FK null est propagée au back par buildMainPayload
* (en modification uniquement).
*/
function setCertification(value: string | null): void {
main.certificationType = value
if (value !== 'QUALIMAT') {
main.qualimatCarrierIri = null
}
}
/**
* Payload du POST principal (groupe `carrier:write:main`). `name` et
* `certificationType` sont omis s'ils sont vides afin que la 422 porte la
@@ -236,9 +251,14 @@ export function useCarrierForm() {
payload.certificationType = main.certificationType
}
// FK QUALIMAT (saisie assistée, § 2.5) envoyée si une ligne a été intégrée.
// En MODIFICATION, on délie explicitement (null) si plus de lien — ex: la
// certification a changé de QUALIMAT vers autre chose (ERP-172).
if (main.qualimatCarrierIri) {
payload.qualimatCarrier = main.qualimatCarrierIri
}
else if (editMode.value) {
payload.qualimatCarrier = null
}
// RG-4.02 : décharge envoyée seulement en certification AUTRE ; omise quand
// absente pour que la 422 « obligatoire » porte sur le champ.
if (main.certificationType === 'AUTRE' && main.dischargeDocumentIri) {
@@ -799,6 +819,7 @@ export function useCarrierForm() {
removePrice,
submitPrices,
// actions
setCertification,
selectDischarge,
clearDischarge,
validateMainFront,
@@ -41,7 +41,7 @@
:required="true"
:readonly="certificationReadonly"
:error="mainErrors.errors.certificationType"
@update:model-value="(v: string | number | null) => main.certificationType = v === null ? null : String(v)"
@update:model-value="(v: string | number | null) => setCertification(v === null ? null : String(v))"
/>
<MalioInputUpload
v-if="showDischarge"
@@ -122,6 +122,15 @@
<!-- ── Onglets éditables (navigation libre, PATCH partiel par onglet) ── -->
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
<!-- Qualimat : actualiser nom + certification depuis le référentiel (ERP-172). -->
<template #qualimat>
<CarrierQualimatTab
:search-name="main.name"
:selected-iri="main.qualimatCarrierIri"
@integrate="onIntegrateQualimat"
/>
</template>
<template #addresses>
<div class="mt-12 flex flex-col gap-6">
<!-- Adresse UNIQUE (ERP-172) : un seul bloc, sans ajouter/supprimer. -->
@@ -201,8 +210,10 @@ 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 { useCarrier } from '~/modules/transport/composables/useCarrier'
import type { QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
import { clampPercent, sanitizeDecimal } from '~/modules/transport/utils/forms/numberInput'
interface SelectOption {
@@ -235,10 +246,12 @@ const {
dischargeUploading,
selectDischarge,
clearDischarge,
setCertification,
isLiot,
certificationReadonly,
showCharteredFields,
showDischarge,
applyQualimatSelection,
address,
addressErrors,
submitAddress,
@@ -267,12 +280,14 @@ const certificationOptions = computed<SelectOption[]>(() => {
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',
}
const activeTab = ref('addresses')
const tabs = computed(() => ['addresses', 'contacts', 'prices'].map(key => ({
// Onglet Qualimat disponible en modif (ERP-172) : « actualiser » nom + certification.
const tabs = computed(() => ['qualimat', 'addresses', 'contacts', 'prices'].map(key => ({
key,
label: t(`transport.carriers.tab.${key}`),
icon: TAB_ICONS[key],
@@ -361,6 +376,15 @@ async function onUpdateMain(): Promise<void> {
}
}
/** Intégration d'une ligne QUALIMAT (onglet Qualimat) : actualise nom + certification
* + FK via PATCH (applyQualimatSelection). L'adresse existante n'est pas touchée. */
async function onIntegrateQualimat(row: QualimatCarrierRow): Promise<void> {
const ok = await applyQualimatSelection(row)
if (ok) {
toast.success({ title: t('transport.carriers.toast.integrateSuccess') })
}
}
async function onSubmitAddresses(): Promise<void> {
const ok = await submitAddress(err => toast.error({ title: t('transport.carriers.toast.error'), message: apiErrorMessage(err) }))
if (ok) toast.success({ title: t('transport.carriers.toast.addressSaved') })
+14 -187
View File
@@ -156,46 +156,14 @@
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 : datatable paginé filtré par le NOM du transporteur
(pas de champ de recherche dédié — RG-4.01 / 4.04). -->
<!-- Onglet Qualimat : saisie assistée (recherche par nom). Composant
mutualisé avec l'écran de modification (ERP-172). -->
<template #qualimat>
<div class="mt-12 flex flex-col gap-6">
<!-- table-fixed : 1re colonne (radio) étroite, les 3 autres à parts égales. -->
<MalioDataTable
class="qualimat-table"
table-class="table-fixed"
:columns="qualimatColumns"
:items="qualimatRows"
:total-items="qualimatTotalDisplay"
:page="qualimatPage"
:per-page="qualimatPerPage"
:per-page-options="qualimatPerPageOptions"
row-clickable
:empty-message="qualimatEmptyMessage"
@row-click="onQualimatRowClick"
@update:page="qualimatGoToPage"
@update:per-page="qualimatSetPerPage"
>
<!-- Radio reflétant la ligne QUALIMAT intégrée (lecture). -->
<template #cell-select="{ item }">
<MalioRadioButton
:model-value="main.qualimatCarrierIri"
name="qualimat-row"
:value="item.iri"
group-class="mt-0"
/>
</template>
<!-- Date de validité : fond rouge si périmée (RG-4.04). -->
<template #cell-validityDate="{ item }">
<span
v-if="item.validityDate"
:class="isExpired(item.validityDate as string) ? 'inline-block rounded px-2 py-0.5 bg-m-danger text-white' : ''"
>
{{ formatDateFr(item.validityDate as string) }}
</span>
</template>
</MalioDataTable>
</div>
<CarrierQualimatTab
:search-name="main.name"
:selected-iri="main.qualimatCarrierIri"
@integrate="onIntegrateQualimat"
/>
</template>
<!-- Onglet Adresses (ERP-167) : un bloc par adresse + BAN. RG-4.05
@@ -306,29 +274,7 @@
</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>
<!-- Modal de confirmation de suppression (bloc adresse). -->
<!-- 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>
@@ -353,15 +299,15 @@
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { debounce } from '~/shared/utils/debounce'
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 { useQualimatSearch, type QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
import type { QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
import { clampPercent, sanitizeDecimal } from '~/modules/transport/utils/forms/numberInput'
interface SelectOption {
@@ -430,17 +376,6 @@ function onClearDischarge(): void {
dischargeFileName.value = ''
}
const {
items: qualimatItems,
totalItems: qualimatTotal,
currentPage: qualimatPage,
itemsPerPage: qualimatPerPage,
itemsPerPageOptions: qualimatPerPageOptions,
goToPage: qualimatGoToPage,
setItemsPerPage: qualimatSetPerPage,
setFilters: qualimatSetFilters,
} = useQualimatSearch()
// 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
@@ -459,40 +394,6 @@ const certificationOptions = computed<SelectOption[]>(() => {
}))
})
// Colonnes du datatable de selection QUALIMAT (radio / Nom / Adresse / Validite).
const qualimatColumns = [
{ key: 'select', label: '' },
{ key: 'name', label: t('transport.carriers.form.qualimat.columns.name') },
{ key: 'address', label: t('transport.carriers.form.qualimat.columns.address') },
{ key: 'validityDate', label: t('transport.carriers.form.qualimat.columns.validityDate') },
]
// Le datatable n'affiche QUE des résultats de recherche : vide tant que le Nom n'est
// pas saisi (pas de liste complète par défaut). `main.name` pilote l'affichage.
const hasQualimatSearch = computed(() => main.name.trim() !== '')
// Lignes « plates » pour MalioDataTable (l'IRI sert au radio + à retrouver la ligne
// source au clic). Le détail QUALIMAT complet reste dans `qualimatItems`.
const qualimatRows = computed(() => {
if (!hasQualimatSearch.value) {
return []
}
return qualimatItems.value.map(row => ({
id: row.id,
iri: row['@id'],
name: row.name,
address: formatQualimatAddress(row),
validityDate: row.validityDate,
}))
})
// Total / message vide alignés sur « tableau vide tant qu'on n'a pas recherché ».
const qualimatTotalDisplay = computed(() => (hasQualimatSearch.value ? qualimatTotal.value : 0))
const qualimatEmptyMessage = computed(() => hasQualimatSearch.value
? t('transport.carriers.form.qualimat.empty')
: t('transport.carriers.form.qualimat.searchHint'))
// Icone (Iconify) affichee dans chaque onglet, par cle.
const TAB_ICONS: Record<string, string> = {
qualimat: 'mdi:truck-fast-outline',
@@ -646,74 +547,9 @@ function runDeleteConfirm(): void {
deleteConfirm.open = false
}
// ── Saisie assistee QUALIMAT (onglet Qualimat) ───────────────────────────────
const confirmOpen = ref(false)
const pendingRow = ref<QualimatCarrierRow | null>(null)
// Le datatable QUALIMAT est filtré par le NOM saisi dans le formulaire principal
// (RG-4.01) — pas de champ de recherche dédié. Aucune recherche tant que le Nom est
// vide (tableau vide par défaut) ; sinon re-filtrage debouncé à chaque frappe.
const filterQualimatByName = debounce((term: string) => {
if (term.trim() === '') {
return
}
void qualimatSetFilters({ search: term })
}, 300)
watch(() => main.name, term => filterQualimatByName(term))
/** 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 du datatable → retrouve la ligne QUALIMAT source + modal. */
function onQualimatRowClick(item: Record<string, unknown>): void {
const row = qualimatItems.value.find(r => r.id === item.id)
if (row) {
askIntegrate(row)
}
}
/** Ouvre la modal de confirmation d'integration pour une ligne QUALIMAT. */
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
}
/** 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') })
@@ -753,12 +589,3 @@ async function onSubmitMain(): Promise<void> {
}
}
</script>
<style scoped>
/* Datatable QUALIMAT en table-fixed : la colonne radio (1re) reste étroite,
les 3 autres (nom / adresse / validité) se partagent l'espace à parts égales. */
.qualimat-table :deep(th:first-child),
.qualimat-table :deep(td:first-child) {
width: 56px;
}
</style>