feat: T4 waves 1-2 - rule configs, schema, suggestions core, season, rule forms
- Rule configs with typed parsers and registry; Berlin day helpers - Schema: T4 enums, Suggestion table, feed vocabulary (feedStatus as ColonyCondition with legacy text mapping), split queen source, varroa sample size; migration harness for data migrations - SuggestionsService: propose/refresh/resolve/obsolete/expire, accept/dismiss/reopen incl. bulk, lock order, concurrency e2e - SeasonService: phenology-based segments, inspection/varroa due dates, queen age; TimeEventsStep with JobPartialFailure - Frontend: per-rule settings forms with visible value sources Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
7097fcd97c
commit
d99a697739
116 files changed
+11239
-801
No files matched your search
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
asRecord,
|
||||
bool,
|
||||
fieldPath,
|
||||
has,
|
||||
intIn,
|
||||
makeParser,
|
||||
optionalIntIn,
|
||||
report,
|
||||
stringIn,
|
||||
type Scope,
|
||||
} from './parse-helpers';
|
||||
import type { PlanStepConfig, SplitScheduleConfig } from './rule-config.types';
|
||||
|
||||
type SplitVariant = 'RAISE_OWN' | 'QUEEN_CELL' | 'QUEEN';
|
||||
|
||||
const MAX_STEPS_PER_VARIANT = 10;
|
||||
const MAX_OFFSET_DAYS = 120;
|
||||
const MAX_WINDOW_DAYS = 30;
|
||||
const STEP_NAME = /^[A-Z][A-Z_]{1,39}$/;
|
||||
|
||||
export const DEFAULT_SPLIT_SCHEDULE_CONFIG: SplitScheduleConfig = {
|
||||
RAISE_OWN: [
|
||||
{ step: 'RAISE_CELLS_CHECK', offsetDays: 9 },
|
||||
{ step: 'MATING_CHECK', offsetDays: 21, windowDays: 3 },
|
||||
{ step: 'EGGS_CHECK', offsetDays: 28 },
|
||||
],
|
||||
QUEEN_CELL: [{ step: 'EGGS_CHECK', offsetDays: 21 }],
|
||||
QUEEN: [{ step: 'ACCEPTANCE_CHECK', offsetDays: 7 }],
|
||||
notify: false,
|
||||
};
|
||||
|
||||
function clonedSteps(variant: SplitVariant): PlanStepConfig[] {
|
||||
return DEFAULT_SPLIT_SCHEDULE_CONFIG[variant].map((step) => ({ ...step }));
|
||||
}
|
||||
|
||||
/** Scope of one list entry: its path is `VARIANT[index]`. */
|
||||
function stepScope(
|
||||
scope: Scope,
|
||||
variant: SplitVariant,
|
||||
index: number,
|
||||
raw: unknown,
|
||||
): Scope | null {
|
||||
const record = asRecord(raw);
|
||||
const path = `${fieldPath(scope, variant)}[${index}]`;
|
||||
if (record === null) {
|
||||
scope.issues.push({
|
||||
field: path,
|
||||
message: 'Bitte jeden Schritt als Gruppe senden.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return { raw: record, path, issues: scope.issues };
|
||||
}
|
||||
|
||||
function readSteps(scope: Scope, variant: SplitVariant): PlanStepConfig[] {
|
||||
if (!has(scope, variant)) {
|
||||
return clonedSteps(variant);
|
||||
}
|
||||
const list = scope.raw[variant];
|
||||
if (!Array.isArray(list) || list.length > MAX_STEPS_PER_VARIANT) {
|
||||
report(
|
||||
scope,
|
||||
variant,
|
||||
`Bitte eine Liste mit höchstens ${MAX_STEPS_PER_VARIANT} Schritten senden.`,
|
||||
);
|
||||
return clonedSteps(variant);
|
||||
}
|
||||
const before = scope.issues.length;
|
||||
const steps: PlanStepConfig[] = [];
|
||||
list.forEach((entry: unknown, index) => {
|
||||
const item = stepScope(scope, variant, index, entry);
|
||||
if (item === null) {
|
||||
return;
|
||||
}
|
||||
if (!has(item, 'step')) {
|
||||
scope.issues.push({
|
||||
field: `${item.path}.step`,
|
||||
message: 'Bitte den Schritt angeben.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const step: PlanStepConfig = {
|
||||
step: stringIn(item, 'step', '', 2, 40, STEP_NAME),
|
||||
offsetDays: intIn(item, 'offsetDays', 0, 0, MAX_OFFSET_DAYS),
|
||||
};
|
||||
const windowDays = optionalIntIn(item, 'windowDays', 0, MAX_WINDOW_DAYS);
|
||||
if (windowDays !== undefined) {
|
||||
step.windowDays = windowDays;
|
||||
}
|
||||
steps.push(step);
|
||||
});
|
||||
// One bad entry makes the whole list unreliable: back to the default plan.
|
||||
return scope.issues.length > before ? clonedSteps(variant) : steps;
|
||||
}
|
||||
|
||||
export const splitScheduleParser = makeParser<SplitScheduleConfig>(
|
||||
'SPLIT_SCHEDULE',
|
||||
DEFAULT_SPLIT_SCHEDULE_CONFIG,
|
||||
(scope) => ({
|
||||
RAISE_OWN: readSteps(scope, 'RAISE_OWN'),
|
||||
QUEEN_CELL: readSteps(scope, 'QUEEN_CELL'),
|
||||
QUEEN: readSteps(scope, 'QUEEN'),
|
||||
notify: bool(scope, 'notify', DEFAULT_SPLIT_SCHEDULE_CONFIG.notify),
|
||||
}),
|
||||
);
|
||||
Reference in new issue
Block a user