feat : refonte de l'affichage des âges et restriction des prix aux admins

- Repository BovineRepository avec getInventoryStats en DQL
- Sécurité ApiProperty ROLE_ADMIN sur pricePerKg et finalPrice
- Endpoint inventory-export passe en ROLE_ADMIN
- Composable useBovineColumns mutualisé entre inventory et case (admin/user séparés)
- Stats par tranche d'âge filtrables par buildingCaseId
- Légende avec cartes colorées pleines + texte blanc
- Coloration de la cellule Age (badge) au lieu de toute la ligne
- Décalage couleurs : red ≥ 24, orange 22-24, yellow 20-22

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 17:09:13 +02:00
parent b3b7746bc5
commit 569d3b373f
9 changed files with 204 additions and 79 deletions

View File

@@ -13,6 +13,7 @@
<h1 class="font-bold text-3xl uppercase text-primary-500">Inventaire bovins</h1>
<span class="text-lg text-slate-500">({{ totalItems }} bovin{{ totalItems > 1 ? 's' : '' }})</span>
<div
v-if="auth.isAdmin"
class="bg-primary-500 p-1 rounded-md flex items-center cursor-pointer hover:opacity-80"
:class="exporting ? 'cursor-not-allowed opacity-60' : ''"
title="Exporter en Excel"
@@ -34,17 +35,17 @@
</div>
<div class="flex flex-wrap gap-3 mt-4">
<div class="flex items-center gap-3 rounded-md border-2 border-violet-300 px-4 py-2">
<span class="text-2xl font-bold text-violet-700">{{ stats.over24 }}</span>
<span class="text-sm uppercase tracking-wide text-violet-700"> 24 mois</span>
<div class="flex items-center gap-3 rounded-md bg-red-500 px-4 py-2">
<span class="text-2xl font-bold text-white">{{ stats.over24 }}</span>
<span class="text-sm uppercase tracking-wide text-white"> 24 mois</span>
</div>
<div class="flex items-center gap-3 rounded-md border-2 border-red-300 px-4 py-2">
<span class="text-2xl font-bold text-red-700">{{ stats.between22And24 }}</span>
<span class="text-sm uppercase tracking-wide text-red-700">22 24 mois</span>
<div class="flex items-center gap-3 rounded-md bg-orange-500 px-4 py-2">
<span class="text-2xl font-bold text-white">{{ stats.between22And24 }}</span>
<span class="text-sm uppercase tracking-wide text-white">22 24 mois</span>
</div>
<div class="flex items-center gap-3 rounded-md border-2 border-orange-300 px-4 py-2">
<span class="text-2xl font-bold text-orange-700">{{ stats.between20And22 }}</span>
<span class="text-sm uppercase tracking-wide text-orange-700">20 22 mois</span>
<div class="flex items-center gap-3 rounded-md bg-yellow-500 px-4 py-2">
<span class="text-2xl font-bold text-white">{{ stats.between20And22 }}</span>
<span class="text-sm uppercase tracking-wide text-white">20 22 mois</span>
</div>
</div>
@@ -56,7 +57,6 @@
:items="items"
:total-items="totalItems"
:loading="loading"
:row-class="rowClass"
>
<template #header-nationalNumber>
<UiTextInput
@@ -112,7 +112,12 @@
{{ formatDate(item.birthDate) }}
</template>
<template #cell-age="{ item }">
{{ formatAgeLabel(item.ageMonths) }}
<span
class="inline-block rounded px-2 py-0.5 font-semibold"
:class="ageBadgeClass(item.ageMonths)"
>
{{ formatAgeLabel(item.ageMonths) }}
</span>
</template>
<template #cell-arrivalDate="{ item }">
{{ formatDate(item.arrivalDate) }}
@@ -139,7 +144,8 @@
import type { BovineData } from '~/services/dto/bovine-data'
import { useAuthStore } from '~/stores/auth'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
import { formatAgeLabel } from '~/utils/bovine-age'
import { useBovineColumns } from '~/composables/useBovineColumns'
import { formatAgeLabel, ageBadgeClass } from '~/utils/bovine-age'
const router = useRouter()
const auth = useAuthStore()
@@ -267,19 +273,7 @@ const singleDateFilter = (afterKey: string, beforeKey: string) =>
const arrivalDateFilter = singleDateFilter('arrivalDate[after]', 'arrivalDate[strictly_before]')
const birthDateFilter = singleDateFilter('birthDate[after]', 'birthDate[strictly_before]')
const columns = [
{ key: 'nationalNumber', label: 'N° National', width: '80px' },
{ key: 'workNumber', label: 'N° Travail', width: '43px' },
{ key: 'sex', label: 'Sexe', width: '70px' },
{ key: 'birthDate', label: 'Né le', width: '72px' },
{ key: 'age', label: 'Age', width: '110px' },
{ key: 'breedCode', label: 'Race', width: '70px' },
{ key: 'buildingCase.building.label', label: 'Bâtiment', width: '75px' },
{ key: 'buildingCase.caseNumber', label: 'Case', width: '60px' },
{ key: 'arrivalDate', label: 'Entrée le', width: '90px' },
{ key: 'pricePerKg', label: 'Prix/kg', width: '65px' },
{ key: 'finalPrice', label: 'Prix total', width: '100px' }
]
const { columns } = useBovineColumns()
const formatDate = (date: string | null) => {
if (!date) return '—'
@@ -297,13 +291,6 @@ const formatPrice = (price: number | null) => {
return `${price.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`
}
const rowClass = (item: BovineData): string => {
if (item.ageMonths === null || item.ageMonths === undefined) return ''
if (item.ageMonths >= 24) return 'bg-violet-300 hover:bg-violet-400'
if (item.ageMonths >= 22) return 'bg-red-300 hover:bg-red-400'
if (item.ageMonths >= 20) return 'bg-orange-300 hover:bg-orange-400'
return ''
}
onMounted(() => {
reload()