feat: ajout du prix au kilo et de l'age moyen bovin + feed bovin via xlsx (!50)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
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>
This commit was merged in pull request #50.
This commit is contained in:
@@ -33,7 +33,22 @@
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 mb-16">
|
||||
<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"
|
||||
@@ -41,7 +56,6 @@
|
||||
:items="items"
|
||||
:total-items="totalItems"
|
||||
:loading="loading"
|
||||
:row-class="rowClass"
|
||||
:row-clickable="auth.isAdmin"
|
||||
empty-message="Aucun bovin dans cette case."
|
||||
@row-click="goToBovine"
|
||||
@@ -74,19 +88,19 @@
|
||||
<template #header-age>
|
||||
<UiTextInput :model-value="''" placeholder="Age" size="compact" disabled />
|
||||
</template>
|
||||
<template #header-breedCode>
|
||||
<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.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>
|
||||
@@ -94,16 +108,21 @@
|
||||
{{ formatDate(item.birthDate) }}
|
||||
</template>
|
||||
<template #cell-age="{ item }">
|
||||
{{ formatAgeLabel(item.ageMonths) }}
|
||||
<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-buildingCase.building.label="{ item }">
|
||||
{{ item.buildingCase?.building?.label ?? '—' }}
|
||||
<template #cell-pricePerKg="{ item }">
|
||||
{{ formatPrice(item.pricePerKg) }}
|
||||
</template>
|
||||
<template #cell-buildingCase.caseNumber="{ item }">
|
||||
{{ item.buildingCase?.caseNumber ?? '—' }}
|
||||
<template #cell-finalPrice="{ item }">
|
||||
{{ formatPrice(item.finalPrice) }}
|
||||
</template>
|
||||
</UiDataTable>
|
||||
</div>
|
||||
@@ -115,7 +134,8 @@ 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 { formatAgeLabel } from '~/utils/bovine-age'
|
||||
import { useBovineColumns } from '~/composables/useBovineColumns'
|
||||
import { formatAgeLabel, ageBadgeClass } from '~/utils/bovine-age'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -128,6 +148,34 @@ const hasCaseId = computed(() => Number.isFinite(caseId.value) && caseId.value >
|
||||
|
||||
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',
|
||||
@@ -136,7 +184,7 @@ const { items, totalItems, page, perPage, filters, loading, reload } =
|
||||
buildingCase: '',
|
||||
nationalNumber: '',
|
||||
workNumber: '',
|
||||
breedCode: '',
|
||||
'bovineType.label': '',
|
||||
sex: '',
|
||||
'arrivalDate[after]': '',
|
||||
'arrivalDate[strictly_before]': '',
|
||||
@@ -174,17 +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 = [
|
||||
{ key: 'nationalNumber', label: 'N° National', width: '160px' },
|
||||
{ key: 'workNumber', label: 'N° Travail', width: '85px' },
|
||||
{ key: 'sex', label: 'Sexe', width: '70px' },
|
||||
{ key: 'birthDate', label: 'Né le', width: '120px' },
|
||||
{ key: 'age', label: 'Age', width: '110px' },
|
||||
{ key: 'breedCode', label: 'Race' },
|
||||
{ key: 'buildingCase.building.label', label: 'Bâtiment', width: '1.5fr' },
|
||||
{ key: 'buildingCase.caseNumber', label: 'Case', width: '80px' },
|
||||
{ key: 'arrivalDate', label: 'Entrée le', width: '120px' }
|
||||
]
|
||||
const { columns } = useBovineColumns({ variant: 'case' })
|
||||
|
||||
const title = computed(() => {
|
||||
if (!buildingCase.value) return ''
|
||||
@@ -209,14 +247,12 @@ const formatDate = (date: string | null) => {
|
||||
})
|
||||
}
|
||||
|
||||
const rowClass = (item: BovineData): string => {
|
||||
if (item.ageMonths === null || item.ageMonths === undefined) return ''
|
||||
if (item.ageMonths >= 24) return 'bg-violet-300 hover:bg-violet-400'
|
||||
if (item.ageMonths >= 22) return 'bg-red-300 hover:bg-red-400'
|
||||
if (item.ageMonths >= 20) return 'bg-orange-300 hover:bg-orange-400'
|
||||
return ''
|
||||
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
|
||||
@@ -247,6 +283,7 @@ watch(caseId, (id) => {
|
||||
}
|
||||
filters.value.buildingCase = `/api/building_cases/${id}`
|
||||
loadCase()
|
||||
loadStats()
|
||||
reload()
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<h1 class="font-bold text-3xl uppercase text-primary-500">Inventaire bovins</h1>
|
||||
<span class="text-lg text-slate-500">({{ totalItems }} bovin{{ totalItems > 1 ? 's' : '' }})</span>
|
||||
<div
|
||||
v-if="auth.isAdmin"
|
||||
class="bg-primary-500 p-1 rounded-md flex items-center cursor-pointer hover:opacity-80"
|
||||
:class="exporting ? 'cursor-not-allowed opacity-60' : ''"
|
||||
title="Exporter en Excel"
|
||||
@@ -34,17 +35,17 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 mt-4">
|
||||
<div class="flex items-center gap-3 rounded-md border-2 border-violet-300 px-4 py-2">
|
||||
<span class="text-2xl font-bold text-violet-700">{{ stats.over24 }}</span>
|
||||
<span class="text-sm uppercase tracking-wide text-violet-700">≥ 24 mois</span>
|
||||
<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 border-2 border-red-300 px-4 py-2">
|
||||
<span class="text-2xl font-bold text-red-700">{{ stats.between22And24 }}</span>
|
||||
<span class="text-sm uppercase tracking-wide text-red-700">22 – 24 mois</span>
|
||||
<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 border-2 border-orange-300 px-4 py-2">
|
||||
<span class="text-2xl font-bold text-orange-700">{{ stats.between20And22 }}</span>
|
||||
<span class="text-sm uppercase tracking-wide text-orange-700">20 – 22 mois</span>
|
||||
<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>
|
||||
|
||||
@@ -56,7 +57,6 @@
|
||||
:items="items"
|
||||
:total-items="totalItems"
|
||||
:loading="loading"
|
||||
:row-class="rowClass"
|
||||
>
|
||||
<template #header-nationalNumber>
|
||||
<UiTextInput
|
||||
@@ -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"
|
||||
/>
|
||||
@@ -102,21 +102,38 @@
|
||||
<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 #cell-birthDate="{ item }">
|
||||
{{ formatDate(item.birthDate) }}
|
||||
</template>
|
||||
<template #cell-age="{ item }">
|
||||
{{ formatAgeLabel(item.ageMonths) }}
|
||||
<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-buildingCase.building.label="{ item }">
|
||||
{{ item.buildingCase?.building?.label ?? '—' }}
|
||||
{{ item.effectiveBuilding?.label ?? '—' }}
|
||||
</template>
|
||||
<template #cell-buildingCase.caseNumber="{ item }">
|
||||
{{ item.buildingCase?.caseNumber ?? '—' }}
|
||||
</template>
|
||||
<template #cell-pricePerKg="{ item }">
|
||||
{{ formatPrice(item.pricePerKg) }}
|
||||
</template>
|
||||
<template #cell-finalPrice="{ item }">
|
||||
{{ formatPrice(item.finalPrice) }}
|
||||
</template>
|
||||
</UiDataTable>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,7 +144,8 @@
|
||||
import type { BovineData } from '~/services/dto/bovine-data'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
||||
import { formatAgeLabel } from '~/utils/bovine-age'
|
||||
import { useBovineColumns } from '~/composables/useBovineColumns'
|
||||
import { formatAgeLabel, ageBadgeClass } from '~/utils/bovine-age'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
@@ -218,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]': '',
|
||||
@@ -255,17 +273,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 = [
|
||||
{ key: 'nationalNumber', label: 'N° National', width: '160px' },
|
||||
{ key: 'workNumber', label: 'N° Travail', width: '85px' },
|
||||
{ key: 'sex', label: 'Sexe', width: '70px' },
|
||||
{ key: 'birthDate', label: 'Né le', width: '120px' },
|
||||
{ key: 'age', label: 'Age', width: '110px' },
|
||||
{ key: 'breedCode', label: 'Race' },
|
||||
{ key: 'buildingCase.building.label', label: 'Bâtiment', width: '1.5fr' },
|
||||
{ key: 'buildingCase.caseNumber', label: 'Case', width: '80px' },
|
||||
{ key: 'arrivalDate', label: 'Entrée le', width: '120px' }
|
||||
]
|
||||
const { columns } = useBovineColumns()
|
||||
|
||||
const formatDate = (date: string | null) => {
|
||||
if (!date) return '—'
|
||||
@@ -278,14 +286,12 @@ const formatDate = (date: string | null) => {
|
||||
})
|
||||
}
|
||||
|
||||
const rowClass = (item: BovineData): string => {
|
||||
if (item.ageMonths === null || item.ageMonths === undefined) return ''
|
||||
if (item.ageMonths >= 24) return 'bg-violet-300 hover:bg-violet-400'
|
||||
if (item.ageMonths >= 22) return 'bg-red-300 hover:bg-red-400'
|
||||
if (item.ageMonths >= 20) return 'bg-orange-300 hover:bg-orange-400'
|
||||
return ''
|
||||
const formatPrice = (price: number | null) => {
|
||||
if (price === null || price === undefined) return '—'
|
||||
return `${price.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
reload()
|
||||
loadStats()
|
||||
|
||||
Reference in New Issue
Block a user