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,93 @@
|
||||
import { RawMaterialKind } from '../../../generated/prisma';
|
||||
import {
|
||||
bool,
|
||||
childScope,
|
||||
has,
|
||||
isCleared,
|
||||
makeParser,
|
||||
numberIn,
|
||||
optionalIntIn,
|
||||
stringIn,
|
||||
} from './parse-helpers';
|
||||
import type {
|
||||
NumberFormatConfig,
|
||||
RuleConfigParser,
|
||||
SimpleRuleConfig,
|
||||
} from './rule-config.types';
|
||||
|
||||
const MAX_DUE_IN_DAYS = 365;
|
||||
/** Far above any minimum stock of wax or propolis. */
|
||||
const MAX_MIN_GRAMS = 1_000_000_000;
|
||||
const MAX_NUMBER_FORMAT_LENGTH = 100;
|
||||
const RAW_MATERIAL_KINDS = Object.values(RawMaterialKind);
|
||||
|
||||
export interface SimpleRuleOptions {
|
||||
/** Whether the rule notifies by default. */
|
||||
notify: boolean;
|
||||
/** RAW_MATERIAL_LOW reads a minimum per kind. */
|
||||
withMinGrams?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rules that create one task: notify, an optional follow-up interval
|
||||
* (dueInDays) and, for the raw material rule, minGrams per kind.
|
||||
*/
|
||||
export function simpleParser(
|
||||
trigger: string,
|
||||
options: SimpleRuleOptions,
|
||||
): RuleConfigParser<SimpleRuleConfig> {
|
||||
return makeParser<SimpleRuleConfig>(
|
||||
trigger,
|
||||
{ notify: options.notify },
|
||||
(scope) => {
|
||||
const config: SimpleRuleConfig = {
|
||||
notify: bool(scope, 'notify', options.notify),
|
||||
};
|
||||
const dueInDays = optionalIntIn(scope, 'dueInDays', 0, MAX_DUE_IN_DAYS);
|
||||
if (dueInDays !== undefined) {
|
||||
config.dueInDays = dueInDays;
|
||||
}
|
||||
if (
|
||||
options.withMinGrams &&
|
||||
has(scope, 'minGrams') &&
|
||||
!isCleared(scope, 'minGrams')
|
||||
) {
|
||||
const minScope = childScope(scope, 'minGrams');
|
||||
const minGrams: Partial<Record<RawMaterialKind, number>> = {};
|
||||
for (const kind of RAW_MATERIAL_KINDS) {
|
||||
if (isCleared(minScope, kind)) {
|
||||
continue;
|
||||
}
|
||||
const grams = numberIn(minScope, kind, Number.NaN, 0, MAX_MIN_GRAMS);
|
||||
if (!Number.isNaN(grams)) {
|
||||
minGrams[kind] = grams;
|
||||
}
|
||||
}
|
||||
if (Object.keys(minGrams).length > 0) {
|
||||
config.minGrams = minGrams;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** The batch/lot number rules: one format string with placeholders. */
|
||||
export function numberFormatParser(
|
||||
trigger: string,
|
||||
defaultFormat: string,
|
||||
): RuleConfigParser<NumberFormatConfig> {
|
||||
return makeParser<NumberFormatConfig>(
|
||||
trigger,
|
||||
{ format: defaultFormat },
|
||||
(scope) => ({
|
||||
format: stringIn(
|
||||
scope,
|
||||
'format',
|
||||
defaultFormat,
|
||||
1,
|
||||
MAX_NUMBER_FORMAT_LENGTH,
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user