feat : relation Bovine -> BovineType, support du bâtiment direct et feed étendu

- Bovine.breedCode (string) remplacé par bovineType (FK BovineType)
- Migration : ajout des races manquantes (Aubrac, Croisé, Blonde d'aquitaine), backfill, drop breed_code
- Sync EDNOTIF : auto-création d'un BovineType placeholder pour code inconnu
- Bovine.building (FK Building, nullable) en plus de buildingCase
- Getter effectiveBuilding (case prime sinon building direct)
- Feed XLSX : colonne E optionnelle (code bâtiment), set uniquement si pas de buildingCase
- Front : DTO + colonnes en variant inventory/case via composable, race et bâtiment ajustés
- Excel export utilise bovineType.label

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 09:24:11 +02:00
parent fcb6f742af
commit f08ab38c2b
15 changed files with 270 additions and 49 deletions

View File

@@ -7,41 +7,77 @@ export interface BovineColumn {
width?: string
}
export interface UseBovineColumnsOptions {
/**
* 'inventory' (par défaut) : colonnes complètes incluant Bâtiment + Case.
* 'case' : pas de Bâtiment ni Case (déjà dans le titre de la page),
* largeurs élargies pour combler l'espace.
*/
variant?: 'inventory' | 'case'
}
/**
* Définition partagée des colonnes des tableaux bovins (inventory + case).
* Deux définitions distinctes admin/user pour pouvoir ajuster les largeurs
* indépendamment selon le contexte.
* Variants distincts pour chaque écran et chaque rôle (admin/user) afin de
* pouvoir ajuster les largeurs indépendamment.
*/
export const useBovineColumns = () => {
export const useBovineColumns = (options: UseBovineColumnsOptions = {}) => {
const auth = useAuthStore()
const adminColumns: BovineColumn[] = [
const adminColumnsInventory: BovineColumn[] = [
{ key: 'nationalNumber', label: 'N° National', width: '80px' },
{ key: 'workNumber', label: 'N° Travail', width: '60px' },
{ 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: 'bovineType.label', label: 'Race', width: '90px' },
{ key: 'buildingCase.building.label', label: 'Bâtiment', width: '1fr' },
{ key: 'buildingCase.caseNumber', label: 'Case', width: '42px' },
{ key: 'arrivalDate', label: 'Entrée le', width: '90px' },
{ key: 'pricePerKg', label: 'Prix/kg', width: '65px' },
{ key: 'finalPrice', label: 'Prix total', width: '100px' }
{ key: 'finalPrice', label: 'Prix total', width: '80px' }
]
const userColumns: BovineColumn[] = [
const userColumnsInventory: BovineColumn[] = [
{ key: 'nationalNumber', label: 'N° National', width: '80px' },
{ key: 'workNumber', label: 'N° Travail', width: '60px' },
{ 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: '1fr' },
{ key: 'bovineType.label', label: 'Race', width: '1fr' },
{ key: 'buildingCase.building.label', label: 'Bâtiment', width: '120px' },
{ key: 'buildingCase.caseNumber', label: 'Case', width: '42px' },
{ key: 'arrivalDate', label: 'Entrée le', width: '90px' }
]
const columns = computed<BovineColumn[]>(() => auth.isAdmin ? adminColumns : userColumns)
const adminColumnsCase: BovineColumn[] = [
{ key: 'nationalNumber', label: 'N° National', width: '110px' },
{ key: 'workNumber', label: 'N° Travail', width: '85px' },
{ key: 'sex', label: 'Sexe', width: '90px' },
{ key: 'birthDate', label: 'Né le', width: '100px' },
{ key: 'age', label: 'Age', width: '90px' },
{ key: 'bovineType.label', label: 'Race', width: '1fr' },
{ key: 'arrivalDate', label: 'Entrée le', width: '110px' },
{ key: 'pricePerKg', label: 'Prix/kg', width: '85px' },
{ key: 'finalPrice', label: 'Prix total', width: '105px' }
]
const userColumnsCase: BovineColumn[] = [
{ key: 'nationalNumber', label: 'N° National', width: '130px' },
{ key: 'workNumber', label: 'N° Travail', width: '100px' },
{ key: 'sex', label: 'Sexe', width: '110px' },
{ key: 'birthDate', label: 'Né le', width: '140px' },
{ key: 'age', label: 'Age', width: '130px' },
{ key: 'bovineType.label', label: 'Race', width: '1fr' },
{ key: 'arrivalDate', label: 'Entrée le', width: '170px' }
]
const columns = computed<BovineColumn[]>(() => {
if (options.variant === 'case') {
return auth.isAdmin ? adminColumnsCase : userColumnsCase
}
return auth.isAdmin ? adminColumnsInventory : userColumnsInventory
})
return { columns }
}

View File

@@ -94,19 +94,13 @@
<template #header-finalPrice>
<UiTextInput :model-value="''" placeholder="Prix total" size="compact" disabled />
</template>
<template #header-breedCode>
<template #header-bovineType.label>
<UiTextInput
v-model="filters.breedCode"
v-model="filters['bovineType.label']"
placeholder="Race"
size="compact"
/>
</template>
<template #header-buildingCase.building.label>
<UiTextInput :model-value="''" placeholder="Bâtiment" size="compact" disabled />
</template>
<template #header-buildingCase.caseNumber>
<UiTextInput :model-value="''" placeholder="Case" size="compact" disabled />
</template>
<template #header-arrivalDate>
<UiDateMaskedInput v-model="arrivalDateFilter" size="compact" placeholder="Entrée le" />
</template>
@@ -124,12 +118,6 @@
<template #cell-arrivalDate="{ item }">
{{ formatDate(item.arrivalDate) }}
</template>
<template #cell-buildingCase.building.label="{ item }">
{{ item.buildingCase?.building?.label ?? '—' }}
</template>
<template #cell-buildingCase.caseNumber="{ item }">
{{ item.buildingCase?.caseNumber ?? '—' }}
</template>
<template #cell-pricePerKg="{ item }">
{{ formatPrice(item.pricePerKg) }}
</template>
@@ -196,7 +184,7 @@ const { items, totalItems, page, perPage, filters, loading, reload } =
buildingCase: '',
nationalNumber: '',
workNumber: '',
breedCode: '',
'bovineType.label': '',
sex: '',
'arrivalDate[after]': '',
'arrivalDate[strictly_before]': '',
@@ -234,7 +222,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 } = useBovineColumns()
const { columns } = useBovineColumns({ variant: 'case' })
const title = computed(() => {
if (!buildingCase.value) return ''

View File

@@ -83,9 +83,9 @@
<template #header-birthDate>
<UiDateMaskedInput v-model="birthDateFilter" size="compact" placeholder="Né le" />
</template>
<template #header-breedCode>
<template #header-bovineType.label>
<UiTextInput
v-model="filters.breedCode"
v-model="filters['bovineType.label']"
placeholder="Race"
size="compact"
/>
@@ -123,7 +123,7 @@
{{ formatDate(item.arrivalDate) }}
</template>
<template #cell-buildingCase.building.label="{ item }">
{{ item.buildingCase?.building?.label ?? '—' }}
{{ item.effectiveBuilding?.label ?? '—' }}
</template>
<template #cell-buildingCase.caseNumber="{ item }">
{{ item.buildingCase?.caseNumber ?? '—' }}
@@ -236,7 +236,7 @@ const { items, totalItems, page, perPage, filters, loading, reload } =
'exists[exitedAt]': 'false',
nationalNumber: '',
workNumber: '',
breedCode: '',
'bovineType.label': '',
sex: '',
'arrivalDate[after]': '',
'arrivalDate[strictly_before]': '',

View File

@@ -1,6 +1,10 @@
export interface BovineBuildingRef {
label: string
}
export interface BovineBuildingCaseRef {
caseNumber: number | null
building: { label: string } | null
building: BovineBuildingRef | null
}
export interface BovineData {
@@ -12,10 +16,12 @@ export interface BovineData {
arrivalDate: string | null
exitDate: string | null
buildingCase: BovineBuildingCaseRef | null
building: BovineBuildingRef | null
effectiveBuilding: BovineBuildingRef | null
supplier: string | null
workNumber: string | null
birthDate: string | null
breedCode: string | null
bovineType: { id: number; label: string; code: string } | null
sex: string | null
ageMonths: number | null
exitedAt: string | null