mAi: #82 - deadline form overhaul: type-modal filter chips, type→rule autofill, Auto mode, Standardtitel
t-paliad-251. Four bundled concerns from m's 2026-05-25 reports, one
worker, one branch.
Part 1 — Event-type browse modal (search + filters)
- Modal already had a search input; added court-type filter chips
(UPC / EPA / DPMA / DE / Allgemein) under the search.
- Chips render only the jurisdictions actually present in the data;
any future flavour lands at the end of the row.
- Active chip uses the lime-tint chip palette already established by
the .event-type-collapsed* family (t-paliad-165).
- Search input keeps autofocus; chip + search filters intersect.
Part 2 — Type → Rule auto-fill + sort options
- Inverted the existing rule.concept_default_event_type_id mapping
client-side: given a chosen event_type X, candidate rules are
those with concept_default_event_type_id === X.
- Resolution picks (1) exact match on the project's
proceeding_type_id, (2) jurisdiction match on the rule's
proceeding (EPA→EPO canonicalised), (3) first candidate.
- Sort dropdown next to the Rule label: by proceeding sequence,
by court (jurisdiction grouping with optgroup), alphabetical.
Defaults to "by court"; localStorage-persisted per browser.
- All sorts are client-side over the existing /api/deadline-rules
payload — no new endpoint.
Part 3 — Auto rule mode + clearer override warning
- Auto badge (.form-hint--auto, lime-tint pill + " — <rule name>")
surfaces whenever the Rule was derived from the chosen Type.
Disappears the moment the user manually picks a different rule.
- Override warning names BOTH sides + the actually-applied rule:
"Typ ergibt Regel: X. Gewählte Regel: Y. Es wird Y angewendet."
- Symmetric `lastAutoFilledRuleID` sticky-replace flag mirrors the
existing `lastAutoFilledEventTypeID` (t-paliad-165) so the auto-
fill only replaces its own previous suggestion, never a manual
pick.
- Collapsed Typ view (t-paliad-165) is suppressed when the rule was
auto-derived from the type — the "vorgegeben durch Regel" copy
reads backwards in that case; show picker + Auto badge instead.
Part 4 — Standardtitel button (create + edit)
- Button rendered next to the Title field on both /deadlines/new
and /deadlines/{id} (edit mode only).
- Recipe (recipe-docs-here-so-future-templates-can-mirror-it):
head =
1. event_type label (if exactly one Typ chip is set)
2. rule code+name (when a Rule is set — "RoP.023 — Klageerwiderung")
3. proceeding type name from project (create form only)
4. fallback: t("deadlines.field.title.default_fallback")
suffix = " — <project.reference>" when ref is set and not
already in head.
Examples:
Klageerwiderung — C-UPC-0042 (type known)
RoP.023 — Klageerwiderung — REF (rule known, no type)
UPC — Verletzungsverfahren — REF (only proceeding type)
Neue Frist — REF (fallback)
- Click REPLACES current title; no destructive confirmation
because the user invoked it explicitly. Focus moves into the
title input afterwards so the user can fine-tune.
Build hygiene:
- go build + go vet + go test ./internal/... clean.
- frontend/build.ts clean (2786 keys, +10 new DE+EN, scan clean).
- All changes client-side / CSS / i18n + 2 small TSX edits; no
schema, no service, no migration.
Files touched:
- frontend/src/client/event-types.ts (browse-modal chips)
- frontend/src/client/deadlines-new.ts (rewrite — Type→Rule, sort,
Auto badge, override warn, Standardtitel)
- frontend/src/client/deadlines-detail.ts (edit-mode Standardtitel
+ show/hide on enter/exit edit)
- frontend/src/deadlines-new.tsx (label-row + sort dropdown + Auto
badge slot + override-warn slot + Standardtitel button)
- frontend/src/deadlines-detail.tsx (Standardtitel button)
- frontend/src/styles/global.css (.event-type-browse-chip*,
.form-hint--auto, .form-hint-badge, .form-field-label-row,
.btn-link-action, .rule-sort-select)
- frontend/src/client/i18n.ts (+10 keys DE+EN)
This commit is contained in:
@@ -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 = " — <project.reference>" 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();
|
||||
|
||||
@@ -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<string, DeadlineRule>();
|
||||
let allRules: DeadlineRule[] = [];
|
||||
let proceedingTypesByID = new Map<number, ProceedingType>();
|
||||
let projectsByID = new Map<string, Project>();
|
||||
|
||||
// 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<string, DeadlineRule>();
|
||||
// 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"))} <a href="/projects/new">${esc(t("deadlines.field.akte.empty.link"))}</a>`;
|
||||
@@ -82,7 +129,7 @@ async function loadProjects() {
|
||||
const ref = p.reference || "";
|
||||
const indent = projectIndent(p.path);
|
||||
options.push(
|
||||
`<option value="${esc(p.id)}"${isSelected}>${indent}${esc(ref)} \u2014 ${esc(p.title)}</option>`,
|
||||
`<option value="${esc(p.id)}"${isSelected}>${indent}${esc(ref)} — ${esc(p.title)}</option>`,
|
||||
);
|
||||
}
|
||||
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[] = [
|
||||
`<option value="" data-i18n="deadlines.field.rule.none">${esc(t("deadlines.field.rule.none"))}</option>`,
|
||||
];
|
||||
for (const r of rules) {
|
||||
const code = r.rule_code || r.code || "";
|
||||
const label = code ? `${code} \u2014 ${r.name}` : r.name;
|
||||
opts.push(`<option value="${esc(r.id)}">${esc(label)}</option>`);
|
||||
}
|
||||
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 <select> from the current sort
|
||||
// mode + the cached rule set. Called whenever the user changes the sort
|
||||
// dropdown, when the language flips, or after rules + proceeding types
|
||||
// finish loading. The "Keine Regel" sentinel always stays at the top.
|
||||
function renderRuleSelect(): void {
|
||||
const sel = document.getElementById("deadline-rule") as HTMLSelectElement | null;
|
||||
if (!sel) return;
|
||||
const previous = sel.value;
|
||||
|
||||
const sort = readRuleSort();
|
||||
const opts: string[] = [
|
||||
`<option value="" data-i18n="deadlines.field.rule.none">${esc(t("deadlines.field.rule.none"))}</option>`,
|
||||
];
|
||||
|
||||
if (sort === "alpha") {
|
||||
const sorted = [...allRules].sort((a, b) => ruleLabel(a).localeCompare(ruleLabel(b)));
|
||||
for (const r of sorted) {
|
||||
opts.push(`<option value="${esc(r.id)}">${esc(ruleLabel(r))}</option>`);
|
||||
}
|
||||
} else if (sort === "by_court") {
|
||||
// Group by proceeding_type.jurisdiction (UPC / EPA / DPMA / DE /
|
||||
// other). Within each group, sort alpha by rule label so the user
|
||||
// can scan a court's rules in stable order.
|
||||
const byJurisdiction = new Map<string, DeadlineRule[]>();
|
||||
for (const r of allRules) {
|
||||
const pt = r.proceeding_type_id ? proceedingTypesByID.get(r.proceeding_type_id) : undefined;
|
||||
const j = pt?.jurisdiction || t("event_types.browse.jurisdiction.none");
|
||||
const list = byJurisdiction.get(j) ?? [];
|
||||
list.push(r);
|
||||
byJurisdiction.set(j, list);
|
||||
}
|
||||
const order = ["UPC", "EPA", "EPO", "DPMA", "DE"];
|
||||
const keys = [...byJurisdiction.keys()].sort((a, b) => {
|
||||
const ai = order.indexOf(a);
|
||||
const bi = order.indexOf(b);
|
||||
if (ai === -1 && bi === -1) return a.localeCompare(b);
|
||||
if (ai === -1) return 1;
|
||||
if (bi === -1) return -1;
|
||||
return ai - bi;
|
||||
});
|
||||
for (const k of keys) {
|
||||
const list = byJurisdiction.get(k)!.sort((a, b) => ruleLabel(a).localeCompare(ruleLabel(b)));
|
||||
opts.push(`<optgroup label="${esc(k === "EPO" ? "EPA" : k)}">`);
|
||||
for (const r of list) {
|
||||
opts.push(`<option value="${esc(r.id)}">${esc(ruleLabel(r))}</option>`);
|
||||
}
|
||||
opts.push(`</optgroup>`);
|
||||
}
|
||||
} else {
|
||||
// by_proceeding — group by proceeding_type, within each preserve the
|
||||
// canonical sequence_order so the user reads "Klageerwiderung →
|
||||
// Replik → Duplik → Verhandlung" in chronological order.
|
||||
const byProceeding = new Map<number | string, DeadlineRule[]>();
|
||||
const noProceedingKey = "__none__";
|
||||
for (const r of allRules) {
|
||||
const k: number | string = r.proceeding_type_id ?? noProceedingKey;
|
||||
const list = byProceeding.get(k) ?? [];
|
||||
list.push(r);
|
||||
byProceeding.set(k, list);
|
||||
}
|
||||
const keys = [...byProceeding.keys()].sort((a, b) => {
|
||||
if (a === noProceedingKey) return 1;
|
||||
if (b === noProceedingKey) return -1;
|
||||
const pa = proceedingTypesByID.get(a as number);
|
||||
const pb = proceedingTypesByID.get(b as number);
|
||||
const sa = pa?.sort_order ?? 9999;
|
||||
const sb = pb?.sort_order ?? 9999;
|
||||
if (sa !== sb) return sa - sb;
|
||||
return (pa?.code ?? "").localeCompare(pb?.code ?? "");
|
||||
});
|
||||
for (const k of keys) {
|
||||
const list = (byProceeding.get(k)!).slice().sort(
|
||||
(a, b) => (a.sequence_order ?? 0) - (b.sequence_order ?? 0),
|
||||
);
|
||||
const pt = typeof k === "number" ? proceedingTypesByID.get(k) : undefined;
|
||||
const groupLabel = pt ? proceedingLabel(pt) : t("deadlines.field.rule.sort.other_proceeding");
|
||||
opts.push(`<optgroup label="${esc(groupLabel)}">`);
|
||||
for (const r of list) {
|
||||
opts.push(`<option value="${esc(r.id)}">${esc(ruleLabel(r))}</option>`);
|
||||
}
|
||||
opts.push(`</optgroup>`);
|
||||
}
|
||||
}
|
||||
|
||||
sel.innerHTML = opts.join("");
|
||||
// Restore previous selection if it still exists in the new order.
|
||||
if (previous && rulesByID.has(previous)) {
|
||||
sel.value = previous;
|
||||
}
|
||||
}
|
||||
|
||||
function readRuleSort(): RuleSort {
|
||||
try {
|
||||
const raw = localStorage.getItem(RULE_SORT_KEY);
|
||||
if (raw === "by_proceeding" || raw === "by_court" || raw === "alpha") return raw;
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
}
|
||||
return "by_court";
|
||||
}
|
||||
|
||||
function writeRuleSort(s: RuleSort): void {
|
||||
try {
|
||||
localStorage.setItem(RULE_SORT_KEY, s);
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
}
|
||||
}
|
||||
|
||||
// resolveAutoRuleForType picks the best-match rule for the chosen event
|
||||
// type, scoring by:
|
||||
// 1. project's proceeding_type_id (if known) — exact match wins,
|
||||
// 2. otherwise event_type.jurisdiction matches the rule's
|
||||
// proceeding's jurisdiction (EPA→EPO canonicalised),
|
||||
// 3. otherwise just the first candidate in the canonical ordering.
|
||||
//
|
||||
// Returns null when no rule maps to this event_type. The caller surfaces
|
||||
// this as "no Auto rule available — pick one manually" rather than
|
||||
// silently leaving the dropdown stuck on whatever the user picked before.
|
||||
function resolveAutoRuleForType(eventTypeID: string, projectID: string): DeadlineRule | null {
|
||||
const candidates = allRules.filter((r) => r.concept_default_event_type_id === eventTypeID);
|
||||
if (candidates.length === 0) return null;
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
|
||||
const project = projectID ? projectsByID.get(projectID) : undefined;
|
||||
if (project?.proceeding_type_id) {
|
||||
const exact = candidates.find((r) => r.proceeding_type_id === project.proceeding_type_id);
|
||||
if (exact) return exact;
|
||||
}
|
||||
|
||||
const et = eventTypesByID.get(eventTypeID);
|
||||
if (et?.jurisdiction && et.jurisdiction !== "any") {
|
||||
const want = et.jurisdiction === "EPO" ? "EPA" : et.jurisdiction;
|
||||
const jurMatch = candidates.find((r) => {
|
||||
const pt = r.proceeding_type_id ? proceedingTypesByID.get(r.proceeding_type_id) : undefined;
|
||||
return pt?.jurisdiction === want;
|
||||
});
|
||||
if (jurMatch) return jurMatch;
|
||||
}
|
||||
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
let preselectedProjectIDLocal = "";
|
||||
|
||||
function initBackLinks() {
|
||||
if (preselectedProjectID) {
|
||||
const back = document.getElementById("deadline-new-back") as HTMLAnchorElement;
|
||||
const cancel = document.getElementById("deadline-new-cancel") as HTMLAnchorElement;
|
||||
back.href = `/projects/${preselectedProjectID}/deadlines`;
|
||||
cancel.href = `/projects/${preselectedProjectID}/deadlines`;
|
||||
}
|
||||
preselectedProjectIDLocal = preselectedProjectID;
|
||||
}
|
||||
|
||||
// t-paliad-165 follow-up — drive the collapsed/expanded view of the Typ
|
||||
// picker. The two modes are mutually exclusive:
|
||||
//
|
||||
@@ -140,8 +345,14 @@ function refreshRuleView(): void {
|
||||
|
||||
const pickerMatchesDefault =
|
||||
expected !== null && picked.length === 1 && picked[0] === expected;
|
||||
// t-paliad-251 — when the rule was auto-derived from a user-picked
|
||||
// type (Typ→Regel direction), the collapsed "vorgegeben durch Regel"
|
||||
// copy reads backwards. Show the picker explicitly + surface the
|
||||
// Auto badge on the Rule field instead.
|
||||
const ruleWasAutoDerivedFromType =
|
||||
lastAutoFilledRuleID !== null && ruleID === lastAutoFilledRuleID;
|
||||
const wantsCollapsed =
|
||||
!expandedOverride && ruleID !== "" && expected !== null && pickerMatchesDefault;
|
||||
!expandedOverride && ruleID !== "" && expected !== null && pickerMatchesDefault && !ruleWasAutoDerivedFromType;
|
||||
|
||||
if (wantsCollapsed) {
|
||||
const et = eventTypesByID.get(expected!);
|
||||
@@ -164,6 +375,58 @@ function refreshRuleView(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// refreshRuleAutoBadgeAndWarning surfaces the Auto badge whenever the
|
||||
// Rule was derived from the Typ (i.e. lastAutoFilledRuleID is currently
|
||||
// selected) AND the warning whenever the user has manually picked a
|
||||
// non-Auto rule that contradicts the Type's derived rule. Both end up
|
||||
// inert when there's no Type chosen.
|
||||
function refreshRuleAutoBadgeAndWarning(): void {
|
||||
const autoEl = document.getElementById("deadline-rule-auto-hint");
|
||||
const autoTextEl = document.getElementById("deadline-rule-auto-hint-text");
|
||||
const warnEl = document.getElementById("deadline-rule-override-warn");
|
||||
if (!autoEl || !autoTextEl || !warnEl) return;
|
||||
|
||||
const ruleSel = document.getElementById("deadline-rule") as HTMLSelectElement | null;
|
||||
if (!ruleSel) return;
|
||||
|
||||
const picked = eventTypePicker?.getIDs() ?? [];
|
||||
if (picked.length !== 1) {
|
||||
autoEl.style.display = "none";
|
||||
warnEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
const projectID = (document.getElementById("deadline-project") as HTMLSelectElement | null)?.value || "";
|
||||
const derived = resolveAutoRuleForType(picked[0], projectID);
|
||||
const currentRuleID = ruleSel.value || "";
|
||||
|
||||
if (currentRuleID && currentRuleID === lastAutoFilledRuleID) {
|
||||
// The current rule was auto-derived (and the user hasn't touched it).
|
||||
autoEl.style.display = "";
|
||||
autoTextEl.textContent = derived ? ` — ${ruleLabel(derived)}` : "";
|
||||
warnEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
autoEl.style.display = "none";
|
||||
|
||||
// Override warning: derived rule exists AND user has picked a
|
||||
// different non-empty rule. The copy names BOTH so the user knows
|
||||
// exactly what's happening — and which one will be applied.
|
||||
if (derived && currentRuleID && currentRuleID !== derived.id) {
|
||||
const current = rulesByID.get(currentRuleID);
|
||||
if (current) {
|
||||
const tmpl = t("deadlines.field.rule.override_warn");
|
||||
const msg = tmpl
|
||||
.replace("{derived}", ruleLabel(derived))
|
||||
.replace("{selected}", ruleLabel(current));
|
||||
warnEl.textContent = msg;
|
||||
warnEl.style.display = "";
|
||||
return;
|
||||
}
|
||||
}
|
||||
warnEl.style.display = "none";
|
||||
}
|
||||
|
||||
// applyRuleAutoFill replaces the picker silently when it still reflects
|
||||
// the previous rule's suggestion (or is empty); leaves a manually-edited
|
||||
// picker alone. Called whenever the Regel select changes.
|
||||
@@ -200,13 +463,97 @@ function applyRuleAutoFill(): void {
|
||||
refreshRuleView();
|
||||
}
|
||||
|
||||
function initBackLinks() {
|
||||
if (preselectedProjectID) {
|
||||
const back = document.getElementById("deadline-new-back") as HTMLAnchorElement;
|
||||
const cancel = document.getElementById("deadline-new-cancel") as HTMLAnchorElement;
|
||||
back.href = `/projects/${preselectedProjectID}/deadlines`;
|
||||
cancel.href = `/projects/${preselectedProjectID}/deadlines`;
|
||||
// applyTypeAutoFillRule is the inverse direction (t-paliad-251 Part 2):
|
||||
// when the user picks a single Typ chip, derive the canonical Rule and
|
||||
// inject it into the Regel select. Like applyRuleAutoFill, it leaves
|
||||
// manual rule picks alone — only replaces when the current rule is the
|
||||
// previous auto-fill (sticky-replace pattern).
|
||||
function applyTypeAutoFillRule(): void {
|
||||
const ruleSel = document.getElementById("deadline-rule") as HTMLSelectElement | null;
|
||||
if (!ruleSel) return;
|
||||
|
||||
const picked = eventTypePicker?.getIDs() ?? [];
|
||||
const projectID = (document.getElementById("deadline-project") as HTMLSelectElement | null)?.value || "";
|
||||
|
||||
if (picked.length !== 1) {
|
||||
// 0 or 2+ Typ chips → no canonical rule to derive. Clear the
|
||||
// sticky auto-fill so a stale Auto suggestion doesn't linger.
|
||||
if (lastAutoFilledRuleID && ruleSel.value === lastAutoFilledRuleID) {
|
||||
ruleSel.value = "";
|
||||
lastAutoFilledRuleID = null;
|
||||
// Mirror to the Regel→Typ path so its mismatch warning recomputes.
|
||||
applyRuleAutoFill();
|
||||
}
|
||||
refreshRuleAutoBadgeAndWarning();
|
||||
return;
|
||||
}
|
||||
|
||||
const derived = resolveAutoRuleForType(picked[0], projectID);
|
||||
const currentRuleID = ruleSel.value || "";
|
||||
const ruleStillReflectsLastSuggestion =
|
||||
lastAutoFilledRuleID !== null && currentRuleID === lastAutoFilledRuleID;
|
||||
const ruleIsEmpty = currentRuleID === "";
|
||||
|
||||
if (derived) {
|
||||
if (ruleIsEmpty || ruleStillReflectsLastSuggestion) {
|
||||
ruleSel.value = derived.id;
|
||||
lastAutoFilledRuleID = derived.id;
|
||||
// Mirror to the Regel→Typ direction — the new rule's collapsed
|
||||
// view + mismatch state needs to recompute now that we changed
|
||||
// the selection programmatically.
|
||||
applyRuleAutoFill();
|
||||
}
|
||||
} else if (ruleStillReflectsLastSuggestion) {
|
||||
// No derived rule for the new type — drop the stale auto-fill.
|
||||
ruleSel.value = "";
|
||||
lastAutoFilledRuleID = null;
|
||||
applyRuleAutoFill();
|
||||
}
|
||||
refreshRuleAutoBadgeAndWarning();
|
||||
}
|
||||
|
||||
// computeDefaultTitle — t-paliad-251 Part 4. Recipe (documented also in
|
||||
// the commit message so future title templates can mirror it):
|
||||
//
|
||||
// priority order picks the head of the title:
|
||||
// 1. event_type label (when exactly one Typ chip is set)
|
||||
// 2. rule name (when a Rule is set — uses ruleLabel = "code — name")
|
||||
// 3. proceeding type name (when project carries a proceeding_type_id)
|
||||
// 4. fallback: t("deadlines.field.title.default_fallback")
|
||||
//
|
||||
// suffix: " — <project-reference>" when the project has a reference
|
||||
// string and the title doesn't already contain it.
|
||||
//
|
||||
// Returns "" only when even the fallback fails (i18n unavailable) —
|
||||
// callers handle that by leaving the field untouched.
|
||||
function computeDefaultTitle(): string {
|
||||
const projectID = (document.getElementById("deadline-project") as HTMLSelectElement | null)?.value || "";
|
||||
const project = projectID ? projectsByID.get(projectID) : undefined;
|
||||
const ruleID = (document.getElementById("deadline-rule") as HTMLSelectElement | null)?.value || "";
|
||||
const rule = ruleID ? rulesByID.get(ruleID) : undefined;
|
||||
const picked = eventTypePicker?.getIDs() ?? [];
|
||||
|
||||
let head = "";
|
||||
if (picked.length === 1) {
|
||||
const et = eventTypesByID.get(picked[0]);
|
||||
if (et) head = eventTypeLabel(et);
|
||||
}
|
||||
if (!head && rule) {
|
||||
head = ruleLabel(rule);
|
||||
}
|
||||
if (!head && project?.proceeding_type_id) {
|
||||
const pt = proceedingTypesByID.get(project.proceeding_type_id);
|
||||
if (pt) head = proceedingLabel(pt);
|
||||
}
|
||||
if (!head) {
|
||||
head = t("deadlines.field.title.default_fallback");
|
||||
}
|
||||
|
||||
const ref = project?.reference?.trim() || "";
|
||||
if (ref && !head.includes(ref)) {
|
||||
return `${head} — ${ref}`;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
async function submitForm(e: Event) {
|
||||
@@ -252,8 +599,8 @@ async function submitForm(e: Event) {
|
||||
return;
|
||||
}
|
||||
const created = await resp.json();
|
||||
if (preselectedProjectID) {
|
||||
window.location.href = `/projects/${preselectedProjectID}/deadlines`;
|
||||
if (preselectedProjectIDLocal) {
|
||||
window.location.href = `/projects/${preselectedProjectIDLocal}/deadlines`;
|
||||
} else {
|
||||
window.location.href = `/deadlines/${created.id}`;
|
||||
}
|
||||
@@ -343,12 +690,33 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Default due to today
|
||||
const dueInput = document.getElementById("deadline-due") as HTMLInputElement;
|
||||
if (!dueInput.value) dueInput.value = new Date().toISOString().split("T")[0];
|
||||
await Promise.all([loadProjects(), loadRules(), loadMe()]);
|
||||
|
||||
// Wire the sort dropdown to read its initial value from localStorage and
|
||||
// persist user picks back.
|
||||
const sortSel = document.getElementById("deadline-rule-sort") as HTMLSelectElement | null;
|
||||
if (sortSel) {
|
||||
sortSel.value = readRuleSort();
|
||||
sortSel.addEventListener("change", () => {
|
||||
writeRuleSort(sortSel.value as RuleSort);
|
||||
renderRuleSelect();
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([loadProjects(), loadProceedingTypes(), loadRules(), loadMe()]);
|
||||
// After both rules + proceeding types are in, re-render with the
|
||||
// chosen sort so groups carry proper labels.
|
||||
renderRuleSelect();
|
||||
|
||||
const pickerHost = document.getElementById("deadline-event-types");
|
||||
if (pickerHost) {
|
||||
eventTypePicker = attachEventTypePicker(pickerHost, {
|
||||
currentUserAdmin,
|
||||
onChange: () => refreshRuleView(),
|
||||
onChange: () => {
|
||||
// Both directions trigger off picker change: refresh the
|
||||
// Regel→Typ collapsed/expanded state AND the Typ→Regel auto-fill.
|
||||
refreshRuleView();
|
||||
applyTypeAutoFillRule();
|
||||
},
|
||||
});
|
||||
}
|
||||
// t-paliad-165 follow-up — preload event_types so the collapsed
|
||||
@@ -358,13 +726,18 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
.then((types) => {
|
||||
eventTypesByID = new Map(types.map((et) => [et.id, et]));
|
||||
refreshRuleView();
|
||||
refreshRuleAutoBadgeAndWarning();
|
||||
})
|
||||
.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.
|
||||
// manually edited away from the previous rule's suggestion. ALSO
|
||||
// resets the Typ→Regel auto-fill marker since the user just made a
|
||||
// manual rule pick.
|
||||
document.getElementById("deadline-rule")?.addEventListener("change", () => {
|
||||
lastAutoFilledRuleID = null;
|
||||
applyRuleAutoFill();
|
||||
refreshRuleAutoBadgeAndWarning();
|
||||
});
|
||||
// "Anderen Typ wählen" — sticky expanded mode so the picker stays
|
||||
// visible even when the chip still matches the rule's default.
|
||||
@@ -381,6 +754,20 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Wire approval-hint refresh: on first render + on project change.
|
||||
void refreshApprovalHint();
|
||||
document.getElementById("deadline-project")?.addEventListener("change", () => {
|
||||
// Project change can shift which rule the Type maps to (via the
|
||||
// project's proceeding_type_id), so re-run the auto-fill.
|
||||
void refreshApprovalHint();
|
||||
applyTypeAutoFillRule();
|
||||
});
|
||||
|
||||
// t-paliad-251 Part 4 — Standardtitel button replaces the title with
|
||||
// a derived default. No destructive confirmation because the user
|
||||
// invoked it explicitly.
|
||||
document.getElementById("deadline-title-default-btn")?.addEventListener("click", () => {
|
||||
const titleInput = document.getElementById("deadline-title") as HTMLInputElement | null;
|
||||
if (!titleInput) return;
|
||||
const derived = computeDefaultTitle();
|
||||
if (derived) titleInput.value = derived;
|
||||
titleInput.focus();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -686,6 +686,33 @@ export function openBrowseEventTypesModal(
|
||||
return new Promise<string[] | null>((resolve) => {
|
||||
let selected = new Set<string>(opts.initialIDs);
|
||||
let searchQuery = "";
|
||||
// t-paliad-251 — court-type filter chips. `null` = "Alle" (any
|
||||
// jurisdiction). Any non-null value matches event_types.jurisdiction;
|
||||
// "any" is mapped to NULL/missing rows via jurisdictionMatches().
|
||||
let activeJurisdiction: string | null = null;
|
||||
|
||||
// Surface every jurisdiction present in the data — "any" stays bucketed
|
||||
// separately so users still have a "show generic-only" chip. EPA is
|
||||
// canonicalised to EPO in event_types (see mig 074); the chip label
|
||||
// shows EPA to match the legal vocabulary the lawyers use.
|
||||
const jurisdictionsPresent = new Set<string>();
|
||||
for (const et of opts.types) {
|
||||
const j = (et.jurisdiction ?? "").trim();
|
||||
if (j) jurisdictionsPresent.add(j);
|
||||
}
|
||||
const JURISDICTION_ORDER = ["UPC", "EPO", "DPMA", "DE", "any"];
|
||||
const chipJurisdictions = JURISDICTION_ORDER.filter((j) => jurisdictionsPresent.has(j));
|
||||
// Any jurisdiction in the data that isn't in our ordered list lands at
|
||||
// the end so the chip row never silently drops a court flavour.
|
||||
for (const j of jurisdictionsPresent) {
|
||||
if (!chipJurisdictions.includes(j)) chipJurisdictions.push(j);
|
||||
}
|
||||
|
||||
function chipLabel(j: string): string {
|
||||
if (j === "EPO") return "EPA";
|
||||
if (j === "any") return t("event_types.browse.jurisdiction.none");
|
||||
return j;
|
||||
}
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "modal-overlay event-type-browse-overlay";
|
||||
@@ -694,6 +721,15 @@ export function openBrowseEventTypesModal(
|
||||
<div class="event-type-browse-header">
|
||||
<h2 id="event-type-browse-title">${esc(t("event_types.browse.title"))}</h2>
|
||||
<input type="text" class="event-type-browse-search" data-role="search" placeholder="${esc(t("event_types.browse.search"))}" autocomplete="off" />
|
||||
<div class="event-type-browse-chips" data-role="chips" role="group" aria-label="${esc(t("event_types.browse.jurisdiction.filter_label"))}">
|
||||
<button type="button" class="event-type-browse-chip event-type-browse-chip--active" data-jurisdiction="" data-role="chip-all">${esc(t("event_types.browse.jurisdiction.all"))}</button>
|
||||
${chipJurisdictions
|
||||
.map(
|
||||
(j) =>
|
||||
`<button type="button" class="event-type-browse-chip" data-jurisdiction="${esc(j)}">${esc(chipLabel(j))}</button>`,
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="event-type-browse-list" data-role="list" tabindex="-1"></div>
|
||||
<div class="event-type-browse-actions">
|
||||
@@ -711,6 +747,7 @@ export function openBrowseEventTypesModal(
|
||||
const countEl = overlay.querySelector<HTMLElement>("[data-role=count]")!;
|
||||
const cancelBtn = overlay.querySelector<HTMLButtonElement>("[data-role=cancel]")!;
|
||||
const applyBtn = overlay.querySelector<HTMLButtonElement>("[data-role=apply]")!;
|
||||
const chipButtons = overlay.querySelectorAll<HTMLButtonElement>(".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();
|
||||
|
||||
@@ -879,6 +879,14 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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…",
|
||||
|
||||
@@ -41,6 +41,19 @@ export function renderDeadlinesDetail(): string {
|
||||
<div className="entity-detail-title-col">
|
||||
<h1 id="deadline-title-display" />
|
||||
<input type="text" id="deadline-title-edit" className="entity-title-input" style="display:none" />
|
||||
{/* 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. */}
|
||||
<button
|
||||
type="button"
|
||||
id="deadline-title-default-btn"
|
||||
className="btn-link-action"
|
||||
style="display:none"
|
||||
data-i18n="deadlines.field.title.default_btn"
|
||||
>
|
||||
Standardtitel
|
||||
</button>
|
||||
<div className="entity-detail-meta">
|
||||
<span id="deadline-due-chip" className="frist-due-chip" />
|
||||
<span id="deadline-status-chip" className="entity-status-chip" />
|
||||
|
||||
@@ -45,7 +45,22 @@ export function renderDeadlinesNew(): string {
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="deadline-title" data-i18n="deadlines.field.title">Titel</label>
|
||||
<div className="form-field-label-row">
|
||||
<label htmlFor="deadline-title" data-i18n="deadlines.field.title">Titel</label>
|
||||
{/* 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. */}
|
||||
<button
|
||||
type="button"
|
||||
id="deadline-title-default-btn"
|
||||
className="btn-link-action"
|
||||
data-i18n="deadlines.field.title.default_btn"
|
||||
>
|
||||
Standardtitel
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="deadline-title"
|
||||
@@ -105,10 +120,44 @@ export function renderDeadlinesNew(): string {
|
||||
picker so the parent/child relationship reads at a
|
||||
glance. Due date is its own row below. */}
|
||||
<div className="form-field">
|
||||
<label htmlFor="deadline-rule" data-i18n="deadlines.field.rule">Regel (optional)</label>
|
||||
<div className="form-field-label-row">
|
||||
<label htmlFor="deadline-rule" data-i18n="deadlines.field.rule">Regel (optional)</label>
|
||||
{/* 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. */}
|
||||
<select id="deadline-rule-sort" className="rule-sort-select" aria-label="Sortierung">
|
||||
<option value="by_proceeding" data-i18n="deadlines.field.rule.sort.by_proceeding">Nach Verfahrensablauf</option>
|
||||
<option value="by_court" data-i18n="deadlines.field.rule.sort.by_court" selected>Nach Gerichtsart</option>
|
||||
<option value="alpha" data-i18n="deadlines.field.rule.sort.alpha">Alphabetisch</option>
|
||||
</select>
|
||||
</div>
|
||||
<select id="deadline-rule">
|
||||
<option value="" data-i18n="deadlines.field.rule.none">Keine Regel</option>
|
||||
</select>
|
||||
{/* 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. */}
|
||||
<p
|
||||
className="form-hint form-hint--auto"
|
||||
id="deadline-rule-auto-hint"
|
||||
style="display:none"
|
||||
>
|
||||
<span
|
||||
className="form-hint-badge"
|
||||
data-i18n="deadlines.field.rule.auto_badge"
|
||||
>Auto</span>
|
||||
<span id="deadline-rule-auto-hint-text" />
|
||||
</p>
|
||||
{/* 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. */}
|
||||
<p
|
||||
className="form-hint form-hint--warning"
|
||||
id="deadline-rule-override-warn"
|
||||
style="display:none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user