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
@@ -14,7 +14,11 @@ import { UpdateColonyDto } from './dto/update-colony.dto';
|
||||
import { MoveColonyDto } from './dto/move-colony.dto';
|
||||
import { SplitColonyDto } from './dto/split-colony.dto';
|
||||
import { MergeColonyDto } from './dto/merge-colony.dto';
|
||||
import { isLivingColonyStatus } from './colony-status';
|
||||
import { releaseQueenIn } from './colony-lifecycle';
|
||||
import {
|
||||
isLivingColonyStatus,
|
||||
TERMINAL_COLONY_STATUSES,
|
||||
} from './colony-status';
|
||||
import { lockRowsForUpdate } from '../prisma/row-lock';
|
||||
import { colonyTransitionEvents, lockColony } from './colony-events';
|
||||
import { berlinToday, dayToDate } from '../common/dates';
|
||||
@@ -129,6 +133,16 @@ export class ColoniesService {
|
||||
if (movesApiary) {
|
||||
await this.assertApiaryActive(dto.apiaryId!);
|
||||
}
|
||||
// An edit into an ended status ends the queen assignment like dissolve
|
||||
// does, before the status is written (the pointer is read under the lock).
|
||||
const today = berlinToday();
|
||||
const endsColony =
|
||||
dto.status !== undefined &&
|
||||
TERMINAL_COLONY_STATUSES.includes(dto.status) &&
|
||||
isLivingColonyStatus(before.status);
|
||||
const releasedQueenId = endsColony
|
||||
? await releaseQueenIn(tx, id, dayToDate(today))
|
||||
: null;
|
||||
const updated = await tx.colony.update({
|
||||
where: { id },
|
||||
data: { ...dto },
|
||||
@@ -148,7 +162,11 @@ export class ColoniesService {
|
||||
}
|
||||
await this.events.recordAll(
|
||||
tx,
|
||||
colonyTransitionEvents(before, updated, actingUserId),
|
||||
colonyTransitionEvents(before, updated, actingUserId, {
|
||||
endedOn: today,
|
||||
releasedQueenId,
|
||||
via: 'EDIT',
|
||||
}),
|
||||
);
|
||||
await this.auditLog.record(
|
||||
{
|
||||
@@ -279,6 +297,7 @@ export class ColoniesService {
|
||||
'Jedes Volk darf nur einmal zur Vereinigung ausgewählt werden',
|
||||
);
|
||||
}
|
||||
const today = berlinToday();
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
// One statement in id order, the same order bulk lockRowsForShare
|
||||
// uses: no deadlock with a concurrent bulk write.
|
||||
@@ -299,28 +318,23 @@ export class ColoniesService {
|
||||
// The survivor keeps its own queen; merged-away queens are simply
|
||||
// released, not auto-transferred (which colony's queen "wins" a
|
||||
// merge is a beekeeper decision, not one to guess here).
|
||||
const releasedQueenId = await releaseQueenIn(
|
||||
tx,
|
||||
mergedColonyId,
|
||||
dayToDate(today),
|
||||
);
|
||||
const mergedAway = await tx.colony.update({
|
||||
where: { id: mergedColonyId },
|
||||
data: {
|
||||
status: 'MERGED',
|
||||
deactivatedAt: new Date(),
|
||||
currentQueenId: null,
|
||||
},
|
||||
data: { status: 'MERGED', deactivatedAt: new Date() },
|
||||
});
|
||||
await this.events.recordAll(
|
||||
tx,
|
||||
colonyTransitionEvents(merged, mergedAway, actingUserId),
|
||||
colonyTransitionEvents(merged, mergedAway, actingUserId, {
|
||||
endedOn: today,
|
||||
releasedQueenId,
|
||||
via: 'MERGE',
|
||||
}),
|
||||
);
|
||||
if (merged.currentQueenId) {
|
||||
await tx.queenColonyHistory.updateMany({
|
||||
where: {
|
||||
queenId: merged.currentQueenId,
|
||||
colonyId: mergedColonyId,
|
||||
endDate: null,
|
||||
},
|
||||
data: { endDate: new Date() },
|
||||
});
|
||||
}
|
||||
await tx.colonyLineage.create({
|
||||
data: {
|
||||
type: 'MERGE',
|
||||
@@ -353,27 +367,19 @@ export class ColoniesService {
|
||||
// Re-dissolving would overwrite deactivatedAt and the terminal status
|
||||
// (e.g. MERGED -> DISSOLVED), losing when and how the colony really ended.
|
||||
this.assertActive(before);
|
||||
const today = berlinToday();
|
||||
const releasedQueenId = await releaseQueenIn(tx, id, dayToDate(today));
|
||||
const updated = await tx.colony.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'DISSOLVED',
|
||||
deactivatedAt: new Date(),
|
||||
currentQueenId: null,
|
||||
},
|
||||
data: { status: 'DISSOLVED', deactivatedAt: new Date() },
|
||||
});
|
||||
if (before.currentQueenId) {
|
||||
await tx.queenColonyHistory.updateMany({
|
||||
where: {
|
||||
queenId: before.currentQueenId,
|
||||
colonyId: id,
|
||||
endDate: null,
|
||||
},
|
||||
data: { endDate: new Date() },
|
||||
});
|
||||
}
|
||||
await this.events.recordAll(
|
||||
tx,
|
||||
colonyTransitionEvents(before, updated, actingUserId),
|
||||
colonyTransitionEvents(before, updated, actingUserId, {
|
||||
endedOn: today,
|
||||
releasedQueenId,
|
||||
via: 'DISSOLVE',
|
||||
}),
|
||||
);
|
||||
await this.auditLog.record(
|
||||
{
|
||||
@@ -404,9 +410,15 @@ export class ColoniesService {
|
||||
where: { id },
|
||||
data: { status: 'ACTIVE', deactivatedAt: null },
|
||||
});
|
||||
// The clean-up of the end is moot now (colony row locked first, then
|
||||
// the suggestions: the order every writer uses).
|
||||
await tx.suggestion.updateMany({
|
||||
where: { colonyId: id, intent: 'CLEANUP', status: 'OPEN' },
|
||||
data: { status: 'OBSOLETE', decidedAt: new Date() },
|
||||
});
|
||||
await this.events.recordAll(
|
||||
tx,
|
||||
colonyTransitionEvents(before, updated, actingUserId),
|
||||
colonyTransitionEvents(before, updated, actingUserId, null),
|
||||
);
|
||||
await this.auditLog.record(
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user