feat : écran bovins, refacto cases, enrichissement bovins, migrations

- Ajout page infrastructure/bovine avec CRUD
- Refacto BuildingCase (suppression Statut, simplification)
- Commande EnrichBovinesCommand pour enrichir les données bovins
- 4 migrations Doctrine
- Mise à jour composables shipment/weighing
- Mise à jour README et CHANGELOG

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-04-10 14:44:53 +02:00
parent 6eb2ee2578
commit 340aa2a3c0
30 changed files with 854 additions and 422 deletions

View File

@@ -36,7 +36,7 @@
v-for="cell in entry.cells"
:key="cell.key"
class="relative text-white flex h-[50px] items-center justify-center border-y-[3px] border-y-black bg-white hover:opacity-85 focus-visible:outline-none"
:class="[cell.sideBorderClass, activeLegendStatutId !== null && cell.caseStatusId !== activeLegendStatutId ? 'opacity-35 hover:opacity-70' : '']"
:class="[cell.sideBorderClass, activeLegendLabel !== null && cell.caseStatusLabel !== activeLegendLabel ? 'opacity-35 hover:opacity-70' : '']"
:style="[cell.spanStyle, cell.sideBorderStyle]"
:to="cell.caseId ? `/infrastructure/case?id=${cell.caseId}` : '/infrastructure/case'"
:title="cell.caseStatusLabel ?? undefined"
@@ -58,25 +58,19 @@
<!-- Légende : survol d'un statut => atténue les autres cases -->
<div class="py-4">
<!-- 3 zones fixes pour forcer gauche / centre / droite sur toute la largeur -->
<div class="grid w-full grid-cols-3 gap-3">
<div class="flex gap-6">
<div
v-for="(statut, index) in statutLegend"
:key="statut.id"
class="flex min-w-0 cursor-pointer items-center gap-2 py-1"
:class="[
index === 0 ? 'justify-self-start' : '',
index === statutLegend.length - 1 ? 'justify-self-end' : '',
index > 0 && index < statutLegend.length - 1 ? 'justify-self-center' : ''
]"
@mouseenter="activeLegendStatutId = statut.id"
@mouseleave="activeLegendStatutId = null"
v-for="statut in statutLegend"
:key="statut.label"
class="flex cursor-pointer items-center gap-2 py-1"
@mouseenter="activeLegendLabel = statut.label"
@mouseleave="activeLegendLabel = null"
>
<span
class="h-5 w-5 border border-slate-300"
:style="statut.couleur ? { backgroundColor: statut.couleur } : {}"
></span>
<span class="truncate text-sm uppercase text-slate-700">
<span class="text-sm uppercase text-slate-700">
{{ statut.label }}
</span>
</div>
@@ -90,33 +84,35 @@
import type {BuildingData} from "~/services/dto/building-data"
import type {BuildingLayoutData} from "~/services/dto/building-layout-data"
import type {BuildingCasePositionData} from "~/services/dto/building-case-position-data"
import type {BuildingCaseStatusData} from "~/services/dto/building-case-status-data"
import {getBuildingList} from "~/services/building"
import {getStatutList} from "~/services/statut"
definePageMeta({layout: "default"})
const router = useRouter()
// Données brutes chargées depuis l'API
const buildingList = ref<BuildingData[]>([])
const statutLegend = ref<BuildingCaseStatusData[]>([])
const statutLegend = [
{ label: 'Libre', couleur: '#A3B18A' },
{ label: 'Occupé', couleur: '#3A506B' },
{ label: 'Malade', couleur: '#E07A5F' },
]
// Statut actuellement survolé dans la légende (pour filtrage visuel)
const activeLegendStatutId = ref<number | null>(null)
const activeLegendLabel = ref<string | null>(null)
// Modèle de vue prêt pour le template (layout + cellules + styles de grille)
const buildingLayouts = computed(() =>
buildingList.value.map((building) => {
// On affiche uniquement le premier layout du bâtiment
const layout = building.layouts?.[0] ?? null
const view = layout ? buildLayoutView(layout) : null
return {building, layout, cells: view?.cells ?? [], gridStyle: view?.gridStyle ?? {}}
})
buildingList.value
.filter((building) => building.layouts && building.layouts.length > 0)
.map((building) => {
const layout = building.layouts![0]
const view = buildLayoutView(layout)
return {building, layout, cells: view?.cells ?? [], gridStyle: view?.gridStyle ?? {}}
})
)
type GridCell = {
key: string
caseId: number | null
display: string
caseStatusId: number | null
caseStatusLabel: string | null
// Couleur de fond de la case (dépend du statut)
caseStyle?: Record<string, string>
@@ -130,7 +126,8 @@ type GridCell = {
contentInsetClass: string
}
// Type intermédiaire : garde des infos utiles au calcul des bordures, retirées ensuite
type GridCellDraft = Omit<GridCell, "sideBorderClass" | "sideBorderStyle" | "contentInsetClass"> & { x: number; columnSpan: number }
type GridCellDraft = Omit<GridCell, "sideBorderClass" | "sideBorderStyle" | "contentInsetClass"> & { x: number; columnSpan: number}
// Nettoie la couleur de statut pour éviter les chaînes vides / espaces
const normalizeCaseStatusColor = (value: string | null | undefined): string | null => {
@@ -181,7 +178,6 @@ const buildLayoutView = (layout: BuildingLayoutData): {
// Métadonnées utiles au rendu / navigation / légende
const caseId = (position.buildingCase?.id ?? null) as number | null
const caseNumber = (position.buildingCase?.caseNumber ?? null) as number | null
const caseStatusId = position.buildingCase?.statut?.id ?? null
const caseStatusLabel = position.buildingCase?.statut?.label ?? null
const statusColor = normalizeCaseStatusColor(position.buildingCase?.statut?.couleur)
@@ -191,7 +187,6 @@ const buildLayoutView = (layout: BuildingLayoutData): {
columnSpan,
caseId,
display: caseNumber !== null ? String(caseNumber) : "Case",
caseStatusId,
caseStatusLabel,
caseStyle: statusColor ? {backgroundColor: statusColor} : undefined,
// Exemple : "14 / span 1" => commence en colonne 14 et occupe 1 colonne
@@ -230,13 +225,6 @@ const buildLayoutView = (layout: BuildingLayoutData): {
}
onMounted(async () => {
// Chargement initial des bâtiments et de la légende des statuts
const buildings = await getBuildingList()
const statuts = await getStatutList()
buildingList.value = buildings
// Tri alphabétique FR pour une légende stable
statutLegend.value = [...statuts].sort((a, b) =>
(a.label ?? "").localeCompare(b.label ?? "", "fr", {sensitivity: "base"})
)
buildingList.value = await getBuildingList()
})
</script>