feat: warn on the dashboard when a swarm check is overdue
Schwarmkontrolle already captures nextCheckDate per check, but it was only ever visible by opening that one colony's tab. During swarm season a missed follow-up check can mean losing half the colony, so this now surfaces as a dashboard alert (OVERDUE_SWARM_CHECK) like overdue tasks already do - only the most recent check per colony is considered, so a completed follow-up clears the warning. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
acbf5f16ce
commit
0881a38800
4 files changed
+88
-3
No files matched your search
@@ -10,6 +10,7 @@ describe('DashboardService', () => {
|
|||||||
task: { count: jest.Mock; findMany: jest.Mock };
|
task: { count: jest.Mock; findMany: jest.Mock };
|
||||||
honeyBatch: { count: jest.Mock };
|
honeyBatch: { count: jest.Mock };
|
||||||
stockItem: { findMany: jest.Mock };
|
stockItem: { findMany: jest.Mock };
|
||||||
|
swarmControlCheck: { findMany: jest.Mock };
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -26,6 +27,7 @@ describe('DashboardService', () => {
|
|||||||
},
|
},
|
||||||
honeyBatch: { count: jest.fn().mockResolvedValue(1) },
|
honeyBatch: { count: jest.fn().mockResolvedValue(1) },
|
||||||
stockItem: { findMany: jest.fn().mockResolvedValue([]) },
|
stockItem: { findMany: jest.fn().mockResolvedValue([]) },
|
||||||
|
swarmControlCheck: { findMany: jest.fn().mockResolvedValue([]) },
|
||||||
};
|
};
|
||||||
service = new DashboardService(prisma as unknown as PrismaService);
|
service = new DashboardService(prisma as unknown as PrismaService);
|
||||||
});
|
});
|
||||||
@@ -63,4 +65,46 @@ describe('DashboardService', () => {
|
|||||||
'LOW_STOCK',
|
'LOW_STOCK',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('flags a colony whose most recent swarm check is overdue', async () => {
|
||||||
|
prisma.swarmControlCheck.findMany.mockResolvedValue([
|
||||||
|
{
|
||||||
|
colonyId: 'colony-1',
|
||||||
|
nextCheckDate: new Date('2020-01-01'),
|
||||||
|
colony: { code: 'V-001' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const alerts = await service.alerts();
|
||||||
|
|
||||||
|
expect(alerts).toEqual([
|
||||||
|
{
|
||||||
|
type: 'OVERDUE_SWARM_CHECK',
|
||||||
|
message: 'Schwarmkontrolle für Volk V-001 überfällig',
|
||||||
|
relatedType: 'Colony',
|
||||||
|
relatedId: 'colony-1',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores an overdue swarm check when a newer check already superseded it', async () => {
|
||||||
|
prisma.swarmControlCheck.findMany.mockResolvedValue([
|
||||||
|
{
|
||||||
|
colonyId: 'colony-1',
|
||||||
|
date: new Date('2026-06-10'),
|
||||||
|
nextCheckDate: null,
|
||||||
|
colony: { code: 'V-001' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colonyId: 'colony-1',
|
||||||
|
date: new Date('2026-06-01'),
|
||||||
|
nextCheckDate: new Date('2020-01-01'),
|
||||||
|
colony: { code: 'V-001' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const alerts = await service.alerts();
|
||||||
|
|
||||||
|
expect(alerts).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -11,7 +11,8 @@ export interface DashboardSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardAlert {
|
export interface DashboardAlert {
|
||||||
type: 'OVERDUE_TASK' | 'COLONY_ATTENTION' | 'LOW_STOCK';
|
type:
|
||||||
|
'OVERDUE_TASK' | 'COLONY_ATTENTION' | 'LOW_STOCK' | 'OVERDUE_SWARM_CHECK';
|
||||||
message: string;
|
message: string;
|
||||||
relatedType: string;
|
relatedType: string;
|
||||||
relatedId: string;
|
relatedId: string;
|
||||||
@@ -60,7 +61,8 @@ export class DashboardService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async alerts(): Promise<DashboardAlert[]> {
|
async alerts(): Promise<DashboardAlert[]> {
|
||||||
const [overdueTasks, attentionColonies, stockItems] = await Promise.all([
|
const [overdueTasks, attentionColonies, stockItems, swarmChecks] =
|
||||||
|
await Promise.all([
|
||||||
this.prisma.task.findMany({
|
this.prisma.task.findMany({
|
||||||
where: {
|
where: {
|
||||||
status: { in: ['OPEN', 'IN_PROGRESS'] },
|
status: { in: ['OPEN', 'IN_PROGRESS'] },
|
||||||
@@ -73,6 +75,15 @@ export class DashboardService {
|
|||||||
this.prisma.stockItem.findMany({
|
this.prisma.stockItem.findMany({
|
||||||
where: { minThreshold: { not: null } },
|
where: { minThreshold: { not: null } },
|
||||||
}),
|
}),
|
||||||
|
this.prisma.swarmControlCheck.findMany({
|
||||||
|
where: { colony: { status: 'ACTIVE' } },
|
||||||
|
orderBy: [{ colonyId: 'asc' }, { date: 'desc' }],
|
||||||
|
select: {
|
||||||
|
colonyId: true,
|
||||||
|
nextCheckDate: true,
|
||||||
|
colony: { select: { code: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const alerts: DashboardAlert[] = [];
|
const alerts: DashboardAlert[] = [];
|
||||||
@@ -102,6 +113,26 @@ export class DashboardService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// swarmChecks is ordered by colonyId then date desc, so the first entry
|
||||||
|
// seen per colonyId is that colony's most recent check - only the most
|
||||||
|
// recent nextCheckDate matters, an older overdue one has been superseded.
|
||||||
|
const now = new Date();
|
||||||
|
const latestCheckByColony = new Map<string, (typeof swarmChecks)[number]>();
|
||||||
|
for (const check of swarmChecks) {
|
||||||
|
if (!latestCheckByColony.has(check.colonyId)) {
|
||||||
|
latestCheckByColony.set(check.colonyId, check);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const check of latestCheckByColony.values()) {
|
||||||
|
if (check.nextCheckDate && check.nextCheckDate < now) {
|
||||||
|
alerts.push({
|
||||||
|
type: 'OVERDUE_SWARM_CHECK',
|
||||||
|
message: `Schwarmkontrolle für Volk ${check.colony.code} überfällig`,
|
||||||
|
relatedType: 'Colony',
|
||||||
|
relatedId: check.colonyId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return alerts;
|
return alerts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Layers,
|
Layers,
|
||||||
PackageSearch,
|
PackageSearch,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
|
Sprout,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/lib/auth/auth-context";
|
import { useAuth } from "@/lib/auth/auth-context";
|
||||||
import { fetchDashboardAlerts, fetchDashboardSummary } from "@/lib/dashboard/api";
|
import { fetchDashboardAlerts, fetchDashboardSummary } from "@/lib/dashboard/api";
|
||||||
@@ -60,6 +61,11 @@ const ALERT_META: Record<
|
|||||||
icon: PackageSearch,
|
icon: PackageSearch,
|
||||||
className: "border-warning/40 bg-warning/10",
|
className: "border-warning/40 bg-warning/10",
|
||||||
},
|
},
|
||||||
|
OVERDUE_SWARM_CHECK: {
|
||||||
|
label: "Schwarmkontrolle überfällig",
|
||||||
|
icon: Sprout,
|
||||||
|
className: "border-destructive/30 bg-destructive/5",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ export interface DashboardSummary {
|
|||||||
stockItemsLowCount: number;
|
stockItemsLowCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DashboardAlertType = "OVERDUE_TASK" | "COLONY_ATTENTION" | "LOW_STOCK";
|
export type DashboardAlertType =
|
||||||
|
| "OVERDUE_TASK"
|
||||||
|
| "COLONY_ATTENTION"
|
||||||
|
| "LOW_STOCK"
|
||||||
|
| "OVERDUE_SWARM_CHECK";
|
||||||
|
|
||||||
export interface DashboardAlert {
|
export interface DashboardAlert {
|
||||||
type: DashboardAlertType;
|
type: DashboardAlertType;
|
||||||
|
|||||||
Reference in new issue
Block a user