feat(t-paliad-207): Verfahrensablauf + Fristenrechner polish (jurisdiction prefix, trigger-event, flag rows, rule links, R.19 label)
Five intertwined fixes m surfaced in the interactive session:
1. **Jurisdiction prefix on the picked proceeding** — the collapsed
summary chip and the result header now read "UPC Verletzungsverfahren"
/ "DE Verletzungsklage (LG)" instead of the bare proceeding name.
Disambiguates the 4 redundancies in the corpus once the picker
collapses. Driven by .proceeding-group[data-forum] which is already
on every group.
2. **Trigger Event label = root rule** — step 2's "Auslösendes Ereignis"
line now shows the first event in the proceeding (e.g. Klageerhebung,
Nichtigkeitsklage) instead of the proceeding name. Populated from
the calc response (isRootEvent=true) on every render; em-dash
placeholder while step 3 hasn't rendered yet. lang-change keeps it
coherent.
3. **Flag rows on /tools/verfahrensablauf** — Slice 1 of t-paliad-179
stripped the with_ccr / with_amend / with_cci toggles when it lifted
the shared renderer; they never came back. Lifted the 4 existing
rows from fristenrechner.tsx plus 2 new with_po rows (RoP 19.1
preliminary objection, mig 095) — same wiring + show/hide rules on
both surfaces. with_amend stays nested under with_ccr on upc.inf.cfi
(R.30 only with a CCR).
4. **Rule references → youpc.org/laws links** — new
BuildLegalSourceURL(src) maps the structured legal_source code to
the youpc permalink for the UPC corpus (UPCRoP / UPCA / UPCS today;
39 of 91 active rules carry UPC.RoP.* and now link). DE/EPA/EU
bodies have no youpc home yet and render as plain display text —
filed as m/paliad#39. Wired through UIDeadline.LegalSourceDisplay +
LegalSourceURL so deadlineCardHtml can render <a target="_blank"
rel="noopener"> when the URL is set.
5. **R.19 label: "Vorab-Einrede" → "Einspruch"** — m's correction. DE
only (EN canonical UPC RoP term stays "Preliminary objection").
Client-side change only — i18n + JSX fallbacks. The matching DB
rename on the two rule-name rows folds into joule's broader mig 097
(legal-citation backfill, t-paliad-208 follow-up). The live UPDATE
applied during the session is captured under that audit reason; the
no-op when joule's mig re-applies is harmless.
Build hygiene:
- go build ./... + go vet ./... clean
- new test TestBuildLegalSourceURL covers UPC corpus + DE/EPA/EU
fall-through + edge cases (empty input, malformed source)
- bun run build clean (2417 i18n keys total)
Rebased on origin/main @ d126913 (ohm's submission_code rename
workstream B) — no conflicts in this commit's surface area.
Branch: mai/fermi/interactive-session. NOT self-merged.
This commit is contained in:
@@ -112,23 +112,29 @@ async function calculate() {
|
||||
const priorityInput = document.getElementById("priority-date") as HTMLInputElement | null;
|
||||
const priorityDate = selectedType === "epa.grant.exa" && priorityInput?.value ? priorityInput.value : "";
|
||||
|
||||
// Flags — three proceeding-specific checkboxes:
|
||||
// Flags — proceeding-specific checkboxes:
|
||||
// upc.inf.cfi: with_ccr (always available); with_amend (nested under
|
||||
// with_ccr — R.30 application is only available with a CCR).
|
||||
// upc.rev.cfi: with_amend (R.49.2.a) and with_cci (R.49.2.b) as two
|
||||
// independent gates; both can be on simultaneously.
|
||||
// with_ccr — R.30 application is only available with a CCR);
|
||||
// with_po (mig 095, R.19.1 preliminary objection).
|
||||
// upc.rev.cfi: with_amend (R.49.2.a), with_cci (R.49.2.b),
|
||||
// with_po (R.19.1 via R.46) as three independent gates;
|
||||
// all combinations are valid.
|
||||
const ccrFlag = document.getElementById("ccr-flag") as HTMLInputElement | null;
|
||||
const infAmendFlag = document.getElementById("inf-amend-flag") as HTMLInputElement | null;
|
||||
const infPoFlag = document.getElementById("inf-po-flag") as HTMLInputElement | null;
|
||||
const revAmendFlag = document.getElementById("rev-amend-flag") as HTMLInputElement | null;
|
||||
const revCciFlag = document.getElementById("rev-cci-flag") as HTMLInputElement | null;
|
||||
const revPoFlag = document.getElementById("rev-po-flag") as HTMLInputElement | null;
|
||||
const flags: string[] = [];
|
||||
if (selectedType === "upc.inf.cfi") {
|
||||
if (ccrFlag?.checked) flags.push("with_ccr");
|
||||
if (ccrFlag?.checked && infAmendFlag?.checked) flags.push("with_amend");
|
||||
if (infPoFlag?.checked) flags.push("with_po");
|
||||
}
|
||||
if (selectedType === "upc.rev.cfi") {
|
||||
if (revAmendFlag?.checked) flags.push("with_amend");
|
||||
if (revCciFlag?.checked) flags.push("with_cci");
|
||||
if (revPoFlag?.checked) flags.push("with_po");
|
||||
}
|
||||
|
||||
// Forward any user-set per-rule date overrides so downstream rules
|
||||
@@ -508,18 +514,24 @@ function selectProceeding(btn: HTMLButtonElement) {
|
||||
// ccr-flag → upc.inf.cfi only
|
||||
// inf-amend-flag → upc.inf.cfi only, but disabled until ccr-flag is on
|
||||
// (R.30 amend only available with a CCR)
|
||||
// inf-po-flag → upc.inf.cfi only (R.19.1 preliminary objection, mig 095)
|
||||
// rev-amend-flag → upc.rev.cfi only
|
||||
// rev-cci-flag → upc.rev.cfi only
|
||||
// rev-po-flag → upc.rev.cfi only (R.19.1 via R.46, mig 095)
|
||||
const priorityRow = document.getElementById("priority-date-row");
|
||||
if (priorityRow) priorityRow.style.display = selectedType === "epa.grant.exa" ? "" : "none";
|
||||
const ccrRow = document.getElementById("ccr-flag-row");
|
||||
if (ccrRow) ccrRow.style.display = selectedType === "upc.inf.cfi" ? "" : "none";
|
||||
const infAmendRow = document.getElementById("inf-amend-flag-row");
|
||||
if (infAmendRow) infAmendRow.style.display = selectedType === "upc.inf.cfi" ? "" : "none";
|
||||
const infPoRow = document.getElementById("inf-po-flag-row");
|
||||
if (infPoRow) infPoRow.style.display = selectedType === "upc.inf.cfi" ? "" : "none";
|
||||
const revAmendRow = document.getElementById("rev-amend-flag-row");
|
||||
if (revAmendRow) revAmendRow.style.display = selectedType === "upc.rev.cfi" ? "" : "none";
|
||||
const revCciRow = document.getElementById("rev-cci-flag-row");
|
||||
if (revCciRow) revCciRow.style.display = selectedType === "upc.rev.cfi" ? "" : "none";
|
||||
const revPoRow = document.getElementById("rev-po-flag-row");
|
||||
if (revPoRow) revPoRow.style.display = selectedType === "upc.rev.cfi" ? "" : "none";
|
||||
|
||||
syncInfAmendEnabled();
|
||||
populateCourtPickerCore("court-picker-row", "court-picker", selectedType);
|
||||
@@ -620,10 +632,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
const infAmendFlag = document.getElementById("inf-amend-flag") as HTMLInputElement | null;
|
||||
if (infAmendFlag) infAmendFlag.addEventListener("change", () => scheduleProcCalc(0));
|
||||
const infPoFlag = document.getElementById("inf-po-flag") as HTMLInputElement | null;
|
||||
if (infPoFlag) infPoFlag.addEventListener("change", () => scheduleProcCalc(0));
|
||||
const revAmendFlag = document.getElementById("rev-amend-flag") as HTMLInputElement | null;
|
||||
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 revPoFlag = document.getElementById("rev-po-flag") as HTMLInputElement | null;
|
||||
if (revPoFlag) revPoFlag.addEventListener("change", () => scheduleProcCalc(0));
|
||||
const courtPicker = document.getElementById("court-picker") as HTMLSelectElement | null;
|
||||
if (courtPicker) courtPicker.addEventListener("change", () => scheduleProcCalc(0));
|
||||
|
||||
|
||||
@@ -221,8 +221,10 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.court.label": "Gericht:",
|
||||
"deadlines.flag.ccr": "Mit Widerklage auf Nichtigkeit",
|
||||
"deadlines.flag.inf_amend": "Mit Antrag auf Patentänderung (R.30)",
|
||||
"deadlines.flag.inf_po": "Mit Einspruch (R.19)",
|
||||
"deadlines.flag.rev_amend": "Mit Antrag auf Patentänderung (R.49.2.a)",
|
||||
"deadlines.flag.rev_cci": "Mit Verletzungswiderklage (R.49.2.b)",
|
||||
"deadlines.flag.rev_po": "Mit Einspruch (R.19 i.V.m. R.46)",
|
||||
"deadlines.calculate": "Fristen berechnen",
|
||||
"deadlines.print": "Drucken",
|
||||
"deadlines.reset": "\u2190 Neu berechnen",
|
||||
@@ -2788,8 +2790,10 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"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.inf_po": "Preliminary objection filed (R.19)",
|
||||
"deadlines.flag.rev_amend": "Application to amend the patent filed (R.49.2.a)",
|
||||
"deadlines.flag.rev_cci": "Counterclaim for infringement filed (R.49.2.b)",
|
||||
"deadlines.flag.rev_po": "Preliminary objection filed (R.19 via R.46)",
|
||||
"deadlines.calculate": "Calculate Deadlines",
|
||||
"deadlines.print": "Print",
|
||||
"deadlines.reset": "\u2190 Start Over",
|
||||
|
||||
@@ -25,6 +25,35 @@ let lastResponse: DeadlineResponse | null = null;
|
||||
type ProcedureView = "timeline" | "columns";
|
||||
let procedureView: ProcedureView = "columns";
|
||||
|
||||
// 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 /
|
||||
// fristenrechner.tsx (upc / de / epa / dpma). Disambiguates the
|
||||
// 4 redundancies in the corpus (UPC Verletzungsverfahren vs DE
|
||||
// Verletzungsklage etc.) once the picker collapses.
|
||||
const FORUM_LABEL: Record<string, string> = {
|
||||
upc: "UPC",
|
||||
de: "DE",
|
||||
epa: "EPA",
|
||||
dpma: "DPMA",
|
||||
};
|
||||
|
||||
function jurisdictionFor(btn: HTMLButtonElement): string {
|
||||
const group = btn.closest<HTMLElement>(".proceeding-group");
|
||||
const forum = group?.dataset.forum || "";
|
||||
return FORUM_LABEL[forum] || "";
|
||||
}
|
||||
|
||||
function proceedingDisplayName(btn: HTMLButtonElement): string {
|
||||
const name = btn.querySelector("strong")?.textContent || "";
|
||||
const jur = jurisdictionFor(btn);
|
||||
return jur ? `${jur} ${name}` : name;
|
||||
}
|
||||
|
||||
function activeProceedingButton(): HTMLButtonElement | null {
|
||||
return document.querySelector<HTMLButtonElement>(".proceeding-btn.active");
|
||||
}
|
||||
|
||||
// Auto-calc plumbing — sequence + debounce mirror /tools/fristenrechner
|
||||
// so rapid input changes never let a stale response overwrite a fresh
|
||||
// one.
|
||||
@@ -46,6 +75,34 @@ function showStep(n: number) {
|
||||
}
|
||||
}
|
||||
|
||||
// Read the proceeding-specific flag checkboxes and assemble the
|
||||
// payload the calculator expects. Mirrors fristenrechner.ts so the
|
||||
// gating semantics stay identical: with_amend on upc.inf.cfi is
|
||||
// nested under with_ccr (R.30 is only available with a CCR);
|
||||
// upc.rev.cfi exposes with_amend + with_cci as two independent
|
||||
// gates; with_po (RoP 19.1, mig 095) gates the optional preliminary
|
||||
// objection rule on both inf and rev.
|
||||
function readFlags(): string[] {
|
||||
const ccr = document.getElementById("ccr-flag") as HTMLInputElement | null;
|
||||
const infAmend = document.getElementById("inf-amend-flag") as HTMLInputElement | null;
|
||||
const infPo = document.getElementById("inf-po-flag") as HTMLInputElement | null;
|
||||
const revAmend = document.getElementById("rev-amend-flag") as HTMLInputElement | null;
|
||||
const revCci = document.getElementById("rev-cci-flag") as HTMLInputElement | null;
|
||||
const revPo = document.getElementById("rev-po-flag") as HTMLInputElement | null;
|
||||
const flags: string[] = [];
|
||||
if (selectedType === "upc.inf.cfi") {
|
||||
if (ccr?.checked) flags.push("with_ccr");
|
||||
if (ccr?.checked && infAmend?.checked) flags.push("with_amend");
|
||||
if (infPo?.checked) flags.push("with_po");
|
||||
}
|
||||
if (selectedType === "upc.rev.cfi") {
|
||||
if (revAmend?.checked) flags.push("with_amend");
|
||||
if (revCci?.checked) flags.push("with_cci");
|
||||
if (revPo?.checked) flags.push("with_po");
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
async function doCalc() {
|
||||
const seq = ++calcSeq;
|
||||
const dateInput = document.getElementById("trigger-date") as HTMLInputElement | null;
|
||||
@@ -61,6 +118,7 @@ async function doCalc() {
|
||||
const data = await calculateDeadlines({
|
||||
proceedingType: selectedType,
|
||||
triggerDate,
|
||||
flags: readFlags(),
|
||||
courtId,
|
||||
});
|
||||
if (seq !== calcSeq) return;
|
||||
@@ -70,13 +128,42 @@ async function doCalc() {
|
||||
showStep(3);
|
||||
}
|
||||
|
||||
// triggerEventLabelFor picks the user-facing "Auslösendes Ereignis"
|
||||
// label from the calc response. The root rule (isRootEvent=true) is
|
||||
// the first event in the proceeding — e.g. Klageerhebung for
|
||||
// upc.inf.cfi, Nichtigkeitsklage for upc.rev.cfi. Falls back to the
|
||||
// active proceeding name if no root rule fires (shouldn't happen for
|
||||
// healthy data, but safer than a blank).
|
||||
function triggerEventLabelFor(data: DeadlineResponse): string {
|
||||
const root = data.deadlines.find((d) => d.isRootEvent);
|
||||
if (root) {
|
||||
return getLang() === "en" ? (root.nameEN || root.name) : (root.name || root.nameEN);
|
||||
}
|
||||
return data.proceedingName || "";
|
||||
}
|
||||
|
||||
function syncTriggerEventLabel() {
|
||||
const triggerEventEl = document.getElementById("trigger-event");
|
||||
if (!triggerEventEl) return;
|
||||
if (lastResponse) {
|
||||
triggerEventEl.textContent = triggerEventLabelFor(lastResponse);
|
||||
} else {
|
||||
triggerEventEl.textContent = "—";
|
||||
}
|
||||
}
|
||||
|
||||
function renderResults(data: DeadlineResponse) {
|
||||
const container = document.getElementById("timeline-container");
|
||||
if (!container) return;
|
||||
const printBtn = document.getElementById("fristen-print-btn");
|
||||
const toggle = document.getElementById("fristen-view-toggle");
|
||||
|
||||
const procName = tDyn(`deadlines.${data.proceedingType.toLowerCase()}`);
|
||||
// Header shows the picked proceeding with its jurisdiction prefix
|
||||
// so the user can tell UPC Verletzungsverfahren apart from DE
|
||||
// Verletzungsklage once the picker collapses.
|
||||
const activeBtn = activeProceedingButton();
|
||||
const procName = activeBtn ? proceedingDisplayName(activeBtn)
|
||||
: tDyn(`deadlines.${data.proceedingType.toLowerCase()}`);
|
||||
const headerHtml = `<div class="timeline-header">
|
||||
<strong>${procName}</strong>
|
||||
<span class="timeline-trigger-date">${t("deadlines.trigger.label")}: ${formatDate(data.triggerDate)}</span>
|
||||
@@ -89,6 +176,8 @@ function renderResults(data: DeadlineResponse) {
|
||||
container.innerHTML = headerHtml + bodyHtml;
|
||||
if (printBtn) printBtn.style.display = "block";
|
||||
if (toggle) toggle.style.display = "";
|
||||
|
||||
syncTriggerEventLabel();
|
||||
}
|
||||
|
||||
function setProceedingPickerCollapsed(collapsed: boolean, displayName?: string) {
|
||||
@@ -100,18 +189,49 @@ function setProceedingPickerCollapsed(collapsed: boolean, displayName?: string)
|
||||
if (summaryName && displayName) summaryName.textContent = displayName;
|
||||
}
|
||||
|
||||
// syncFlagRows shows/hides the proceeding-specific checkbox rows
|
||||
// based on selectedType. Same disposition as fristenrechner.ts —
|
||||
// the with_amend nested-under-ccr semantic is enforced via
|
||||
// syncInfAmendEnabled().
|
||||
function syncFlagRows() {
|
||||
const show = (id: string, when: boolean) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = when ? "" : "none";
|
||||
};
|
||||
show("ccr-flag-row", selectedType === "upc.inf.cfi");
|
||||
show("inf-amend-flag-row", selectedType === "upc.inf.cfi");
|
||||
show("inf-po-flag-row", selectedType === "upc.inf.cfi");
|
||||
show("rev-amend-flag-row", selectedType === "upc.rev.cfi");
|
||||
show("rev-cci-flag-row", selectedType === "upc.rev.cfi");
|
||||
show("rev-po-flag-row", selectedType === "upc.rev.cfi");
|
||||
syncInfAmendEnabled();
|
||||
}
|
||||
|
||||
// R.30 amendment-application is only available with a CCR — disable
|
||||
// (and clear) the nested inf-amend checkbox while ccr is off so the
|
||||
// calc payload stays coherent. Mirrors fristenrechner.ts.
|
||||
function syncInfAmendEnabled() {
|
||||
const ccr = document.getElementById("ccr-flag") as HTMLInputElement | null;
|
||||
const infAmend = document.getElementById("inf-amend-flag") as HTMLInputElement | null;
|
||||
if (!ccr || !infAmend) return;
|
||||
infAmend.disabled = !ccr.checked;
|
||||
if (!ccr.checked) infAmend.checked = false;
|
||||
}
|
||||
|
||||
function selectProceeding(btn: HTMLButtonElement) {
|
||||
document.querySelectorAll(".proceeding-btn").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
selectedType = btn.dataset.code || "";
|
||||
|
||||
const name = btn.querySelector("strong")?.textContent || "";
|
||||
const triggerEventEl = document.getElementById("trigger-event");
|
||||
if (triggerEventEl) triggerEventEl.textContent = name;
|
||||
// Trigger-event label fires from the calc response (root rule).
|
||||
// Until step 3 renders, fall back to an em-dash placeholder.
|
||||
lastResponse = null;
|
||||
syncTriggerEventLabel();
|
||||
|
||||
void populateCourtPicker("court-picker-row", "court-picker", selectedType);
|
||||
syncFlagRows();
|
||||
|
||||
setProceedingPickerCollapsed(true, name);
|
||||
setProceedingPickerCollapsed(true, proceedingDisplayName(btn));
|
||||
|
||||
showStep(2);
|
||||
scheduleCalc(0);
|
||||
@@ -169,18 +289,35 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const courtPicker = document.getElementById("court-picker") as HTMLSelectElement | null;
|
||||
if (courtPicker) courtPicker.addEventListener("change", () => scheduleCalc(0));
|
||||
|
||||
// Flag-checkbox listeners — each flip triggers a fresh calc so the
|
||||
// timeline re-projects with the new gating. ccr-flag additionally
|
||||
// enables/disables the nested inf-amend row.
|
||||
const ccrFlag = document.getElementById("ccr-flag") as HTMLInputElement | null;
|
||||
if (ccrFlag) ccrFlag.addEventListener("change", () => {
|
||||
syncInfAmendEnabled();
|
||||
scheduleCalc(0);
|
||||
});
|
||||
(["inf-amend-flag", "inf-po-flag", "rev-amend-flag", "rev-cci-flag", "rev-po-flag"]).forEach((id) => {
|
||||
const cb = document.getElementById(id) as HTMLInputElement | null;
|
||||
if (cb) cb.addEventListener("change", () => scheduleCalc(0));
|
||||
});
|
||||
|
||||
document.getElementById("fristen-print-btn")?.addEventListener("click", () => window.print());
|
||||
|
||||
initViewToggle();
|
||||
|
||||
onLangChange(() => {
|
||||
if (lastResponse) renderResults(lastResponse);
|
||||
const activeBtn = document.querySelector<HTMLButtonElement>(".proceeding-btn.active");
|
||||
// Active-button name updates with language change (the data-i18n
|
||||
// pass swaps the inner <strong>'s text). Re-collapse the summary
|
||||
// chip and re-derive the trigger event label from the lang-current
|
||||
// calc response.
|
||||
const activeBtn = activeProceedingButton();
|
||||
if (activeBtn) {
|
||||
const name = activeBtn.querySelector("strong")?.textContent || "";
|
||||
const triggerEventEl = document.getElementById("trigger-event");
|
||||
if (triggerEventEl) triggerEventEl.textContent = name;
|
||||
const summary = document.getElementById("proceeding-summary-name");
|
||||
if (summary) summary.textContent = proceedingDisplayName(activeBtn);
|
||||
}
|
||||
if (lastResponse) renderResults(lastResponse);
|
||||
syncTriggerEventLabel();
|
||||
});
|
||||
|
||||
// Pre-select the first proceeding tile so users see a timeline
|
||||
|
||||
@@ -38,6 +38,14 @@ export interface CalculatedDeadline {
|
||||
priority: "mandatory" | "recommended" | "optional" | "informational";
|
||||
ruleRef: string;
|
||||
legalSource?: string;
|
||||
// legalSourceDisplay is the pretty form ("UPC RoP R.220(1)") produced
|
||||
// by FormatLegalSourceDisplay on the backend. Renderer prefers this
|
||||
// over ruleRef when set; falls back to ruleRef otherwise.
|
||||
legalSourceDisplay?: string;
|
||||
// legalSourceURL is the youpc.org/laws permalink when the cited body
|
||||
// is hosted there (UPCRoP / UPCA / UPCS today). Empty for DE/EPA/EU
|
||||
// bodies — the renderer shows display text without a link.
|
||||
legalSourceURL?: string;
|
||||
notes?: string;
|
||||
notesEN?: string;
|
||||
dueDate: string;
|
||||
@@ -240,9 +248,20 @@ export function deadlineCardHtml(dl: CalculatedDeadline, opts: CardOpts): string
|
||||
? `<div class="timeline-adjusted">⚠ ${formatAdjustedNote(dl)}</div>`
|
||||
: "";
|
||||
|
||||
const ruleRef = dl.ruleRef
|
||||
? `<span class="timeline-rule">${dl.ruleRef}</span>`
|
||||
: "";
|
||||
// Prefer the structured legalSource (pretty display + youpc.org link
|
||||
// when hosted there) over the bare rule_code fallback. UPC.RoP rules
|
||||
// link to /laws/UPCRoP/<n>; DE / EPA / EU bodies have no youpc home
|
||||
// yet so we render display text plain.
|
||||
const legalDisplay = dl.legalSourceDisplay || "";
|
||||
const legalURL = dl.legalSourceURL || "";
|
||||
let ruleRef = "";
|
||||
if (legalDisplay && legalURL) {
|
||||
ruleRef = `<a class="timeline-rule timeline-rule--link" href="${escAttr(legalURL)}" target="_blank" rel="noopener noreferrer">${escHtml(legalDisplay)}</a>`;
|
||||
} else if (legalDisplay) {
|
||||
ruleRef = `<span class="timeline-rule">${escHtml(legalDisplay)}</span>`;
|
||||
} else if (dl.ruleRef) {
|
||||
ruleRef = `<span class="timeline-rule">${escHtml(dl.ruleRef)}</span>`;
|
||||
}
|
||||
|
||||
const noteText = getLang() === "en" ? (dl.notesEN || dl.notes) : dl.notes;
|
||||
const notes = noteText
|
||||
|
||||
@@ -494,6 +494,12 @@ export function renderFristenrechner(): string {
|
||||
<span data-i18n="deadlines.flag.inf_amend">Mit Antrag auf Patentänderung (R.30)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="date-field-row" id="inf-po-flag-row" style="display:none">
|
||||
<label className="date-label">
|
||||
<input type="checkbox" id="inf-po-flag" />
|
||||
<span data-i18n="deadlines.flag.inf_po">Mit Einspruch (R.19)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="date-field-row" id="rev-amend-flag-row" style="display:none">
|
||||
<label className="date-label">
|
||||
<input type="checkbox" id="rev-amend-flag" />
|
||||
@@ -506,6 +512,12 @@ export function renderFristenrechner(): string {
|
||||
<span data-i18n="deadlines.flag.rev_cci">Mit Verletzungswiderklage (R.49.2.b)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="date-field-row" id="rev-po-flag-row" style="display:none">
|
||||
<label className="date-label">
|
||||
<input type="checkbox" id="rev-po-flag" />
|
||||
<span data-i18n="deadlines.flag.rev_po">Mit Einspruch (R.19 i.V.m. R.46)</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" id="calculate-btn" className="calculate-btn" data-i18n="deadlines.calculate">
|
||||
Fristen berechnen
|
||||
</button>
|
||||
|
||||
@@ -1026,8 +1026,10 @@ export type I18nKey =
|
||||
| "deadlines.filter.today"
|
||||
| "deadlines.flag.ccr"
|
||||
| "deadlines.flag.inf_amend"
|
||||
| "deadlines.flag.inf_po"
|
||||
| "deadlines.flag.rev_amend"
|
||||
| "deadlines.flag.rev_cci"
|
||||
| "deadlines.flag.rev_po"
|
||||
| "deadlines.form.approval_hint"
|
||||
| "deadlines.heading"
|
||||
| "deadlines.inbox.all"
|
||||
|
||||
@@ -156,6 +156,47 @@ export function renderVerfahrensablauf(): string {
|
||||
<label htmlFor="court-picker" className="date-label" data-i18n="deadlines.court.label">Gericht:</label>
|
||||
<select id="court-picker" className="date-input"></select>
|
||||
</div>
|
||||
{/* Proceeding-specific flag rows — mirror /tools/fristenrechner
|
||||
so an abstract-browse user can model the same variants
|
||||
(CCR, Patentänderung, Verletzungswiderklage,
|
||||
Vorab-Einrede). Show/hide driven by selectedType in
|
||||
the client. */}
|
||||
<div className="date-field-row" id="ccr-flag-row" style="display:none">
|
||||
<label className="date-label">
|
||||
<input type="checkbox" id="ccr-flag" />
|
||||
<span data-i18n="deadlines.flag.ccr">Mit Widerklage auf Nichtigkeit</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="date-field-row date-field-row--nested" id="inf-amend-flag-row" style="display:none">
|
||||
<label className="date-label">
|
||||
<input type="checkbox" id="inf-amend-flag" />
|
||||
<span data-i18n="deadlines.flag.inf_amend">Mit Antrag auf Patentänderung (R.30)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="date-field-row" id="inf-po-flag-row" style="display:none">
|
||||
<label className="date-label">
|
||||
<input type="checkbox" id="inf-po-flag" />
|
||||
<span data-i18n="deadlines.flag.inf_po">Mit Einspruch (R.19)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="date-field-row" id="rev-amend-flag-row" style="display:none">
|
||||
<label className="date-label">
|
||||
<input type="checkbox" id="rev-amend-flag" />
|
||||
<span data-i18n="deadlines.flag.rev_amend">Mit Antrag auf Patentänderung (R.49.2.a)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="date-field-row" id="rev-cci-flag-row" style="display:none">
|
||||
<label className="date-label">
|
||||
<input type="checkbox" id="rev-cci-flag" />
|
||||
<span data-i18n="deadlines.flag.rev_cci">Mit Verletzungswiderklage (R.49.2.b)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="date-field-row" id="rev-po-flag-row" style="display:none">
|
||||
<label className="date-label">
|
||||
<input type="checkbox" id="rev-po-flag" />
|
||||
<span data-i18n="deadlines.flag.rev_po">Mit Einspruch (R.19 i.V.m. R.46)</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" id="calculate-btn" className="calculate-btn" data-i18n="deadlines.calculate">
|
||||
Fristen berechnen
|
||||
</button>
|
||||
|
||||
@@ -870,6 +870,49 @@ func FormatLegalSourceDisplay(src string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// BuildLegalSourceURL maps a structured legal_source code to a
|
||||
// youpc.org/laws permalink when the cited body is hosted there. Today
|
||||
// youpc only carries the UPC corpus (UPCA, UPCS, UPCRoP); DE national
|
||||
// codes (PatG, ZPO) and EPO bodies (EPÜ, EPC-R, RPBA) have no youpc
|
||||
// home yet, so the helper returns the empty string for those and the
|
||||
// caller renders the display string as plain text.
|
||||
//
|
||||
// Inputs mirror FormatLegalSourceDisplay — structured dot-separated
|
||||
// codes like UPC.RoP.220.1, UPC.UPCA.83. Sub-paragraph segments
|
||||
// beyond the law-number position are dropped; youpc resolves the page
|
||||
// at <type>/<number> granularity.
|
||||
//
|
||||
// UPC.RoP.220.1 → https://youpc.org/laws/UPCRoP/220
|
||||
// UPC.RoP.29.a → https://youpc.org/laws/UPCRoP/29
|
||||
// UPC.UPCA.83 → https://youpc.org/laws/UPCA/83
|
||||
// DE.ZPO.276.1 → "" (no youpc home — render display text plain)
|
||||
func BuildLegalSourceURL(src string) string {
|
||||
src = strings.TrimSpace(src)
|
||||
if src == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(src, ".")
|
||||
if len(parts) < 3 {
|
||||
return ""
|
||||
}
|
||||
var lawType string
|
||||
switch parts[0] + "." + parts[1] {
|
||||
case "UPC.RoP":
|
||||
lawType = "UPCRoP"
|
||||
case "UPC.UPCA":
|
||||
lawType = "UPCA"
|
||||
case "UPC.UPCS":
|
||||
lawType = "UPCS"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
number := parts[2]
|
||||
if number == "" {
|
||||
return ""
|
||||
}
|
||||
return "https://youpc.org/laws/" + lawType + "/" + number
|
||||
}
|
||||
|
||||
// RefreshSearchView re-populates the materialised view. Safe to call on
|
||||
// every server boot — it's a CONCURRENTLY refresh against a < 1k row
|
||||
// view, well under 100 ms in practice. Called from cmd/server/main.go
|
||||
|
||||
@@ -40,6 +40,37 @@ func TestFormatLegalSourceDisplay(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildLegalSourceURL covers the structured-form → youpc.org/laws
|
||||
// permalink mapping. Only the UPC corpus has a youpc home today;
|
||||
// DE/EPA/EU bodies fall through to the empty string and the renderer
|
||||
// shows display text without a link.
|
||||
func TestBuildLegalSourceURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"UPC.RoP.23.1", "https://youpc.org/laws/UPCRoP/23"},
|
||||
{"UPC.RoP.139", "https://youpc.org/laws/UPCRoP/139"},
|
||||
{"UPC.RoP.220.1", "https://youpc.org/laws/UPCRoP/220"},
|
||||
{"UPC.RoP.29.a", "https://youpc.org/laws/UPCRoP/29"},
|
||||
{"UPC.RoP.49.2.a", "https://youpc.org/laws/UPCRoP/49"},
|
||||
{"UPC.UPCA.83", "https://youpc.org/laws/UPCA/83"},
|
||||
{"UPC.UPCS.40.1", "https://youpc.org/laws/UPCS/40"},
|
||||
{"DE.PatG.82.1", ""},
|
||||
{"DE.ZPO.276.1", ""},
|
||||
{"EU.EPÜ.108", ""},
|
||||
{"EU.EPC-R.79.1", ""},
|
||||
{"EU.RPBA.12.1.c", ""},
|
||||
{"UPC.RoP", ""},
|
||||
{"", ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := BuildLegalSourceURL(c.in)
|
||||
if got != c.want {
|
||||
t.Errorf("BuildLegalSourceURL(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeQuery covers the input-side legal-prefix stripping that
|
||||
// keeps "§ 82" / "Art. 108" findable against structured legal_source
|
||||
// values that don't carry the prefix.
|
||||
|
||||
@@ -54,6 +54,15 @@ type UIDeadline struct {
|
||||
Priority string `json:"priority"`
|
||||
RuleRef string `json:"ruleRef"`
|
||||
LegalSource string `json:"legalSource,omitempty"`
|
||||
// LegalSourceDisplay is the pretty form (e.g. "UPC RoP R.220(1)")
|
||||
// of LegalSource, produced by FormatLegalSourceDisplay. Frontend
|
||||
// renders this in the deadline card meta line; falls back to
|
||||
// RuleRef when LegalSource is empty.
|
||||
LegalSourceDisplay string `json:"legalSourceDisplay,omitempty"`
|
||||
// LegalSourceURL is the youpc.org/laws permalink when the cited
|
||||
// body is hosted there (UPCRoP / UPCA / UPCS today). Empty for
|
||||
// DE/EPA/EU bodies — the renderer shows display text without a link.
|
||||
LegalSourceURL string `json:"legalSourceURL,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
NotesEN string `json:"notesEN,omitempty"`
|
||||
DueDate string `json:"dueDate"`
|
||||
@@ -283,6 +292,8 @@ func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, t
|
||||
}
|
||||
if r.LegalSource != nil {
|
||||
d.LegalSource = *r.LegalSource
|
||||
d.LegalSourceDisplay = FormatLegalSourceDisplay(*r.LegalSource)
|
||||
d.LegalSourceURL = BuildLegalSourceURL(*r.LegalSource)
|
||||
}
|
||||
if r.DeadlineNotes != nil {
|
||||
d.Notes = *r.DeadlineNotes
|
||||
@@ -599,6 +610,7 @@ type RuleCalculationRule struct {
|
||||
RuleRef string `json:"ruleRef,omitempty"`
|
||||
LegalSource string `json:"legalSource,omitempty"`
|
||||
LegalSourceDisplay string `json:"legalSourceDisplay,omitempty"`
|
||||
LegalSourceURL string `json:"legalSourceURL,omitempty"`
|
||||
DurationValue int `json:"durationValue"`
|
||||
DurationUnit string `json:"durationUnit"`
|
||||
Party string `json:"party,omitempty"`
|
||||
@@ -670,6 +682,7 @@ func (s *FristenrechnerService) CalculateRule(ctx context.Context, params CalcRu
|
||||
if rule.LegalSource != nil {
|
||||
out.Rule.LegalSource = *rule.LegalSource
|
||||
out.Rule.LegalSourceDisplay = FormatLegalSourceDisplay(*rule.LegalSource)
|
||||
out.Rule.LegalSourceURL = BuildLegalSourceURL(*rule.LegalSource)
|
||||
}
|
||||
if rule.PrimaryParty != nil {
|
||||
out.Rule.Party = *rule.PrimaryParty
|
||||
@@ -1217,6 +1230,8 @@ func (s *FristenrechnerService) calculateByTriggerEvent(
|
||||
}
|
||||
if r.LegalSource != nil {
|
||||
d.LegalSource = *r.LegalSource
|
||||
d.LegalSourceDisplay = FormatLegalSourceDisplay(*r.LegalSource)
|
||||
d.LegalSourceURL = BuildLegalSourceURL(*r.LegalSource)
|
||||
}
|
||||
if r.DeadlineNotes != nil {
|
||||
d.Notes = *r.DeadlineNotes
|
||||
|
||||
Reference in New Issue
Block a user