feat : migration de la page expéditions finies sur UiDataTable

- Filtres SearchFilter et DateFilter ajoutés sur l'entité Shipment
- Colonnes typées, filtre date single input, placeholder disabled sur adresse et poids

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 14:50:16 +02:00
parent 0008631099
commit ee2fb0fe8f
2 changed files with 141 additions and 46 deletions

View File

@@ -5,51 +5,148 @@
</div> </div>
<div class="px-[86px]"> <div class="px-[86px]">
<div class="mt-6 border border-slate-200 mb-16 "> <div class="mt-6 mb-16">
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"> <UiDataTable
<div>Numéro</div> v-model:page="page"
<div>Date</div> v-model:per-page="perPage"
<div>Client</div> :columns="columns"
<div>Adresse</div> :items="items"
<div>Type d'expéditon</div> :total-items="totalItems"
<div>Poids</div> :loading="loading"
</div> row-clickable
<div @row-click="goToShipment"
v-for="shipment in shipmentList"
:key="shipment
.id"
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
role="button"
tabindex="0"
@click="goShipment(shipment.id)"
> >
<div>{{ shipment.identificationNumber }}</div> <template #header-identificationNumber>
<div>{{ shipment.shipmentDate }}</div> <UiTextInput
<div>{{ shipment.customer?.name }}</div> v-model="filters.identificationNumber"
<div>{{ shipment.address?.fullAddress }}</div> placeholder="Numéro"
<div> size="compact"
<template v-if="formatShipmentLines(shipment).length"> />
</template>
<template #header-shipmentDate>
<UiDateInput v-model="shipmentDateFilter" size="compact" />
</template>
<template #header-customer.name>
<UiTextInput
v-model="filters['customer.name']"
placeholder="Client"
size="compact"
/>
</template>
<template #header-address.fullAddress>
<UiTextInput :model-value="''" placeholder="Adresse" size="compact" disabled />
</template>
<template #header-shipmentType.label>
<UiSelect
v-model="filters['shipmentType.id']"
placeholder="Type d'expédition"
:options="shipmentTypeOptions"
size="compact"
/>
</template>
<template #header-weighing>
<UiTextInput :model-value="''" placeholder="Poids" size="compact" disabled />
</template>
<template #cell-shipmentDate="{ item }">
{{ formatDate(item.shipmentDate) }}
</template>
<template #cell-shipmentType.label="{ item }">
<template v-if="formatShipmentLines(item).length">
<div <div
v-for="(line, index) in formatShipmentLines(shipment)" v-for="(line, index) in formatShipmentLines(item)"
:key="index" :key="index"
class="leading-5" class="leading-5"
> >
{{ line }} {{ line }}
</div> </div>
</template> </template>
</div> <template v-else></template>
<div>{{ formatWeighing(shipment) }}</div> </template>
</div> <template #cell-weighing="{ item }">
{{ formatWeighing(item) }}
</template>
</UiDataTable>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type {ShipmentData} from "~/services/dto/shipment-data"; import type { ShipmentData } from '~/services/dto/shipment-data'
import {getShipmentList} from "~/services/shipment"; import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
import { getShipmentTypeList } from '~/services/shipment-type'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
const shipmentList = ref<ShipmentData[]>()
const router = useRouter() const router = useRouter()
const shipmentTypes = ref<ShipmentTypeData[]>([])
const shipmentTypeOptions = computed(() =>
shipmentTypes.value.map(st => ({ value: st.id, label: st.label }))
)
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<ShipmentData>(
'shipments',
{
isValid: true,
'identificationNumber': '',
'customer.name': '',
'shipmentType.id': '',
'shipmentDate[after]': '',
'shipmentDate[strictly_before]': ''
},
{ initialPerPage: 10 }
)
const addOneDay = (dateString: string): string => {
const [year, month, day] = dateString.split('-').map(Number)
const next = new Date(Date.UTC(year, month - 1, day + 1))
return next.toISOString().slice(0, 10)
}
const shipmentDateFilter = computed<string>({
get: () => (filters.value['shipmentDate[after]'] as string) ?? '',
set: (value: string) => {
if (!value) {
filters.value['shipmentDate[after]'] = ''
filters.value['shipmentDate[strictly_before]'] = ''
return
}
filters.value['shipmentDate[after]'] = value
filters.value['shipmentDate[strictly_before]'] = addOneDay(value)
}
})
const columns = [
{ key: 'identificationNumber', label: 'Numéro', width: '75px' },
{ key: 'shipmentDate', label: 'Date', width: '120px' },
{ key: 'customer.name', label: 'Client', width: '1.5fr' },
{ key: 'address.fullAddress', label: 'Adresse', width: '2fr' },
{ key: 'shipmentType.label', label: "Type d'expédition", width: '1.1fr' },
{ key: 'weighing', label: 'Poids', width: '82px' }
]
const formatDate = (date: string | null) => {
if (!date) return '—'
const d = new Date(date.replace(' ', 'T'))
if (isNaN(d.getTime())) return date
return d.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const formatShipmentLines = (shipment: ShipmentData) => {
if (!shipment.shipmentType && shipment.nbBovinSend == null) {
return []
}
const label = typeof shipment.shipmentType === 'string'
? shipment.shipmentType
: shipment.shipmentType?.label
return [`${label ?? '—'} : ${shipment.nbBovinSend ?? '—'}`]
}
const formatWeighing = (shipment: ShipmentData) => { const formatWeighing = (shipment: ShipmentData) => {
const gross = shipment.weights?.find((weight) => weight.type === 'gross')?.weight const gross = shipment.weights?.find((weight) => weight.type === 'gross')?.weight
@@ -62,24 +159,12 @@ const formatWeighing = (shipment: ShipmentData) => {
return `${gross - tare} kg` return `${gross - tare} kg`
} }
const goToShipment = (shipment: ShipmentData) => {
const formatShipmentLines = (shipment: ShipmentData) => { router.push(`/shipment/update/${shipment.id}`)
if (!shipment.shipmentType && shipment.nbBovinSend == null) {
return []
}
const label = typeof shipment.shipmentType === 'string'
? shipment.shipmentType
: shipment.shipmentType?.label
return [`${label ?? ''} : ${shipment.nbBovinSend ?? ''}`]
}
const goShipment = (id: number) => {
router.push(`/shipment/update/${id}`)
} }
onMounted(async () => { onMounted(async () => {
shipmentList.value = await getShipmentList(true) shipmentTypes.value = await getShipmentTypeList()
reload()
}) })
</script> </script>

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
@@ -30,6 +32,14 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'shipment')] #[ORM\Table(name: 'shipment')]
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])] #[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
#[ApiFilter(SearchFilter::class, properties: [
'identificationNumber' => 'ipartial',
'customer.name' => 'ipartial',
'carrier.name' => 'ipartial',
'licensePlate' => 'ipartial',
'shipmentType.id' => 'exact',
])]
#[ApiFilter(DateFilter::class, properties: ['shipmentDate'])]
#[ApiResource( #[ApiResource(
order: ['id' => 'DESC'], order: ['id' => 'DESC'],
operations: [ operations: [