Compare commits
1 Commits
mai/cronus
...
mai/hermes
| Author | SHA1 | Date | |
|---|---|---|---|
| 3097df3918 |
@@ -312,6 +312,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.view.timeline": "Zeitstrahl",
|
||||
"deadlines.view.columns": "Spalten",
|
||||
"deadlines.notes.show": "Hinweise anzeigen",
|
||||
"deadlines.durations.show": "Dauern anzeigen",
|
||||
"deadlines.col.ours": "Unsere Seite",
|
||||
"deadlines.col.court": "Gericht",
|
||||
"deadlines.col.opponent": "Gegnerseite",
|
||||
@@ -462,6 +463,10 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.side.from_project": "Aus Akte:",
|
||||
"deadlines.side.override": "Andere Seite wählen",
|
||||
"deadlines.side.hint": "Wählen Sie eine Seite, um die Spalten zu fokussieren.",
|
||||
"deadlines.appellant.label": "Berufung durch:",
|
||||
"deadlines.appellant.claimant": "Klägerseite",
|
||||
"deadlines.appellant.defendant": "Beklagtenseite",
|
||||
"deadlines.appellant.none": "—",
|
||||
"deadlines.event.composite.label": "Zusammengesetzt:",
|
||||
"deadlines.event.unit.days.one": "Tag",
|
||||
"deadlines.event.unit.days.many": "Tage",
|
||||
@@ -3406,6 +3411,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.view.timeline": "Timeline",
|
||||
"deadlines.view.columns": "Columns",
|
||||
"deadlines.notes.show": "Show details",
|
||||
"deadlines.durations.show": "Show durations",
|
||||
"deadlines.col.ours": "Client Side",
|
||||
"deadlines.col.court": "Court",
|
||||
"deadlines.col.opponent": "Opponent Side",
|
||||
@@ -3563,6 +3569,10 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.side.from_project": "From case:",
|
||||
"deadlines.side.override": "Choose other side",
|
||||
"deadlines.side.hint": "Pick a side to focus the columns.",
|
||||
"deadlines.appellant.label": "Appeal filed by:",
|
||||
"deadlines.appellant.claimant": "Claimant",
|
||||
"deadlines.appellant.defendant": "Defendant",
|
||||
"deadlines.appellant.none": "—",
|
||||
"deadlines.event.composite.label": "Composite:",
|
||||
"deadlines.event.unit.days.one": "day",
|
||||
"deadlines.event.unit.days.many": "days",
|
||||
|
||||
@@ -32,20 +32,18 @@ import {
|
||||
let selectedType = "";
|
||||
let lastResponse: DeadlineResponse | null = null;
|
||||
|
||||
// Perspective state. URL-driven so the view is shareable + survives
|
||||
// reload:
|
||||
// ?side=claimant|defendant — swaps which column owns the user's
|
||||
// side (proactive vs reactive label).
|
||||
// Default null = claimant-on-the-left.
|
||||
//
|
||||
// t-paliad-301 / m/paliad#132 collapsed the duplicate ?side= +
|
||||
// ?appellant= selectors into the single proactive-side picker above.
|
||||
// For role-swap proceedings (Appeal / EPA Opposition / DE Revision /
|
||||
// DPMA Appeal) the picker's labels swap to per-proceeding role
|
||||
// strings (Berufungskläger / Berufungsbeklagter, …) via ROLE_LABELS
|
||||
// below — but the underlying claimant/defendant value the engine
|
||||
// consumes is unchanged.
|
||||
// Perspective state (t-paliad-250 / m/paliad#81). URL-driven so the
|
||||
// view is shareable and survives reload:
|
||||
// ?side=claimant|defendant → swaps which column owns the user's
|
||||
// side (proactive vs reactive label).
|
||||
// Default null = claimant-on-the-left.
|
||||
// ?appellant=claimant|defendant → collapses party=both rows into the
|
||||
// appellant's column (no mirror).
|
||||
// Only meaningful for role-swap
|
||||
// proceedings (Appeal etc.). Default
|
||||
// null = legacy mirror behaviour.
|
||||
let currentSide: Side = null;
|
||||
let currentAppellant: Side = null;
|
||||
|
||||
// Project-driven auto-fill state (t-paliad-279 / m/paliad#111). When the
|
||||
// page is opened with ?project=<id> and that project has our_side set,
|
||||
@@ -54,13 +52,17 @@ let currentSide: Side = null;
|
||||
// link, which clears this flag (radio cluster takes over again).
|
||||
let sidePrefilledFromProject = false;
|
||||
|
||||
// Role-swap proceedings — the side picker doubles as the appellant
|
||||
// axis. After t-paliad-301 collapsed the duplicate selectors, the
|
||||
// engine reads "appellant" from the single side value for these
|
||||
// proceedings (so a row with primary_party=both renders only in the
|
||||
// chosen side's column). For first-instance proceedings (Inf, Rev,
|
||||
// …) the side picker still narrows columns but doesn't collapse
|
||||
// the "both" rows.
|
||||
// Proceedings where one party initiates and "both" rows are role-swap
|
||||
// (i.e. either party files depending on who acted at the lower
|
||||
// instance). For these proceedings the appellant selector is meaningful
|
||||
// — when set, "both" rows collapse to a single row in the appellant's
|
||||
// column. For first-instance proceedings (Inf, Rev, …) the selector is
|
||||
// hidden because there's no appellant axis.
|
||||
//
|
||||
// Today: every upc.apl.* family member plus dpma.appeal.* and
|
||||
// de.inf.olg / de.inf.bgh / de.null.bgh (DE Berufung / Revision).
|
||||
// Conservative — false negatives just hide a control; false positives
|
||||
// would show an irrelevant control.
|
||||
const APPELLANT_AXIS_PROCEEDINGS = new Set([
|
||||
"upc.apl.unified",
|
||||
"de.inf.olg",
|
||||
@@ -71,44 +73,6 @@ const APPELLANT_AXIS_PROCEEDINGS = new Set([
|
||||
"epa.opp.boa",
|
||||
]);
|
||||
|
||||
// Per-proceeding role labels (t-paliad-301 / m/paliad#132 Bug A).
|
||||
// Mirrors paliad.proceeding_types.role_*_label_* — the canonical
|
||||
// definition lives in the DB; this map is the frontend's view of
|
||||
// it. Proceedings absent from the map fall back to the generic
|
||||
// "deadlines.side.claimant" / "deadlines.side.defendant" i18n keys.
|
||||
//
|
||||
// Keep in sync with mig 137's backfill. Adding a row here without a
|
||||
// matching DB row is fine (the DB col is NULL → still falls back to
|
||||
// default; UI shows the override). Adding to the DB without here
|
||||
// means the UI uses defaults — harmless but inconsistent.
|
||||
type RoleLabels = { proDE: string; reDE: string; proEN: string; reEN: string };
|
||||
const ROLE_LABELS: Record<string, RoleLabels> = {
|
||||
"upc.apl.unified": {
|
||||
proDE: "Berufungskläger",
|
||||
reDE: "Berufungsbeklagter",
|
||||
proEN: "Appellant",
|
||||
reEN: "Appellee",
|
||||
},
|
||||
"upc.rev.cfi": {
|
||||
proDE: "Antragsteller (Nichtigkeit)",
|
||||
reDE: "Antragsgegner (Nichtigkeit)",
|
||||
proEN: "Revocation claimant",
|
||||
reEN: "Revocation defendant",
|
||||
},
|
||||
"epa.opp.opd": {
|
||||
proDE: "Einsprechende(r)",
|
||||
reDE: "Patentinhaber(in)",
|
||||
proEN: "Opponent",
|
||||
reEN: "Patentee",
|
||||
},
|
||||
"epa.opp.boa": {
|
||||
proDE: "Einsprechende(r)",
|
||||
reDE: "Patentinhaber(in)",
|
||||
proEN: "Opponent",
|
||||
reEN: "Patentee",
|
||||
},
|
||||
};
|
||||
|
||||
// Slice B1 (m/paliad#124 §18.1) — Berufung unification.
|
||||
// Proceedings that surface the appeal-target chip group. Currently
|
||||
// only the unified upc.apl proceeding; future variants (e.g. de.apl)
|
||||
@@ -141,6 +105,11 @@ function readSideFromURL(): Side {
|
||||
return raw === "claimant" || raw === "defendant" ? raw : null;
|
||||
}
|
||||
|
||||
function readAppellantFromURL(): Side {
|
||||
const raw = new URLSearchParams(window.location.search).get("appellant");
|
||||
return raw === "claimant" || raw === "defendant" ? raw : null;
|
||||
}
|
||||
|
||||
function writeSideToURL(s: Side) {
|
||||
const url = new URL(window.location.href);
|
||||
if (s === null) url.searchParams.delete("side");
|
||||
@@ -148,31 +117,11 @@ function writeSideToURL(s: Side) {
|
||||
window.history.replaceState(null, "", url.pathname + (url.search ? url.search : "") + url.hash);
|
||||
}
|
||||
|
||||
// t-paliad-301 / m/paliad#132: applies ROLE_LABELS to the side-row
|
||||
// radio labels for the currently selected proceeding. Proceedings
|
||||
// without an entry fall back to the existing
|
||||
// "deadlines.side.claimant" / "deadlines.side.defendant" i18n keys.
|
||||
function applyRoleLabels(proceedingType: string) {
|
||||
const lang = getLang() === "en" ? "en" : "de";
|
||||
const claimantSpan = document.querySelector<HTMLElement>(
|
||||
"input[type=radio][name=side][value=claimant] + span"
|
||||
);
|
||||
const defendantSpan = document.querySelector<HTMLElement>(
|
||||
"input[type=radio][name=side][value=defendant] + span"
|
||||
);
|
||||
if (!claimantSpan || !defendantSpan) return;
|
||||
|
||||
const labels = ROLE_LABELS[proceedingType];
|
||||
if (labels) {
|
||||
claimantSpan.textContent = lang === "en" ? labels.proEN : labels.proDE;
|
||||
defendantSpan.textContent = lang === "en" ? labels.reEN : labels.reDE;
|
||||
} else {
|
||||
// Default — let i18n drive via data-i18n attribute. Reset to the
|
||||
// canonical i18n value so a previous override doesn't stick when
|
||||
// switching from upc.apl.unified back to upc.inf.cfi.
|
||||
claimantSpan.textContent = t("deadlines.side.claimant");
|
||||
defendantSpan.textContent = t("deadlines.side.defendant");
|
||||
}
|
||||
function writeAppellantToURL(a: Side) {
|
||||
const url = new URL(window.location.href);
|
||||
if (a === null) url.searchParams.delete("appellant");
|
||||
else url.searchParams.set("appellant", a);
|
||||
window.history.replaceState(null, "", url.pathname + (url.search ? url.search : "") + url.hash);
|
||||
}
|
||||
|
||||
// Slice B1 — appeal-target URL state. Empty string = no target picked
|
||||
@@ -276,6 +225,21 @@ function writeNotesPref(on: boolean): void {
|
||||
}
|
||||
let showNotes = readNotesPref();
|
||||
|
||||
// Durations toggle (m/paliad#133, t-paliad-302) — when off (default),
|
||||
// the per-rule duration label ("2 Mo. nach") only shows on hover via
|
||||
// the date span's `title` attribute. When on, the label renders inline
|
||||
// in the timeline meta row of every event card. Persisted in
|
||||
// localStorage under its own key so the preference is independent of
|
||||
// "Hinweise anzeigen".
|
||||
const DURATIONS_PREF_KEY = "paliad.verfahrensablauf.durations-show";
|
||||
function readDurationsPref(): boolean {
|
||||
try { return localStorage.getItem(DURATIONS_PREF_KEY) === "1"; } catch { return false; }
|
||||
}
|
||||
function writeDurationsPref(on: boolean): void {
|
||||
try { localStorage.setItem(DURATIONS_PREF_KEY, on ? "1" : "0"); } catch { /* no-op */ }
|
||||
}
|
||||
let showDurations = readDurationsPref();
|
||||
|
||||
// Jurisdiction display prefix for the proceeding-summary chip + the
|
||||
// trigger-event placeholder. Same forum slugs the .proceeding-group
|
||||
// `data-forum` attribute carries in verfahrensablauf.tsx /
|
||||
@@ -482,15 +446,11 @@ function renderResults(data: DeadlineResponse) {
|
||||
? renderColumnsBody(data, {
|
||||
editable: true,
|
||||
showNotes,
|
||||
showDurations,
|
||||
side: currentSide,
|
||||
// t-paliad-301: the appellant axis collapses into the single
|
||||
// side picker. For role-swap proceedings, currentSide IS the
|
||||
// appellant pick (so a row with primary_party=both renders only
|
||||
// in the picked side's column). For non-role-swap proceedings,
|
||||
// the appellant axis is irrelevant — pass null.
|
||||
appellant: hasAppellantAxis(selectedType) ? currentSide : null,
|
||||
appellant: hasAppellantAxis(selectedType) ? currentAppellant : null,
|
||||
})
|
||||
: renderTimelineBody(data, { showParty: true, editable: true, showNotes });
|
||||
: renderTimelineBody(data, { showParty: true, editable: true, showNotes, showDurations });
|
||||
|
||||
container.innerHTML = headerHtml + noteHtml + bodyHtml;
|
||||
if (printBtn) printBtn.style.display = "block";
|
||||
@@ -557,8 +517,8 @@ function selectProceeding(btn: HTMLButtonElement) {
|
||||
|
||||
void populateCourtPicker("court-picker-row", "court-picker", selectedType);
|
||||
syncFlagRows();
|
||||
syncAppellantRowVisibility();
|
||||
syncAppealTargetRowVisibility();
|
||||
applyRoleLabels(selectedType);
|
||||
|
||||
setProceedingPickerCollapsed(true, proceedingDisplayName(btn));
|
||||
|
||||
@@ -566,6 +526,23 @@ function selectProceeding(btn: HTMLButtonElement) {
|
||||
scheduleCalc(0);
|
||||
}
|
||||
|
||||
// syncAppellantRowVisibility hides the appellant selector for
|
||||
// proceedings that have no appellant axis (first-instance Inf, Rev,
|
||||
// …). Clears the in-memory state and the URL param when hidden so a
|
||||
// shared link with ?appellant= doesn't leak into an unrelated
|
||||
// proceeding's render.
|
||||
function syncAppellantRowVisibility() {
|
||||
const row = document.getElementById("appellant-row");
|
||||
if (!row) return;
|
||||
const visible = hasAppellantAxis(selectedType);
|
||||
row.style.display = visible ? "" : "none";
|
||||
if (!visible && currentAppellant !== null) {
|
||||
currentAppellant = null;
|
||||
writeAppellantToURL(null);
|
||||
syncRadioGroup("appellant", "");
|
||||
}
|
||||
}
|
||||
|
||||
// Slice B1 (m/paliad#124 §18.1) — Berufung unification.
|
||||
// syncAppealTargetRowVisibility shows the appeal-target chip group
|
||||
// when the unified upc.apl Berufung tile is selected, hides it
|
||||
@@ -766,8 +743,10 @@ function initViewToggle() {
|
||||
// projection of the last response, no backend involved.
|
||||
function initPerspectiveControls() {
|
||||
currentSide = readSideFromURL();
|
||||
currentAppellant = readAppellantFromURL();
|
||||
currentAppealTarget = readAppealTargetFromURL();
|
||||
syncRadioGroup("side", currentSide ?? "");
|
||||
syncRadioGroup("appellant", currentAppellant ?? "");
|
||||
syncRadioGroup("appeal-target", currentAppealTarget || "endentscheidung");
|
||||
syncSideHintVisibility();
|
||||
|
||||
@@ -782,6 +761,16 @@ function initPerspectiveControls() {
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll<HTMLInputElement>("input[type=radio][name=appellant]").forEach((input) => {
|
||||
input.addEventListener("change", () => {
|
||||
if (!input.checked) return;
|
||||
const v = input.value;
|
||||
currentAppellant = (v === "claimant" || v === "defendant") ? v : null;
|
||||
writeAppellantToURL(currentAppellant);
|
||||
if (lastResponse) renderResults(lastResponse);
|
||||
});
|
||||
});
|
||||
|
||||
// Slice B1 (m/paliad#124 §18.1) — appeal-target chip handler.
|
||||
// Each chip change re-fetches with the new target slug so the
|
||||
// timeline re-renders against the matching rule subset.
|
||||
@@ -868,6 +857,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Durations toggle (m/paliad#133, t-paliad-302) — sibling of the
|
||||
// notes toggle. Hover-only labels (default) become inline labels when
|
||||
// the user opts in.
|
||||
const durationsShowCb = document.getElementById("verfahrensablauf-durations-show") as HTMLInputElement | null;
|
||||
if (durationsShowCb) {
|
||||
durationsShowCb.checked = showDurations;
|
||||
durationsShowCb.addEventListener("change", () => {
|
||||
showDurations = durationsShowCb.checked;
|
||||
writeDurationsPref(showDurations);
|
||||
if (lastResponse) renderResults(lastResponse);
|
||||
});
|
||||
}
|
||||
|
||||
// t-paliad-290 — show-hidden toggle. Hydrate from URL, wire change
|
||||
// to URL + recalc (the backend reshapes the response — we can't just
|
||||
// re-render lastResponse since the hidden rows aren't in it when the
|
||||
|
||||
@@ -95,6 +95,36 @@ export interface CalculatedDeadline {
|
||||
parentRuleCode?: string;
|
||||
parentRuleName?: string;
|
||||
parentRuleNameEN?: string;
|
||||
// durationValue / durationUnit / timing surface the rule's arithmetic
|
||||
// so the timeline card can show "2 Mo. nach" on hover (and inline when
|
||||
// the "Dauern anzeigen" toggle is on). Zero-duration rules (root
|
||||
// event, court-set) carry durationValue=0 and the renderer suppresses
|
||||
// the affordance — those don't have an explainable interval.
|
||||
// (m/paliad#133, t-paliad-302)
|
||||
durationValue?: number;
|
||||
durationUnit?: string;
|
||||
timing?: string;
|
||||
}
|
||||
|
||||
// formatDurationLabel renders the per-rule duration ("2 Mo. nach") for
|
||||
// the Verfahrensablauf card affordance (m/paliad#133, t-paliad-302).
|
||||
// Returns empty string for rules without a usable duration so the
|
||||
// caller can skip the tooltip / inline span entirely.
|
||||
//
|
||||
// Pluralisation key naming mirrors the Fristenrechner event-mode
|
||||
// renderer (deadlines.event.unit.<unit>.{one,many}) — the unit and
|
||||
// timing translations already exist for /tools/fristenrechner's
|
||||
// "Was kommt nach…" mode and are reused here as the single
|
||||
// source of truth.
|
||||
export function formatDurationLabel(dl: CalculatedDeadline): string {
|
||||
const value = dl.durationValue ?? 0;
|
||||
const unit = dl.durationUnit || "";
|
||||
if (value <= 0 || !unit) return "";
|
||||
const unitKey = `deadlines.event.unit.${unit}` + (value === 1 ? ".one" : ".many");
|
||||
const unitStr = tDyn(unitKey);
|
||||
const timing = dl.timing || "";
|
||||
const timingStr = timing ? tDyn(`deadlines.event.timing.${timing}`) : "";
|
||||
return timingStr ? `${value} ${unitStr} ${timingStr}` : `${value} ${unitStr}`;
|
||||
}
|
||||
|
||||
// priorityRendering returns the per-priority UX hints the save-modal
|
||||
@@ -321,15 +351,38 @@ export interface CardOpts {
|
||||
// Page shells expose a toggle ("Hinweise anzeigen") that flips this and
|
||||
// re-renders. Default false — notes are noisy on long timelines.
|
||||
showNotes?: boolean;
|
||||
// showDurations controls per-rule duration rendering on event cards
|
||||
// (m/paliad#133, t-paliad-302):
|
||||
// true → inline `<span class="timeline-duration">2 Mo. nach</span>`
|
||||
// next to the date.
|
||||
// false → hover-only tooltip on the date span (browser-native
|
||||
// `title` attribute). Cards without a usable
|
||||
// `durationValue > 0` get neither — court-set and trigger-
|
||||
// event cards have no explainable interval.
|
||||
// /tools/verfahrensablauf exposes a toggle ("Dauern anzeigen") that
|
||||
// flips this and re-renders; persisted via the localStorage key
|
||||
// `paliad.verfahrensablauf.durations-show`. Default false.
|
||||
showDurations?: boolean;
|
||||
}
|
||||
|
||||
export function deadlineCardHtml(dl: CalculatedDeadline, opts: CardOpts): string {
|
||||
const wantsEditable = !!opts.editable;
|
||||
const editable = wantsEditable && !dl.isRootEvent && dl.code !== "";
|
||||
const overriddenClass = dl.isOverridden ? " timeline-date--overridden" : "";
|
||||
// Duration affordance (m/paliad#133, t-paliad-302). Computed once so
|
||||
// both the date-span tooltip and the inline meta-row span pull from
|
||||
// the same string. Empty for rules without a usable duration.
|
||||
const durationLabel = formatDurationLabel(dl);
|
||||
// Hover affordance on the date span: prefer the duration tooltip when
|
||||
// we have one, else fall back to the edit-hint when the cell is
|
||||
// click-to-edit. The edit affordance still works either way — the
|
||||
// title is purely advisory.
|
||||
const dateTitle = durationLabel
|
||||
? durationLabel
|
||||
: (editable ? t("deadlines.date.edit.hint") : "");
|
||||
const editAttrs = editable
|
||||
? ` data-rule-code="${escAttr(dl.code)}" data-current-date="${escAttr(dl.dueDate)}" role="button" tabindex="0" title="${escAttr(t("deadlines.date.edit.hint"))}"`
|
||||
: "";
|
||||
? ` data-rule-code="${escAttr(dl.code)}" data-current-date="${escAttr(dl.dueDate)}" role="button" tabindex="0"${dateTitle ? ` title="${escAttr(dateTitle)}"` : ""}`
|
||||
: (dateTitle ? ` title="${escAttr(dateTitle)}"` : "");
|
||||
// Conditional rows (t-paliad-289) replace the date column with an
|
||||
// "abhängig von <parent>" chip. The chip remains click-to-edit so
|
||||
// the user can pin a real date once known (e.g. once the oral
|
||||
@@ -434,9 +487,19 @@ export function deadlineCardHtml(dl: CalculatedDeadline, opts: CardOpts): string
|
||||
? `<span class="timeline-note-hint" tabindex="0" role="note" aria-label="${escAttr(noteText)}" title="${escAttr(noteText)}">ⓘ</span>`
|
||||
: "";
|
||||
|
||||
const meta = (opts.showParty || ruleRef || noteHint)
|
||||
// Inline duration affordance (m/paliad#133, t-paliad-302). Only
|
||||
// emitted when the "Dauern anzeigen" toggle is on AND the rule has a
|
||||
// usable duration; the default-off hover-tooltip path is wired
|
||||
// separately on the date span itself.
|
||||
const showDurations = opts.showDurations === true;
|
||||
const durationInline = showDurations && durationLabel
|
||||
? `<span class="timeline-duration">${escHtml(durationLabel)}</span>`
|
||||
: "";
|
||||
|
||||
const meta = (opts.showParty || ruleRef || noteHint || durationInline)
|
||||
? `<div class="timeline-meta">
|
||||
${opts.showParty ? partyBadge(dl.party) : ""}
|
||||
${durationInline}
|
||||
${ruleRef}
|
||||
${noteHint}
|
||||
</div>`
|
||||
@@ -614,6 +677,9 @@ type ColumnPosition = "ours" | "opponent";
|
||||
export interface ColumnsBodyOpts {
|
||||
editable?: boolean;
|
||||
showNotes?: boolean;
|
||||
// Forwarded to deadlineCardHtml — see CardOpts.showDurations.
|
||||
// (m/paliad#133, t-paliad-302)
|
||||
showDurations?: boolean;
|
||||
// side: which side the user is on. Drives column placement;
|
||||
// does NOT filter rows. Default null = claimant-on-the-left
|
||||
// (i.e. "ours = claimant", legacy default).
|
||||
@@ -724,7 +790,12 @@ export function renderColumnsBody(data: DeadlineResponse, opts: ColumnsBodyOpts
|
||||
const rows = bucketDeadlinesIntoColumns(data.deadlines, { side: userSide, appellant: opts.appellant });
|
||||
const appellantPinned = opts.appellant === "claimant" || opts.appellant === "defendant";
|
||||
|
||||
const cardOpts: CardOpts = { showParty: false, editable: opts.editable, showNotes: opts.showNotes };
|
||||
const cardOpts: CardOpts = {
|
||||
showParty: false,
|
||||
editable: opts.editable,
|
||||
showNotes: opts.showNotes,
|
||||
showDurations: opts.showDurations,
|
||||
};
|
||||
|
||||
// Collapsed "both" rows lose their mirror tag — there's no longer
|
||||
// a sibling row to mirror to, so the "↔ beide Seiten" hint would
|
||||
|
||||
@@ -1190,6 +1190,10 @@ export type I18nKey =
|
||||
| "deadlines.appeal_target.kostenentscheidung"
|
||||
| "deadlines.appeal_target.label"
|
||||
| "deadlines.appeal_target.schadensbemessung"
|
||||
| "deadlines.appellant.claimant"
|
||||
| "deadlines.appellant.defendant"
|
||||
| "deadlines.appellant.label"
|
||||
| "deadlines.appellant.none"
|
||||
| "deadlines.calculate"
|
||||
| "deadlines.card.calc.add_to_project"
|
||||
| "deadlines.card.calc.add_to_project.disabled"
|
||||
@@ -1264,6 +1268,7 @@ export type I18nKey =
|
||||
| "deadlines.dpma.appeal.bgh"
|
||||
| "deadlines.dpma.appeal.bpatg"
|
||||
| "deadlines.dpma.opp.dpma"
|
||||
| "deadlines.durations.show"
|
||||
| "deadlines.empty.filtered"
|
||||
| "deadlines.empty.hint"
|
||||
| "deadlines.empty.title"
|
||||
|
||||
@@ -3750,6 +3750,16 @@ input[type="range"]::-moz-range-thumb {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Per-rule duration label rendered inline in the meta row when
|
||||
"Dauern anzeigen" is on (m/paliad#133, t-paliad-302). Matches the
|
||||
sibling .timeline-rule weight so the meta line reads as one band of
|
||||
secondary metadata; non-mono so the value reads as prose ("2 Mo. nach")
|
||||
rather than a code reference. */
|
||||
.timeline-duration {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.timeline-adjusted {
|
||||
font-size: 0.78rem;
|
||||
color: var(--status-amber-fg-2);
|
||||
|
||||
@@ -250,6 +250,23 @@ export function renderVerfahrensablauf(): string {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="verfahrensablauf-perspective-row" id="appellant-row" style="display:none">
|
||||
<span className="date-label" data-i18n="deadlines.appellant.label">Berufung durch:</span>
|
||||
<div className="fristen-view-toggle" role="radiogroup" aria-label="Appellant">
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="appellant" value="claimant" />
|
||||
<span data-i18n="deadlines.appellant.claimant">Klägerseite</span>
|
||||
</label>
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="appellant" value="defendant" />
|
||||
<span data-i18n="deadlines.appellant.defendant">Beklagtenseite</span>
|
||||
</label>
|
||||
<label className="fristen-view-option">
|
||||
<input type="radio" name="appellant" value="" checked />
|
||||
<span data-i18n="deadlines.appellant.none">—</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/* Show-hidden toggle (t-paliad-290 / m/paliad#122).
|
||||
Re-surfaces optional cards the user has previously
|
||||
marked "Überspringen" via the per-card popover.
|
||||
@@ -341,6 +358,13 @@ export function renderVerfahrensablauf(): string {
|
||||
<input type="checkbox" id="fristen-notes-show" />
|
||||
<span data-i18n="deadlines.notes.show">Hinweise anzeigen</span>
|
||||
</label>
|
||||
{/* Durations toggle (m/paliad#133, t-paliad-302).
|
||||
Default off — hover-tooltips on date spans are
|
||||
the always-on path. */}
|
||||
<label className="fristen-notes-option">
|
||||
<input type="checkbox" id="verfahrensablauf-durations-show" />
|
||||
<span data-i18n="deadlines.durations.show">Dauern anzeigen</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="timeline-container">
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
-- 137_proceeding_role_labels — DOWN
|
||||
--
|
||||
-- Drops the 4 role-label columns. Backfilled data is lost on
|
||||
-- down-migration; that's acceptable because the frontend renderer
|
||||
-- falls back to the default labels ("Klägerseite" / "Beklagtenseite")
|
||||
-- when the columns are absent.
|
||||
|
||||
ALTER TABLE paliad.proceeding_types
|
||||
DROP COLUMN IF EXISTS role_reactive_label_en;
|
||||
|
||||
ALTER TABLE paliad.proceeding_types
|
||||
DROP COLUMN IF EXISTS role_reactive_label_de;
|
||||
|
||||
ALTER TABLE paliad.proceeding_types
|
||||
DROP COLUMN IF EXISTS role_proactive_label_en;
|
||||
|
||||
ALTER TABLE paliad.proceeding_types
|
||||
DROP COLUMN IF EXISTS role_proactive_label_de;
|
||||
@@ -1,137 +0,0 @@
|
||||
-- 137_proceeding_role_labels — t-paliad-301, m/paliad#132
|
||||
--
|
||||
-- Bug A fix: per-proceeding role labels so the Verfahrensablauf side
|
||||
-- selector can render "Berufungskläger / Berufungsbeklagter" for the
|
||||
-- unified UPC Berufung tile instead of the generic "Klägerseite /
|
||||
-- Beklagtenseite".
|
||||
--
|
||||
-- Four new optional columns on paliad.proceeding_types. NULL on a
|
||||
-- column falls back to the language-default ("Klägerseite" / "Claimant
|
||||
-- side" / "Beklagtenseite" / "Defendant side") in the frontend renderer.
|
||||
-- Only the proceedings whose role-naming actually differs get a backfill.
|
||||
--
|
||||
-- Live-DB audit (mcp__supabase__execute_sql) before drafting:
|
||||
-- - paliad.proceeding_types has 14 columns; the 4 target columns do
|
||||
-- NOT exist (zero name collisions).
|
||||
-- - Zero triggers on paliad.proceeding_types. No audit_reason
|
||||
-- setup needed.
|
||||
-- - No updated_at / created_at on the table — DO NOT include
|
||||
-- timestamp UPDATEs (lesson from mig 134 HOTFIX 3).
|
||||
--
|
||||
-- ADDITIVE ONLY. ALTER + UPDATE statements; no CHECK constraints
|
||||
-- (the columns are free-text labels, validated at the application layer).
|
||||
-- Down migration drops the 4 columns.
|
||||
--
|
||||
-- See m/paliad#132 for the full design rationale + the role-label
|
||||
-- matrix per proceeding code.
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
-- 1. Schema additions
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
ALTER TABLE paliad.proceeding_types
|
||||
ADD COLUMN role_proactive_label_de text NULL;
|
||||
|
||||
ALTER TABLE paliad.proceeding_types
|
||||
ADD COLUMN role_proactive_label_en text NULL;
|
||||
|
||||
ALTER TABLE paliad.proceeding_types
|
||||
ADD COLUMN role_reactive_label_de text NULL;
|
||||
|
||||
ALTER TABLE paliad.proceeding_types
|
||||
ADD COLUMN role_reactive_label_en text NULL;
|
||||
|
||||
COMMENT ON COLUMN paliad.proceeding_types.role_proactive_label_de IS
|
||||
'DE label for the proactive (claimant-equivalent) side of this '
|
||||
'proceeding. NULL = renderer falls back to "Klägerseite". '
|
||||
't-paliad-301 / m/paliad#132 Bug A.';
|
||||
COMMENT ON COLUMN paliad.proceeding_types.role_proactive_label_en IS
|
||||
'EN label for the proactive side. NULL = "Claimant side".';
|
||||
COMMENT ON COLUMN paliad.proceeding_types.role_reactive_label_de IS
|
||||
'DE label for the reactive (defendant-equivalent) side. NULL = '
|
||||
'"Beklagtenseite".';
|
||||
COMMENT ON COLUMN paliad.proceeding_types.role_reactive_label_en IS
|
||||
'EN label for the reactive side. NULL = "Defendant side".';
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
-- 2. Audit-first NOTICE pass.
|
||||
--
|
||||
-- Lists which proceeding_types are about to receive a backfill so
|
||||
-- the operator sees the scope before the UPDATE fires. NULL columns
|
||||
-- on every other row stay NULL (the frontend falls back to defaults).
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
rec record;
|
||||
backfill_count int := 0;
|
||||
BEGIN
|
||||
RAISE NOTICE '[mig 137] Proceedings that will receive role-label backfill:';
|
||||
FOR rec IN
|
||||
SELECT code, name
|
||||
FROM paliad.proceeding_types
|
||||
WHERE code IN ('upc.apl.unified', 'upc.rev.cfi', 'epa.opp.opd', 'epa.opp.boa')
|
||||
ORDER BY code
|
||||
LOOP
|
||||
RAISE NOTICE '[mig 137] % %', rec.code, rec.name;
|
||||
backfill_count := backfill_count + 1;
|
||||
END LOOP;
|
||||
RAISE NOTICE '[mig 137] Total: % proceedings (others stay NULL → renderer default)', backfill_count;
|
||||
END $$;
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
-- 3. Backfill.
|
||||
--
|
||||
-- Per the design matrix in m/paliad#132:
|
||||
-- - upc.apl.unified → Berufungskläger / Berufungsbeklagter / Appellant / Appellee
|
||||
-- - upc.rev.cfi → Antragsteller (Nichtigkeit) / Antragsgegner (Nichtigkeit) /
|
||||
-- Revocation claimant / Revocation defendant
|
||||
-- - epa.opp.opd → Einsprechende(r) / Patentinhaber(in) /
|
||||
-- Opponent / Patentee
|
||||
-- - epa.opp.boa → Einsprechende(r) / Patentinhaber(in) /
|
||||
-- Opponent / Patentee
|
||||
-- - (others) → stay NULL → frontend defaults
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
UPDATE paliad.proceeding_types
|
||||
SET role_proactive_label_de = 'Berufungskläger',
|
||||
role_reactive_label_de = 'Berufungsbeklagter',
|
||||
role_proactive_label_en = 'Appellant',
|
||||
role_reactive_label_en = 'Appellee'
|
||||
WHERE code = 'upc.apl.unified';
|
||||
|
||||
UPDATE paliad.proceeding_types
|
||||
SET role_proactive_label_de = 'Antragsteller (Nichtigkeit)',
|
||||
role_reactive_label_de = 'Antragsgegner (Nichtigkeit)',
|
||||
role_proactive_label_en = 'Revocation claimant',
|
||||
role_reactive_label_en = 'Revocation defendant'
|
||||
WHERE code = 'upc.rev.cfi';
|
||||
|
||||
UPDATE paliad.proceeding_types
|
||||
SET role_proactive_label_de = 'Einsprechende(r)',
|
||||
role_reactive_label_de = 'Patentinhaber(in)',
|
||||
role_proactive_label_en = 'Opponent',
|
||||
role_reactive_label_en = 'Patentee'
|
||||
WHERE code IN ('epa.opp.opd', 'epa.opp.boa');
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
-- 4. Post-migration NOTICE — informational only.
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
rec record;
|
||||
BEGIN
|
||||
RAISE NOTICE '[mig 137] post: backfilled role-label distribution:';
|
||||
FOR rec IN
|
||||
SELECT code,
|
||||
role_proactive_label_de,
|
||||
role_reactive_label_de
|
||||
FROM paliad.proceeding_types
|
||||
WHERE role_proactive_label_de IS NOT NULL
|
||||
ORDER BY code
|
||||
LOOP
|
||||
RAISE NOTICE '[mig 137] % proactive=% reactive=%',
|
||||
rec.code, rec.role_proactive_label_de, rec.role_reactive_label_de;
|
||||
END LOOP;
|
||||
END $$;
|
||||
@@ -40,9 +40,7 @@ const ruleColumns = `id, proceeding_type_id, parent_id, submission_code, name, n
|
||||
const proceedingTypeColumns = `id, code, name, name_en, description, jurisdiction,
|
||||
category, default_color, sort_order, is_active,
|
||||
trigger_event_label_de, trigger_event_label_en,
|
||||
appeal_target,
|
||||
role_proactive_label_de, role_proactive_label_en,
|
||||
role_reactive_label_de, role_reactive_label_en`
|
||||
appeal_target`
|
||||
|
||||
// List returns active rules, optionally filtered by proceeding type.
|
||||
// Each row has ConceptDefaultEventTypeID hydrated from
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package litigationplanner
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestTriggerEventLabelForAppealTarget pins the per-target trigger-
|
||||
// event label matrix (t-paliad-301 / m/paliad#132 Bug B). The 5
|
||||
// canonical AppealTargets each have a DE + EN label; unknown targets
|
||||
// return empty so the caller can fall back to the proceeding's own
|
||||
// trigger_event_label.
|
||||
func TestTriggerEventLabelForAppealTarget(t *testing.T) {
|
||||
cases := []struct {
|
||||
target string
|
||||
lang string
|
||||
want string
|
||||
}{
|
||||
{AppealTargetEndentscheidung, "de", "Endentscheidung (R.118)"},
|
||||
{AppealTargetEndentscheidung, "en", "Final decision (R.118)"},
|
||||
{AppealTargetKostenentscheidung, "de", "Kostenentscheidung"},
|
||||
{AppealTargetKostenentscheidung, "en", "Cost decision"},
|
||||
{AppealTargetAnordnung, "de", "Anordnung"},
|
||||
{AppealTargetAnordnung, "en", "Order"},
|
||||
{AppealTargetSchadensbemessung, "de", "Entscheidung im Schadensbemessungsverfahren"},
|
||||
{AppealTargetSchadensbemessung, "en", "Damages-assessment decision"},
|
||||
{AppealTargetBucheinsicht, "de", "Anordnung der Bucheinsicht"},
|
||||
{AppealTargetBucheinsicht, "en", "Book-inspection order"},
|
||||
// Unknown lang falls through to DE so the caller never gets
|
||||
// an empty string for a known target.
|
||||
{AppealTargetEndentscheidung, "fr", "Endentscheidung (R.118)"},
|
||||
// Unknown target → empty so caller falls back to proceeding's
|
||||
// trigger_event_label.
|
||||
{"", "de", ""},
|
||||
{"foo", "en", ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := TriggerEventLabelForAppealTarget(c.target, c.lang); got != c.want {
|
||||
t.Errorf("TriggerEventLabelForAppealTarget(%q, %q) = %q, want %q",
|
||||
c.target, c.lang, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAppealTargetsCoverage ensures every entry in AppealTargets has
|
||||
// a non-empty label in both languages. Adding a target to the slice
|
||||
// without populating the switch would silently emit empty labels —
|
||||
// this test catches that.
|
||||
func TestAppealTargetsCoverage(t *testing.T) {
|
||||
for _, target := range AppealTargets {
|
||||
for _, lang := range []string{"de", "en"} {
|
||||
if got := TriggerEventLabelForAppealTarget(target, lang); got == "" {
|
||||
t.Errorf("AppealTarget %q has empty label for lang %q — add it to the switch",
|
||||
target, lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,6 +249,10 @@ func Calculate(
|
||||
appellantContext[r.ID] = ctxVal
|
||||
}
|
||||
|
||||
ruleTiming := ""
|
||||
if r.Timing != nil {
|
||||
ruleTiming = *r.Timing
|
||||
}
|
||||
d := TimelineEntry{
|
||||
RuleID: r.ID.String(),
|
||||
Name: r.Name,
|
||||
@@ -258,6 +262,9 @@ func Calculate(
|
||||
AppellantContext: ctxVal,
|
||||
ChoicesOffered: json.RawMessage(r.ChoicesOffered),
|
||||
IsHidden: isHidden,
|
||||
DurationValue: r.DurationValue,
|
||||
DurationUnit: r.DurationUnit,
|
||||
Timing: ruleTiming,
|
||||
}
|
||||
if r.SubmissionCode != nil {
|
||||
d.Code = *r.SubmissionCode
|
||||
@@ -571,21 +578,6 @@ func Calculate(
|
||||
if pickedProceeding.TriggerEventLabelEN != nil {
|
||||
resp.TriggerEventLabelEN = *pickedProceeding.TriggerEventLabelEN
|
||||
}
|
||||
// t-paliad-301 / m/paliad#132 Bug B — appeal_target-driven trigger
|
||||
// label. When the request narrows to a specific appeal target, the
|
||||
// "Auslösendes Ereignis" label describes the underlying decision
|
||||
// (Endentscheidung / Kostenentscheidung / Anordnung /
|
||||
// Schadensbemessung / Bucheinsicht) rather than the appeal
|
||||
// proceeding itself. Overrides the proceeding's own
|
||||
// trigger_event_label set above.
|
||||
if opts.AppealTarget != "" {
|
||||
if de := TriggerEventLabelForAppealTarget(opts.AppealTarget, "de"); de != "" {
|
||||
resp.TriggerEventLabel = de
|
||||
}
|
||||
if en := TriggerEventLabelForAppealTarget(opts.AppealTarget, "en"); en != "" {
|
||||
resp.TriggerEventLabelEN = en
|
||||
}
|
||||
}
|
||||
if hasSubTrackNote {
|
||||
resp.ContextualNote = subTrackNote.NoteDE
|
||||
resp.ContextualNoteEN = subTrackNote.NoteEN
|
||||
@@ -671,6 +663,9 @@ func calculateByTriggerEvent(
|
||||
OriginalDate: original.Format("2006-01-02"),
|
||||
WasAdjusted: wasAdj,
|
||||
AdjustmentReason: reason,
|
||||
DurationValue: r.DurationValue,
|
||||
DurationUnit: r.DurationUnit,
|
||||
Timing: timing,
|
||||
}
|
||||
if r.SubmissionCode != nil {
|
||||
d.Code = *r.SubmissionCode
|
||||
|
||||
@@ -185,65 +185,6 @@ type ProceedingType struct {
|
||||
// — today the unified upc.apl row has this NULL (per-rule targets
|
||||
// live on Rule.AppliesToTarget).
|
||||
AppealTarget *string `db:"appeal_target" json:"appeal_target,omitempty"`
|
||||
|
||||
// Role label overrides (t-paliad-301 / m/paliad#132, mig 137).
|
||||
// NULL = renderer falls back to the language-default labels
|
||||
// ("Klägerseite" / "Beklagtenseite" / "Claimant side" / "Defendant side").
|
||||
// Set on proceedings where the role-naming diverges from the
|
||||
// claimant/defendant default (Appeal → Berufungskläger /
|
||||
// Berufungsbeklagter; Revocation → Antragsteller /
|
||||
// Antragsgegner Nichtigkeit; EPA Opposition → Einsprechende(r) /
|
||||
// Patentinhaber(in)).
|
||||
RoleProactiveLabelDE *string `db:"role_proactive_label_de" json:"role_proactive_label_de,omitempty"`
|
||||
RoleProactiveLabelEN *string `db:"role_proactive_label_en" json:"role_proactive_label_en,omitempty"`
|
||||
RoleReactiveLabelDE *string `db:"role_reactive_label_de" json:"role_reactive_label_de,omitempty"`
|
||||
RoleReactiveLabelEN *string `db:"role_reactive_label_en" json:"role_reactive_label_en,omitempty"`
|
||||
}
|
||||
|
||||
// TriggerEventLabelForAppealTarget returns the per-target
|
||||
// "Auslösendes Ereignis" label for the unified UPC Berufung
|
||||
// proceeding (t-paliad-301 / m/paliad#132 Bug B). The trigger event
|
||||
// for an appeal is the underlying decision, not the appeal
|
||||
// proceeding itself — these labels override the proceeding's own
|
||||
// trigger_event_label when appeal_target is set.
|
||||
//
|
||||
// lang ∈ {"de", "en"}; any other value falls through to "de" so the
|
||||
// caller never gets an empty string.
|
||||
//
|
||||
// Returns empty when target is empty / unknown (caller must fall
|
||||
// back to the proceeding's own trigger_event_label).
|
||||
func TriggerEventLabelForAppealTarget(target, lang string) string {
|
||||
if lang != "en" {
|
||||
lang = "de"
|
||||
}
|
||||
switch target {
|
||||
case AppealTargetEndentscheidung:
|
||||
if lang == "en" {
|
||||
return "Final decision (R.118)"
|
||||
}
|
||||
return "Endentscheidung (R.118)"
|
||||
case AppealTargetKostenentscheidung:
|
||||
if lang == "en" {
|
||||
return "Cost decision"
|
||||
}
|
||||
return "Kostenentscheidung"
|
||||
case AppealTargetAnordnung:
|
||||
if lang == "en" {
|
||||
return "Order"
|
||||
}
|
||||
return "Anordnung"
|
||||
case AppealTargetSchadensbemessung:
|
||||
if lang == "en" {
|
||||
return "Damages-assessment decision"
|
||||
}
|
||||
return "Entscheidung im Schadensbemessungsverfahren"
|
||||
case AppealTargetBucheinsicht:
|
||||
if lang == "en" {
|
||||
return "Book-inspection order"
|
||||
}
|
||||
return "Anordnung der Bucheinsicht"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// AdjustmentReason describes why a date was rolled forward / backward
|
||||
@@ -430,6 +371,17 @@ type TimelineEntry struct {
|
||||
ChoicesOffered json.RawMessage `json:"choicesOffered,omitempty"`
|
||||
AppellantContext string `json:"appellantContext,omitempty"`
|
||||
IsHidden bool `json:"isHidden,omitempty"`
|
||||
// DurationValue / DurationUnit / Timing surface the rule's
|
||||
// arithmetic so /tools/verfahrensablauf can show "2 Mo. nach" on
|
||||
// each event card (m/paliad#133, t-paliad-302). Source values from
|
||||
// the Rule row (not the post-alt-swap arithmetic) — the tooltip
|
||||
// reads as a property of the rule, not a recap of which branch
|
||||
// fired. Zero-duration rules (root event, court-set) emit
|
||||
// DurationValue=0 and the frontend suppresses the affordance.
|
||||
// Timing is "before" | "after" — empty when r.Timing is NULL.
|
||||
DurationValue int `json:"durationValue,omitempty"`
|
||||
DurationUnit string `json:"durationUnit,omitempty"`
|
||||
Timing string `json:"timing,omitempty"`
|
||||
}
|
||||
|
||||
// RuleCalculation is the single-rule calc response that backs the
|
||||
|
||||
Reference in New Issue
Block a user