feat : page case sur UiDataTable server-side

- SearchFilter et DateFilter ajoutés sur l'entité Bovine
- Filtres serveur sur numéro national, poids exact et date d'arrivée
- Scope automatique via buildingCase IRI sur l'endpoint /bovines

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 15:25:24 +02:00
parent f945ae72a7
commit 9694c12afb
2 changed files with 98 additions and 37 deletions

View File

@@ -33,42 +33,51 @@
</NuxtLink> </NuxtLink>
</div> </div>
<div class="mt-8 border border-slate-200 mb-16"> <div class="mt-8 mb-16">
<div <UiDataTable
class="grid grid-cols-3 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide" v-model:page="page"
v-model:per-page="perPage"
:columns="columns"
:items="items"
:total-items="totalItems"
:loading="loading"
:row-clickable="auth.isAdmin"
empty-message="Aucun bovin dans cette case."
@row-click="goToBovine"
> >
<div>Numéro national</div> <template #header-nationalNumber>
<div>Poids à l'arrivée (kg)</div> <UiTextInput
<div>Date d'arrivée</div> v-model="filters.nationalNumber"
</div> placeholder="Numéro national"
<template v-if="bovines.length > 0"> size="compact"
<div />
v-for="bovine in bovines"
:key="bovine.id"
class="grid grid-cols-3 gap-4 px-4 py-3 text-sm border-t border-slate-200"
:class="auth.isAdmin ? 'cursor-pointer hover:bg-slate-50' : ''"
:role="auth.isAdmin ? 'button' : undefined"
:tabindex="auth.isAdmin ? 0 : undefined"
@click="goToBovine(bovine.id)"
@keydown.enter="goToBovine(bovine.id)"
>
<div>{{ bovine.nationalNumber }}</div>
<div>{{ bovine.receivedWeight ?? '—' }}</div>
<div>{{ formatDate(bovine.arrivalDate) }}</div>
</div>
</template> </template>
<div <template #header-receivedWeight>
v-else <UiTextInput
class="px-4 py-3 text-sm border-t border-slate-200 text-slate-500" v-model="filters.receivedWeight"
> placeholder="Poids (kg)"
Aucun bovin dans cette case. size="compact"
</div> />
</template>
<template #header-arrivalDate>
<UiDateInput v-model="arrivalDateFilter" size="compact" />
</template>
<template #cell-arrivalDate="{ item }">
{{ formatDate(item.arrivalDate) }}
</template>
<template #cell-receivedWeight="{ item }">
{{ item.receivedWeight ?? '—' }}
</template>
</UiDataTable>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { BuildingCaseData } from '~/services/dto/building-case-data' import type { BuildingCaseData } from '~/services/dto/building-case-data'
import type { BovineData } from '~/services/dto/bovine-data'
import { useAuthStore } from '~/stores/auth' import { useAuthStore } from '~/stores/auth'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -80,7 +89,44 @@ const caseId = computed(() => Number(route.query.id))
const hasCaseId = computed(() => Number.isFinite(caseId.value) && caseId.value > 0) const hasCaseId = computed(() => Number.isFinite(caseId.value) && caseId.value > 0)
const buildingCase = ref<BuildingCaseData | null>(null) const buildingCase = ref<BuildingCaseData | null>(null)
const bovines = computed(() => buildingCase.value?.bovines ?? [])
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<BovineData>(
'bovines',
{
buildingCase: '',
nationalNumber: '',
receivedWeight: '',
'arrivalDate[after]': '',
'arrivalDate[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 arrivalDateFilter = computed<string>({
get: () => (filters.value['arrivalDate[after]'] as string) ?? '',
set: (value: string) => {
if (!value) {
filters.value['arrivalDate[after]'] = ''
filters.value['arrivalDate[strictly_before]'] = ''
return
}
filters.value['arrivalDate[after]'] = value
filters.value['arrivalDate[strictly_before]'] = addOneDay(value)
}
})
const columns = [
{ key: 'nationalNumber', label: 'Numéro national' },
{ key: 'receivedWeight', label: "Poids à l'arrivée (kg)" },
{ key: 'arrivalDate', label: "Date d'arrivée" }
]
const title = computed(() => { const title = computed(() => {
if (!buildingCase.value) return '' if (!buildingCase.value) return ''
@@ -114,21 +160,27 @@ const loadCase = async () => {
} }
const printCaseReport = async () => { const printCaseReport = async () => {
if (!hasCaseId.value) { if (!hasCaseId.value) return
return
}
const filename = `tableau_poids_case_${caseId.value}.pdf` const filename = `tableau_poids_case_${caseId.value}.pdf`
await printPdf(`/building_cases/${caseId.value}/weights-report`, filename) await printPdf(`/building_cases/${caseId.value}/weights-report`, filename)
} }
const goToBovine = (id: number) => { const goToBovine = (bovine: BovineData) => {
if (!auth.isAdmin) return if (!auth.isAdmin) return
router.push({ router.push({
path: '/infrastructure/bovine', path: '/infrastructure/bovine',
query: { id: String(id), caseId: String(caseId.value) } query: { id: String(bovine.id), caseId: String(caseId.value) }
}) })
} }
watch(caseId, loadCase, { immediate: true }) watch(caseId, (id) => {
if (!hasCaseId.value) {
filters.value.buildingCase = ''
buildingCase.value = null
return
}
filters.value.buildingCase = `/api/building_cases/${id}`
loadCase()
reload()
}, { immediate: true })
</script> </script>

View File

@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\GetCollection;
@@ -19,6 +22,12 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
#[ORM\Entity] #[ORM\Entity]
#[ORM\Table(name: 'bovine')] #[ORM\Table(name: 'bovine')]
#[ORM\UniqueConstraint(name: 'uniq_bovine_national_number', columns: ['national_number'])] #[ORM\UniqueConstraint(name: 'uniq_bovine_national_number', columns: ['national_number'])]
#[ApiFilter(SearchFilter::class, properties: [
'nationalNumber' => 'ipartial',
'buildingCase' => 'exact',
'receivedWeight' => 'exact',
])]
#[ApiFilter(DateFilter::class, properties: ['arrivalDate'])]
#[ApiResource( #[ApiResource(
operations: [ operations: [
new Get( new Get(