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
@@ -8,7 +8,7 @@ import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { WebhooksService } from '../webhooks/webhooks.service';
|
||||
import { DomainEventsService } from '../domain-events/domain-events.service';
|
||||
import { Colony, ColonyStatus } from '../../generated/prisma';
|
||||
import { Colony, ColonyStatus, Prisma } from '../../generated/prisma';
|
||||
import { CreateColonyDto } from './dto/create-colony.dto';
|
||||
import { UpdateColonyDto } from './dto/update-colony.dto';
|
||||
import { MoveColonyDto } from './dto/move-colony.dto';
|
||||
@@ -105,51 +105,63 @@ export class ColoniesService {
|
||||
dto: UpdateColonyDto,
|
||||
actingUserId: string,
|
||||
): Promise<Colony> {
|
||||
// Fail fast with 404; everything else is decided on the locked row.
|
||||
await this.findById(id);
|
||||
const { after } = await this.prisma.$transaction((tx) =>
|
||||
this.updateIn(tx, id, dto, actingUserId),
|
||||
);
|
||||
return after;
|
||||
}
|
||||
|
||||
const colony = await this.prisma.$transaction(async (tx) => {
|
||||
const before = await lockColony(tx, id);
|
||||
const movesApiary =
|
||||
dto.apiaryId !== undefined && dto.apiaryId !== before.apiaryId;
|
||||
if (movesApiary) {
|
||||
await this.assertApiaryActive(dto.apiaryId!);
|
||||
}
|
||||
const updated = await tx.colony.update({
|
||||
where: { id },
|
||||
data: { ...dto },
|
||||
});
|
||||
// Correcting the apiary has to leave the same trace a deliberate move
|
||||
// does - otherwise the colony's location history would silently skip a
|
||||
// stand it was recorded on.
|
||||
if (movesApiary) {
|
||||
await tx.colonyApiaryHistory.create({
|
||||
data: {
|
||||
colonyId: id,
|
||||
apiaryId: updated.apiaryId,
|
||||
movedByUserId: actingUserId,
|
||||
notes: 'Korrektur über Volk bearbeiten',
|
||||
},
|
||||
});
|
||||
}
|
||||
await this.events.recordAll(
|
||||
tx,
|
||||
colonyTransitionEvents(before, updated, actingUserId),
|
||||
);
|
||||
await this.auditLog.record(
|
||||
{
|
||||
entityType: 'Colony',
|
||||
entityId: id,
|
||||
action: 'UPDATE',
|
||||
userId: actingUserId,
|
||||
beforeData: before,
|
||||
afterData: updated,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
return updated;
|
||||
/**
|
||||
* The core of update, for callers that already hold a transaction (an
|
||||
* inspection handing values over to the colony card). Locks the colony row
|
||||
* and reads "before" only afterwards (404 when missing); writes the apiary
|
||||
* history, the status events and the audit entry with the change.
|
||||
*/
|
||||
async updateIn(
|
||||
tx: Prisma.TransactionClient,
|
||||
id: string,
|
||||
dto: UpdateColonyDto,
|
||||
actingUserId: string,
|
||||
): Promise<{ before: Colony; after: Colony }> {
|
||||
const before = await lockColony(tx, id);
|
||||
const movesApiary =
|
||||
dto.apiaryId !== undefined && dto.apiaryId !== before.apiaryId;
|
||||
if (movesApiary) {
|
||||
await this.assertApiaryActive(dto.apiaryId!);
|
||||
}
|
||||
const updated = await tx.colony.update({
|
||||
where: { id },
|
||||
data: { ...dto },
|
||||
});
|
||||
return colony;
|
||||
// Correcting the apiary has to leave the same trace a deliberate move
|
||||
// does - otherwise the colony's location history would silently skip a
|
||||
// stand it was recorded on.
|
||||
if (movesApiary) {
|
||||
await tx.colonyApiaryHistory.create({
|
||||
data: {
|
||||
colonyId: id,
|
||||
apiaryId: updated.apiaryId,
|
||||
movedByUserId: actingUserId,
|
||||
notes: 'Korrektur über Volk bearbeiten',
|
||||
},
|
||||
});
|
||||
}
|
||||
await this.events.recordAll(
|
||||
tx,
|
||||
colonyTransitionEvents(before, updated, actingUserId),
|
||||
);
|
||||
await this.auditLog.record(
|
||||
{
|
||||
entityType: 'Colony',
|
||||
entityId: id,
|
||||
action: 'UPDATE',
|
||||
userId: actingUserId,
|
||||
beforeData: before,
|
||||
afterData: updated,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
return { before, after: updated };
|
||||
}
|
||||
|
||||
async move(
|
||||
|
||||
Reference in new issue
Block a user