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
@@ -1,4 +1,175 @@
|
||||
import type { SuggestionTextRegister } from '../suggestion-texts';
|
||||
import { daysBetween } from '../../common/dates';
|
||||
import type { SuggestionEvidence } from '../suggestion.types';
|
||||
import {
|
||||
formatDayDe,
|
||||
type SuggestionTextRegister,
|
||||
type SuggestionTextTemplate,
|
||||
} from '../suggestion-texts';
|
||||
|
||||
const MAX_PRODUCT_LENGTH = 40;
|
||||
|
||||
const SEGMENT_PHRASES: Readonly<Record<string, string>> = {
|
||||
FRUEHJAHR: 'Im Frühjahr',
|
||||
SCHWARMZEIT: 'In der Schwarmzeit',
|
||||
SOMMER: 'Im Sommer',
|
||||
SPAETSOMMER: 'Im Spätsommer',
|
||||
HERBST: 'Im Herbst',
|
||||
};
|
||||
|
||||
function text(evidence: SuggestionEvidence, key: string): string | null {
|
||||
const value = evidence[key];
|
||||
return typeof value === 'string' ? value : null;
|
||||
}
|
||||
|
||||
function count(evidence: SuggestionEvidence, key: string): number | null {
|
||||
const value = evidence[key];
|
||||
return typeof value === 'number' ? value : null;
|
||||
}
|
||||
|
||||
/** 'vor 8 Tagen', 'vor 1 Tag', 'heute'. */
|
||||
function agoDe(from: string, today: string): string {
|
||||
const days = Math.max(0, daysBetween(from, today));
|
||||
if (days === 0) {
|
||||
return 'heute';
|
||||
}
|
||||
return `vor ${days} ${days === 1 ? 'Tag' : 'Tagen'}`;
|
||||
}
|
||||
|
||||
function lastInspectionReason(
|
||||
evidence: SuggestionEvidence,
|
||||
today: string,
|
||||
): string {
|
||||
const last = text(evidence, 'lastInspectedOn');
|
||||
return last === null
|
||||
? 'Noch keine Durchsicht erfasst.'
|
||||
: `Letzte Durchsicht ${agoDe(last, today)}.`;
|
||||
}
|
||||
|
||||
function intervalReason(evidence: SuggestionEvidence): string[] {
|
||||
const phrase = SEGMENT_PHRASES[text(evidence, 'segment') ?? ''];
|
||||
const days = count(evidence, 'intervalDays');
|
||||
return phrase !== undefined && days !== null
|
||||
? [`${phrase} alle ${days} Tage.`]
|
||||
: [];
|
||||
}
|
||||
|
||||
const INSPECTION_DUE: SuggestionTextTemplate = {
|
||||
ruleKey: 'INSPECTION_INTERVAL',
|
||||
example: {
|
||||
colonyId: 'c1',
|
||||
inspectionId: 'i1',
|
||||
lastInspectedOn: '2026-05-04',
|
||||
dueOn: '2026-05-11',
|
||||
intervalDays: 7,
|
||||
segment: 'SCHWARMZEIT',
|
||||
},
|
||||
render: (evidence, _lookup, today) => ({
|
||||
title: 'Durchsicht',
|
||||
reasons: [
|
||||
lastInspectionReason(evidence, today),
|
||||
...intervalReason(evidence),
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const INSPECTION_FIRST_OF_YEAR: SuggestionTextTemplate = {
|
||||
ruleKey: 'INSPECTION_INTERVAL',
|
||||
example: {
|
||||
colonyId: 'c1',
|
||||
inspectionId: 'i1',
|
||||
lastInspectedOn: '2025-09-20',
|
||||
dueOn: '2026-03-20',
|
||||
intervalDays: 14,
|
||||
segment: 'FRUEHJAHR',
|
||||
},
|
||||
render: (evidence) => {
|
||||
const last = text(evidence, 'lastInspectedOn');
|
||||
return {
|
||||
title: 'Erste Durchsicht im Jahr',
|
||||
reasons: [
|
||||
'Die Saison hat begonnen.',
|
||||
last === null
|
||||
? 'Noch keine Durchsicht erfasst.'
|
||||
: `Letzte Durchsicht am ${formatDayDe(last, true)}.`,
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const VARROA_MEASURE_DUE: SuggestionTextTemplate = {
|
||||
ruleKey: 'SEASON_AUTOPILOT',
|
||||
example: {
|
||||
colonyId: 'c1',
|
||||
measurementId: 'm1',
|
||||
lastMeasuredOn: '2026-05-06',
|
||||
dueOn: '2026-06-03',
|
||||
intervalDays: 28,
|
||||
},
|
||||
render: (evidence, _lookup, today) => {
|
||||
const last = text(evidence, 'lastMeasuredOn');
|
||||
const days = count(evidence, 'intervalDays');
|
||||
return {
|
||||
title: 'Varroa messen',
|
||||
reasons: [
|
||||
last === null
|
||||
? 'Noch keine Messung erfasst.'
|
||||
: `Letzte Messung ${agoDe(last, today)}.`,
|
||||
...(days === null ? [] : [`Alle ${days} Tage messen.`]),
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const VARROA_WINTER_MEASURE: SuggestionTextTemplate = {
|
||||
ruleKey: 'SEASON_AUTOPILOT',
|
||||
example: {
|
||||
colonyId: 'c1',
|
||||
measurementId: 'm1',
|
||||
lastMeasuredOn: '2026-09-01',
|
||||
dueOn: '2026-11-15',
|
||||
intervalDays: 28,
|
||||
},
|
||||
render: () => ({
|
||||
title: 'Varroa messen',
|
||||
reasons: ['Vor der Winterbehandlung den Milbenfall messen.'],
|
||||
}),
|
||||
};
|
||||
|
||||
const VARROA_RECHECK_DUE: SuggestionTextTemplate = {
|
||||
ruleKey: 'SEASON_AUTOPILOT',
|
||||
example: {
|
||||
colonyId: 'c1',
|
||||
treatmentId: 't1',
|
||||
endedOn: '2026-08-02',
|
||||
recheckOn: '2026-08-16',
|
||||
},
|
||||
render: (evidence, lookup) => {
|
||||
const treatmentId = text(evidence, 'treatmentId');
|
||||
const product =
|
||||
(treatmentId === null ? null : lookup.treatmentProduct(treatmentId)) ??
|
||||
'Behandlung';
|
||||
const name = product.slice(0, MAX_PRODUCT_LENGTH);
|
||||
const ended = text(evidence, 'endedOn');
|
||||
const recheck = text(evidence, 'recheckOn');
|
||||
return {
|
||||
title: 'Behandlungserfolg messen',
|
||||
reasons: [
|
||||
ended === null
|
||||
? `Behandlung mit ${name} beendet.`
|
||||
: `Behandlung mit ${name} endete am ${formatDayDe(ended)}`,
|
||||
...(recheck === null
|
||||
? []
|
||||
: [`Nachkontrolle ab ${formatDayDe(recheck)}`]),
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/** Texts of the season autopilot suggestions, by reason code. */
|
||||
export const AUTOPILOT_TEXTS: SuggestionTextRegister = {};
|
||||
export const AUTOPILOT_TEXTS: SuggestionTextRegister = {
|
||||
INSPECTION_DUE,
|
||||
INSPECTION_FIRST_OF_YEAR,
|
||||
VARROA_MEASURE_DUE,
|
||||
VARROA_WINTER_MEASURE,
|
||||
VARROA_RECHECK_DUE,
|
||||
};
|
||||
Reference in new issue
Block a user