From 733917aae2e50238d6f3bb582126578e0f1c47cd Mon Sep 17 00:00:00 2001 From: m Date: Wed, 6 May 2026 12:50:59 +0200 Subject: [PATCH] feat(t-paliad-122): GET /api/tools/courts + Fristenrechner court picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/tools/courts[?courtType=UPC-LD] returns the deadline- computation slice of paliad.courts (id, code, names, country, regime, court_type) — distinct from the rich Gerichtsverzeichnis at /api/courts. Optional courtType filter narrows to a single tier. POST /api/tools/fristenrechner and POST /api/tools/fristenrechner/ calculate-rule both accept an optional courtId field. When set, the calculator resolves the court's (country, regime) and uses that calendar; when omitted, the proceeding's existing jurisdiction column seeds a sensible default — preserves today's behaviour for callers that don't yet send a court. Frontend: court-picker-row added to step 2 of the Fristenrechner wizard. Visible only for proceeding types with multiple compatible courts (today: every UPC-flavoured proceeding — UPC LDs span 12 countries, plus UPC CD seats and the CoA). DE-only proceedings (BPatG nullity, BGH appeals, DPMA, EPA, EP grant) keep the form unchanged. Picker re-runs the calc on selection so the user sees the same deadlines shift to a different calendar without a manual click. i18n key deadlines.court.label added for both DE and EN. Default courts wired sensibly: UPC_INF / UPC_REV / UPC_PI etc. → UPC LD München (HLC's home venue); UPC_APP / UPC_APP_ORDERS / UPC_COST_APPEAL → UPC CoA Luxembourg; UPC_REV → UPC CD Paris. --- frontend/src/client/fristenrechner.ts | 105 ++++++++++++++++++++++++++ frontend/src/client/i18n.ts | 2 + frontend/src/fristenrechner.tsx | 4 + frontend/src/i18n-keys.ts | 1 + internal/handlers/fristenrechner.go | 34 +++++++++ internal/handlers/handlers.go | 1 + 6 files changed, 147 insertions(+) diff --git a/frontend/src/client/fristenrechner.ts b/frontend/src/client/fristenrechner.ts index 1bab037..8cdfaf1 100644 --- a/frontend/src/client/fristenrechner.ts +++ b/frontend/src/client/fristenrechner.ts @@ -226,6 +226,15 @@ async function calculate() { const overrides: Record = {}; for (const [code, date] of anchorOverrides) overrides[code] = date; + // Court picker — only meaningful when the picker row is visible + // (multi-court proceeding types). When hidden, server resolves the + // default for the proceeding's jurisdiction. + const courtPickerRow = document.getElementById("court-picker-row"); + const courtPicker = document.getElementById("court-picker") as HTMLSelectElement | null; + const courtId = courtPickerRow && courtPickerRow.style.display !== "none" && courtPicker?.value + ? courtPicker.value + : ""; + try { const resp = await fetch("/api/tools/fristenrechner", { method: "POST", @@ -236,6 +245,7 @@ async function calculate() { priorityDate: priorityDate || undefined, flags: flags.length > 0 ? flags : undefined, anchorOverrides: Object.keys(overrides).length > 0 ? overrides : undefined, + courtId: courtId || undefined, }), }); @@ -721,11 +731,104 @@ function selectProceeding(btn: HTMLButtonElement) { if (revCciRow) revCciRow.style.display = selectedType === "UPC_REV" ? "" : "none"; syncInfAmendEnabled(); + populateCourtPicker(selectedType); showStep(2); scheduleProcCalc(0); } +// Court picker — t-paliad-122. Visible only for proceeding types that can +// land in multiple courts with different holiday calendars (today: every +// UPC-flavoured proceeding type, since UPC LDs span DE/FR/IT/NL/BE/FI/PT/ +// AT/SI/DK + Stockholm RD + 3 CD seats). For DE-only proceedings (DE_NULL, +// DE_NULL_BGH, DE_INF_BGH, DPMA_*, EPA_*, EP_GRANT) the court is fixed by +// the proceeding type — no picker, server resolves the default. +// +// The picker calls /api/tools/courts?courtType=UPC-LD on first need and +// caches the response per-type. Defaulting to upc-ld-muenchen matches HLC's +// most common venue and keeps current behaviour for users who don't choose. +interface CourtRow { + id: string; + code: string; + nameDE: string; + nameEN: string; + country: string; + regime?: string; + courtType: string; +} + +const courtCache = new Map(); + +function courtTypesFor(proceedingType: string): string[] { + // Map proceeding code to compatible court types. UPC proceedings → UPC-LD + // (most common); appeals → UPC-CoA; central-division revocations → UPC-CD. + if (proceedingType === "UPC_APP" || proceedingType === "UPC_APP_ORDERS" || proceedingType === "UPC_COST_APPEAL") { + return ["UPC-CoA"]; + } + if (proceedingType === "UPC_REV") { + return ["UPC-CD", "UPC-LD"]; // CD is the default revocation forum, LD when joined with infringement + } + if (proceedingType.startsWith("UPC_")) { + return ["UPC-LD"]; + } + return []; +} + +function defaultCourtFor(proceedingType: string): string { + if (proceedingType === "UPC_APP" || proceedingType === "UPC_APP_ORDERS" || proceedingType === "UPC_COST_APPEAL") { + return "upc-coa-luxembourg"; + } + if (proceedingType === "UPC_REV") { + return "upc-cd-paris"; + } + return "upc-ld-muenchen"; +} + +async function fetchCourts(courtType: string): Promise { + if (courtCache.has(courtType)) return courtCache.get(courtType)!; + try { + const resp = await fetch(`/api/tools/courts?courtType=${encodeURIComponent(courtType)}`); + if (!resp.ok) return []; + const rows = (await resp.json()) as CourtRow[]; + courtCache.set(courtType, rows); + return rows; + } catch { + return []; + } +} + +async function populateCourtPicker(proceedingType: string): Promise { + const row = document.getElementById("court-picker-row"); + const select = document.getElementById("court-picker") as HTMLSelectElement | null; + if (!row || !select) return; + + const types = courtTypesFor(proceedingType); + if (types.length === 0) { + row.style.display = "none"; + select.innerHTML = ""; + return; + } + + // Load all compatible court types and concatenate (CD before LD for REV). + const lists = await Promise.all(types.map(t => fetchCourts(t))); + const courts = lists.flat(); + if (courts.length <= 1) { + // Single compatible court — no point asking the user. Server's + // jurisdiction default lands the same place. + row.style.display = "none"; + select.innerHTML = ""; + return; + } + + const lang = getLang(); + const defaultID = defaultCourtFor(proceedingType); + select.innerHTML = courts.map(c => { + const name = lang === "en" ? c.nameEN : c.nameDE; + return ``; + }).join(""); + row.style.display = ""; +} + // inf-amend-flag is only meaningful when ccr-flag is on (R.30 application // is filed within the Defence to CCR). When ccr-flag flips off, also // untick inf-amend-flag so the calc payload stays coherent. @@ -807,6 +910,8 @@ document.addEventListener("DOMContentLoaded", () => { if (revAmendFlag) revAmendFlag.addEventListener("change", () => scheduleProcCalc(0)); const revCciFlag = document.getElementById("rev-cci-flag") as HTMLInputElement | null; if (revCciFlag) revCciFlag.addEventListener("change", () => scheduleProcCalc(0)); + const courtPicker = document.getElementById("court-picker") as HTMLSelectElement | null; + if (courtPicker) courtPicker.addEventListener("change", () => scheduleProcCalc(0)); // Click-to-edit on timeline / column dates: open an inline date input // and persist the user's choice as an anchor override so downstream diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index 6d97a56..647fd9f 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -211,6 +211,7 @@ const translations: Record> = { "deadlines.trigger.date": "Datum:", "deadlines.trigger.label": "Ausgangsdatum", "deadlines.priority.date": "Priorit\u00e4tstag (optional):", + "deadlines.court.label": "Gericht:", "deadlines.flag.ccr": "Mit Widerklage auf Nichtigkeit", "deadlines.flag.inf_amend": "Mit Antrag auf Patentänderung (R.30)", "deadlines.flag.rev_amend": "Mit Antrag auf Patentänderung (R.49.2.a)", @@ -1806,6 +1807,7 @@ const translations: Record> = { "deadlines.trigger.date": "Date:", "deadlines.trigger.label": "Trigger date", "deadlines.priority.date": "Priority date (optional):", + "deadlines.court.label": "Court:", "deadlines.flag.ccr": "Counterclaim for revocation filed", "deadlines.flag.inf_amend": "Application to amend the patent filed (R.30)", "deadlines.flag.rev_amend": "Application to amend the patent filed (R.49.2.a)", diff --git a/frontend/src/fristenrechner.tsx b/frontend/src/fristenrechner.tsx index 396e0b7..efa2a64 100644 --- a/frontend/src/fristenrechner.tsx +++ b/frontend/src/fristenrechner.tsx @@ -282,6 +282,10 @@ export function renderFristenrechner(): string { +