Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| 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>
290 lines
10 KiB
Vue
290 lines
10 KiB
Vue
<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>
|