feat(bovine) : page Vie du bovin + tabs réutilisables + parents EDNOTIF
- Nouvelle page /bovine/[id] avec tabs Mouvement / Passeport bovin / Santé - Composant UiTabs partagé, réutilisé sur réception et expédition - Champs père/mère (numéro national + type de race) sur Bovine, alimentés via la sync EDNOTIF - Inventaire : ligne cliquable vers le passeport Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
35
frontend/components/ui/UiTabs.vue
Normal file
35
frontend/components/ui/UiTabs.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||
<h1
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="font-bold text-3xl uppercase px-12 cursor-pointer"
|
||||
:class="[
|
||||
modelValue === tab.key
|
||||
? 'border-b-[6px] border-primary-500 text-primary-500'
|
||||
: 'text-primary-500/50',
|
||||
tab.error ? '!text-red-500 !border-red-500' : ''
|
||||
]"
|
||||
@click="emit('update:modelValue', tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T extends string">
|
||||
export interface UiTab<K extends string = string> {
|
||||
key: K
|
||||
label: string
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
modelValue: T
|
||||
tabs: UiTab<T>[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: T): void
|
||||
}>()
|
||||
</script>
|
||||
135
frontend/pages/bovine/[id].vue
Normal file
135
frontend/pages/bovine/[id].vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="px-[86px]">
|
||||
<div class="flex items-center justify-between relative mb-10">
|
||||
<div class="flex flex-row absolute -left-[60px]">
|
||||
<Icon
|
||||
@click="router.push('/inventory')"
|
||||
name="gg:arrow-left-o"
|
||||
size="44"
|
||||
class="cursor-pointer text-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<h1 class="font-bold text-3xl uppercase text-primary-500">Vie du bovin</h1>
|
||||
</div>
|
||||
|
||||
<UiTabs
|
||||
v-model="activeTab"
|
||||
:tabs="[
|
||||
{ key: 'mouvement', label: 'Mouvement' },
|
||||
{ key: 'passeport', label: 'Passeport bovin' },
|
||||
{ key: 'sante', label: 'Santé' }
|
||||
]"
|
||||
/>
|
||||
|
||||
<div v-show="activeTab === 'passeport'">
|
||||
<div class="mt-6">
|
||||
<div class="grid grid-cols-[3rem_repeat(6,minmax(0,1fr))] grid-rows-2 border-2 border-black">
|
||||
<div class="row-span-2 flex items-center justify-center border-r-2 border-black">
|
||||
<span class="uppercase font-bold -rotate-90 whitespace-nowrap transform-gpu">Veau</span>
|
||||
</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Numéro national</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">N° de travail</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Sexe</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Code race</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Type de race</div>
|
||||
<div class="border-b border-black px-2 py-1 text-center font-semibold text-sm">Date de naissance</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.nationalNumber) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.workNumber) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.sex) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.bovineType?.code) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.bovineType?.label) }}</div>
|
||||
<div class="px-2 py-1 text-center">{{ formatDate(bovine?.birthDate) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-9">
|
||||
<div class="grid grid-cols-[3rem_repeat(6,minmax(0,1fr))] grid-rows-2 border-2 border-black">
|
||||
<div class="row-span-2 flex items-center justify-center border-r-2 border-black">
|
||||
<span class="uppercase font-bold -rotate-90 whitespace-nowrap transform-gpu">Père</span>
|
||||
</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Numéro national</div>
|
||||
<div class="col-span-2 border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">N° de travail</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Code race</div>
|
||||
<div class="col-span-2 border-b border-black px-2 py-1 text-center font-semibold text-sm">Type de race</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.fatherNationalNumber) }}</div>
|
||||
<div class="col-span-2 border-r border-black px-2 py-1 text-center">{{ display(workNumberFromNational(bovine?.fatherNationalNumber)) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.fatherBovineType?.code) }}</div>
|
||||
<div class="col-span-2 px-2 py-1 text-center">{{ display(bovine?.fatherBovineType?.label) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-9">
|
||||
<div class="grid grid-cols-[3rem_repeat(6,minmax(0,1fr))] grid-rows-2 border-2 border-black">
|
||||
<div class="row-span-2 flex items-center justify-center border-r-2 border-black">
|
||||
<span class="uppercase font-bold -rotate-90 whitespace-nowrap transform-gpu">Mère</span>
|
||||
</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Numéro national</div>
|
||||
<div class="col-span-2 border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">N° de travail</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Code race</div>
|
||||
<div class="col-span-2 border-b border-black px-2 py-1 text-center font-semibold text-sm">Type de race</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.motherNationalNumber) }}</div>
|
||||
<div class="col-span-2 border-r border-black px-2 py-1 text-center">{{ display(workNumberFromNational(bovine?.motherNationalNumber)) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.motherBovineType?.code) }}</div>
|
||||
<div class="col-span-2 px-2 py-1 text-center">{{ display(bovine?.motherBovineType?.label) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Vie du bovin' })
|
||||
|
||||
type BovineTab = 'mouvement' | 'passeport' | 'sante'
|
||||
const activeTab = ref<BovineTab>('passeport')
|
||||
|
||||
interface BovineTypeRef {
|
||||
id: number
|
||||
label: string | null
|
||||
code: string | null
|
||||
}
|
||||
|
||||
interface BovinePassportData {
|
||||
id: number
|
||||
nationalNumber: string
|
||||
workNumber: string | null
|
||||
sex: string | null
|
||||
birthDate: string | null
|
||||
bovineType: BovineTypeRef | null
|
||||
motherNationalNumber: string | null
|
||||
motherBovineType: BovineTypeRef | null
|
||||
fatherNationalNumber: string | null
|
||||
fatherBovineType: BovineTypeRef | null
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const api = useApi()
|
||||
|
||||
const bovine = ref<BovinePassportData | null>(null)
|
||||
|
||||
const bovineId = computed(() => {
|
||||
const raw = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
||||
const n = Number(raw)
|
||||
return Number.isFinite(n) ? n : null
|
||||
})
|
||||
|
||||
const display = (value: string | null | undefined) => (value && value !== '' ? value : '—')
|
||||
|
||||
const workNumberFromNational = (nationalNumber: string | null | undefined) => {
|
||||
if (!nationalNumber) return null
|
||||
return nationalNumber.slice(-4)
|
||||
}
|
||||
|
||||
const formatDate = (date: string | null | undefined) => {
|
||||
if (!date) return '—'
|
||||
const d = new Date(date)
|
||||
if (isNaN(d.getTime())) return date
|
||||
return d.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (bovineId.value === null) return
|
||||
bovine.value = await api.get<BovinePassportData>(`bovines/${bovineId.value}`)
|
||||
})
|
||||
</script>
|
||||
@@ -57,6 +57,8 @@
|
||||
:items="items"
|
||||
:total-items="totalItems"
|
||||
:loading="loading"
|
||||
row-clickable
|
||||
@row-click="(item: BovineData) => router.push(`/bovine/${item.id}`)"
|
||||
>
|
||||
<template #header-nationalNumber>
|
||||
<UiTextInput
|
||||
|
||||
@@ -145,38 +145,14 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-if="formIsLoading">
|
||||
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||
<h1
|
||||
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer"
|
||||
:class="[
|
||||
activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||
hasGrossWeightError ? '!text-red-500 !border-red-500' : ''
|
||||
]"
|
||||
@click="activeTab = 'weights'"
|
||||
>
|
||||
pesée à plein
|
||||
</h1>
|
||||
<h1
|
||||
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer"
|
||||
:class="[
|
||||
activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||
hasTareWeightError ? '!text-red-500 !border-red-500' : ''
|
||||
]"
|
||||
@click="activeTab = 'weightsEmpty'"
|
||||
>
|
||||
pesée à vide
|
||||
</h1>
|
||||
<h1
|
||||
class="font-bold text-3xl uppercase px-12 col-start-2 row-start-1 cursor-pointer"
|
||||
:class="[
|
||||
activeTab === 'merchandise' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||
hasMerchandiseTabError ? '!text-red-500 !border-red-500' : ''
|
||||
]"
|
||||
@click="activeTab = 'merchandise'"
|
||||
>
|
||||
{{ isMerchandise ? "Marchandise" : "Bovins" }}
|
||||
</h1>
|
||||
</div>
|
||||
<UiTabs
|
||||
v-model="activeTab"
|
||||
:tabs="[
|
||||
{ key: 'weights', label: 'pesée à plein', error: hasGrossWeightError },
|
||||
{ key: 'weightsEmpty', label: 'pesée à vide', error: hasTareWeightError },
|
||||
{ key: 'merchandise', label: isMerchandise ? 'Marchandise' : 'Bovins', error: hasMerchandiseTabError }
|
||||
]"
|
||||
/>
|
||||
<div class="mb-12 ">
|
||||
<update-weight
|
||||
v-show="activeTab === 'weights'"
|
||||
|
||||
@@ -146,28 +146,13 @@
|
||||
</div>
|
||||
|
||||
<div v-if="formIsLoading">
|
||||
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||
<h1
|
||||
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer"
|
||||
:class="[
|
||||
activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||
hasTareWeightError ? '!text-red-500 !border-red-500' : ''
|
||||
]"
|
||||
@click="activeTab = 'weightsEmpty'"
|
||||
>
|
||||
pesée à vide
|
||||
</h1>
|
||||
<h1
|
||||
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer"
|
||||
:class="[
|
||||
activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||
hasGrossWeightError ? '!text-red-500 !border-red-500' : ''
|
||||
]"
|
||||
@click="activeTab = 'weights'"
|
||||
>
|
||||
pesée à plein
|
||||
</h1>
|
||||
</div>
|
||||
<UiTabs
|
||||
v-model="activeTab"
|
||||
:tabs="[
|
||||
{ key: 'weightsEmpty', label: 'pesée à vide', error: hasTareWeightError },
|
||||
{ key: 'weights', label: 'pesée à plein', error: hasGrossWeightError }
|
||||
]"
|
||||
/>
|
||||
<div class="mb-12">
|
||||
<update-weight
|
||||
v-show="activeTab === 'weights'"
|
||||
|
||||
Reference in New Issue
Block a user