diff --git a/frontend/components/ui/UiDataTable.vue b/frontend/components/ui/UiDataTable.vue
index e385879..b3a4eee 100644
--- a/frontend/components/ui/UiDataTable.vue
+++ b/frontend/components/ui/UiDataTable.vue
@@ -19,7 +19,10 @@
v-for="(item, index) in paginatedItems"
:key="item.id ?? index"
class="grid gap-6 px-4 py-3 text-sm border-t border-slate-200"
- :class="rowClickable ? 'hover:bg-slate-50 cursor-pointer' : ''"
+ :class="[
+ rowClickable ? 'hover:bg-slate-50 cursor-pointer' : '',
+ rowClass ? rowClass(item) : ''
+ ]"
:style="{ gridTemplateColumns: gridCols }"
:role="rowClickable ? 'button' : undefined"
:tabindex="rowClickable ? 0 : undefined"
@@ -83,12 +86,12 @@
@@ -145,6 +148,7 @@ const props = withDefaults(defineProps<{
showActions?: boolean
emptyMessage?: string
loading?: boolean
+ rowClass?: (item: T) => string | undefined
}>(), {
totalItems: undefined,
page: 1,
@@ -153,7 +157,8 @@ const props = withDefaults(defineProps<{
rowClickable: false,
showActions: false,
emptyMessage: 'Aucune donnée',
- loading: false
+ loading: false,
+ rowClass: undefined
})
const emit = defineEmits<{
diff --git a/frontend/pages/inventory.vue b/frontend/pages/inventory.vue
index 9f681a4..40ddfb3 100644
--- a/frontend/pages/inventory.vue
+++ b/frontend/pages/inventory.vue
@@ -30,6 +30,7 @@
:items="items"
:total-items="totalItems"
:loading="loading"
+ :row-class="rowClass"
>
+
+
+
{{ formatDate(item.birthDate) }}
+
+ {{ formatAgeLabel(item.ageMonths) }}
+
{{ formatDate(item.arrivalDate) }}
@@ -94,6 +101,7 @@
import type { BovineData } from '~/services/dto/bovine-data'
import { useAuthStore } from '~/stores/auth'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
+import { formatAgeLabel } from '~/utils/bovine-age'
const router = useRouter()
const auth = useAuthStore()
@@ -177,11 +185,12 @@ const birthDateFilter = singleDateFilter('birthDate[after]', 'birthDate[strictly
const columns = [
{ key: 'nationalNumber', label: 'N° National', width: '160px' },
- { key: 'workNumber', label: 'N° Travail', width: '110px' },
- { key: 'sex', label: 'Sexe', width: '90px' },
+ { 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' },
+ { 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' }
]
@@ -197,5 +206,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-red-100 hover:bg-red-200'
+ if (item.ageMonths >= 22) return 'bg-orange-100 hover:bg-orange-200'
+ return ''
+}
+
onMounted(reload)
diff --git a/frontend/services/dto/bovine-data.ts b/frontend/services/dto/bovine-data.ts
index 32378ad..db5d857 100644
--- a/frontend/services/dto/bovine-data.ts
+++ b/frontend/services/dto/bovine-data.ts
@@ -15,6 +15,7 @@ export interface BovineData {
birthDate: string | null
breedCode: string | null
sex: string | null
+ ageMonths: number | null
exitedAt: string | null
}
diff --git a/frontend/utils/bovine-age.ts b/frontend/utils/bovine-age.ts
new file mode 100644
index 0000000..6613bc9
--- /dev/null
+++ b/frontend/utils/bovine-age.ts
@@ -0,0 +1,10 @@
+export const formatAgeLabel = (months: number | null | undefined): string => {
+ if (months === null || months === undefined) return '—'
+ const years = Math.floor(months / 12)
+ const remaining = months % 12
+ let label = ''
+ if (years > 0) label = `${years} an${years > 1 ? 's' : ''}`
+ if (remaining > 0) label += `${label ? ' ' : ''}${remaining} mois`
+ if (!label) label = '< 1 mois'
+ return label
+}
diff --git a/migrations/Version20260424074454.php b/migrations/Version20260424074454.php
new file mode 100644
index 0000000..7a9b6b8
--- /dev/null
+++ b/migrations/Version20260424074454.php
@@ -0,0 +1,31 @@
+addSql('ALTER TABLE bovine ADD age_months INT DEFAULT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE bovine DROP age_months');
+ }
+}
diff --git a/src/Entity/Bovine.php b/src/Entity/Bovine.php
index 229af14..de80aaa 100644
--- a/src/Entity/Bovine.php
+++ b/src/Entity/Bovine.php
@@ -22,6 +22,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
#[ORM\Entity]
+#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'bovine')]
#[ORM\UniqueConstraint(name: 'uniq_bovine_national_number', columns: ['national_number'])]
#[ApiFilter(SearchFilter::class, properties: [
@@ -106,6 +107,10 @@ class Bovine
#[Groups(['bovine:read', 'building_case:read'])]
private ?string $sex = null;
+ #[ORM\Column(type: 'integer', nullable: true)]
+ #[Groups(['bovine:read', 'building_case:read'])]
+ private ?int $ageMonths = null;
+
#[ORM\Column(type: 'date_immutable', nullable: true)]
#[Groups(['bovine:read', 'building_case:read'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
@@ -252,4 +257,30 @@ class Bovine
return $this;
}
+
+ public function getAgeMonths(): ?int
+ {
+ return $this->ageMonths;
+ }
+
+ public function setAgeMonths(?int $ageMonths): static
+ {
+ $this->ageMonths = $ageMonths;
+
+ return $this;
+ }
+
+ #[ORM\PrePersist]
+ #[ORM\PreUpdate]
+ public function refreshAgeMonths(): void
+ {
+ if (null === $this->birthDate) {
+ $this->ageMonths = null;
+
+ return;
+ }
+
+ $diff = $this->birthDate->diff(new DateTimeImmutable());
+ $this->ageMonths = ($diff->y * 12) + $diff->m;
+ }
}
diff --git a/src/State/Bovin/BovineSyncInventoryProcessor.php b/src/State/Bovin/BovineSyncInventoryProcessor.php
index 58cff5b..58e2db7 100644
--- a/src/State/Bovin/BovineSyncInventoryProcessor.php
+++ b/src/State/Bovin/BovineSyncInventoryProcessor.php
@@ -100,5 +100,6 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
}
$bovine->setArrivalDate($latestEntry);
$bovine->setExitDate($latestExit);
+ $bovine->refreshAgeMonths();
}
}