diff --git a/frontend/src/client/fristenrechner.ts b/frontend/src/client/fristenrechner.ts index a2814bb5..58908b98 100644 --- a/frontend/src/client/fristenrechner.ts +++ b/frontend/src/client/fristenrechner.ts @@ -251,6 +251,19 @@ function closeSaveModal() { if (modal) modal.style.display = "none"; } +// preselectedProjectId returns the project the user picked in Step 1 +// (if any) so the various save/add flows can default their project +// pickers to it. Carries through anywhere a "save to Akte" pop-out +// renders \u2014 preselection is *only* a default; the picker still +// renders every available project and the user can override. +// m/paliad#57 part 1: 2026-05-20 user complaint \u2014 "the pre-selected +// project should be pre-selected" on Add. +function preselectedProjectId(): string { + return currentStep1Context.kind === "project" && currentStep1Context.projectId + ? currentStep1Context.projectId + : ""; +} + async function openSaveModal() { if (!lastResponse) return; ensureSaveModal(); @@ -267,6 +280,7 @@ async function openSaveModal() { sel.style.display = ""; noProjects.style.display = "none"; submit.disabled = false; + const preselected = preselectedProjectId(); sel.innerHTML = projects .map((p) => { const ref = (p.reference || "").trim(); @@ -274,9 +288,11 @@ async function openSaveModal() { const label = ref ? `${indent}${escHtml(ref)} \u2014 ${escHtml(p.title)}` : `${indent}${escHtml(p.title)}`; - return ``; + const selected = p.id === preselected ? " selected" : ""; + return ``; }) .join(""); + if (preselected) sel.value = preselected; } const list = document.getElementById("frist-save-list")!; @@ -1260,19 +1276,27 @@ function expandCardCalc(card: HTMLElement, autoSelectPill: HTMLElement | null) { card.classList.add("is-expanded"); card.setAttribute("aria-expanded", "true"); - const panel = buildCalcPanel(cardData, rulePills); - card.appendChild(panel); + // m/paliad#57 part 4: when the user clicked a specific rule pill, the + // context is already known — the calc panel renders with that pill + // locked in and no "Which context?" picker. The card's pill list is + // hidden via CSS while is-expanded so the rules aren't listed twice. + // When the user clicked the card body (no autoSelectPill), the picker + // is the primary surface — still no duplicate pill list above it. + const lockedPill = (autoSelectPill && autoSelectPill.dataset.kind === "rule") + ? rulePills.find((p) => + p.proceeding?.code === autoSelectPill.dataset.proc + && (autoSelectPill.dataset.focus + ? p.rule_local_code === autoSelectPill.dataset.focus + : true)) + : undefined; - // Auto-select the clicked pill if it's a rule pill; otherwise the - // first pill is preselected by buildCalcPanel. - if (autoSelectPill && autoSelectPill.dataset.kind === "rule") { - selectCalcPill(card, autoSelectPill.dataset.proc, autoSelectPill.dataset.focus); - } + const panel = buildCalcPanel(cardData, rulePills, lockedPill || null); + card.appendChild(panel); scheduleCardCalc(card); } -function buildCalcPanel(_cardData: SearchCard, rulePills: SearchPill[]): HTMLElement { +function buildCalcPanel(_cardData: SearchCard, rulePills: SearchPill[], lockedPill: SearchPill | null = null): HTMLElement { const panel = document.createElement("div"); panel.className = "fristen-card-calc"; // stopPropagation so clicks inside the panel don't bubble to the @@ -1283,10 +1307,38 @@ function buildCalcPanel(_cardData: SearchCard, rulePills: SearchPill[]): HTMLEle const lang = getLang(); const today = new Date().toISOString().split("T")[0]; - // Pill picker (only when >1 rule pill). - const pickerHtml = rulePills.length <= 1 - ? `` - : `
+ // 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;