feat : refonte de l'affichage des âges et restriction des prix aux admins
- Repository BovineRepository avec getInventoryStats en DQL - Sécurité ApiProperty ROLE_ADMIN sur pricePerKg et finalPrice - Endpoint inventory-export passe en ROLE_ADMIN - Composable useBovineColumns mutualisé entre inventory et case (admin/user séparés) - Stats par tranche d'âge filtrables par buildingCaseId - Légende avec cartes colorées pleines + texte blanc - Coloration de la cellule Age (badge) au lieu de toute la ligne - Décalage couleurs : red ≥ 24, orange 22-24, yellow 20-22 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
47
frontend/composables/useBovineColumns.ts
Normal file
47
frontend/composables/useBovineColumns.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { computed } from 'vue'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
export interface BovineColumn {
|
||||
key: string
|
||||
label: string
|
||||
width?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const useBovineColumns = () => {
|
||||
const auth = useAuthStore()
|
||||
|
||||
const adminColumns: 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: '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' }
|
||||
]
|
||||
|
||||
const userColumns: 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: 'buildingCase.caseNumber', label: 'Case', width: '42px' },
|
||||
{ key: 'arrivalDate', label: 'Entrée le', width: '90px' }
|
||||
]
|
||||
|
||||
const columns = computed<BovineColumn[]>(() => auth.isAdmin ? adminColumns : userColumns)
|
||||
|
||||
return { columns }
|
||||
}
|
||||
@@ -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"
|
||||
@@ -100,7 +114,12 @@
|
||||
{{ 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) }}
|
||||
@@ -127,7 +146,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()
|
||||
@@ -140,6 +160,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',
|
||||
@@ -186,19 +234,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: '80px' },
|
||||
{ key: 'workNumber', label: 'N° Travail', width: '43px' },
|
||||
{ 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: '75px' },
|
||||
{ key: 'buildingCase.caseNumber', label: 'Case', width: '60px' },
|
||||
{ key: 'arrivalDate', label: 'Entrée le', width: '90px' },
|
||||
{ key: 'pricePerKg', label: 'Prix/kg', width: '65px' },
|
||||
{ key: 'finalPrice', label: 'Prix total', width: '100px' }
|
||||
]
|
||||
const { columns } = useBovineColumns()
|
||||
|
||||
const title = computed(() => {
|
||||
if (!buildingCase.value) return ''
|
||||
@@ -228,13 +264,6 @@ const formatPrice = (price: number | null) => {
|
||||
return `${price.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`
|
||||
}
|
||||
|
||||
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 loadCase = async () => {
|
||||
if (!hasCaseId.value) {
|
||||
@@ -266,6 +295,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
|
||||
@@ -112,7 +112,12 @@
|
||||
{{ 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) }}
|
||||
@@ -139,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()
|
||||
@@ -267,19 +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: '80px' },
|
||||
{ key: 'workNumber', label: 'N° Travail', width: '43px' },
|
||||
{ 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: '75px' },
|
||||
{ key: 'buildingCase.caseNumber', label: 'Case', width: '60px' },
|
||||
{ key: 'arrivalDate', label: 'Entrée le', width: '90px' },
|
||||
{ key: 'pricePerKg', label: 'Prix/kg', width: '65px' },
|
||||
{ key: 'finalPrice', label: 'Prix total', width: '100px' }
|
||||
]
|
||||
const { columns } = useBovineColumns()
|
||||
|
||||
const formatDate = (date: string | null) => {
|
||||
if (!date) return '—'
|
||||
@@ -297,13 +291,6 @@ const formatPrice = (price: number | null) => {
|
||||
return `${price.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`
|
||||
}
|
||||
|
||||
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 ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
reload()
|
||||
|
||||
@@ -8,3 +8,11 @@ export const formatAgeLabel = (months: number | null | undefined): string => {
|
||||
if (!label) label = '< 1 mois'
|
||||
return label
|
||||
}
|
||||
|
||||
export const ageBadgeClass = (months: number | null | undefined): string => {
|
||||
if (months === null || months === undefined) return ''
|
||||
if (months >= 24) return 'bg-red-500 text-white'
|
||||
if (months >= 22) return 'bg-orange-500 text-white'
|
||||
if (months >= 20) return 'bg-yellow-500 text-white'
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use App\State\Bovin\BovineInventoryExportProvider;
|
||||
description: "Retourne un fichier XLSX listant tous les bovins actifs (exitedAt IS NULL) triés par date de naissance croissante, avec colorisation des lignes selon l'âge.",
|
||||
tags: ['Bovines'],
|
||||
),
|
||||
security: "is_granted('ROLE_USER')",
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
output: false,
|
||||
provider: BovineInventoryExportProvider::class,
|
||||
),
|
||||
|
||||
@@ -14,6 +14,7 @@ use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Repository\BovineRepository;
|
||||
use App\State\Bovin\BovineProcessor;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -21,7 +22,7 @@ use Symfony\Component\Serializer\Attribute\Context;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Entity(repositoryClass: BovineRepository::class)]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ORM\Table(name: 'bovine')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_bovine_national_number', columns: ['national_number'])]
|
||||
@@ -79,6 +80,7 @@ class Bovine
|
||||
|
||||
#[ORM\Column(type: 'float', nullable: true)]
|
||||
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN')")]
|
||||
private ?float $pricePerKg = null;
|
||||
|
||||
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||
@@ -168,6 +170,7 @@ class Bovine
|
||||
}
|
||||
|
||||
#[Groups(['bovine:read', 'building_case:read'])]
|
||||
#[ApiProperty(security: "is_granted('ROLE_ADMIN')")]
|
||||
public function getFinalPrice(): ?float
|
||||
{
|
||||
if (null === $this->receivedWeight || null === $this->pricePerKg) {
|
||||
|
||||
53
src/Repository/BovineRepository.php
Normal file
53
src/Repository/BovineRepository.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Bovine;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Bovine>
|
||||
*/
|
||||
final class BovineRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Bovine::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compteurs des bovins actifs par tranche d'âge.
|
||||
*
|
||||
* @return array{total: int, over24: int, between22And24: int, between20And22: int}
|
||||
*/
|
||||
public function getInventoryStats(?int $buildingCaseId = null): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('b')
|
||||
->select(
|
||||
'COUNT(b.id) AS total',
|
||||
'SUM(CASE WHEN b.ageMonths >= 24 THEN 1 ELSE 0 END) AS over24',
|
||||
'SUM(CASE WHEN b.ageMonths >= 22 AND b.ageMonths < 24 THEN 1 ELSE 0 END) AS between22And24',
|
||||
'SUM(CASE WHEN b.ageMonths >= 20 AND b.ageMonths < 22 THEN 1 ELSE 0 END) AS between20And22',
|
||||
)
|
||||
->where('b.exitedAt IS NULL')
|
||||
;
|
||||
|
||||
if (null !== $buildingCaseId) {
|
||||
$qb->andWhere('b.buildingCase = :caseId')
|
||||
->setParameter('caseId', $buildingCaseId)
|
||||
;
|
||||
}
|
||||
|
||||
$row = $qb->getQuery()->getSingleResult();
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'over24' => (int) ($row['over24'] ?? 0),
|
||||
'between22And24' => (int) ($row['between22And24'] ?? 0),
|
||||
'between20And22' => (int) ($row['between20And22'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,12 @@ final class BovineInventoryExportProvider implements ProviderInterface
|
||||
{
|
||||
private const HEADER_FILL = 'FFF1F5F9';
|
||||
|
||||
private const COLOR_VIOLET = 'FFC4B5FD';
|
||||
|
||||
private const COLOR_RED = 'FFFCA5A5';
|
||||
|
||||
private const COLOR_ORANGE = 'FFFDBA74';
|
||||
|
||||
private const COLOR_YELLOW = 'FFFDE047';
|
||||
|
||||
private const HEADERS = [
|
||||
'N° National',
|
||||
'N° Travail',
|
||||
@@ -182,14 +182,14 @@ final class BovineInventoryExportProvider implements ProviderInterface
|
||||
return null;
|
||||
}
|
||||
if ($ageMonths >= 24) {
|
||||
return self::COLOR_VIOLET;
|
||||
}
|
||||
if ($ageMonths >= 22) {
|
||||
return self::COLOR_RED;
|
||||
}
|
||||
if ($ageMonths >= 20) {
|
||||
if ($ageMonths >= 22) {
|
||||
return self::COLOR_ORANGE;
|
||||
}
|
||||
if ($ageMonths >= 20) {
|
||||
return self::COLOR_YELLOW;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace App\State\Bovin;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\BovineInventoryStats;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use App\Repository\BovineRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* @implements ProviderInterface<BovineInventoryStats>
|
||||
@@ -15,26 +16,22 @@ use Doctrine\DBAL\Connection;
|
||||
final class BovineInventoryStatsProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private BovineRepository $bovineRepository,
|
||||
private RequestStack $requestStack,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): BovineInventoryStats
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(<<<'SQL'
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE age_months >= 24) AS over_24,
|
||||
COUNT(*) FILTER (WHERE age_months >= 22 AND age_months < 24) AS between_22_and_24,
|
||||
COUNT(*) FILTER (WHERE age_months >= 20 AND age_months < 22) AS between_20_and_22
|
||||
FROM bovine
|
||||
WHERE exited_at IS NULL
|
||||
SQL);
|
||||
$rawCaseId = $this->requestStack->getCurrentRequest()?->query->get('buildingCaseId');
|
||||
$caseId = null !== $rawCaseId && ctype_digit((string) $rawCaseId) ? (int) $rawCaseId : null;
|
||||
|
||||
$row = $this->bovineRepository->getInventoryStats($caseId);
|
||||
|
||||
$stats = new BovineInventoryStats();
|
||||
$stats->total = (int) ($row['total'] ?? 0);
|
||||
$stats->over24 = (int) ($row['over_24'] ?? 0);
|
||||
$stats->between22And24 = (int) ($row['between_22_and_24'] ?? 0);
|
||||
$stats->between20And22 = (int) ($row['between_20_and_22'] ?? 0);
|
||||
$stats->total = $row['total'];
|
||||
$stats->over24 = $row['over24'];
|
||||
$stats->between22And24 = $row['between22And24'];
|
||||
$stats->between20And22 = $row['between20And22'];
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user