Files
Ferme/frontend/pages/infrastructure/case.vue
tristan 08a17f91b3
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
feat: ajout du prix au kilo et de l'age moyen bovin + feed bovin via xlsx (!50)
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [x] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

Reviewed-on: #50
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-04-28 07:25:31 +00:00

290 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="px-[86px]">
<div class="flex items-center justify-between relative">
<div class="flex flex-row absolute -left-[60px]">
<Icon
@click="router.push('/infrastructure/building')"
name="gg:arrow-left-o"
size="44"
class="cursor-pointer text-primary-500"
/>
</div>
<div class="flex items-center gap-4">
<h1 class="font-bold text-4xl text-primary-500 uppercase">
{{ title }}
</h1>
<span class="text-lg text-slate-500">({{ totalItems }} bovin{{ totalItems > 1 ? 's' : '' }})</span>
<div
v-if="hasCaseId"
class="bg-primary-500 p-1 rounded-md flex items-center cursor-pointer"
title="Imprimer"
@click="printCaseReport"
>
<Icon name="mdi:printer-outline" size="32" class="text-white" />
</div>
</div>
<NuxtLink
v-if="hasCaseId && auth.isAdmin"
:to="addBovineRoute"
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-6 rounded hover:opacity-80 gap-2"
>
<Icon name="mdi:plus" size="28" />
Ajouter
</NuxtLink>
</div>
<div class="flex flex-wrap gap-3 mt-4">
<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 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 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>
<div class="mt-6 mb-16">
<UiDataTable
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"
>
<template #header-nationalNumber>
<UiTextInput
v-model="filters.nationalNumber"
placeholder="N° National"
size="compact"
/>
</template>
<template #header-workNumber>
<UiTextInput
v-model="filters.workNumber"
placeholder="N° Travail"
size="compact"
/>
</template>
<template #header-sex>
<UiSelect
v-model="filters.sex"
placeholder="Sexe"
:options="sexOptions"
size="compact"
/>
</template>
<template #header-birthDate>
<UiDateMaskedInput v-model="birthDateFilter" size="compact" placeholder="Né le" />
</template>
<template #header-age>
<UiTextInput :model-value="''" placeholder="Age" size="compact" disabled />
</template>
<template #header-pricePerKg>
<UiTextInput :model-value="''" placeholder="Prix/kg" size="compact" disabled />
</template>
<template #header-finalPrice>
<UiTextInput :model-value="''" placeholder="Prix total" size="compact" disabled />
</template>
<template #header-bovineType.label>
<UiTextInput
v-model="filters['bovineType.label']"
placeholder="Race"
size="compact"
/>
</template>
<template #header-arrivalDate>
<UiDateMaskedInput v-model="arrivalDateFilter" size="compact" placeholder="Entrée le" />
</template>
<template #cell-birthDate="{ item }">
{{ formatDate(item.birthDate) }}
</template>
<template #cell-age="{ item }">
<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) }}
</template>
<template #cell-pricePerKg="{ item }">
{{ formatPrice(item.pricePerKg) }}
</template>
<template #cell-finalPrice="{ item }">
{{ formatPrice(item.finalPrice) }}
</template>
</UiDataTable>
</div>
</div>
</template>
<script setup lang="ts">
import type { BuildingCaseData } from '~/services/dto/building-case-data'
import type { BovineData } from '~/services/dto/bovine-data'
import { useAuthStore } from '~/stores/auth'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
import { useBovineColumns } from '~/composables/useBovineColumns'
import { formatAgeLabel, ageBadgeClass } from '~/utils/bovine-age'
const route = useRoute()
const router = useRouter()
const { printPdf } = usePdfPrinter()
const api = useApi()
const auth = useAuthStore()
const caseId = computed(() => Number(route.query.id))
const hasCaseId = computed(() => Number.isFinite(caseId.value) && caseId.value > 0)
const buildingCase = ref<BuildingCaseData | null>(null)
interface InventoryStats {
total: number
over24: number
between22And24: number
between20And22: number
}
const stats = ref<InventoryStats>({
total: 0,
over24: 0,
between22And24: 0,
between20And22: 0
})
const loadStats = async () => {
if (!hasCaseId.value) {
stats.value = { total: 0, over24: 0, between22And24: 0, between20And22: 0 }
return
}
try {
stats.value = await api.get<InventoryStats>('bovines/inventory-stats', {
buildingCaseId: caseId.value
}, { toast: false })
} catch {
// silencieux
}
}
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<BovineData>(
'bovines',
{
'exists[exitedAt]': 'false',
buildingCase: '',
nationalNumber: '',
workNumber: '',
'bovineType.label': '',
sex: '',
'arrivalDate[after]': '',
'arrivalDate[strictly_before]': '',
'birthDate[after]': '',
'birthDate[strictly_before]': ''
},
{ initialPerPage: 10 }
)
const sexOptions = [
{ value: 'M', label: 'Mâle' },
{ value: 'F', label: 'Femelle' }
]
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 singleDateFilter = (afterKey: string, beforeKey: string) =>
computed<string>({
get: () => (filters.value[afterKey] as string) ?? '',
set: (value: string) => {
if (!value) {
filters.value[afterKey] = ''
filters.value[beforeKey] = ''
return
}
filters.value[afterKey] = value
filters.value[beforeKey] = addOneDay(value)
}
})
const arrivalDateFilter = singleDateFilter('arrivalDate[after]', 'arrivalDate[strictly_before]')
const birthDateFilter = singleDateFilter('birthDate[after]', 'birthDate[strictly_before]')
const { columns } = useBovineColumns({ variant: 'case' })
const title = computed(() => {
if (!buildingCase.value) return ''
const buildingLabel = buildingCase.value.building?.label ?? ''
const caseNumber = buildingCase.value.caseNumber ?? ''
return `${buildingLabel} case ${caseNumber}`.trim()
})
const addBovineRoute = computed(() => ({
path: '/infrastructure/bovine',
query: { caseId: String(caseId.value) }
}))
const formatDate = (date: string | null) => {
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'
})
}
const formatPrice = (price: number | null) => {
if (price === null || price === undefined) return '—'
return `${price.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`
}
const loadCase = async () => {
if (!hasCaseId.value) {
buildingCase.value = null
return
}
buildingCase.value = await api.get<BuildingCaseData>(`/building_cases/${caseId.value}`)
}
const printCaseReport = async () => {
if (!hasCaseId.value) return
const filename = `tableau_poids_case_${caseId.value}.pdf`
await printPdf(`/building_cases/${caseId.value}/weights-report`, filename)
}
const goToBovine = (bovine: BovineData) => {
if (!auth.isAdmin) return
router.push({
path: '/infrastructure/bovine',
query: { id: String(bovine.id), caseId: String(caseId.value) }
})
}
watch(caseId, (id) => {
if (!hasCaseId.value) {
filters.value.buildingCase = ''
buildingCase.value = null
return
}
filters.value.buildingCase = `/api/building_cases/${id}`
loadCase()
loadStats()
reload()
}, { immediate: true })
</script>