From f6df857def96058c7c769fd1fd84fcbebf296e32 Mon Sep 17 00:00:00 2001 From: mAi Date: Wed, 20 May 2026 14:41:02 +0200 Subject: [PATCH] =?UTF-8?q?fix(fristenrechner):=20m/paliad#57=20=E2=80=94?= =?UTF-8?q?=20cleanup=20(Custom=20labels,=20forward-workflow=20root,=20sam?= =?UTF-8?q?e-context-twice,=20Add=20prefill)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four UX cleanups on /tools/fristenrechner per m's 2026-05-20 14:02–14:04 report: 1. **Pre-fill project on 'Add'** — when Step 1 binds an Akte, both the Pathway A "Save to Project" modal and the Pathway B card-calc inline 'Add' picker now default their ` - : `
+ // Picker semantics (m/paliad#57 part 4): + // - lockedPill set → context known (user clicked a specific + // rule pill on the card). Render as a + // hidden input only; the calc panel shows + // no "Which context?" question. A small + // "ändern" link reopens the picker fieldset. + // - rulePills.length <= 1 → only one possible context, never a + // picker (hidden input carries the data). + // - otherwise → show the picker as primary surface; the + // card's pill list is hidden via CSS while + // the panel is open, so the user isn't + // asked the same thing twice. + let pickerHtml: string; + if (lockedPill) { + const procName = lockedPill.proceeding + ? (lang === "en" && lockedPill.proceeding.name_en ? lockedPill.proceeding.name_en : lockedPill.proceeding.name_de) + : ""; + const ruleName = lang === "en" && lockedPill.rule_name_en ? lockedPill.rule_name_en : lockedPill.rule_name_de; + const src = lockedPill.legal_source_display || lockedPill.legal_source || ""; + const reopenLabel = t("deadlines.card.calc.pill_picker.change"); + pickerHtml = `
+ ${escHtml(t("deadlines.card.calc.pill_picker.locked_label"))} + ${escHtml(procName)} + ${escHtml(ruleName)} + ${src ? `${escHtml(src)}` : ""} + ${rulePills.length > 1 ? `` : ""} + +
`; + } else if (rulePills.length <= 1) { + pickerHtml = ``; + } else { + pickerHtml = `
${escHtml(t("deadlines.card.calc.pill_picker.label"))} ${rulePills.map((p, i) => { const procName = p.proceeding ? (lang === "en" && p.proceeding.name_en ? p.proceeding.name_en : p.proceeding.name_de) : ""; @@ -1300,6 +1352,7 @@ function buildCalcPanel(_cardData: SearchCard, rulePills: SearchPill[]): HTMLEle `; }).join("")}
`; + } panel.innerHTML = ` @@ -1352,6 +1405,38 @@ function buildCalcPanel(_cardData: SearchCard, rulePills: SearchPill[]): HTMLEle void addCalcToProject(card, last); }); + // "ändern" — swap the locked-context caption for the full radio + // picker so the user can change context without collapsing the panel. + panel.querySelector(".fristen-card-calc-pill-change")?.addEventListener("click", () => { + const card = panel.closest(".fristen-card"); + const locked = panel.querySelector(".fristen-card-calc-pill-locked"); + if (!card || !locked) return; + const fieldset = document.createElement("fieldset"); + fieldset.className = "fristen-card-calc-pill-picker"; + fieldset.setAttribute("role", "radiogroup"); + const lockedProc = locked.querySelector("input.fristen-card-calc-pill-picker")?.dataset.proc || ""; + const lockedFocus = locked.querySelector("input.fristen-card-calc-pill-picker")?.dataset.focus || ""; + fieldset.innerHTML = ` + ${escHtml(t("deadlines.card.calc.pill_picker.label"))} + ${rulePills.map((p, i) => { + const procName = p.proceeding ? (lang === "en" && p.proceeding.name_en ? p.proceeding.name_en : p.proceeding.name_de) : ""; + const ruleName = lang === "en" && p.rule_name_en ? p.rule_name_en : p.rule_name_de; + const src = p.legal_source_display || p.legal_source || ""; + const isChecked = (p.proceeding?.code || "") === lockedProc + && (p.rule_local_code || "") === lockedFocus; + return ``; + }).join("")}`; + locked.replaceWith(fieldset); + fieldset.querySelectorAll('input[name="fristen-card-calc-pill"]').forEach((r) => { + r.addEventListener("change", () => scheduleCardCalc(card, 0)); + }); + }); + return panel; } @@ -1555,6 +1640,7 @@ async function addCalcToProject(card: HTMLElement, calc: RuleCalcResponse) { const lang = getLang(); const ruleName = lang === "en" ? calc.rule.nameEN : calc.rule.nameDE; const dueLabel = formatDate(calc.dueDate); + const preselected = preselectedProjectId(); msgEl.innerHTML = `
@@ -1573,6 +1660,7 @@ async function addCalcToProject(card: HTMLElement, calc: RuleCalcResponse) { `; const sel = msgEl.querySelector(".fristen-card-calc-add-select")!; + if (preselected) sel.value = preselected; msgEl.querySelector(".fristen-card-calc-add-cancel")!.addEventListener("click", () => { msgEl.innerHTML = ""; addBtn.disabled = false; @@ -1642,12 +1730,12 @@ function renderConceptCard(card: SearchCard, lang: "de" | "en"): string { const triggerPills = card.pills.filter((p) => p.kind === "trigger"); const ruleSection = rulePills.length === 0 ? "" : ` -
+

${escHtml(t("deadlines.search.pills.heading"))}

${rulePills.map((p) => renderPill(p, lang)).join("")}
`; const triggerSection = triggerPills.length === 0 ? "" : ` -
+

${escHtml(t("deadlines.search.pills.cross_cutting"))}

${triggerPills.map((p) => renderPill(p, lang)).join("")}
`; @@ -2423,6 +2511,17 @@ interface EventCategoryNode { let eventCategoryTree: EventCategoryNode[] | null = null; let eventCategoryFetchInflight: Promise | null = null; +// Top-level cascade roots that represent forward-looking workflows ("I +// want to file X, what deadlines does my action trigger?") rather than +// the backward-looking calc the Fristenrechner is built for ("event Y +// happened, what deadlines spawn?"). m's 2026-05-20 ask (m/paliad#57): +// remove these from the "Was ist passiert?" picker — they belong in a +// future forward-workflow tool, not here. The DB rows stay so that +// future tool can pick them back up; we just hide them at the UI layer. +const HIDDEN_CASCADE_ROOTS: ReadonlySet = new Set([ + "ich-moechte-einreichen", +]); + async function loadEventCategoryTree(): Promise { if (eventCategoryTree) return eventCategoryTree; if (eventCategoryFetchInflight) return eventCategoryFetchInflight; @@ -2431,7 +2530,8 @@ async function loadEventCategoryTree(): Promise { const r = await fetch("/api/tools/fristenrechner/event-categories"); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await r.json(); - eventCategoryTree = (data.tree || []) as EventCategoryNode[]; + const raw = (data.tree || []) as EventCategoryNode[]; + eventCategoryTree = raw.filter((n) => !HIDDEN_CASCADE_ROOTS.has(n.slug)); return eventCategoryTree; } finally { eventCategoryFetchInflight = null; diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index 5922a53c..413367f3 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -272,10 +272,10 @@ const translations: Record> = { "deadlines.step1.divider.new": "oder eine neue Akte", "deadlines.step1.divider.adhoc": "oder ad-hoc, ohne Akte", "deadlines.step1.new.cta": "+ Neue Akte anlegen", - "deadlines.step1.adhoc.upc": "Custom UPC-Verfahren", - "deadlines.step1.adhoc.de": "Custom DE-Verfahren", - "deadlines.step1.adhoc.epa": "Custom EPA-Verfahren", - "deadlines.step1.adhoc.dpma": "Custom DPMA-Verfahren", + "deadlines.step1.adhoc.upc": "UPC-Verfahren", + "deadlines.step1.adhoc.de": "DE-Verfahren", + "deadlines.step1.adhoc.epa": "EPA-Verfahren", + "deadlines.step1.adhoc.dpma": "DPMA-Verfahren", "deadlines.step1.selected": "Akte:", "deadlines.step1.reselect": "Andere Akte", "deadlines.step1.summary.adhoc.suffix": "ohne Akte (Erkundung)", @@ -345,6 +345,8 @@ const translations: Record> = { "deadlines.card.calc.expand_hint": "Frist berechnen oder zu Akte hinzufügen", "deadlines.card.calc.close": "schließen", "deadlines.card.calc.pill_picker.label": "Welcher Kontext?", + "deadlines.card.calc.pill_picker.locked_label": "Kontext:", + "deadlines.card.calc.pill_picker.change": "ändern", "deadlines.card.calc.trigger.label": "Datum des auslösenden Ereignisses", "deadlines.card.calc.flags.label": "Bedingungen:", "deadlines.card.calc.flag.with_ccr": "Mit Nichtigkeitswiderklage", @@ -2960,10 +2962,10 @@ const translations: Record> = { "deadlines.step1.divider.new": "or a new matter", "deadlines.step1.divider.adhoc": "or ad-hoc, without a matter", "deadlines.step1.new.cta": "+ Create new matter", - "deadlines.step1.adhoc.upc": "Custom UPC proceeding", - "deadlines.step1.adhoc.de": "Custom DE proceeding", - "deadlines.step1.adhoc.epa": "Custom EPA proceeding", - "deadlines.step1.adhoc.dpma": "Custom DPMA proceeding", + "deadlines.step1.adhoc.upc": "UPC proceeding", + "deadlines.step1.adhoc.de": "DE proceeding", + "deadlines.step1.adhoc.epa": "EPA proceeding", + "deadlines.step1.adhoc.dpma": "DPMA proceeding", "deadlines.step1.selected": "Matter:", "deadlines.step1.reselect": "Other matter", "deadlines.step1.summary.adhoc.suffix": "no matter (exploration)", @@ -3040,6 +3042,8 @@ const translations: Record> = { "deadlines.card.calc.expand_hint": "Calculate deadline or add to project", "deadlines.card.calc.close": "close", "deadlines.card.calc.pill_picker.label": "Which context?", + "deadlines.card.calc.pill_picker.locked_label": "Context:", + "deadlines.card.calc.pill_picker.change": "change", "deadlines.card.calc.trigger.label": "Date of triggering event", "deadlines.card.calc.flags.label": "Conditions:", "deadlines.card.calc.flag.with_ccr": "With counterclaim for revocation", diff --git a/frontend/src/fristenrechner.tsx b/frontend/src/fristenrechner.tsx index cfaea26a..95f46946 100644 --- a/frontend/src/fristenrechner.tsx +++ b/frontend/src/fristenrechner.tsx @@ -161,19 +161,19 @@ export function renderFristenrechner(): string {
diff --git a/frontend/src/i18n-keys.ts b/frontend/src/i18n-keys.ts index f415d1e2..2154a45b 100644 --- a/frontend/src/i18n-keys.ts +++ b/frontend/src/i18n-keys.ts @@ -971,7 +971,9 @@ export type I18nKey = | "deadlines.card.calc.flag.with_cci" | "deadlines.card.calc.flag.with_ccr" | "deadlines.card.calc.flags.label" + | "deadlines.card.calc.pill_picker.change" | "deadlines.card.calc.pill_picker.label" + | "deadlines.card.calc.pill_picker.locked_label" | "deadlines.card.calc.result.calculating" | "deadlines.card.calc.result.court_set" | "deadlines.card.calc.result.due" diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index b91192e3..6a463754 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -2670,6 +2670,61 @@ input[type="range"]::-moz-range-thumb { font-family: ui-monospace, monospace; } +/* m/paliad#57 part 4 — once a card is expanded into a calc panel, + the rule-pill list is redundant with the calc panel's context + picker (locked caption or fieldset). Hide it so the user isn't + asked the same thing twice. The cross-cutting section stays — + those pills are alternative concepts to explore, not the same + proceeding context. */ +.fristen-card.is-expanded .fristen-card-pills-section--rules { + display: none; +} + +/* Locked-context caption when the user clicked a specific rule pill + to expand. Shows the picked (proceeding, rule) tuple compactly + with a small "ändern" button to swap back to the radio picker. */ +.fristen-card-calc-pill-locked { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.4rem; + padding: 0.35rem 0.55rem; + border: 1px solid var(--color-border-subtle, #ececec); + border-radius: 5px; + background: rgba(198, 244, 28, 0.06); + font-size: 0.88rem; +} +.fristen-card-calc-pill-locked-label { + font-weight: 600; + color: var(--color-muted, #777); + text-transform: uppercase; + font-size: 0.74rem; + letter-spacing: 0.04em; +} +.fristen-card-calc-pill-locked-proc { + font-weight: 600; + color: var(--color-text, #222); +} +.fristen-card-calc-pill-locked-rule { + color: var(--color-text, #222); +} +.fristen-card-calc-pill-locked-source { + font-size: 0.8rem; + color: var(--color-muted, #888); + font-family: ui-monospace, monospace; +} +.fristen-card-calc-pill-change { + margin-left: auto; + background: transparent; + border: 0; + padding: 0; + color: var(--color-link, #1267a8); + cursor: pointer; + font-size: 0.82rem; + text-decoration: underline; +} +.fristen-card-calc-pill-change:hover { text-decoration: none; } + .fristen-card-calc-inputs { display: flex; flex-wrap: wrap; -- 2.49.1