diff --git a/frontend/build.ts b/frontend/build.ts index 4be7bb2..98f40e0 100644 --- a/frontend/build.ts +++ b/frontend/build.ts @@ -3,6 +3,7 @@ import { join } from "path"; import { renderIndex } from "./src/index"; import { renderLogin } from "./src/login"; import { renderKostenrechner } from "./src/kostenrechner"; +import { renderFristenrechner } from "./src/fristenrechner"; const DIST = join(import.meta.dir, "dist"); @@ -16,6 +17,7 @@ async function build() { entrypoints: [ join(import.meta.dir, "src/client/login.ts"), join(import.meta.dir, "src/client/kostenrechner.ts"), + join(import.meta.dir, "src/client/fristenrechner.ts"), ], outdir: join(DIST, "assets"), naming: "[name].js", @@ -40,6 +42,7 @@ async function build() { await Bun.write(join(DIST, "index.html"), renderIndex()); await Bun.write(join(DIST, "login.html"), renderLogin("login.js")); await Bun.write(join(DIST, "kostenrechner.html"), renderKostenrechner()); + await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner()); console.log("Build complete \u2192 dist/"); } diff --git a/frontend/src/client/fristenrechner.ts b/frontend/src/client/fristenrechner.ts new file mode 100644 index 0000000..a1ccaab --- /dev/null +++ b/frontend/src/client/fristenrechner.ts @@ -0,0 +1,189 @@ +// Fristenrechner client-side logic +// 3-step wizard: select proceeding → enter date → view timeline + +interface CalculatedDeadline { + code: string; + name: string; + nameEN: string; + party: string; + isMandatory: boolean; + ruleRef: string; + notes?: string; + dueDate: string; + originalDate: string; + wasAdjusted: boolean; + isRootEvent: boolean; + isCourtSet: boolean; +} + +interface DeadlineResponse { + proceedingType: string; + proceedingName: string; + triggerDate: string; + deadlines: CalculatedDeadline[]; +} + +const PARTY_LABELS: Record = { + claimant: ["Kl\u00e4ger", "party-claimant"], + defendant: ["Beklagter", "party-defendant"], + court: ["Gericht", "party-court"], + both: ["Beide", "party-both"], +}; + +function formatDate(dateStr: string): string { + if (!dateStr) return "\u2014"; + const d = new Date(dateStr + "T00:00:00"); + return d.toLocaleDateString("de-DE", { + weekday: "short", + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +} + +function partyBadge(party: string): string { + const [label, cls] = PARTY_LABELS[party] || [party, "party-both"]; + return `${label}`; +} + +let selectedType = ""; + +function showStep(n: number) { + for (let i = 1; i <= 3; i++) { + const el = document.getElementById(`step-${i}`); + if (el) el.style.display = i <= n ? "block" : "none"; + } + const resetBtn = document.getElementById("reset-btn")!; + resetBtn.style.display = n > 1 ? "block" : "none"; +} + +async function calculate() { + const dateInput = document.getElementById("trigger-date") as HTMLInputElement; + const triggerDate = dateInput.value; + if (!triggerDate || !selectedType) return; + + try { + const resp = await fetch("/api/tools/fristenrechner", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + proceedingType: selectedType, + triggerDate, + }), + }); + + if (!resp.ok) { + const err = await resp.json(); + console.error("API error:", err); + return; + } + + const data: DeadlineResponse = await resp.json(); + renderTimeline(data); + showStep(3); + } catch (e) { + console.error("Fetch error:", e); + } +} + +function renderTimeline(data: DeadlineResponse) { + const container = document.getElementById("timeline-container")!; + const printBtn = document.getElementById("fristen-print-btn")!; + + let html = `
+ ${data.proceedingName} + Trigger: ${formatDate(data.triggerDate)} +
`; + + html += '
'; + + for (const dl of data.deadlines) { + const dateStr = dl.isCourtSet + ? 'vom Gericht bestimmt
set by court
' + : `${formatDate(dl.dueDate)}`; + + const mandatoryBadge = dl.isMandatory + ? "" + : 'optional'; + + const adjustedNote = dl.wasAdjusted + ? `
\u26a0 Verschoben: ${formatDate(dl.originalDate)} \u2192 ${formatDate(dl.dueDate)} (Wochenende/Feiertag)
` + : ""; + + const ruleRef = dl.ruleRef + ? `${dl.ruleRef}` + : ""; + + const notes = dl.notes + ? `
${dl.notes}
` + : ""; + + html += ` +
+
+
+
+
+
+
+ + ${dl.name} + ${dl.nameEN} + ${mandatoryBadge} + + ${dateStr} +
+
+ ${partyBadge(dl.party)} + ${ruleRef} +
+ ${adjustedNote} + ${notes} +
+
+ `; + } + + html += "
"; + container.innerHTML = html; + printBtn.style.display = "block"; +} + +function reset() { + selectedType = ""; + document.querySelectorAll(".proceeding-btn").forEach((b) => b.classList.remove("active")); + document.getElementById("timeline-container")!.innerHTML = ""; + document.getElementById("fristen-print-btn")!.style.display = "none"; + showStep(1); +} + +document.addEventListener("DOMContentLoaded", () => { + // Proceeding type selection + document.querySelectorAll(".proceeding-btn").forEach((btn) => { + btn.addEventListener("click", () => { + document.querySelectorAll(".proceeding-btn").forEach((b) => b.classList.remove("active")); + btn.classList.add("active"); + selectedType = btn.dataset.code!; + + // Update trigger event name + const name = btn.querySelector("strong")?.textContent || ""; + document.getElementById("trigger-event")!.textContent = name; + + showStep(2); + }); + }); + + // Calculate button + document.getElementById("calculate-btn")!.addEventListener("click", calculate); + + // Also calculate on Enter in date field + document.getElementById("trigger-date")!.addEventListener("keydown", (e) => { + if ((e as KeyboardEvent).key === "Enter") calculate(); + }); + + // Reset button + document.getElementById("reset-btn")!.addEventListener("click", reset); + + // Print button + document.getElementById("fristen-print-btn")!.addEventListener("click", () => window.print()); +}); diff --git a/frontend/src/fristenrechner.tsx b/frontend/src/fristenrechner.tsx new file mode 100644 index 0000000..e0f0bce --- /dev/null +++ b/frontend/src/fristenrechner.tsx @@ -0,0 +1,141 @@ +import { h } from "./jsx"; +import { Header } from "./components/Header"; +import { Footer } from "./components/Footer"; + +interface ProceedingDef { + code: string; + name: string; + nameEN: string; +} + +function proceedingBtn(p: ProceedingDef): string { + return ( + + ); +} + +const UPC_TYPES: ProceedingDef[] = [ + { code: "UPC_INF", name: "Verletzungsverfahren", nameEN: "Infringement" }, + { code: "UPC_REV", name: "Nichtigkeitsklage", nameEN: "Revocation" }, + { code: "UPC_PI", name: "Einstw. Ma\u00dfnahmen", nameEN: "Provisional Measures" }, + { code: "UPC_APP", name: "Berufung", nameEN: "Appeal" }, +]; + +const DE_TYPES: ProceedingDef[] = [ + { code: "DE_INF", name: "Verletzungsklage (LG)", nameEN: "Infringement" }, + { code: "DE_NULL", name: "Nichtigkeitsverfahren", nameEN: "Nullity" }, +]; + +const EPA_TYPES: ProceedingDef[] = [ + { code: "EPA_OPP", name: "Einspruchsverfahren", nameEN: "Opposition" }, + { code: "EPA_APP", name: "Beschwerdeverfahren", nameEN: "Appeal" }, + { code: "EP_GRANT", name: "EP-Erteilungsverfahren", nameEN: "Grant Procedure" }, +]; + +export function renderFristenrechner(): string { + const today = new Date().toISOString().split("T")[0]; + + return "" + ( + + + + + Fristenrechner — patholo + + + +
+ +
+
+
+
+

Fristenrechner Patent Deadline Calculator

+

+ Berechnung von Verfahrensfristen für UPC-, deutsche und EPA-Verfahren. +
+ Calculate procedural deadlines for UPC, German, and EPA proceedings. +

+
+ +
+
+

+ 1 + Verfahrensart wählen Select Proceeding Type +

+ +
+

UPC Unified Patent Court

+
+ {UPC_TYPES.map((p) => proceedingBtn(p))} +
+
+ +
+

Deutsche Gerichte German Courts

+
+ {DE_TYPES.map((p) => proceedingBtn(p))} +
+
+ +
+

EPA European Patent Office

+
+ {EPA_TYPES.map((p) => proceedingBtn(p))} +
+
+
+ + + + + + +
+
+
+
+ +