diff --git a/frontend/src/client/deadlines-detail.ts b/frontend/src/client/deadlines-detail.ts index aa633cd..c1aa98d 100644 --- a/frontend/src/client/deadlines-detail.ts +++ b/frontend/src/client/deadlines-detail.ts @@ -366,6 +366,7 @@ function initEdit() { const etEdit = document.getElementById("deadline-event-types-edit"); const projectLink = document.getElementById("deadline-project-link") as HTMLAnchorElement; const projectEdit = document.getElementById("deadline-project-edit") as HTMLSelectElement | null; + const titleDefaultBtn = document.getElementById("deadline-title-default-btn") as HTMLButtonElement | null; function enterEdit() { titleDisplay.style.display = "none"; @@ -381,6 +382,7 @@ function initEdit() { projectEdit.style.display = ""; projectEdit.value = deadline.project_id; } + if (titleDefaultBtn) titleDefaultBtn.style.display = ""; saveBtn.style.display = ""; editBtn.style.display = "none"; titleEdit.focus(); @@ -399,12 +401,41 @@ function initEdit() { projectEdit.style.display = "none"; projectLink.style.display = ""; } + if (titleDefaultBtn) titleDefaultBtn.style.display = "none"; saveBtn.style.display = "none"; editBtn.style.display = ""; } editBtn.addEventListener("click", enterEdit); + // t-paliad-251 Part 4 — Standardtitel button. + // Recipe (mirror of computeDefaultTitle in deadlines-new.ts): + // head = event_type label (if exactly one Typ chip is in edit) + // || rule code+name (when deadline carries a rule) + // || "Neue Frist" fallback + // suffix = " — " when not already in head + titleDefaultBtn?.addEventListener("click", () => { + if (!deadline) return; + let head = ""; + const ids = eventTypePicker?.getIDs() ?? deadline.event_type_ids ?? []; + if (ids.length === 1) { + const et = eventTypeByID.get(ids[0]); + if (et) head = eventTypeLabel(et); + } + if (!head && rule) { + const code = rule.rule_code || rule.code || ""; + head = code ? `${code} — ${rule.name}` : rule.name; + } + if (!head && deadline.rule_code) { + head = deadline.rule_code; + } + if (!head) head = t("deadlines.field.title.default_fallback"); + const ref = project?.reference?.trim() || ""; + if (ref && !head.includes(ref)) head = `${head} — ${ref}`; + titleEdit.value = head; + titleEdit.focus(); + }); + saveBtn.addEventListener("click", async () => { if (!deadline) return; const newTitle = titleEdit.value.trim(); diff --git a/frontend/src/client/deadlines-new.ts b/frontend/src/client/deadlines-new.ts index 9859228..2a72890 100644 --- a/frontend/src/client/deadlines-new.ts +++ b/frontend/src/client/deadlines-new.ts @@ -1,4 +1,4 @@ -import { initI18n, t, tDyn } from "./i18n"; +import { initI18n, t, tDyn, getLang } from "./i18n"; import { initSidebar } from "./sidebar"; import { attachEventTypePicker, @@ -24,6 +24,10 @@ interface Project { reference?: string | null; title: string; path: string; + // t-paliad-251 — used by Type→Rule autofill to narrow rule candidates + // to the project's own proceeding. Optional because not every project + // is a case/proceeding (clients + matters carry no proceeding type). + proceeding_type_id?: number | null; } interface DeadlineRule { @@ -32,15 +36,32 @@ interface DeadlineRule { name: string; name_en: string; rule_code?: string; + proceeding_type_id?: number | null; + sequence_order?: number; // t-paliad-165 — canonical event_type for this rule's concept, // hydrated server-side from paliad.deadline_concept_event_types. - // Drives auto-fill of the Typ chip when the user picks this rule. + // Drives auto-fill of the Typ chip when the user picks this rule, + // AND is inverted to power Typ→Regel auto-fill (t-paliad-251 Part 2): + // given a chosen event_type X, candidate rules are those whose + // concept_default_event_type_id === X. concept_default_event_type_id?: string | null; } +interface ProceedingType { + id: number; + code: string; + name: string; + name_en?: string; + jurisdiction: string; + sort_order?: number; +} + // Rules indexed by id so the Regel-change handler can look up the // concept's canonical event_type without re-fetching. let rulesByID = new Map(); +let allRules: DeadlineRule[] = []; +let proceedingTypesByID = new Map(); +let projectsByID = new Map(); // Last event_type the rule auto-filled. Tracked so we can tell whether // the picker still reflects the rule's suggestion (replace silently on @@ -48,6 +69,17 @@ let rulesByID = new Map(); // surface the mismatch warning instead). let lastAutoFilledEventTypeID: string | null = null; +// t-paliad-251 — symmetric flag for the inverse direction. Tracks the +// rule ID we most recently injected as the Auto-derived default for the +// chosen event_type, so we can replace it silently when the user picks +// a different type but leave manual rule picks alone. +let lastAutoFilledRuleID: string | null = null; + +// Current sort mode for the Rule select. Persisted to localStorage so +// repeat-form users don't have to re-pick their preferred ordering. +type RuleSort = "by_proceeding" | "by_court" | "alpha"; +const RULE_SORT_KEY = "paliad.deadline.rule.sort"; + let preselectedProjectID = ""; function esc(s: string): string { @@ -62,6 +94,20 @@ function showError(msg: string) { el.className = "form-msg form-msg-error"; } +function ruleLabel(r: DeadlineRule): string { + const lang = getLang(); + const name = (lang === "en" && r.name_en) ? r.name_en : r.name; + const code = r.rule_code || r.code || ""; + return code ? `${code} — ${name}` : name; +} + +function proceedingLabel(pt: ProceedingType | undefined): string { + if (!pt) return ""; + const lang = getLang(); + const name = (lang === "en" && pt.name_en) ? pt.name_en : pt.name; + return `${pt.jurisdiction} — ${name}`; +} + async function loadProjects() { const sel = document.getElementById("deadline-project") as HTMLSelectElement; const hint = document.getElementById("deadline-project-empty-hint")!; @@ -69,6 +115,7 @@ async function loadProjects() { const resp = await fetch("/api/projects"); if (!resp.ok) return; const projects: Project[] = await resp.json(); + projectsByID = new Map(projects.map((p) => [p.id, p])); if (projects.length === 0) { hint.style.display = ""; hint.innerHTML = `${esc(t("deadlines.field.akte.empty"))} ${esc(t("deadlines.field.akte.empty.link"))}`; @@ -82,7 +129,7 @@ async function loadProjects() { const ref = p.reference || ""; const indent = projectIndent(p.path); options.push( - ``, + ``, ); } sel.innerHTML = options.join(""); @@ -91,28 +138,186 @@ async function loadProjects() { } } +async function loadProceedingTypes() { + try { + const resp = await fetch("/api/proceeding-types-db"); + if (!resp.ok) return; + const types: ProceedingType[] = await resp.json(); + proceedingTypesByID = new Map(types.map((pt) => [pt.id, pt])); + } catch { + /* non-fatal — rule sort falls back to alpha when proceeding-type + metadata is missing */ + } +} + async function loadRules() { // Optional: load rules so user can attach. We pull all rules; small set. - const sel = document.getElementById("deadline-rule") as HTMLSelectElement; try { const resp = await fetch("/api/deadline-rules"); if (!resp.ok) return; - const rules: DeadlineRule[] = await resp.json(); - rulesByID = new Map(rules.map((r) => [r.id, r])); - const opts: string[] = [ - ``, - ]; - for (const r of rules) { - const code = r.rule_code || r.code || ""; - const label = code ? `${code} \u2014 ${r.name}` : r.name; - opts.push(``); - } - sel.innerHTML = opts.join(""); + allRules = (await resp.json()) as DeadlineRule[]; + rulesByID = new Map(allRules.map((r) => [r.id, r])); + renderRuleSelect(); } catch { /* non-fatal — rule select stays at "no rule" */ } } +// renderRuleSelect rebuilds the Rule +
+ + ${chipJurisdictions + .map( + (j) => + ``, + ) + .join("")} +
@@ -711,6 +747,7 @@ export function openBrowseEventTypesModal( const countEl = overlay.querySelector("[data-role=count]")!; const cancelBtn = overlay.querySelector("[data-role=cancel]")!; const applyBtn = overlay.querySelector("[data-role=apply]")!; + const chipButtons = overlay.querySelectorAll(".event-type-browse-chip"); const groups = groupByCategory(opts.types); @@ -721,6 +758,12 @@ export function openBrowseEventTypesModal( return j; } + function jurisdictionMatches(et: EventType): boolean { + if (activeJurisdiction === null) return true; + const j = (et.jurisdiction ?? "").trim(); + return j === activeJurisdiction; + } + function updateCount() { countEl.textContent = t("event_types.browse.selected_count").replace( "{n}", @@ -731,6 +774,7 @@ export function openBrowseEventTypesModal( function renderList() { const q = searchQuery.trim().toLowerCase(); const matches = (et: EventType) => { + if (!jurisdictionMatches(et)) return false; if (!q) return true; return ( et.label_de.toLowerCase().includes(q) || @@ -783,6 +827,16 @@ export function openBrowseEventTypesModal( renderList(); }); + chipButtons.forEach((btn) => { + btn.addEventListener("click", () => { + const raw = btn.dataset.jurisdiction ?? ""; + activeJurisdiction = raw === "" ? null : raw; + chipButtons.forEach((b) => b.classList.remove("event-type-browse-chip--active")); + btn.classList.add("event-type-browse-chip--active"); + renderList(); + }); + }); + function close(value: string[] | null) { document.removeEventListener("keydown", onKey); overlay.remove(); diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index c85bbc9..52ed76f 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -879,6 +879,14 @@ const translations: Record> = { "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.rule.auto_badge": "Auto", + "deadlines.field.rule.override_warn": "Typ ergibt Regel: {derived}. Gewählte Regel: {selected}. Es wird {selected} angewendet.", + "deadlines.field.rule.sort.by_proceeding": "Nach Verfahrensablauf", + "deadlines.field.rule.sort.by_court": "Nach Gerichtsart", + "deadlines.field.rule.sort.alpha": "Alphabetisch", + "deadlines.field.rule.sort.other_proceeding": "Sonstige Regeln", + "deadlines.field.title.default_btn": "Standardtitel", + "deadlines.field.title.default_fallback": "Neue Frist", "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.", @@ -2437,6 +2445,8 @@ const translations: Record> = { "event_types.browse.cancel": "Abbrechen", "event_types.browse.selected_count": "{n} ausgewählt", "event_types.browse.jurisdiction.none": "Allgemein", + "event_types.browse.jurisdiction.all": "Alle Gerichte", + "event_types.browse.jurisdiction.filter_label": "Nach Gerichtsart filtern", "event_types.filter.all": "Alle Typen", "event_types.filter.untyped": "— Ohne Typ —", "event_types.filter.search": "Typ suchen…", @@ -3825,6 +3835,14 @@ const translations: Record> = { "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.rule.auto_badge": "Auto", + "deadlines.field.rule.override_warn": "Type derives rule: {derived}. Selected rule: {selected}. {selected} will be applied.", + "deadlines.field.rule.sort.by_proceeding": "By proceeding sequence", + "deadlines.field.rule.sort.by_court": "By court type", + "deadlines.field.rule.sort.alpha": "Alphabetical", + "deadlines.field.rule.sort.other_proceeding": "Other rules", + "deadlines.field.title.default_btn": "Default title", + "deadlines.field.title.default_fallback": "New deadline", "deadlines.field.notes": "Notes (optional)", "deadlines.field.notes.placeholder": "References, hints, next steps\u2026", "deadlines.error.required": "Matter, title and due date are required.", @@ -5355,6 +5373,8 @@ const translations: Record> = { "event_types.browse.cancel": "Cancel", "event_types.browse.selected_count": "{n} selected", "event_types.browse.jurisdiction.none": "Any", + "event_types.browse.jurisdiction.all": "All courts", + "event_types.browse.jurisdiction.filter_label": "Filter by court type", "event_types.filter.all": "All types", "event_types.filter.untyped": "— Untyped —", "event_types.filter.search": "Search type…", diff --git a/frontend/src/deadlines-detail.tsx b/frontend/src/deadlines-detail.tsx index 02eb61c..17382fa 100644 --- a/frontend/src/deadlines-detail.tsx +++ b/frontend/src/deadlines-detail.tsx @@ -41,6 +41,19 @@ export function renderDeadlinesDetail(): string {

+ {/* t-paliad-251 Part 4 — Standardtitel button only + visible in edit mode; clicking replaces the + title with a default derived from the project + and the deadline's event types / rule. */} +
diff --git a/frontend/src/deadlines-new.tsx b/frontend/src/deadlines-new.tsx index 0354d04..1922bde 100644 --- a/frontend/src/deadlines-new.tsx +++ b/frontend/src/deadlines-new.tsx @@ -45,7 +45,22 @@ export function renderDeadlinesNew(): string {
- +
+ + {/* t-paliad-251 Part 4 — derive a Standardtitel from the + currently-known context (event type → rule → proceeding + type → fallback) with the project reference as suffix. + Always replaces the title; no destructive confirmation + because the user invoked it explicitly. */} + +
- +
+ + {/* t-paliad-251 Part 2 — sort options for the Rule + select. Defaults to "by_court" so users in the + UPC bucket find UPC rules quickly. */} + +
+ {/* t-paliad-251 Part 3 — explicit Auto badge surfaces + whenever the Rule was auto-derived from the Typ. + Hidden when the user has manually picked a rule. */} + + {/* t-paliad-251 Part 3 — clearer override warning that + names BOTH the type-derived rule and the actually- + applied rule. Replaces the older Regel→Typ-only + mismatch warning when the contradiction goes the + other direction. */} +
diff --git a/frontend/src/i18n-keys.ts b/frontend/src/i18n-keys.ts index 4528ce9..2f6284f 100644 --- a/frontend/src/i18n-keys.ts +++ b/frontend/src/i18n-keys.ts @@ -1227,12 +1227,20 @@ export type I18nKey = | "deadlines.field.notes" | "deadlines.field.notes.placeholder" | "deadlines.field.rule" + | "deadlines.field.rule.auto_badge" | "deadlines.field.rule.autofill" | "deadlines.field.rule.autofill_inline" | "deadlines.field.rule.mismatch" | "deadlines.field.rule.none" | "deadlines.field.rule.override" + | "deadlines.field.rule.override_warn" + | "deadlines.field.rule.sort.alpha" + | "deadlines.field.rule.sort.by_court" + | "deadlines.field.rule.sort.by_proceeding" + | "deadlines.field.rule.sort.other_proceeding" | "deadlines.field.title" + | "deadlines.field.title.default_btn" + | "deadlines.field.title.default_fallback" | "deadlines.field.title.placeholder" | "deadlines.filter.akte" | "deadlines.filter.akte.all" @@ -1574,6 +1582,8 @@ export type I18nKey = | "event_types.browse.apply" | "event_types.browse.cancel" | "event_types.browse.empty" + | "event_types.browse.jurisdiction.all" + | "event_types.browse.jurisdiction.filter_label" | "event_types.browse.jurisdiction.none" | "event_types.browse.search" | "event_types.browse.selected_count" diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index d22c866..6cc1f3b 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -7520,6 +7520,78 @@ dialog.modal::backdrop { border-left: 2px solid #b88800; } +/* t-paliad-251 — Auto-derived hint variant. Lime-tint, sibling of the + yellow warning variant. Carries a small pill-badge in front (the + "Auto" label) followed by the derived rule name. */ +.form-hint--auto { + display: inline-flex; + align-items: center; + gap: 0.4rem; + background: var(--color-bg-lime-tint); + color: var(--color-text); + padding: 0.3rem 0.5rem; + border-radius: var(--radius-sm, 4px); + border-left: 2px solid var(--color-accent); +} +.form-hint-badge { + display: inline-block; + padding: 0.05rem 0.45rem; + border-radius: 999px; + background: var(--color-accent); + color: var(--color-text); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +/* t-paliad-251 — label row that hosts both the form label and an + inline action (Standardtitel button, Rule-sort dropdown). The label + keeps growing to push the action to the right edge. */ +.form-field-label-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + margin-bottom: 0.25rem; +} +.form-field-label-row > label { + margin: 0; +} + +/* Inline action button rendered next to a form label (Standardtitel). + Text-link styling so it doesn't compete with the primary CTA. */ +.btn-link-action { + background: transparent; + border: none; + color: var(--color-link, var(--color-text)); + padding: 0; + font-family: var(--font-sans); + font-size: 0.82rem; + cursor: pointer; + text-decoration: underline; + text-underline-offset: 2px; +} +.btn-link-action:hover { + color: var(--color-accent); +} + +/* Small dropdown rendered alongside the Rule label to switch the + ordering. Tone-down sizing so it doesn't look like a co-equal + form field. Specificity-bumped to win over `.form-field select`'s + width: 100% baseline. */ +.form-field select.rule-sort-select, +select.rule-sort-select { + width: auto; + padding: 0.2rem 0.4rem; + font-size: 0.82rem; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm, 4px); + color: var(--color-text); + font-family: var(--font-sans); +} + /* Inline checkbox label inside the attach-unit form. */ .form-checkbox { display: inline-flex; @@ -12517,6 +12589,37 @@ dialog.quick-add-sheet::backdrop { transition: border-color 0.15s ease; } .event-type-browse-search:focus { border-color: var(--color-accent); } +/* t-paliad-251 — jurisdiction filter chips inside the browse modal + header. Sits below the search input, between the search and the + results list. Active chip uses the lime-tint chip palette already + established by .event-type-collapsed* (t-paliad-165). */ +.event-type-browse-chips { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} +.event-type-browse-chip { + padding: 0.2rem 0.7rem; + border: 1px solid var(--color-border); + border-radius: 999px; + background: var(--color-surface); + color: var(--color-text-muted); + font-family: var(--font-sans); + font-size: 0.78rem; + font-weight: 500; + cursor: pointer; + transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease; +} +.event-type-browse-chip:hover { + background: var(--color-bg-subtle); + color: var(--color-text); +} +.event-type-browse-chip--active { + background: var(--color-bg-lime-tint); + border-color: var(--color-accent); + color: var(--color-text); + font-weight: 600; +} .event-type-browse-list { flex: 1 1 auto; overflow-y: auto;