feat: T4 wave 4 - rules suggest, takeover, chains, autopilot A1-A3, suggestions page, varroa bands UI
- Automation rules propose suggestions instead of creating tasks; notify per rule config; corrected varroa measurements refresh or resolve their suggestion - Inspection values can be taken over into the colony card (occupiedFrames 0-40, strength deprecated); warning counts use the same visibility rule as the notification list - Biotech chains: swarm check after queen cells, queen acceptance check, split schedules (raise own, queen cell, queen) - Season autopilot core with A1 inspection due, A2 varroa measurement, A3 recheck after treatment; colony sync and daily step - Frontend: suggestions page with groups, bulk accept, undo, paging, focus management and permission gating - Frontend: varroa traffic light from seasonal bands, sample size, one range request for the colony chart - Notifications: mark-read answers 404 for rows the user may no longer see Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
64a2b7dad9
commit
02eb16924e
142 files changed
+11847
-1251
No files matched your search
@@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PermissionResolverService } from '../auth/permissions/permission-resolver.service';
|
||||
import { LIVING_COLONY_WHERE } from '../colonies/colony-status';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { Prisma } from '../../generated/prisma';
|
||||
import type {
|
||||
ColonyOverview,
|
||||
ColonyOverviewRow,
|
||||
@@ -83,6 +84,7 @@ export class ApiaryFieldService {
|
||||
colonyType: true,
|
||||
hiveType: true,
|
||||
strength: true,
|
||||
occupiedFrames: true,
|
||||
broodStatus: true,
|
||||
feedStatus: true,
|
||||
healthStatus: true,
|
||||
@@ -96,13 +98,14 @@ export class ApiaryFieldService {
|
||||
const inspectionsVisible = permissions.has('inspections:read');
|
||||
const [lastInspections, warnings] = await Promise.all([
|
||||
inspectionsVisible ? this.lastInspectionDates(colonies) : null,
|
||||
this.warningCounts(colonies, userId),
|
||||
this.warningCounts(colonies, userId, permissions),
|
||||
]);
|
||||
const rows = colonies.map((colony): ColonyOverviewRow => ({
|
||||
id: colony.id,
|
||||
code: colony.code,
|
||||
status: colony.status,
|
||||
strength: colony.strength,
|
||||
occupiedFrames: colony.occupiedFrames,
|
||||
broodStatus: colony.broodStatus,
|
||||
feedStatus: colony.feedStatus,
|
||||
healthStatus: colony.healthStatus,
|
||||
@@ -138,27 +141,33 @@ export class ApiaryFieldService {
|
||||
private async warningCounts(
|
||||
colonies: PackColony[],
|
||||
userId: string,
|
||||
held: ReadonlySet<string>,
|
||||
): Promise<Map<string, { WARNUNG?: number; KRITISCH?: number }>> {
|
||||
const groups = await this.prisma.notification.groupBy({
|
||||
by: ['relatedId', 'priority'],
|
||||
where: {
|
||||
relatedType: 'COLONY',
|
||||
relatedId: { in: colonies.map((colony) => colony.id) },
|
||||
read: false,
|
||||
priority: { in: ['WARNUNG', 'KRITISCH'] },
|
||||
userId,
|
||||
},
|
||||
_count: { _all: true },
|
||||
});
|
||||
const counts = new Map<string, { WARNUNG?: number; KRITISCH?: number }>();
|
||||
if (colonies.length === 0) {
|
||||
return counts;
|
||||
}
|
||||
// Exactly the rule of NotificationsService.findOwn, decided in SQL: a
|
||||
// row counts only when every key in requiredPermissions is held (subset
|
||||
// test, so a key outside the catalog hides the row too).
|
||||
const groups = await this.prisma.$queryRaw<
|
||||
{ relatedId: string; priority: string; count: number }[]
|
||||
>(Prisma.sql`
|
||||
SELECT "relatedId", "priority"::text AS "priority", count(*)::int AS "count"
|
||||
FROM "Notification"
|
||||
WHERE "relatedType" = 'COLONY'
|
||||
AND "relatedId" IN (${Prisma.join(colonies.map((colony) => colony.id))})
|
||||
AND "read" = false
|
||||
AND "priority"::text IN ('WARNUNG', 'KRITISCH')
|
||||
AND "userId" = ${userId}
|
||||
AND "requiredPermissions" <@ ${[...held]}::text[]
|
||||
GROUP BY "relatedId", "priority"
|
||||
`);
|
||||
for (const group of groups) {
|
||||
if (
|
||||
group.relatedId &&
|
||||
(group.priority === 'WARNUNG' || group.priority === 'KRITISCH')
|
||||
) {
|
||||
if (group.priority === 'WARNUNG' || group.priority === 'KRITISCH') {
|
||||
counts.set(group.relatedId, {
|
||||
...counts.get(group.relatedId),
|
||||
[group.priority]: group._count._all,
|
||||
[group.priority]: group.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user