diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index 6647ab1..060571d 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -462,10 +462,6 @@ const translations: Record> = { "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", @@ -3567,10 +3563,6 @@ const translations: Record> = { "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", diff --git a/frontend/src/client/verfahrensablauf.ts b/frontend/src/client/verfahrensablauf.ts index 30928b1..cf65b26 100644 --- a/frontend/src/client/verfahrensablauf.ts +++ b/frontend/src/client/verfahrensablauf.ts @@ -32,18 +32,20 @@ import { let selectedType = ""; let lastResponse: DeadlineResponse | null = null; -// 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. +// 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. 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= and that project has our_side set, @@ -52,17 +54,13 @@ let currentAppellant: Side = null; // link, which clears this flag (radio cluster takes over again). let sidePrefilledFromProject = false; -// 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. +// 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. const APPELLANT_AXIS_PROCEEDINGS = new Set([ "upc.apl.unified", "de.inf.olg", @@ -73,6 +71,44 @@ 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 = { + "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) @@ -105,11 +141,6 @@ 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"); @@ -117,11 +148,31 @@ function writeSideToURL(s: Side) { window.history.replaceState(null, "", url.pathname + (url.search ? url.search : "") + url.hash); } -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); +// 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( + "input[type=radio][name=side][value=claimant] + span" + ); + const defendantSpan = document.querySelector( + "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"); + } } // Slice B1 — appeal-target URL state. Empty string = no target picked @@ -432,7 +483,12 @@ function renderResults(data: DeadlineResponse) { editable: true, showNotes, side: currentSide, - appellant: hasAppellantAxis(selectedType) ? currentAppellant : null, + // 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, }) : renderTimelineBody(data, { showParty: true, editable: true, showNotes }); @@ -501,8 +557,8 @@ function selectProceeding(btn: HTMLButtonElement) { void populateCourtPicker("court-picker-row", "court-picker", selectedType); syncFlagRows(); - syncAppellantRowVisibility(); syncAppealTargetRowVisibility(); + applyRoleLabels(selectedType); setProceedingPickerCollapsed(true, proceedingDisplayName(btn)); @@ -510,23 +566,6 @@ 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 @@ -727,10 +766,8 @@ 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(); @@ -745,16 +782,6 @@ function initPerspectiveControls() { }); }); - document.querySelectorAll("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. diff --git a/frontend/src/i18n-keys.ts b/frontend/src/i18n-keys.ts index dc2717d..7d335a6 100644 --- a/frontend/src/i18n-keys.ts +++ b/frontend/src/i18n-keys.ts @@ -1190,10 +1190,6 @@ 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" diff --git a/frontend/src/verfahrensablauf.tsx b/frontend/src/verfahrensablauf.tsx index 9700816..6ae72bc 100644 --- a/frontend/src/verfahrensablauf.tsx +++ b/frontend/src/verfahrensablauf.tsx @@ -250,23 +250,6 @@ export function renderVerfahrensablauf(): string { - {/* Show-hidden toggle (t-paliad-290 / m/paliad#122). Re-surfaces optional cards the user has previously marked "Überspringen" via the per-card popover. diff --git a/internal/db/migrations/137_proceeding_role_labels.down.sql b/internal/db/migrations/137_proceeding_role_labels.down.sql new file mode 100644 index 0000000..616cb1d --- /dev/null +++ b/internal/db/migrations/137_proceeding_role_labels.down.sql @@ -0,0 +1,18 @@ +-- 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; diff --git a/internal/db/migrations/137_proceeding_role_labels.up.sql b/internal/db/migrations/137_proceeding_role_labels.up.sql new file mode 100644 index 0000000..3616f94 --- /dev/null +++ b/internal/db/migrations/137_proceeding_role_labels.up.sql @@ -0,0 +1,137 @@ +-- 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 $$; diff --git a/internal/services/deadline_rule_service.go b/internal/services/deadline_rule_service.go index 744b1ee..4f0127b 100644 --- a/internal/services/deadline_rule_service.go +++ b/internal/services/deadline_rule_service.go @@ -40,7 +40,9 @@ 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` + appeal_target, + role_proactive_label_de, role_proactive_label_en, + role_reactive_label_de, role_reactive_label_en` // List returns active rules, optionally filtered by proceeding type. // Each row has ConceptDefaultEventTypeID hydrated from diff --git a/pkg/litigationplanner/appeal_target_label_test.go b/pkg/litigationplanner/appeal_target_label_test.go new file mode 100644 index 0000000..002c3f9 --- /dev/null +++ b/pkg/litigationplanner/appeal_target_label_test.go @@ -0,0 +1,55 @@ +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) + } + } + } +} diff --git a/pkg/litigationplanner/engine.go b/pkg/litigationplanner/engine.go index ceeb840..80df60e 100644 --- a/pkg/litigationplanner/engine.go +++ b/pkg/litigationplanner/engine.go @@ -571,6 +571,21 @@ 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 diff --git a/pkg/litigationplanner/types.go b/pkg/litigationplanner/types.go index 217db1d..fd48979 100644 --- a/pkg/litigationplanner/types.go +++ b/pkg/litigationplanner/types.go @@ -185,6 +185,65 @@ 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