feat(deadlines/new): collapse Regel + Typ to ONE field when rule sets type

m's 2026-05-08 22:08 dogfood: my first auto-fill landed but kept Regel
and Typ as TWO separate input fields. m wanted ONE — "these two are
connected, it's the same thing".

Now: when a Regel is selected and the rule's concept resolves to a
canonical event_type via the jurisdiction-aware junction, the Typ
chip cluster is HIDDEN and replaced by an inline summary —

    Klageerwiderung (vorgegeben durch Regel)   Anderen Typ wählen

Clicking "Anderen Typ wählen" sets a sticky expandedOverride flag
that forces the picker visible for the rest of the form session.
The chip stays in the picker so the user can edit / remove it.
The picker also stays visible when the rule has no canonical
event_type (fallback to free-text Typ) or when the user has picked
a different event_type from the canonical default (mismatch
warning surfaces yellow next to the picker, never blocking).

DE+EN i18n: deadlines.field.rule.{autofill_inline,override}.
New CSS: .event-type-collapsed{,-label,-source,-override} reusing
the existing lime-tint chip palette.
This commit is contained in:
m
2026-05-08 22:20:48 +02:00
parent 6058d21ce6
commit 7a35cad09f
5 changed files with 157 additions and 31 deletions

View File

@@ -1,10 +1,23 @@
import { initI18n, t, tDyn } from "./i18n";
import { initSidebar } from "./sidebar";
import { attachEventTypePicker, type PickerHandle } from "./event-types";
import {
attachEventTypePicker,
eventTypeLabel,
fetchEventTypes,
type EventType,
type PickerHandle,
} from "./event-types";
import { projectIndent } from "./project-indent";
let eventTypePicker: PickerHandle | null = null;
let currentUserAdmin = false;
let eventTypesByID = new Map<string, EventType>();
// expandedOverride flips to true when the user clicks "Anderen Typ
// wählen" on the collapsed inline summary. Sticky for the rest of the
// form session — cleared only when the user reverts the rule to "Keine
// Regel". When true, the picker stays visible regardless of whether
// the chip matches the rule's canonical default.
let expandedOverride = false;
interface Project {
id: string;
@@ -100,33 +113,55 @@ async function loadRules() {
}
}
// t-paliad-165 — refresh the autofill hint + mismatch warning state. The
// hint shows when the picker exactly matches the current rule's
// suggestion (so the chip is "vorgegeben durch Regel"). The warning
// shows when the user has picked event_types that don't include the
// rule's canonical default (override is fine, just call it out).
function refreshRuleHints(): void {
const hint = document.getElementById("deadline-event-type-rule-hint");
// t-paliad-165 follow-up — drive the collapsed/expanded view of the Typ
// picker. The two modes are mutually exclusive:
//
// collapsed: rule selected + canonical event_type known + picker
// contains exactly [default] + user hasn't clicked "Anderen Typ
// wählen". Hides the chip cluster, surfaces a single inline
// summary "Klageerwiderung (vorgegeben durch Regel)" + an
// override link.
//
// expanded: every other case — no rule, no default for the rule,
// picker has been edited, or expandedOverride is sticky after the
// user clicked the override link. Picker visible; mismatch warning
// surfaces yellow when the rule expected a different event_type.
function refreshRuleView(): void {
const collapsed = document.getElementById("deadline-event-type-collapsed");
const collapsedLabel = document.getElementById("deadline-event-type-collapsed-label");
const pickerHost = document.getElementById("deadline-event-types");
const warn = document.getElementById("deadline-event-type-rule-mismatch");
if (!hint || !warn) return;
if (!collapsed || !collapsedLabel || !pickerHost || !warn) return;
const ruleID = (document.getElementById("deadline-rule") as HTMLSelectElement | null)?.value || "";
const rule = ruleID ? rulesByID.get(ruleID) : undefined;
const expected = rule?.concept_default_event_type_id ?? null;
const picked = eventTypePicker?.getIDs() ?? [];
if (!ruleID || !expected) {
hint.style.display = "none";
const pickerMatchesDefault =
expected !== null && picked.length === 1 && picked[0] === expected;
const wantsCollapsed =
!expandedOverride && ruleID !== "" && expected !== null && pickerMatchesDefault;
if (wantsCollapsed) {
const et = eventTypesByID.get(expected!);
collapsedLabel.textContent = et ? eventTypeLabel(et) : "";
collapsed.style.display = "";
pickerHost.style.display = "none";
warn.style.display = "none";
return;
}
if (picked.length === 1 && picked[0] === expected) {
hint.style.display = "";
collapsed.style.display = "none";
pickerHost.style.display = "";
// Mismatch warning: rule expected an event_type AND the picker
// doesn't contain it. (When the picker is empty + no override, no
// warning — user is free to leave it blank.)
if (expected && picked.length > 0 && !picked.includes(expected)) {
warn.style.display = "";
} else {
warn.style.display = "none";
return;
}
hint.style.display = "none";
warn.style.display = picked.includes(expected) ? "none" : "";
}
// applyRuleAutoFill replaces the picker silently when it still reflects
@@ -139,6 +174,12 @@ function applyRuleAutoFill(): void {
const expected = rule?.concept_default_event_type_id ?? null;
const current = eventTypePicker.getIDs();
// Reset the override on transition to "Keine Regel" — fresh form
// session. Otherwise expandedOverride stays sticky.
if (ruleID === "") {
expandedOverride = false;
}
const pickerStillReflectsLastSuggestion =
lastAutoFilledEventTypeID !== null &&
current.length === 1 &&
@@ -152,11 +193,11 @@ function applyRuleAutoFill(): void {
}
} else if (pickerStillReflectsLastSuggestion) {
// New rule has no canonical event_type — clear the stale auto-fill
// so the picker doesn't carry a hint from the old rule.
// so the picker doesn't carry a chip from the old rule.
eventTypePicker.setIDs([]);
lastAutoFilledEventTypeID = null;
}
refreshRuleHints();
refreshRuleView();
}
function initBackLinks() {
@@ -307,15 +348,36 @@ document.addEventListener("DOMContentLoaded", async () => {
if (pickerHost) {
eventTypePicker = attachEventTypePicker(pickerHost, {
currentUserAdmin,
onChange: () => refreshRuleHints(),
onChange: () => refreshRuleView(),
});
}
// t-paliad-165 follow-up — preload event_types so the collapsed
// summary can render the type's label inline without an extra round
// trip when the user picks a Regel.
fetchEventTypes()
.then((types) => {
eventTypesByID = new Map(types.map((et) => [et.id, et]));
refreshRuleView();
})
.catch(() => {/* non-fatal — collapsed view falls back to empty label */});
// t-paliad-165 — Regel change auto-fills the Typ chip from the rule's
// concept's canonical event_type, when the picker hasn't been
// manually edited away from the previous rule's suggestion.
document.getElementById("deadline-rule")?.addEventListener("change", () => {
applyRuleAutoFill();
});
// "Anderen Typ wählen" — sticky expanded mode so the picker stays
// visible even when the chip still matches the rule's default.
document.getElementById("deadline-event-type-override-btn")?.addEventListener("click", () => {
expandedOverride = true;
refreshRuleView();
// Move focus into the picker's search box so the user can type
// immediately without an extra click.
const search = document.querySelector<HTMLInputElement>(
"#deadline-event-types .event-type-search",
);
search?.focus();
});
// Wire approval-hint refresh: on first render + on project change.
void refreshApprovalHint();
document.getElementById("deadline-project")?.addEventListener("change", () => {

View File

@@ -742,7 +742,9 @@ const translations: Record<Lang, Record<string, string>> = {
"deadlines.field.rule": "Regel (optional)",
"deadlines.field.rule.none": "Keine Regel",
"deadlines.field.rule.autofill": "Typ vorgegeben durch Regel — entfernen, um zu überschreiben.",
"deadlines.field.rule.autofill_inline": " (vorgegeben durch Regel)",
"deadlines.field.rule.mismatch": "Hinweis: Typ widerspricht Regel — Sie haben den Typ überschrieben.",
"deadlines.field.rule.override": "Anderen Typ wählen",
"deadlines.field.notes": "Notizen (optional)",
"deadlines.field.notes.placeholder": "Hinweise, Verweise, n\u00e4chste Schritte\u2026",
"deadlines.error.required": "Akte, Titel und F\u00e4lligkeitsdatum sind Pflichtfelder.",
@@ -2949,7 +2951,9 @@ const translations: Record<Lang, Record<string, string>> = {
"deadlines.field.rule": "Rule (optional)",
"deadlines.field.rule.none": "No rule",
"deadlines.field.rule.autofill": "Type set by rule — remove to override.",
"deadlines.field.rule.autofill_inline": " (set by rule)",
"deadlines.field.rule.mismatch": "Note: type contradicts rule — you have overridden the type.",
"deadlines.field.rule.override": "Choose another type",
"deadlines.field.notes": "Notes (optional)",
"deadlines.field.notes.placeholder": "References, hints, next steps\u2026",
"deadlines.error.required": "Matter, title and due date are required.",

View File

@@ -55,21 +55,42 @@ export function renderDeadlinesNew(): string {
/>
</div>
<div className="form-field">
<div className="form-field" id="deadline-event-type-field">
<label data-i18n="deadlines.field.event_type">Typ (optional)</label>
<div id="deadline-event-types" className="event-type-picker-host" />
{/* t-paliad-165 — soft hint when the Regel auto-populated
the chip, and a soft warning when the user picked a
Regel + Typ combination that contradicts the rule's
concept. Both rendered yellow, never blocking. */}
<p
className="form-hint"
id="deadline-event-type-rule-hint"
{/* t-paliad-165 follow-up — collapsed view: when a Regel
is selected and a default event_type is known, the
Typ chip is hidden and the type is rendered inline
as a single read-only summary with an "Anderen Typ
wählen" link that re-expands the picker. */}
<div
className="event-type-collapsed"
id="deadline-event-type-collapsed"
style="display:none"
data-i18n="deadlines.field.rule.autofill"
>
Typ vorgegeben durch Regel &mdash; entfernen, um zu &uuml;berschreiben.
</p>
<span
className="event-type-collapsed-label"
id="deadline-event-type-collapsed-label"
/>
<span
className="event-type-collapsed-source"
data-i18n="deadlines.field.rule.autofill_inline"
>
&nbsp;(vorgegeben durch Regel)
</span>
<button
type="button"
className="event-type-collapsed-override"
id="deadline-event-type-override-btn"
data-i18n="deadlines.field.rule.override"
>
Anderen Typ w&auml;hlen
</button>
</div>
<div id="deadline-event-types" className="event-type-picker-host" />
{/* Soft warning when the user is in expanded mode AND
has picked an event_type that doesn't include the
rule's canonical default. Reuses the existing
yellow form-hint--warning style; never blocking. */}
<p
className="form-hint form-hint--warning"
id="deadline-event-type-rule-mismatch"

View File

@@ -821,8 +821,10 @@ export type I18nKey =
| "deadlines.field.notes.placeholder"
| "deadlines.field.rule"
| "deadlines.field.rule.autofill"
| "deadlines.field.rule.autofill_inline"
| "deadlines.field.rule.mismatch"
| "deadlines.field.rule.none"
| "deadlines.field.rule.override"
| "deadlines.field.title"
| "deadlines.field.title.placeholder"
| "deadlines.filter.akte"

View File

@@ -10420,6 +10420,43 @@ dialog.quick-add-sheet::backdrop {
t-paliad-088 — Event Types: picker, multi-select filter, add modal
============================================================================ */
/* t-paliad-165 follow-up — collapsed read-only view used on
/deadlines/new when a Regel is selected and a default event_type is
known. Replaces the picker with a single inline label + an
"Anderen Typ wählen" override link. */
.event-type-collapsed {
display: inline-flex;
align-items: baseline;
gap: 0.4rem;
padding: 0.35rem 0.55rem;
background: var(--color-bg-lime-tint);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
font-size: 0.95rem;
line-height: 1.3;
flex-wrap: wrap;
}
.event-type-collapsed-label {
font-weight: 600;
color: var(--color-text);
}
.event-type-collapsed-source {
color: var(--color-text-muted);
font-size: 0.85rem;
}
.event-type-collapsed-override {
margin-left: auto;
background: transparent;
border: 0;
padding: 0;
color: var(--color-link, #1d4ed8);
text-decoration: underline;
cursor: pointer;
font: inherit;
font-size: 0.85rem;
}
.event-type-collapsed-override:hover { color: var(--color-link-hover, #1e40af); }
/* Picker host — chip cluster + search + suggest dropdown */
.event-type-picker {
display: flex;