feat(t-paliad-122): GET /api/tools/courts + Fristenrechner court picker
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.
This commit is contained in:
@@ -226,6 +226,15 @@ async function calculate() {
|
||||
const overrides: Record<string, string> = {};
|
||||
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<string, CourtRow[]>();
|
||||
|
||||
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<CourtRow[]> {
|
||||
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<void> {
|
||||
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 `<option value="${escAttr(c.id)}"${c.id === defaultID ? " selected" : ""}>${escHtml(name)}</option>`;
|
||||
}).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
|
||||
|
||||
@@ -211,6 +211,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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)",
|
||||
|
||||
@@ -282,6 +282,10 @@ export function renderFristenrechner(): string {
|
||||
<label htmlFor="trigger-date" className="date-label" data-i18n="deadlines.trigger.date">Datum:</label>
|
||||
<input type="date" id="trigger-date" className="date-input" value={today} />
|
||||
</div>
|
||||
<div className="date-field-row" id="court-picker-row" style="display:none">
|
||||
<label htmlFor="court-picker" className="date-label" data-i18n="deadlines.court.label">Gericht:</label>
|
||||
<select id="court-picker" className="date-input"></select>
|
||||
</div>
|
||||
<div className="date-field-row" id="priority-date-row" style="display:none">
|
||||
<label htmlFor="priority-date" className="date-label" data-i18n="deadlines.priority.date">Prioritätstag (optional):</label>
|
||||
<input type="date" id="priority-date" className="date-input" />
|
||||
|
||||
@@ -569,6 +569,7 @@ export type I18nKey =
|
||||
| "deadlines.col.status"
|
||||
| "deadlines.col.title"
|
||||
| "deadlines.complete.action"
|
||||
| "deadlines.court.label"
|
||||
| "deadlines.court.set"
|
||||
| "deadlines.date.edit.hint"
|
||||
| "deadlines.de"
|
||||
|
||||
@@ -31,6 +31,7 @@ func handleFristenrechnerAPI(w http.ResponseWriter, r *http.Request) {
|
||||
PriorityDate string `json:"priorityDate,omitempty"`
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
AnchorOverrides map[string]string `json:"anchorOverrides,omitempty"`
|
||||
CourtID string `json:"courtId,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage"})
|
||||
@@ -45,6 +46,7 @@ func handleFristenrechnerAPI(w http.ResponseWriter, r *http.Request) {
|
||||
PriorityDateStr: req.PriorityDate,
|
||||
Flags: req.Flags,
|
||||
AnchorOverrides: req.AnchorOverrides,
|
||||
CourtID: req.CourtID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrUnknownProceedingType) {
|
||||
@@ -81,6 +83,7 @@ func handleFristenrechnerCalculateRule(w http.ResponseWriter, r *http.Request) {
|
||||
RuleLocalCode string `json:"ruleLocalCode"`
|
||||
TriggerDate string `json:"triggerDate"`
|
||||
Flags []string `json:"flags,omitempty"`
|
||||
CourtID string `json:"courtId,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage"})
|
||||
@@ -103,6 +106,7 @@ func handleFristenrechnerCalculateRule(w http.ResponseWriter, r *http.Request) {
|
||||
RuleLocalCode: req.RuleLocalCode,
|
||||
TriggerDate: req.TriggerDate,
|
||||
Flags: req.Flags,
|
||||
CourtID: req.CourtID,
|
||||
})
|
||||
if err != nil {
|
||||
switch {
|
||||
@@ -187,3 +191,33 @@ func handleEventDeadlinesCalculate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// GET /api/tools/courts — list active courts for the Fristenrechner court
|
||||
// picker. Optional ?courtType=UPC-LD filter narrows to a single tier so the
|
||||
// UI can render only the courts compatible with the selected proceeding.
|
||||
// Returns the deadline-computation slice (id, code, names, country, regime,
|
||||
// court_type, sort_order) — NOT the full Gerichtsverzeichnis catalog. The
|
||||
// rich addresses / phone / languages payload still lives at /api/courts.
|
||||
func handleCourtsList(w http.ResponseWriter, r *http.Request) {
|
||||
if dbSvc == nil || dbSvc.courts == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{
|
||||
"error": "Gerichte vorübergehend nicht verfügbar (keine Datenbank).",
|
||||
})
|
||||
return
|
||||
}
|
||||
courtType := r.URL.Query().Get("courtType")
|
||||
var (
|
||||
courts []services.Court
|
||||
err error
|
||||
)
|
||||
if courtType != "" {
|
||||
courts, err = dbSvc.courts.ByCourtType(courtType)
|
||||
} else {
|
||||
courts, err = dbSvc.courts.All()
|
||||
}
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "konnte Gerichte nicht laden"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, courts)
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc
|
||||
protected.HandleFunc("GET /api/tools/proceeding-types", handleProceedingTypes)
|
||||
protected.HandleFunc("GET /api/tools/trigger-events", handleTriggerEventsList)
|
||||
protected.HandleFunc("POST /api/tools/event-deadlines", handleEventDeadlinesCalculate)
|
||||
protected.HandleFunc("GET /api/tools/courts", handleCourtsList)
|
||||
protected.HandleFunc("GET /api/tools/fristenrechner/search", handleFristenrechnerSearch)
|
||||
protected.HandleFunc("GET /api/tools/fristenrechner/event-categories", handleFristenrechnerEventCategories)
|
||||
protected.HandleFunc("GET /downloads", handleDownloadsPage)
|
||||
|
||||
Reference in New Issue
Block a user