feat(automation): number formats for raw material and production batches, raw material minimum
- RAW_MATERIAL_LOT_NUMBER_FORMAT / PRODUCTION_BATCH_NUMBER_FORMAT fill in the lot number when it is left empty, like honey batches and bottlings. - RAW_MATERIAL_LOW: task when the total of a raw material kind drops below config.minGrams after consumption, withdrawal or inventory count. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
fc887d8431
commit
5408711b2d
18 files changed
+311
-26
No files matched your search
@@ -70,7 +70,7 @@ export default function NewProductionBatchPage() {
|
||||
mutationFn: () =>
|
||||
createProductionBatch({
|
||||
productId,
|
||||
lotNumber: lotNumber.trim(),
|
||||
lotNumber: lotNumber.trim() || undefined,
|
||||
date: new Date(date).toISOString(),
|
||||
quantity: Number(quantity),
|
||||
bestBeforeDate: bestBeforeDate
|
||||
@@ -150,7 +150,7 @@ export default function NewProductionBatchPage() {
|
||||
<Label htmlFor="pb-lot">Chargennummer</Label>
|
||||
<Input
|
||||
id="pb-lot"
|
||||
required
|
||||
placeholder="Automatisch, falls Automatisierung aktiv"
|
||||
value={lotNumber}
|
||||
onChange={(event) => setLotNumber(event.target.value)}
|
||||
/>
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function NewRawMaterialPage() {
|
||||
mutationFn: () =>
|
||||
createRawMaterialLot({
|
||||
kind,
|
||||
lotNumber: lotNumber.trim(),
|
||||
lotNumber: lotNumber.trim() || undefined,
|
||||
date: new Date(date).toISOString(),
|
||||
quantity: Number(quantity),
|
||||
apiaryId: apiaryId === NO_APIARY ? undefined : apiaryId,
|
||||
@@ -110,7 +110,7 @@ export default function NewRawMaterialPage() {
|
||||
<Label htmlFor="raw-lot">Chargennummer</Label>
|
||||
<Input
|
||||
id="raw-lot"
|
||||
required
|
||||
placeholder="Automatisch, falls Automatisierung aktiv"
|
||||
value={lotNumber}
|
||||
onChange={(event) => setLotNumber(event.target.value)}
|
||||
/>
|
||||
|
||||
@@ -28,6 +28,9 @@ const TRIGGER_LABELS: Record<AutomationTriggerType, string> = {
|
||||
BOTTLING_LOT_NUMBER_FORMAT: "Los-/Chargennummer-Format (Abfüllungen)",
|
||||
TREATMENT_FOLLOW_UP: "Kontrolle nach Behandlung",
|
||||
FEEDING_FOLLOW_UP: "Kontrolle nach Fütterung",
|
||||
RAW_MATERIAL_LOT_NUMBER_FORMAT: "Chargennummer-Format (Rohstoffe)",
|
||||
PRODUCTION_BATCH_NUMBER_FORMAT: "Chargennummer-Format (Herstellung)",
|
||||
RAW_MATERIAL_LOW: "Rohstoff unter Mindestmenge",
|
||||
};
|
||||
|
||||
const TRIGGER_DESCRIPTIONS: Record<AutomationTriggerType, string> = {
|
||||
@@ -51,13 +54,51 @@ const TRIGGER_DESCRIPTIONS: Record<AutomationTriggerType, string> = {
|
||||
"Nach jeder erfassten Behandlung eine Kontrollaufgabe vorschlagen, fällig die eingestellten Tage nach Behandlungsende. Die passende Frist hängt vom Mittel ab (Packungsbeilage) – deshalb ab Werk aus und ohne voreingestellte Frist.",
|
||||
FEEDING_FOLLOW_UP:
|
||||
"Nach jeder erfassten Fütterung eine Kontrollaufgabe vorschlagen, fällig die eingestellten Tage nach der Fütterung. Ab Werk aus und ohne voreingestellte Frist.",
|
||||
RAW_MATERIAL_LOT_NUMBER_FORMAT:
|
||||
"Wenn aktiv, wird die Chargennummer neuer Rohstoff-Chargen (Wachs, Propolis) automatisch nach diesem Format erzeugt, statt sie manuell einzugeben.",
|
||||
PRODUCTION_BATCH_NUMBER_FORMAT:
|
||||
"Wenn aktiv, wird die Chargennummer neuer Herstellungschargen (Kerzen, Tinkturen, …) automatisch nach diesem Format erzeugt, statt sie manuell einzugeben.",
|
||||
RAW_MATERIAL_LOW:
|
||||
"Fällt der Gesamtbestand einer Rohstoff-Art nach Verbrauch, Entnahme oder Inventur unter die Mindestmenge, dann Aufgabe zum Nachschub. Leere Mindestmenge = diese Art wird nicht überwacht.",
|
||||
};
|
||||
|
||||
const NUMBER_FORMAT_TRIGGER_TYPES: AutomationTriggerType[] = [
|
||||
"HONEY_BATCH_NUMBER_FORMAT",
|
||||
"BOTTLING_LOT_NUMBER_FORMAT",
|
||||
"RAW_MATERIAL_LOT_NUMBER_FORMAT",
|
||||
"PRODUCTION_BATCH_NUMBER_FORMAT",
|
||||
];
|
||||
|
||||
const MIN_GRAMS_FIELDS: { kind: "BEESWAX" | "PROPOLIS"; label: string }[] = [
|
||||
{ kind: "BEESWAX", label: "Mindestmenge Bienenwachs (g)" },
|
||||
{ kind: "PROPOLIS", label: "Mindestmenge Propolis (g)" },
|
||||
];
|
||||
|
||||
type MinGrams = Partial<Record<"BEESWAX" | "PROPOLIS", string>>;
|
||||
|
||||
function ruleMinGrams(rule: AutomationRule): MinGrams {
|
||||
const config = rule.config as { minGrams?: Record<string, unknown> } | null;
|
||||
const result: MinGrams = {};
|
||||
for (const { kind } of MIN_GRAMS_FIELDS) {
|
||||
const value = config?.minGrams?.[kind];
|
||||
result[kind] = typeof value === "number" ? String(value) : "";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Empty fields drop out; anything else must be whole grams >= 0. */
|
||||
function parseMinGrams(input: MinGrams): Record<string, number> | null {
|
||||
const result: Record<string, number> = {};
|
||||
for (const { kind } of MIN_GRAMS_FIELDS) {
|
||||
const raw = (input[kind] ?? "").trim();
|
||||
if (raw === "") continue;
|
||||
const grams = Number(raw);
|
||||
if (!Number.isInteger(grams) || grams < 0) return null;
|
||||
result[kind] = grams;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isNumberFormatRule(rule: AutomationRule): boolean {
|
||||
return NUMBER_FORMAT_TRIGGER_TYPES.includes(rule.triggerType);
|
||||
}
|
||||
@@ -83,6 +124,7 @@ function RuleCard({ rule }: { rule: AutomationRule }) {
|
||||
const [threshold, setThreshold] = useState(ruleThreshold(rule));
|
||||
const [format, setFormat] = useState(ruleFormat(rule));
|
||||
const [dueInDays, setDueInDays] = useState(ruleDueInDays(rule));
|
||||
const [minGrams, setMinGrams] = useState<MinGrams>(ruleMinGrams(rule));
|
||||
const numberFormatRule = isNumberFormatRule(rule);
|
||||
|
||||
const mutation = useMutation({
|
||||
@@ -119,6 +161,14 @@ function RuleCard({ rule }: { rule: AutomationRule }) {
|
||||
if (rule.triggerType === "VARROA_THRESHOLD") {
|
||||
config.thresholdMitesPerDay = Number(threshold) || 10;
|
||||
}
|
||||
if (rule.triggerType === "RAW_MATERIAL_LOW") {
|
||||
const parsed = parseMinGrams(minGrams);
|
||||
if (!parsed) {
|
||||
toast.error("Mindestmenge bitte als ganze Gramm angeben – oder leer lassen.");
|
||||
return;
|
||||
}
|
||||
config.minGrams = parsed;
|
||||
}
|
||||
mutation.mutate({ actionTemplate: actionTemplate || undefined, config });
|
||||
}
|
||||
|
||||
@@ -194,6 +244,25 @@ function RuleCard({ rule }: { rule: AutomationRule }) {
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{rule.triggerType === "RAW_MATERIAL_LOW"
|
||||
? MIN_GRAMS_FIELDS.map(({ kind, label }) => (
|
||||
<div key={kind} className="flex flex-col gap-2">
|
||||
<Label htmlFor={`min-${kind}-${rule.id}`}>{label}</Label>
|
||||
<Input
|
||||
id={`min-${kind}-${rule.id}`}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={0}
|
||||
step={1}
|
||||
placeholder="leer = nicht überwachen"
|
||||
value={minGrams[kind] ?? ""}
|
||||
onChange={(event) =>
|
||||
setMinGrams((current) => ({ ...current, [kind]: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
Reference in new issue
Block a user