feat: T4 wave 5 - colony end cleanup, withdrawal chain, next steps, inspection takeover UI
- Colony end releases the queen in the same transaction; open suggestions become obsolete and a CLEANUP suggestion lists open tasks and the queen; apply-cleanup validates evidence and blocks reactivated colonies - Withdrawal chain: plausibility warnings on harvest and bottling, conflict notifications also when a treatment is entered or corrected after the harvest (naming bottled lots), withdrawal spans in the calendar and M365 sync, Berlin-day end events - Next 3 steps per colony and the "Heute" endpoint with ranking and an explicit merge table by intent - Inspection-triggered rule suggestions follow the colony's latest inspection (resolve when the finding is gone, refresh when it stays) - Frontend: colony card takeover in the inspection form, feed status as condition, occupied frames 0-40; legacy offline entries keep syncing - Notifications and preferences are deleted with their user Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
02eb16924e
commit
ef33e5549b
122 files changed
+10707
-504
No files matched your search
@@ -89,6 +89,9 @@ describe('Apiaries (e2e)', () => {
|
||||
await prisma.task.deleteMany({
|
||||
where: { title: { startsWith: 'field-e2e-' } },
|
||||
});
|
||||
await prisma.suggestion.deleteMany({
|
||||
where: { colony: { apiaryId: fieldApiaryId } },
|
||||
});
|
||||
await prisma.colony.deleteMany({ where: { apiaryId: fieldApiaryId } });
|
||||
await prisma.apiary.delete({ where: { id: fieldApiaryId } });
|
||||
}
|
||||
@@ -210,7 +213,14 @@ describe('Apiaries (e2e)', () => {
|
||||
describe('apiary field endpoints', () => {
|
||||
interface OverviewBody {
|
||||
apiaryId: string;
|
||||
rows: { code: string }[];
|
||||
rows: {
|
||||
code: string;
|
||||
inspectionDueOn: string | null;
|
||||
segment: string;
|
||||
nextStepTitle: string | null;
|
||||
openWarnings: number;
|
||||
openCritical: number;
|
||||
}[];
|
||||
}
|
||||
interface PackBody {
|
||||
version: number;
|
||||
@@ -285,6 +295,109 @@ describe('Apiaries (e2e)', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('colony-overview carries inspectionDueOn, segment and nextStepTitle; warnings come from suggestions', async () => {
|
||||
const permissions = [
|
||||
'colonies:read',
|
||||
'tasks:read',
|
||||
'inspections:read',
|
||||
'varroa:read',
|
||||
];
|
||||
const role = await prisma.role.create({
|
||||
data: {
|
||||
key: `FIELD_OVERVIEW_${Date.now()}`,
|
||||
name: 'Feld-Übersicht',
|
||||
permissions: {
|
||||
create: permissions.map((key) => ({
|
||||
permission: { connect: { key } },
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
const email = `apiaries-e2e-overview-${Date.now()}@example.com`;
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
passwordHash: await bcrypt.hash(password, 12),
|
||||
name: 'Overview',
|
||||
roleId: role.id,
|
||||
},
|
||||
});
|
||||
const colony = await prisma.colony.findFirstOrThrow({
|
||||
where: { apiaryId: fieldApiaryId, code: { startsWith: 'V-10-' } },
|
||||
});
|
||||
const base = {
|
||||
ruleKey: 'VARROA_THRESHOLD' as const,
|
||||
source: 'RULE' as const,
|
||||
mode: 'PLAN' as const,
|
||||
intent: 'VARROA_TREAT' as const,
|
||||
relatedType: 'COLONY' as const,
|
||||
relatedId: colony.id,
|
||||
colonyId: colony.id,
|
||||
reasonCode: 'FIELD_E2E',
|
||||
evidence: { colonyId: colony.id },
|
||||
};
|
||||
try {
|
||||
await prisma.suggestion.createMany({
|
||||
data: [
|
||||
{ ...base, priority: 'HIGH', requiredPermission: 'varroa:read' },
|
||||
{
|
||||
...base,
|
||||
intent: 'HONEY_SUPER',
|
||||
priority: 'URGENT',
|
||||
requiredPermission: 'inspections:read',
|
||||
},
|
||||
{
|
||||
...base,
|
||||
intent: 'FEED',
|
||||
priority: 'URGENT',
|
||||
requiredPermission: 'feedings:read',
|
||||
},
|
||||
{ ...base, intent: 'INSPECT', priority: 'NORMAL' },
|
||||
],
|
||||
});
|
||||
const token = (await login(app, email, password)).accessToken;
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v1/apiaries/${fieldApiaryId}/colony-overview`)
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
const rows = (response.body as OverviewBody).rows;
|
||||
const row = rows.find((item) => item.code.startsWith('V-10-'));
|
||||
// HIGH (varroa:read) and URGENT (inspections:read) are visible, the
|
||||
// URGENT one that needs feedings:read is not.
|
||||
expect(row).toMatchObject({ openWarnings: 1, openCritical: 1 });
|
||||
expect(row?.nextStepTitle).toEqual(expect.any(String));
|
||||
expect(row?.segment).toEqual(expect.any(String));
|
||||
expect(row).toHaveProperty('inspectionDueOn');
|
||||
if (row?.segment === 'WINTERRUHE') {
|
||||
expect(row.inspectionDueOn).toBeNull();
|
||||
} else {
|
||||
expect(row?.inspectionDueOn).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
}
|
||||
const quiet = rows.find((item) => item.code.startsWith('V-2-'));
|
||||
expect(quiet).toMatchObject({
|
||||
openWarnings: 0,
|
||||
openCritical: 0,
|
||||
nextStepTitle: null,
|
||||
});
|
||||
|
||||
const pack = await request(app.getHttpServer())
|
||||
.get(`/api/v1/apiaries/${fieldApiaryId}/offline-pack`)
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
const steps = (pack.body as { nextSteps?: Record<string, unknown[]> })
|
||||
.nextSteps;
|
||||
expect(steps?.[colony.id]?.length).toBeGreaterThan(0);
|
||||
expect(steps?.[colony.id]?.length).toBeLessThanOrEqual(3);
|
||||
} finally {
|
||||
await prisma.suggestion.deleteMany({ where: { colonyId: colony.id } });
|
||||
await prisma.refreshToken.deleteMany({ where: { userId: user.id } });
|
||||
await prisma.user.delete({ where: { id: user.id } });
|
||||
await prisma.role.delete({ where: { id: role.id } });
|
||||
}
|
||||
});
|
||||
|
||||
it('offline-pack contains the open tasks of the apiary and its colonies', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v1/apiaries/${fieldApiaryId}/offline-pack`)
|
||||
|
||||
Reference in new issue
Block a user