// Tab „Overrides" (grafische Overrides, Regel-Engine A1) — Master-Detail wie
// Linien/Wandstile: Liste links = Regeln in PRIORITÄTS-Reihenfolge (oben
// gewinnt pro Aktions-Feld) mit Aktiv-Checkbox, Auf/Ab und Löschen; Detail
// rechts = Bedingung (Kriterium/Operator/Wert) + optionale Aktionen (Farbe/
// Strichstärke/Linienstil). Reines Rendering-Overlay — die Auswertung liegt in
// src/overrides/engine.ts, der Einhängepunkt in plan/generatePlan.ts;
// Elementdaten bleiben unverändert.
import { useState } from "react";
import type { OverrideConditionType, OverrideOperator, OverrideRule, Project } from "../../model/types";
import { PEN_WEIGHTS } from "../../model/types";
import { t } from "../../i18n";
import { ColorField, DeleteButton, FieldRow, NumberField, SelectField, TextField } from "./fields";
/** Bedingungs-Typen (UI-Beschriftung übersetzt, Werte englisch). */
const OVERRIDE_TYPE_OPTIONS: { value: OverrideConditionType; labelKey: string }[] = [
{ value: "layer_name", labelKey: "resources.overrides.condType.layer" },
{ value: "object_name", labelKey: "resources.overrides.condType.object" },
];
/** Vergleichs-Operatoren (UI-Beschriftung übersetzt, Werte englisch). */
const OVERRIDE_OP_OPTIONS: { value: OverrideOperator; labelKey: string }[] = [
{ value: "equals", labelKey: "resources.overrides.op.equals" },
{ value: "contains", labelKey: "resources.overrides.op.contains" },
{ value: "starts_with", labelKey: "resources.overrides.op.startsWith" },
{ value: "not_equals", labelKey: "resources.overrides.op.notEquals" },
];
/** Kompakte Bedingungs-Zusammenfassung für die Listenzeile. */
function overrideConditionSummary(rule: OverrideRule): string {
const type = OVERRIDE_TYPE_OPTIONS.find((o) => o.value === rule.condition.type);
const op = OVERRIDE_OP_OPTIONS.find((o) => o.value === rule.condition.operator);
return `${type ? t(type.labelKey) : rule.condition.type} ${
op ? t(op.labelKey) : rule.condition.operator
} „${rule.condition.value}"`;
}
/** Optionale Aktion: Checkbox (an/aus) + Steuerelement, solange sie an ist. */
function OptionalAction({
label,
active,
onToggle,
children,
}: {
label: string;
active: boolean;
onToggle: (on: boolean) => void;
children?: React.ReactNode;
}) {
return (
{active ? children : –}
);
}
/** Detail-Panel EINER Override-Regel (rechte Seite des Master-Detail-Layouts). */
function OverrideRuleDetail({
rule,
project,
onPatch,
onDelete,
}: {
rule: OverrideRule;
project: Project;
onPatch: (patch: Partial) => void;
onDelete: () => void;
}) {
const patchCondition = (patch: Partial) =>
onPatch({ condition: { ...rule.condition, ...patch } });
const patchActions = (patch: Partial) =>
onPatch({ actions: { ...rule.actions, ...patch } });
// Sentinel „—": kein Linienstil-Override.
const NONE = "__none__";
const lineOptions = [
{ value: NONE, label: t("resources.overrides.linetype.none") },
...project.lineStyles.map((l) => ({ value: l.id, label: l.name })),
];
return (
onPatch({ name: e.target.value })}
/>
{t("resources.overrides.condition")}
patchCondition({ type })}
options={OVERRIDE_TYPE_OPTIONS.map((o) => ({
value: o.value,
label: t(o.labelKey),
}))}
/>
patchCondition({ operator })}
options={OVERRIDE_OP_OPTIONS.map((o) => ({
value: o.value,
label: t(o.labelKey),
}))}
/>
patchCondition({ value })}
/>
{t("resources.overrides.actions")}
{/* Vorschlagswerte für die Strichstärke (Standard-Stiftbreiten in mm). */}
patchActions({ color: on ? "#808080" : undefined })}
>
patchActions({ color })}
/>
patchActions({ lineweight: on ? 0.35 : undefined })}
>
patchActions({ lineweight: Math.max(0, lineweight) })}
step={0.05}
min={0}
list="ov-pen-weights"
/>
patchActions({
linetypeId: on ? project.lineStyles[0]?.id : undefined,
})
}
>
patchActions({ linetypeId: v === NONE ? undefined : v })
}
options={lineOptions}
/>
);
}
/** Tab „Overrides": Regelliste (Priorität von oben nach unten) + Detail. */
export function OverridesTab({
project,
onSetOverrideRules,
}: {
project: Project;
onSetOverrideRules?: (rules: OverrideRule[]) => void;
}) {
const rules = project.overrideRules ?? [];
const [selectedId, setSelectedId] = useState(null);
const selected = rules.find((r) => r.id === selectedId) ?? rules[0];
const setRules = (next: OverrideRule[]) => onSetOverrideRules?.(next);
const patchRule = (id: string, patch: Partial) =>
setRules(rules.map((r) => (r.id === id ? { ...r, ...patch } : r)));
const addRule = () => {
const rule: OverrideRule = {
id: `ov-${Date.now()}`,
name: t("resources.overrides.defaultName"),
enabled: true,
condition: { type: "layer_name", operator: "contains", value: "" },
actions: {},
};
setRules([...rules, rule]);
setSelectedId(rule.id);
};
const deleteRule = (id: string) => setRules(rules.filter((r) => r.id !== id));
const moveRule = (id: string, dir: -1 | 1) => {
const i = rules.findIndex((r) => r.id === id);
const j = i + dir;
if (i < 0 || j < 0 || j >= rules.length) return;
const next = rules.slice();
[next[i], next[j]] = [next[j], next[i]];
setRules(next);
};
return (
{t("resources.overrides.hint")}
{rules.map((r) => (
setSelectedId(r.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") setSelectedId(r.id);
}}
>
e.stopPropagation()}
onChange={(e) => patchRule(r.id, { enabled: e.target.checked })}
/>
{r.name || t("resources.overrides.placeholder")}
{overrideConditionSummary(r)}
))}
{rules.length === 0 && (
{t("resources.overrides.empty")}
)}
{selected ? (
patchRule(selected.id, patch)}
onDelete={() => deleteRule(selected.id)}
/>
) : (
{t("resources.overrides.empty")}
)}
);
}