feat: warn on the dashboard when a queen is 3+ years old
Queen.hatchYear was already captured but never used anywhere - no signal existed for "this colony's queen is overdue for evaluation or replacement," despite that being one of the most common productivity and swarming risk factors in beekeeping. Adds an OLD_QUEEN dashboard alert alongside the existing overdue-task/swarm-check ones, flagging any active colony whose current queen has completed 3+ seasons. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
8c4ae64e76
commit
edf13d3ae4
4 files changed
+104
-26
No files matched your search
@@ -107,4 +107,35 @@ describe('DashboardService', () => {
|
|||||||
|
|
||||||
expect(alerts).toHaveLength(0);
|
expect(alerts).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('OLD_QUEEN', () => {
|
||||||
|
it('flags a colony whose queen is at least 3 years old', async () => {
|
||||||
|
prisma.colony.findMany
|
||||||
|
.mockResolvedValueOnce([]) // attentionColonies
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{ id: 'colony-1', code: 'V-001', currentQueen: { hatchYear: 2020 } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const alerts = await service.alerts();
|
||||||
|
|
||||||
|
expect(alerts).toHaveLength(1);
|
||||||
|
expect(alerts[0].type).toBe('OLD_QUEEN');
|
||||||
|
expect(alerts[0].relatedId).toBe('colony-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not flag a queen younger than the threshold', async () => {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
prisma.colony.findMany.mockResolvedValueOnce([]).mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
id: 'colony-1',
|
||||||
|
code: 'V-001',
|
||||||
|
currentQueen: { hatchYear: currentYear },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const alerts = await service.alerts();
|
||||||
|
|
||||||
|
expect(alerts).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -12,13 +12,21 @@ export interface DashboardSummary {
|
|||||||
|
|
||||||
export interface DashboardAlert {
|
export interface DashboardAlert {
|
||||||
type:
|
type:
|
||||||
'OVERDUE_TASK' | 'COLONY_ATTENTION' | 'LOW_STOCK' | 'OVERDUE_SWARM_CHECK';
|
| 'OVERDUE_TASK'
|
||||||
|
| 'COLONY_ATTENTION'
|
||||||
|
| 'LOW_STOCK'
|
||||||
|
| 'OVERDUE_SWARM_CHECK'
|
||||||
|
| 'OLD_QUEEN';
|
||||||
message: string;
|
message: string;
|
||||||
relatedType: string;
|
relatedType: string;
|
||||||
relatedId: string;
|
relatedId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ATTENTION_COLONY_STATUSES = ['WEAK', 'QUEENLESS', 'CONSPICUOUS'] as const;
|
const ATTENTION_COLONY_STATUSES = ['WEAK', 'QUEENLESS', 'CONSPICUOUS'] as const;
|
||||||
|
// Queens rarely stay reliably productive past their third season - laying
|
||||||
|
// rate and swarming risk both worsen well before then, so this flags a
|
||||||
|
// colony for evaluation rather than demanding immediate requeening.
|
||||||
|
const OLD_QUEEN_AGE_YEARS = 3;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DashboardService {
|
export class DashboardService {
|
||||||
@@ -61,30 +69,46 @@ export class DashboardService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async alerts(): Promise<DashboardAlert[]> {
|
async alerts(): Promise<DashboardAlert[]> {
|
||||||
const [overdueTasks, attentionColonies, stockItems, swarmChecks] =
|
const [
|
||||||
await Promise.all([
|
overdueTasks,
|
||||||
this.prisma.task.findMany({
|
attentionColonies,
|
||||||
where: {
|
stockItems,
|
||||||
status: { in: ['OPEN', 'IN_PROGRESS'] },
|
swarmChecks,
|
||||||
dueDate: { lt: new Date() },
|
queenedColonies,
|
||||||
},
|
] = await Promise.all([
|
||||||
}),
|
this.prisma.task.findMany({
|
||||||
this.prisma.colony.findMany({
|
where: {
|
||||||
where: { status: { in: [...ATTENTION_COLONY_STATUSES] } },
|
status: { in: ['OPEN', 'IN_PROGRESS'] },
|
||||||
}),
|
dueDate: { lt: new Date() },
|
||||||
this.prisma.stockItem.findMany({
|
},
|
||||||
where: { minThreshold: { not: null } },
|
}),
|
||||||
}),
|
this.prisma.colony.findMany({
|
||||||
this.prisma.swarmControlCheck.findMany({
|
where: { status: { in: [...ATTENTION_COLONY_STATUSES] } },
|
||||||
where: { colony: { status: 'ACTIVE' } },
|
}),
|
||||||
orderBy: [{ colonyId: 'asc' }, { date: 'desc' }],
|
this.prisma.stockItem.findMany({
|
||||||
select: {
|
where: { minThreshold: { not: null } },
|
||||||
colonyId: true,
|
}),
|
||||||
nextCheckDate: true,
|
this.prisma.swarmControlCheck.findMany({
|
||||||
colony: { select: { code: true } },
|
where: { colony: { status: 'ACTIVE' } },
|
||||||
},
|
orderBy: [{ colonyId: 'asc' }, { date: 'desc' }],
|
||||||
}),
|
select: {
|
||||||
]);
|
colonyId: true,
|
||||||
|
nextCheckDate: true,
|
||||||
|
colony: { select: { code: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.prisma.colony.findMany({
|
||||||
|
where: {
|
||||||
|
status: 'ACTIVE',
|
||||||
|
currentQueen: { hatchYear: { not: null } },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
code: true,
|
||||||
|
currentQueen: { select: { hatchYear: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const alerts: DashboardAlert[] = [];
|
const alerts: DashboardAlert[] = [];
|
||||||
for (const task of overdueTasks) {
|
for (const task of overdueTasks) {
|
||||||
@@ -133,6 +157,23 @@ export class DashboardService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const currentYear = now.getFullYear();
|
||||||
|
for (const colony of queenedColonies) {
|
||||||
|
// The where clause above already requires currentQueen and its
|
||||||
|
// hatchYear to exist, but the relation is still nullable in the type.
|
||||||
|
if (!colony.currentQueen?.hatchYear) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ageYears = currentYear - colony.currentQueen.hatchYear;
|
||||||
|
if (ageYears >= OLD_QUEEN_AGE_YEARS) {
|
||||||
|
alerts.push({
|
||||||
|
type: 'OLD_QUEEN',
|
||||||
|
message: `Königin in Volk ${colony.code} ist ${ageYears} Jahre alt - Nachzucht prüfen`,
|
||||||
|
relatedType: 'Colony',
|
||||||
|
relatedId: colony.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return alerts;
|
return alerts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +66,11 @@ const ALERT_META: Record<
|
|||||||
icon: Sprout,
|
icon: Sprout,
|
||||||
className: "border-destructive/30 bg-destructive/5",
|
className: "border-destructive/30 bg-destructive/5",
|
||||||
},
|
},
|
||||||
|
OLD_QUEEN: {
|
||||||
|
label: "Königin überaltert",
|
||||||
|
icon: Crown,
|
||||||
|
className: "border-warning/40 bg-warning/10",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export type DashboardAlertType =
|
|||||||
| "OVERDUE_TASK"
|
| "OVERDUE_TASK"
|
||||||
| "COLONY_ATTENTION"
|
| "COLONY_ATTENTION"
|
||||||
| "LOW_STOCK"
|
| "LOW_STOCK"
|
||||||
| "OVERDUE_SWARM_CHECK";
|
| "OVERDUE_SWARM_CHECK"
|
||||||
|
| "OLD_QUEEN";
|
||||||
|
|
||||||
export interface DashboardAlert {
|
export interface DashboardAlert {
|
||||||
type: DashboardAlertType;
|
type: DashboardAlertType;
|
||||||
|
|||||||
Reference in new issue
Block a user