feat: Checklisten — interactive filing checklists with localStorage state
Six bilingual patent-workflow checklists (UPC Statement of Claim, Defence, Confidentiality Application, Representative Registration; BPatG Nullity; EPO Opposition) with grouped items, rule references, and tips. Index page lists cards with regime filter and per-checklist progress; detail page persists check state in localStorage (patholo:checklist:<slug>), shows a live progress bar, supports reset and print, and submits feedback via Supabase checklisten_feedback.
This commit is contained in:
20
docs/migrations/002_checklisten_feedback.sql
Normal file
20
docs/migrations/002_checklisten_feedback.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Migration: Checklisten feedback table for patholo
|
||||
-- Target: ydb.youpc.org (patholo's Supabase instance)
|
||||
-- Apply via Supabase SQL editor or psql
|
||||
|
||||
CREATE TABLE IF NOT EXISTS checklisten_feedback (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
feedback_type text NOT NULL,
|
||||
checklist text NOT NULL DEFAULT '',
|
||||
message text NOT NULL,
|
||||
submitted_by text DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
ALTER TABLE checklisten_feedback ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Authenticated users may insert feedback; read access is admin-only
|
||||
-- (matches the gebuehrentabellen_feedback / glossar_suggestions pattern).
|
||||
CREATE POLICY "authenticated_insert_feedback" ON checklisten_feedback
|
||||
FOR INSERT TO authenticated
|
||||
WITH CHECK (true);
|
||||
@@ -8,6 +8,8 @@ import { renderDownloads } from "./src/downloads";
|
||||
import { renderLinks } from "./src/links";
|
||||
import { renderGlossar } from "./src/glossar";
|
||||
import { renderGebuehrentabellen } from "./src/gebuehrentabellen";
|
||||
import { renderChecklisten } from "./src/checklisten";
|
||||
import { renderChecklistenDetail } from "./src/checklisten-detail";
|
||||
|
||||
const DIST = join(import.meta.dir, "dist");
|
||||
|
||||
@@ -27,6 +29,8 @@ async function build() {
|
||||
join(import.meta.dir, "src/client/links.ts"),
|
||||
join(import.meta.dir, "src/client/glossar.ts"),
|
||||
join(import.meta.dir, "src/client/gebuehrentabellen.ts"),
|
||||
join(import.meta.dir, "src/client/checklisten.ts"),
|
||||
join(import.meta.dir, "src/client/checklisten-detail.ts"),
|
||||
],
|
||||
outdir: join(DIST, "assets"),
|
||||
naming: "[name].js",
|
||||
@@ -56,6 +60,8 @@ async function build() {
|
||||
await Bun.write(join(DIST, "links.html"), renderLinks());
|
||||
await Bun.write(join(DIST, "glossar.html"), renderGlossar());
|
||||
await Bun.write(join(DIST, "gebuehrentabellen.html"), renderGebuehrentabellen());
|
||||
await Bun.write(join(DIST, "checklisten.html"), renderChecklisten());
|
||||
await Bun.write(join(DIST, "checklisten-detail.html"), renderChecklistenDetail());
|
||||
|
||||
console.log("Build complete \u2192 dist/");
|
||||
}
|
||||
|
||||
95
frontend/src/checklisten-detail.tsx
Normal file
95
frontend/src/checklisten-detail.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { h } from "./jsx";
|
||||
import { Sidebar } from "./components/Sidebar";
|
||||
import { Footer } from "./components/Footer";
|
||||
|
||||
export function renderChecklistenDetail(): string {
|
||||
return "<!DOCTYPE html>" + (
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title data-i18n="checklisten.title">Checkliste — patHoLo</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
<body className="has-sidebar">
|
||||
<Sidebar currentPath="/checklisten" />
|
||||
|
||||
<main>
|
||||
<section className="tool-page">
|
||||
<div className="container">
|
||||
<a href="/checklisten" className="checklist-back">
|
||||
<span className="checklist-back-arrow">←</span>
|
||||
<span data-i18n="checklisten.back">Zurück zur Übersicht</span>
|
||||
</a>
|
||||
|
||||
<div className="tool-header checklist-detail-header">
|
||||
<div className="checklist-detail-head-row">
|
||||
<div>
|
||||
<h1 id="checklist-title"> </h1>
|
||||
<p className="tool-subtitle" id="checklist-subtitle"> </p>
|
||||
<dl className="checklist-meta" id="checklist-meta" />
|
||||
</div>
|
||||
<div className="checklist-actions">
|
||||
<button type="button" id="btn-print" className="btn-ghost" data-i18n="checklisten.print">Drucken</button>
|
||||
<button type="button" id="btn-reset" className="btn-ghost" data-i18n="checklisten.reset">Zurücksetzen</button>
|
||||
<button type="button" id="btn-feedback" className="btn-suggest">
|
||||
<span data-i18n="checklisten.feedback.btn">Feedback</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="checklist-progress">
|
||||
<div className="checklist-progress-bar">
|
||||
<div className="checklist-progress-fill" id="progress-fill" />
|
||||
</div>
|
||||
<span className="checklist-progress-label" id="progress-label">0 / 0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="checklist-groups" className="checklist-groups" />
|
||||
|
||||
<div className="checklist-print-footer">
|
||||
<p className="checklist-disclaimer" data-i18n="checklisten.disclaimer">
|
||||
Hinweis: Diese Checklisten dienen als Gedächtnisstütze und ersetzen keine Prüfung im Einzelfall. Maßgeblich sind die jeweils geltenden Verfahrensregeln.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Feedback modal */}
|
||||
<div className="modal-overlay" id="feedback-modal" style="display:none">
|
||||
<div className="modal-card">
|
||||
<div className="modal-header">
|
||||
<h2 data-i18n="checklisten.feedback.title">Feedback zur Checkliste</h2>
|
||||
<button className="modal-close" id="modal-close" type="button">×</button>
|
||||
</div>
|
||||
<form id="feedback-form">
|
||||
<div className="form-field">
|
||||
<label htmlFor="feedback-type" data-i18n="checklisten.feedback.type">Art</label>
|
||||
<select id="feedback-type" required>
|
||||
<option value="error" data-i18n="checklisten.feedback.error">Fehler gefunden</option>
|
||||
<option value="missing" data-i18n="checklisten.feedback.missing">Fehlender Punkt</option>
|
||||
<option value="suggestion" data-i18n="checklisten.feedback.suggestion">Verbesserungsvorschlag</option>
|
||||
<option value="other" data-i18n="checklisten.feedback.other">Sonstiges</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="feedback-message" data-i18n="checklisten.feedback.message">Nachricht</label>
|
||||
<textarea id="feedback-message" rows={4} required />
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
<button type="button" className="btn-cancel" id="modal-cancel" data-i18n="checklisten.feedback.cancel">Abbrechen</button>
|
||||
<button type="submit" className="btn-submit" data-i18n="checklisten.feedback.submit">Absenden</button>
|
||||
</div>
|
||||
<p className="form-msg" id="feedback-msg" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
<script src="/assets/checklisten-detail.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
44
frontend/src/checklisten.tsx
Normal file
44
frontend/src/checklisten.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { h } from "./jsx";
|
||||
import { Sidebar } from "./components/Sidebar";
|
||||
import { Footer } from "./components/Footer";
|
||||
|
||||
export function renderChecklisten(): string {
|
||||
return "<!DOCTYPE html>" + (
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title data-i18n="checklisten.title">Checklisten — patHoLo</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
<body className="has-sidebar">
|
||||
<Sidebar currentPath="/checklisten" />
|
||||
|
||||
<main>
|
||||
<section className="tool-page">
|
||||
<div className="container">
|
||||
<div className="tool-header">
|
||||
<h1 data-i18n="checklisten.heading">Checklisten</h1>
|
||||
<p className="tool-subtitle" data-i18n="checklisten.subtitle">
|
||||
Interaktive Checklisten für typische Verfahrensschritte vor UPC, BPatG und EPA.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="checklist-filters" id="checklist-filters">
|
||||
<button className="filter-pill active" data-regime="all" type="button" data-i18n="checklisten.filter.all">Alle</button>
|
||||
<button className="filter-pill" data-regime="UPC" type="button">UPC</button>
|
||||
<button className="filter-pill" data-regime="DE" type="button" data-i18n="checklisten.filter.de">DE</button>
|
||||
<button className="filter-pill" data-regime="EPA" type="button">EPA</button>
|
||||
</div>
|
||||
|
||||
<div className="checklist-grid" id="checklist-grid" />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<script src="/assets/checklisten.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
272
frontend/src/client/checklisten-detail.ts
Normal file
272
frontend/src/client/checklisten-detail.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { initI18n, onLangChange, t, getLang } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
|
||||
interface ChecklistItem {
|
||||
labelDE: string;
|
||||
labelEN: string;
|
||||
noteDE?: string;
|
||||
noteEN?: string;
|
||||
rule?: string;
|
||||
}
|
||||
|
||||
interface ChecklistGroup {
|
||||
titleDE: string;
|
||||
titleEN: string;
|
||||
items: ChecklistItem[];
|
||||
}
|
||||
|
||||
interface Checklist {
|
||||
slug: string;
|
||||
titleDE: string;
|
||||
titleEN: string;
|
||||
descriptionDE: string;
|
||||
descriptionEN: string;
|
||||
regime: string;
|
||||
courtDE: string;
|
||||
courtEN: string;
|
||||
deadlineDE?: string;
|
||||
deadlineEN?: string;
|
||||
referenceDE?: string;
|
||||
referenceEN?: string;
|
||||
groups: ChecklistGroup[];
|
||||
}
|
||||
|
||||
let checklist: Checklist | null = null;
|
||||
let state: Record<string, boolean> = {};
|
||||
|
||||
function storageKey(slug: string): string {
|
||||
return `patholo:checklist:${slug}`;
|
||||
}
|
||||
|
||||
function esc(s: string): string {
|
||||
const d = document.createElement("div");
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function itemKey(groupIdx: number, itemIdx: number): string {
|
||||
return `g${groupIdx}-i${itemIdx}`;
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
if (!checklist) return;
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey(checklist.slug));
|
||||
state = raw ? (JSON.parse(raw) as Record<string, boolean>) : {};
|
||||
} catch {
|
||||
state = {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
if (!checklist) return;
|
||||
localStorage.setItem(storageKey(checklist.slug), JSON.stringify(state));
|
||||
}
|
||||
|
||||
function totalItems(): number {
|
||||
if (!checklist) return 0;
|
||||
return checklist.groups.reduce((n, g) => n + g.items.length, 0);
|
||||
}
|
||||
|
||||
function doneItems(): number {
|
||||
return Object.values(state).filter(Boolean).length;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const slug = window.location.pathname.split("/").pop() ?? "";
|
||||
const resp = await fetch(`/api/checklisten/${encodeURIComponent(slug)}`);
|
||||
if (!resp.ok) {
|
||||
document.title = "404 — patHoLo";
|
||||
const title = document.getElementById("checklist-title")!;
|
||||
title.textContent = t("checklisten.notfound");
|
||||
return;
|
||||
}
|
||||
checklist = await resp.json();
|
||||
loadState();
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
if (!checklist) return;
|
||||
renderHeader();
|
||||
renderGroups();
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
if (!checklist) return;
|
||||
const isEN = getLang() === "en";
|
||||
const title = isEN ? checklist.titleEN : checklist.titleDE;
|
||||
const desc = isEN ? checklist.descriptionEN : checklist.descriptionDE;
|
||||
const court = isEN ? checklist.courtEN : checklist.courtDE;
|
||||
const deadline = isEN ? checklist.deadlineEN : checklist.deadlineDE;
|
||||
const reference = isEN ? checklist.referenceEN : checklist.referenceDE;
|
||||
|
||||
document.title = `${title} — patHoLo`;
|
||||
document.getElementById("checklist-title")!.textContent = title;
|
||||
document.getElementById("checklist-subtitle")!.textContent = desc;
|
||||
|
||||
const courtLabel = isEN ? "Court / Authority" : "Gericht / Behörde";
|
||||
const deadlineLabel = isEN ? "Deadline" : "Frist";
|
||||
const refLabel = isEN ? "Reference" : "Rechtsgrundlage";
|
||||
const regimeLabel = isEN ? "Regime" : "Bereich";
|
||||
|
||||
const parts: string[] = [];
|
||||
parts.push(`<div class="checklist-meta-item"><dt>${regimeLabel}</dt><dd><span class="checklist-regime checklist-regime-${esc(checklist.regime)}">${esc(checklist.regime)}</span></dd></div>`);
|
||||
parts.push(`<div class="checklist-meta-item"><dt>${courtLabel}</dt><dd>${esc(court)}</dd></div>`);
|
||||
if (deadline) {
|
||||
parts.push(`<div class="checklist-meta-item"><dt>${deadlineLabel}</dt><dd>${esc(deadline)}</dd></div>`);
|
||||
}
|
||||
if (reference) {
|
||||
parts.push(`<div class="checklist-meta-item"><dt>${refLabel}</dt><dd>${esc(reference)}</dd></div>`);
|
||||
}
|
||||
document.getElementById("checklist-meta")!.innerHTML = parts.join("");
|
||||
}
|
||||
|
||||
function renderGroups() {
|
||||
if (!checklist) return;
|
||||
const isEN = getLang() === "en";
|
||||
const container = document.getElementById("checklist-groups")!;
|
||||
|
||||
container.innerHTML = checklist.groups.map((g, gi) => {
|
||||
const groupTitle = isEN ? g.titleEN : g.titleDE;
|
||||
const items = g.items.map((item, ii) => {
|
||||
const key = itemKey(gi, ii);
|
||||
const checked = !!state[key];
|
||||
const label = isEN ? item.labelEN : item.labelDE;
|
||||
const note = isEN ? item.noteEN : item.noteDE;
|
||||
const rule = item.rule;
|
||||
|
||||
const noteHTML = note ? `<p class="checklist-item-note">${esc(note)}</p>` : "";
|
||||
const ruleHTML = rule ? `<span class="checklist-item-rule">${esc(rule)}</span>` : "";
|
||||
|
||||
return `<li class="checklist-item${checked ? " checked" : ""}" data-key="${key}">
|
||||
<label class="checklist-item-label">
|
||||
<input type="checkbox" class="checklist-checkbox" data-key="${key}"${checked ? " checked" : ""} />
|
||||
<span class="checklist-item-body">
|
||||
<span class="checklist-item-row">
|
||||
<span class="checklist-item-text">${esc(label)}</span>
|
||||
${ruleHTML}
|
||||
</span>
|
||||
${noteHTML}
|
||||
</span>
|
||||
</label>
|
||||
</li>`;
|
||||
}).join("");
|
||||
|
||||
return `<section class="checklist-group">
|
||||
<h2 class="checklist-group-title">${esc(groupTitle)}</h2>
|
||||
<ol class="checklist-list">${items}</ol>
|
||||
</section>`;
|
||||
}).join("");
|
||||
|
||||
container.querySelectorAll<HTMLInputElement>(".checklist-checkbox").forEach((cb) => {
|
||||
cb.addEventListener("change", () => {
|
||||
const key = cb.dataset.key!;
|
||||
state[key] = cb.checked;
|
||||
saveState();
|
||||
const li = cb.closest(".checklist-item");
|
||||
if (li) li.classList.toggle("checked", cb.checked);
|
||||
updateProgress();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
const total = totalItems();
|
||||
const done = doneItems();
|
||||
const pct = total === 0 ? 0 : Math.round((done / total) * 100);
|
||||
|
||||
const fill = document.getElementById("progress-fill");
|
||||
if (fill) (fill as HTMLElement).style.width = `${pct}%`;
|
||||
|
||||
const label = document.getElementById("progress-label");
|
||||
if (label) {
|
||||
const doneLabel = getLang() === "en" ? "done" : "erledigt";
|
||||
label.textContent = `${done} / ${total} ${doneLabel}`;
|
||||
}
|
||||
}
|
||||
|
||||
function initReset() {
|
||||
document.getElementById("btn-reset")!.addEventListener("click", () => {
|
||||
if (!checklist) return;
|
||||
const ok = confirm(t("checklisten.reset.confirm"));
|
||||
if (!ok) return;
|
||||
state = {};
|
||||
saveState();
|
||||
renderGroups();
|
||||
updateProgress();
|
||||
});
|
||||
}
|
||||
|
||||
function initPrint() {
|
||||
document.getElementById("btn-print")!.addEventListener("click", () => {
|
||||
window.print();
|
||||
});
|
||||
}
|
||||
|
||||
function initFeedback() {
|
||||
const modal = document.getElementById("feedback-modal")!;
|
||||
const form = document.getElementById("feedback-form")!;
|
||||
const msg = document.getElementById("feedback-msg")!;
|
||||
|
||||
document.getElementById("btn-feedback")!.addEventListener("click", () => {
|
||||
msg.textContent = "";
|
||||
msg.className = "form-msg";
|
||||
modal.style.display = "flex";
|
||||
});
|
||||
|
||||
document.getElementById("modal-close")!.addEventListener("click", () => { modal.style.display = "none"; });
|
||||
document.getElementById("modal-cancel")!.addEventListener("click", () => { modal.style.display = "none"; });
|
||||
modal.addEventListener("click", (e) => { if (e.target === e.currentTarget) modal.style.display = "none"; });
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
if (!checklist) return;
|
||||
const submitBtn = form.querySelector(".btn-submit") as HTMLButtonElement;
|
||||
const payload = {
|
||||
feedback_type: (document.getElementById("feedback-type") as HTMLSelectElement).value,
|
||||
checklist: checklist.slug,
|
||||
message: (document.getElementById("feedback-message") as HTMLTextAreaElement).value.trim(),
|
||||
};
|
||||
|
||||
if (!payload.message) {
|
||||
msg.textContent = t("checklisten.feedback.error.required");
|
||||
msg.className = "form-msg form-msg-error";
|
||||
return;
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
try {
|
||||
const resp = await fetch("/api/checklisten/feedback", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
msg.textContent = t("checklisten.feedback.error.generic");
|
||||
msg.className = "form-msg form-msg-error";
|
||||
return;
|
||||
}
|
||||
msg.textContent = t("checklisten.feedback.success");
|
||||
msg.className = "form-msg form-msg-success";
|
||||
(document.getElementById("feedback-message") as HTMLTextAreaElement).value = "";
|
||||
setTimeout(() => { modal.style.display = "none"; }, 1500);
|
||||
} catch {
|
||||
msg.textContent = t("checklisten.feedback.error.generic");
|
||||
msg.className = "form-msg form-msg-error";
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initI18n();
|
||||
initSidebar();
|
||||
initReset();
|
||||
initPrint();
|
||||
initFeedback();
|
||||
onLangChange(renderAll);
|
||||
load();
|
||||
});
|
||||
104
frontend/src/client/checklisten.ts
Normal file
104
frontend/src/client/checklisten.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { initI18n, onLangChange, t, getLang } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
|
||||
interface ChecklistSummary {
|
||||
slug: string;
|
||||
titleDE: string;
|
||||
titleEN: string;
|
||||
descriptionDE: string;
|
||||
descriptionEN: string;
|
||||
regime: string;
|
||||
courtDE: string;
|
||||
courtEN: string;
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
let allChecklists: ChecklistSummary[] = [];
|
||||
let activeRegime = "all";
|
||||
|
||||
function storageKey(slug: string): string {
|
||||
return `patholo:checklist:${slug}`;
|
||||
}
|
||||
|
||||
function progressFor(slug: string, total: number): { done: number; total: number } {
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey(slug));
|
||||
if (!raw) return { done: 0, total };
|
||||
const obj = JSON.parse(raw) as Record<string, boolean>;
|
||||
const done = Object.values(obj).filter(Boolean).length;
|
||||
return { done: Math.min(done, total), total };
|
||||
} catch {
|
||||
return { done: 0, total };
|
||||
}
|
||||
}
|
||||
|
||||
function esc(s: string): string {
|
||||
const d = document.createElement("div");
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const resp = await fetch("/api/checklisten");
|
||||
if (!resp.ok) return;
|
||||
allChecklists = await resp.json();
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
const grid = document.getElementById("checklist-grid")!;
|
||||
const isEN = getLang() === "en";
|
||||
|
||||
const filtered = activeRegime === "all"
|
||||
? allChecklists
|
||||
: allChecklists.filter((c) => c.regime === activeRegime);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
grid.innerHTML = `<p class="checklist-empty" data-i18n="checklisten.empty">${t("checklisten.empty")}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = filtered.map((c) => {
|
||||
const title = isEN ? c.titleEN : c.titleDE;
|
||||
const desc = isEN ? c.descriptionEN : c.descriptionDE;
|
||||
const court = isEN ? c.courtEN : c.courtDE;
|
||||
const { done, total } = progressFor(c.slug, c.itemCount);
|
||||
const pct = total === 0 ? 0 : Math.round((done / total) * 100);
|
||||
const doneLabel = isEN ? "done" : "erledigt";
|
||||
return `<a href="/checklisten/${esc(c.slug)}" class="checklist-card">
|
||||
<div class="checklist-card-top">
|
||||
<span class="checklist-regime checklist-regime-${esc(c.regime)}">${esc(c.regime)}</span>
|
||||
<span class="checklist-card-count">${total} ${isEN ? "items" : "Punkte"}</span>
|
||||
</div>
|
||||
<h2 class="checklist-card-title">${esc(title)}</h2>
|
||||
<p class="checklist-card-desc">${esc(desc)}</p>
|
||||
<p class="checklist-card-court">${esc(court)}</p>
|
||||
<div class="checklist-card-progress">
|
||||
<div class="checklist-progress-bar">
|
||||
<div class="checklist-progress-fill" style="width:${pct}%"></div>
|
||||
</div>
|
||||
<span class="checklist-progress-label">${done} / ${total} ${doneLabel}</span>
|
||||
</div>
|
||||
</a>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function initFilters() {
|
||||
const container = document.getElementById("checklist-filters")!;
|
||||
container.addEventListener("click", (e) => {
|
||||
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>(".filter-pill");
|
||||
if (!btn) return;
|
||||
container.querySelectorAll(".filter-pill").forEach((p) => p.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
activeRegime = btn.dataset.regime ?? "all";
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initI18n();
|
||||
initSidebar();
|
||||
initFilters();
|
||||
onLangChange(render);
|
||||
load();
|
||||
});
|
||||
@@ -18,6 +18,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"nav.links": "Links",
|
||||
"nav.glossar": "Glossar",
|
||||
"nav.gebuehrentabellen": "Geb\u00fchrentabellen",
|
||||
"nav.checklisten": "Checklisten",
|
||||
"nav.logout": "Abmelden",
|
||||
|
||||
// Footer
|
||||
@@ -275,6 +276,35 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
// Index — Geb\u00fchrentabellen card
|
||||
"index.gebuehren.title": "Geb\u00fchrentabellen",
|
||||
"index.gebuehren.desc": "Interaktive Geb\u00fchrentabellen f\u00fcr GKG, RVG, UPC, EPA und PatKostG. Streitwert eingeben, Geb\u00fchr ablesen.",
|
||||
|
||||
// Checklisten
|
||||
"index.checklisten.title": "Checklisten",
|
||||
"index.checklisten.desc": "Interaktive Checklisten f\u00fcr UPC-, DE- und EPA-Verfahren. Fortschritt wird lokal gespeichert.",
|
||||
"checklisten.title": "Checklisten \u2014 patHoLo",
|
||||
"checklisten.heading": "Checklisten",
|
||||
"checklisten.subtitle": "Interaktive Checklisten f\u00fcr typische Verfahrensschritte vor UPC, BPatG und EPA. Abhaken, ausdrucken, kein Punkt vergessen.",
|
||||
"checklisten.filter.all": "Alle",
|
||||
"checklisten.filter.de": "DE",
|
||||
"checklisten.empty": "Keine Checklisten in dieser Kategorie.",
|
||||
"checklisten.back": "Zur\u00fcck zur \u00dcbersicht",
|
||||
"checklisten.print": "Drucken",
|
||||
"checklisten.reset": "Zur\u00fccksetzen",
|
||||
"checklisten.reset.confirm": "Alle H\u00e4kchen dieser Checkliste wirklich zur\u00fccksetzen?",
|
||||
"checklisten.notfound": "Checkliste nicht gefunden.",
|
||||
"checklisten.disclaimer": "Hinweis: Diese Checklisten dienen als Ged\u00e4chtnisst\u00fctze und ersetzen keine Pr\u00fcfung im Einzelfall. Ma\u00dfgeblich sind die jeweils geltenden Verfahrensregeln.",
|
||||
"checklisten.feedback.btn": "Feedback",
|
||||
"checklisten.feedback.title": "Feedback zur Checkliste",
|
||||
"checklisten.feedback.type": "Art",
|
||||
"checklisten.feedback.error": "Fehler gefunden",
|
||||
"checklisten.feedback.missing": "Fehlender Punkt",
|
||||
"checklisten.feedback.suggestion": "Verbesserungsvorschlag",
|
||||
"checklisten.feedback.other": "Sonstiges",
|
||||
"checklisten.feedback.message": "Nachricht",
|
||||
"checklisten.feedback.submit": "Absenden",
|
||||
"checklisten.feedback.cancel": "Abbrechen",
|
||||
"checklisten.feedback.success": "Danke f\u00fcr Ihr Feedback!",
|
||||
"checklisten.feedback.error.required": "Bitte geben Sie eine Nachricht ein.",
|
||||
"checklisten.feedback.error.generic": "Fehler beim Senden. Bitte versuchen Sie es erneut.",
|
||||
},
|
||||
|
||||
en: {
|
||||
@@ -286,6 +316,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"nav.links": "Links",
|
||||
"nav.glossar": "Glossary",
|
||||
"nav.gebuehrentabellen": "Fee Schedules",
|
||||
"nav.checklisten": "Checklists",
|
||||
"nav.logout": "Sign Out",
|
||||
|
||||
// Footer
|
||||
@@ -543,6 +574,35 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
// Index \u2014 Geb\u00fchrentabellen card
|
||||
"index.gebuehren.title": "Fee Schedules",
|
||||
"index.gebuehren.desc": "Interactive fee schedules for GKG, RVG, UPC, EPO, and PatKostG. Enter a dispute value, read the fee.",
|
||||
|
||||
// Checklisten
|
||||
"index.checklisten.title": "Checklists",
|
||||
"index.checklisten.desc": "Interactive filing checklists for UPC, German, and EPO proceedings. Progress is stored locally.",
|
||||
"checklisten.title": "Checklists \u2014 patHoLo",
|
||||
"checklisten.heading": "Checklists",
|
||||
"checklisten.subtitle": "Interactive checklists for typical procedural steps before the UPC, German Patent Court, and EPO. Tick off, print, miss nothing.",
|
||||
"checklisten.filter.all": "All",
|
||||
"checklisten.filter.de": "DE",
|
||||
"checklisten.empty": "No checklists in this category.",
|
||||
"checklisten.back": "Back to overview",
|
||||
"checklisten.print": "Print",
|
||||
"checklisten.reset": "Reset",
|
||||
"checklisten.reset.confirm": "Really reset all checkboxes for this checklist?",
|
||||
"checklisten.notfound": "Checklist not found.",
|
||||
"checklisten.disclaimer": "Note: These checklists are aides-memoire only and do not replace case-by-case review. The applicable procedural rules are controlling.",
|
||||
"checklisten.feedback.btn": "Feedback",
|
||||
"checklisten.feedback.title": "Checklist feedback",
|
||||
"checklisten.feedback.type": "Type",
|
||||
"checklisten.feedback.error": "Error found",
|
||||
"checklisten.feedback.missing": "Missing item",
|
||||
"checklisten.feedback.suggestion": "Improvement suggestion",
|
||||
"checklisten.feedback.other": "Other",
|
||||
"checklisten.feedback.message": "Message",
|
||||
"checklisten.feedback.submit": "Submit",
|
||||
"checklisten.feedback.cancel": "Cancel",
|
||||
"checklisten.feedback.success": "Thank you for your feedback!",
|
||||
"checklisten.feedback.error.required": "Please enter a message.",
|
||||
"checklisten.feedback.error.generic": "Error submitting. Please try again.",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const ICON_DOWNLOAD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor
|
||||
const ICON_LINK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
|
||||
const ICON_BOOK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>';
|
||||
const ICON_TABLE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/></svg>';
|
||||
const ICON_CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
|
||||
const ICON_GLOBE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/></svg>';
|
||||
const ICON_LOGOUT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>';
|
||||
const ICON_PIN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v6l-2 4h10l-2-4V4"/><line x1="12" y1="16" x2="12" y2="21"/><line x1="8" y1="4" x2="16" y2="4"/></svg>';
|
||||
@@ -45,6 +46,7 @@ export function Sidebar({ currentPath }: SidebarProps): string {
|
||||
{navItem("/tools/kostenrechner", ICON_CALC, "nav.kostenrechner", "Kostenrechner", currentPath)}
|
||||
{navItem("/tools/fristenrechner", ICON_CLOCK, "nav.fristenrechner", "Fristenrechner", currentPath)}
|
||||
{navItem("/tools/gebuehrentabellen", ICON_TABLE, "nav.gebuehrentabellen", "Gebührentabellen", currentPath)}
|
||||
{navItem("/checklisten", ICON_CHECK, "nav.checklisten", "Checklisten", currentPath)}
|
||||
{navItem("/glossar", ICON_BOOK, "nav.glossar", "Glossar", currentPath)}
|
||||
{navItem("/downloads", ICON_DOWNLOAD, "nav.downloads", "Downloads", currentPath)}
|
||||
{navItem("/links", ICON_LINK, "nav.links", "Links", currentPath)}
|
||||
|
||||
@@ -10,6 +10,7 @@ const ICON_CLOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" s
|
||||
const ICON_DOWNLOAD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
|
||||
const ICON_GLOSSAR = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>';
|
||||
const ICON_TABLE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/></svg>';
|
||||
const ICON_CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
|
||||
|
||||
export function renderIndex(): string {
|
||||
return "<!DOCTYPE html>" + (
|
||||
@@ -84,6 +85,12 @@ export function renderIndex(): string {
|
||||
<h2 data-i18n="index.gebuehren.title">Gebührentabellen</h2>
|
||||
<p data-i18n="index.gebuehren.desc">Interaktive Gebührentabellen für GKG, RVG, UPC, EPA und PatKostG. Streitwert eingeben, Gebühr ablesen.</p>
|
||||
</a>
|
||||
|
||||
<a href="/checklisten" className="card card-link">
|
||||
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_CHECK }} />
|
||||
<h2 data-i18n="index.checklisten.title">Checklisten</h2>
|
||||
<p data-i18n="index.checklisten.desc">Interaktive Checklisten für UPC-, DE- und EPA-Verfahren. Fortschritt wird lokal gespeichert.</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2804,3 +2804,382 @@ input[type="range"]::-moz-range-thumb {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Checklisten --- */
|
||||
|
||||
.checklist-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.checklist-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.checklist-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
padding: 1.1rem 1.2rem 1.2rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-surface);
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.checklist-card:hover {
|
||||
border-color: var(--color-accent);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.checklist-card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.checklist-card-count {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.checklist-card-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checklist-card-desc {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checklist-card-court {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.checklist-card-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.checklist-regime {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.55rem;
|
||||
border-radius: 99px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
background: rgba(101, 163, 13, 0.12);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.checklist-regime-UPC {
|
||||
background: rgba(101, 163, 13, 0.12);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.checklist-regime-DE {
|
||||
background: rgba(26, 54, 93, 0.08);
|
||||
color: #1b365d;
|
||||
}
|
||||
|
||||
.checklist-regime-EPA {
|
||||
background: rgba(100, 100, 122, 0.12);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.checklist-empty {
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* --- Checklist detail --- */
|
||||
|
||||
.checklist-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
margin-bottom: 1rem;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.checklist-back:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.checklist-back-arrow {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.checklist-detail-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.checklist-detail-head-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.checklist-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.55rem 0.9rem;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.checklist-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem 1.5rem;
|
||||
margin-top: 0.85rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.checklist-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.checklist-meta-item dt {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.checklist-meta-item dd {
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checklist-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.checklist-progress-bar {
|
||||
flex: 1;
|
||||
height: 0.4rem;
|
||||
background: var(--color-border);
|
||||
border-radius: 99px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.checklist-progress-fill {
|
||||
height: 100%;
|
||||
background: var(--color-accent);
|
||||
border-radius: 99px;
|
||||
transition: width 0.2s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.checklist-progress-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checklist-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.75rem;
|
||||
}
|
||||
|
||||
.checklist-group-title {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-muted);
|
||||
padding-bottom: 0.45rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.checklist-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
padding: 0.55rem 0.6rem;
|
||||
border-radius: var(--radius);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.checklist-item:hover {
|
||||
background: rgba(101, 163, 13, 0.04);
|
||||
}
|
||||
|
||||
.checklist-item.checked .checklist-item-text {
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.checklist-item-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.7rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checklist-checkbox {
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
margin-top: 0.2rem;
|
||||
accent-color: var(--color-accent);
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checklist-item-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.checklist-item-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.checklist-item-text {
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.checklist-item-rule {
|
||||
font-size: 0.72rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 99px;
|
||||
background: rgba(26, 54, 93, 0.06);
|
||||
color: #1b365d;
|
||||
font-family: var(--font-mono);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checklist-item-note {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checklist-print-footer {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.checklist-disclaimer {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.checklist-detail-head-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.checklist-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.checklist-back,
|
||||
.checklist-actions,
|
||||
.checklist-filters,
|
||||
.checklist-card-progress {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
break-inside: avoid;
|
||||
border-bottom: 1px dotted #ccc;
|
||||
padding: 0.3rem 0;
|
||||
}
|
||||
|
||||
.checklist-item:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.checklist-checkbox {
|
||||
/* Keep checkbox visible with actual state */
|
||||
accent-color: #000;
|
||||
}
|
||||
|
||||
.checklist-group {
|
||||
break-inside: avoid-page;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.checklist-progress {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.checklist-progress-bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
510
internal/handlers/checklisten.go
Normal file
510
internal/handlers/checklisten.go
Normal file
@@ -0,0 +1,510 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"mgit.msbls.de/m/patholo/internal/auth"
|
||||
)
|
||||
|
||||
type ChecklistItem struct {
|
||||
LabelDE string `json:"labelDE"`
|
||||
LabelEN string `json:"labelEN"`
|
||||
NoteDE string `json:"noteDE,omitempty"`
|
||||
NoteEN string `json:"noteEN,omitempty"`
|
||||
Rule string `json:"rule,omitempty"`
|
||||
}
|
||||
|
||||
type ChecklistGroup struct {
|
||||
TitleDE string `json:"titleDE"`
|
||||
TitleEN string `json:"titleEN"`
|
||||
Items []ChecklistItem `json:"items"`
|
||||
}
|
||||
|
||||
type Checklist struct {
|
||||
Slug string `json:"slug"`
|
||||
TitleDE string `json:"titleDE"`
|
||||
TitleEN string `json:"titleEN"`
|
||||
DescriptionDE string `json:"descriptionDE"`
|
||||
DescriptionEN string `json:"descriptionEN"`
|
||||
Regime string `json:"regime"` // UPC | DE | EPA
|
||||
CourtDE string `json:"courtDE"`
|
||||
CourtEN string `json:"courtEN"`
|
||||
DeadlineDE string `json:"deadlineDE,omitempty"`
|
||||
DeadlineEN string `json:"deadlineEN,omitempty"`
|
||||
ReferenceDE string `json:"referenceDE,omitempty"`
|
||||
ReferenceEN string `json:"referenceEN,omitempty"`
|
||||
Groups []ChecklistGroup `json:"groups"`
|
||||
}
|
||||
|
||||
type ChecklistSummary struct {
|
||||
Slug string `json:"slug"`
|
||||
TitleDE string `json:"titleDE"`
|
||||
TitleEN string `json:"titleEN"`
|
||||
DescriptionDE string `json:"descriptionDE"`
|
||||
DescriptionEN string `json:"descriptionEN"`
|
||||
Regime string `json:"regime"`
|
||||
CourtDE string `json:"courtDE"`
|
||||
CourtEN string `json:"courtEN"`
|
||||
ItemCount int `json:"itemCount"`
|
||||
}
|
||||
|
||||
type ChecklistFeedback struct {
|
||||
FeedbackType string `json:"feedback_type"`
|
||||
Checklist string `json:"checklist"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
var checklists = []Checklist{
|
||||
{
|
||||
Slug: "upc-statement-of-claim",
|
||||
TitleDE: "UPC Klageschrift",
|
||||
TitleEN: "UPC Statement of Claim",
|
||||
DescriptionDE: "Pflichtbestandteile einer Klageschrift vor dem Einheitlichen Patentgericht.",
|
||||
DescriptionEN: "Mandatory content of a statement of claim before the Unified Patent Court.",
|
||||
Regime: "UPC",
|
||||
CourtDE: "Einheitliches Patentgericht (Lokal-/Regional-/Zentralkammer)",
|
||||
CourtEN: "Unified Patent Court (local/regional/central division)",
|
||||
ReferenceDE: "RoP 13",
|
||||
ReferenceEN: "RoP 13",
|
||||
Groups: []ChecklistGroup{
|
||||
{
|
||||
TitleDE: "Formale Angaben",
|
||||
TitleEN: "Formal details",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Bezeichnung der Parteien (inkl. Vertreter)", LabelEN: "Names and addresses of the parties (and representatives)", Rule: "RoP 13(1)(a)-(b)"},
|
||||
{LabelDE: "Angabe der Lokal-, Regional- oder Zentralkammer", LabelEN: "Identification of the division where the action is brought", Rule: "RoP 13(1)(c)"},
|
||||
{LabelDE: "Verfahrenssprache gewählt und ggf. begründet", LabelEN: "Language of proceedings selected and justified if required", Rule: "RoP 14"},
|
||||
{LabelDE: "Anschrift für Zustellungen", LabelEN: "Address for service", Rule: "RoP 8.1, 13(1)(b)"},
|
||||
{LabelDE: "Vollmacht bzw. Vertretungsnachweis des Prozessbevollmächtigten", LabelEN: "Evidence of representation / authorisation", NoteDE: "Art. 48 EPGÜ; Vertretung durch zugelassenen Rechtsanwalt oder EPA mit EPLC.", NoteEN: "Art. 48 UPCA; representation by authorised lawyer or EPA with EPLC."},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Patent und Verletzung",
|
||||
TitleEN: "Patent and infringement",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Patentnummer und Angabe, ob Einheitspatent oder EP-Bündelpatent", LabelEN: "Patent number and indication whether unitary or classical EP", Rule: "RoP 13(1)(d)"},
|
||||
{LabelDE: "Nachweis der Inhaberschaft / Aktivlegitimation", LabelEN: "Evidence of ownership / entitlement to sue", Rule: "RoP 13(1)(e)"},
|
||||
{LabelDE: "Angriffsgegenstand (streitige Ausführungsform) konkret bezeichnet", LabelEN: "Identification of the contested embodiments", Rule: "RoP 13(1)(g)"},
|
||||
{LabelDE: "Verletzungsargumentation mit Merkmalsgliederung", LabelEN: "Infringement analysis with feature-by-feature mapping", Rule: "RoP 13(1)(l)"},
|
||||
{LabelDE: "Angabe von Hintergrund und Stand der Technik (soweit erforderlich)", LabelEN: "Relevant technical and factual background", Rule: "RoP 13(1)(k)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Anträge und Streitwert",
|
||||
TitleEN: "Relief sought and value",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Konkrete Anträge (Unterlassung, Rückruf, Vernichtung, Schadensersatz, Auskunft)", LabelEN: "Orders sought (injunction, recall, destruction, damages, information)", Rule: "RoP 13(1)(p)"},
|
||||
{LabelDE: "Streitwert und Begründung der Streitwertangabe", LabelEN: "Value of the action with reasoned indication", Rule: "RoP 13(1)(o), 370"},
|
||||
{LabelDE: "Antrag zu den Kosten", LabelEN: "Request regarding costs", Rule: "RoP 13(1)(p)"},
|
||||
{LabelDE: "Ggf. Antrag auf einstweilige Maßnahmen oder Beschleunigung", LabelEN: "If applicable: request for provisional or accelerated measures", Rule: "RoP 206, 118"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Zuständigkeit und Zulässigkeit",
|
||||
TitleEN: "Jurisdiction and admissibility",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Begründung der Zuständigkeit der gewählten Kammer", LabelEN: "Reasoned statement on competence of the chosen division", Rule: "Art. 33 EPGÜ / UPCA; RoP 13(1)(i)"},
|
||||
{LabelDE: "Kein Opt-out des Patents (sofern klassisches EP)", LabelEN: "No opt-out in force (for classical EPs)", NoteDE: "Register des UPC prüfen.", NoteEN: "Check the UPC case management register."},
|
||||
{LabelDE: "Keine parallele nationale Verletzungsklage ohne Abstimmung", LabelEN: "No parallel national infringement action without coordination"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Anlagen und Gebühren",
|
||||
TitleEN: "Annexes and fees",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Patentschrift (B-Schrift) und ggf. T3/Übersetzung", LabelEN: "Patent specification (B-publication) and translation if required"},
|
||||
{LabelDE: "Übersetzung der Ansprüche in die Verfahrenssprache", LabelEN: "Translation of the claims into the language of proceedings", Rule: "RoP 14.2"},
|
||||
{LabelDE: "Relevante Beweismittel (Produktunterlagen, Testberichte, Lizenzverträge)", LabelEN: "Supporting evidence (product materials, test reports, licence agreements)"},
|
||||
{LabelDE: "Festgebühr und streitwertabhängige Gerichtsgebühr eingezahlt", LabelEN: "Fixed fee and value-based court fee paid", Rule: "RoP 15.2, Rules on Court Fees"},
|
||||
{LabelDE: "Einreichung über CMS (elektronisch)", LabelEN: "Electronic filing via CMS", Rule: "RoP 4.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Slug: "upc-statement-of-defence",
|
||||
TitleDE: "UPC Klageerwiderung",
|
||||
TitleEN: "UPC Statement of Defence",
|
||||
DescriptionDE: "Pflichtbestandteile der Klageerwiderung (mit optionaler Nichtigkeits-Widerklage).",
|
||||
DescriptionEN: "Mandatory content of the statement of defence (with optional counterclaim for revocation).",
|
||||
Regime: "UPC",
|
||||
CourtDE: "Einheitliches Patentgericht",
|
||||
CourtEN: "Unified Patent Court",
|
||||
DeadlineDE: "3 Monate ab Zustellung der Klage",
|
||||
DeadlineEN: "3 months from service of the statement of claim",
|
||||
ReferenceDE: "RoP 23, 24, 25",
|
||||
ReferenceEN: "RoP 23, 24, 25",
|
||||
Groups: []ChecklistGroup{
|
||||
{
|
||||
TitleDE: "Formale Angaben",
|
||||
TitleEN: "Formal details",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Aktenzeichen und Bezeichnung der Parteien", LabelEN: "Case number and names of the parties", Rule: "RoP 24(a)"},
|
||||
{LabelDE: "Anschrift für Zustellungen und Vertretungsnachweis", LabelEN: "Address for service and evidence of representation", Rule: "RoP 24(b)-(c)"},
|
||||
{LabelDE: "Einhaltung der 3-Monats-Frist", LabelEN: "Compliance with the 3-month deadline", Rule: "RoP 23", NoteDE: "Frist läuft ab Zustellung der Klageschrift.", NoteEN: "Runs from service of the statement of claim."},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Inhaltliche Verteidigung",
|
||||
TitleEN: "Substantive defence",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Stellungnahme zu Zuständigkeit, Zulässigkeit und Sprache", LabelEN: "Position on competence, admissibility and language", Rule: "RoP 24(d)-(e)"},
|
||||
{LabelDE: "Bestrittene und unbestrittene Tatsachen konkret benannt", LabelEN: "Indicate contested and uncontested facts", Rule: "RoP 24(g)"},
|
||||
{LabelDE: "Gegenargumentation zur Verletzung (Merkmal für Merkmal)", LabelEN: "Non-infringement arguments (feature by feature)", Rule: "RoP 24(g)-(h)"},
|
||||
{LabelDE: "Ggf. Einreden (Erschöpfung, Vorbenutzung, FRAND, Verjährung, private use)", LabelEN: "Defences raised (exhaustion, prior use, FRAND, statute of limitations, private use)", Rule: "Art. 27, 28 EPGÜ / UPCA"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Widerklage und Gegenanträge",
|
||||
TitleEN: "Counterclaims and counter-requests",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Ggf. Nichtigkeits-Widerklage mit separatem Antrag", LabelEN: "If applicable: counterclaim for revocation with separate request", Rule: "RoP 25"},
|
||||
{LabelDE: "Bei Nichtigkeits-Widerklage: Nichtigkeitsgründe und Stand der Technik", LabelEN: "For revocation counterclaim: grounds for invalidity and prior art", Rule: "RoP 25.1(b)"},
|
||||
{LabelDE: "Antrag zu Kosten und Sicherheitsleistung", LabelEN: "Request regarding costs and security"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Anlagen und Gebühren",
|
||||
TitleEN: "Annexes and fees",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Beweismittel für bestrittene Tatsachen", LabelEN: "Evidence for contested facts", Rule: "RoP 24(i)"},
|
||||
{LabelDE: "Ggf. Gutachten, Entgegenhaltungen, Übersetzungen", LabelEN: "Expert opinions, prior art, translations if applicable"},
|
||||
{LabelDE: "Gerichtsgebühr für Widerklage (bei Nichtigkeit)", LabelEN: "Court fee for counterclaim (if revocation)", Rule: "RoP 25.2"},
|
||||
{LabelDE: "Einreichung über CMS", LabelEN: "Electronic filing via CMS"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Slug: "upc-confidentiality",
|
||||
TitleDE: "UPC Vertraulichkeitsantrag",
|
||||
TitleEN: "UPC Confidentiality Application",
|
||||
DescriptionDE: "Antrag auf Schutz vertraulicher Informationen (z.B. Geschäftsgeheimnisse) im UPC-Verfahren.",
|
||||
DescriptionEN: "Application for protection of confidential information (e.g. trade secrets) in UPC proceedings.",
|
||||
Regime: "UPC",
|
||||
CourtDE: "Einheitliches Patentgericht",
|
||||
CourtEN: "Unified Patent Court",
|
||||
ReferenceDE: "RoP 262A",
|
||||
ReferenceEN: "RoP 262A",
|
||||
Groups: []ChecklistGroup{
|
||||
{
|
||||
TitleDE: "Antrag und Begründung",
|
||||
TitleEN: "Request and reasoning",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Konkreter Antrag zur Behandlung als vertraulich", LabelEN: "Specific request for confidential treatment", Rule: "RoP 262A.1"},
|
||||
{LabelDE: "Bezeichnung der konkret betroffenen Informationen/Dokumente", LabelEN: "Identification of the specific information/documents concerned", Rule: "RoP 262A.2(a)"},
|
||||
{LabelDE: "Begründung der Vertraulichkeit (Geschäftsgeheimnis, wirtschaftlicher Wert)", LabelEN: "Reasoned justification (trade secret, economic value)", Rule: "RoP 262A.2(b)"},
|
||||
{LabelDE: "Beschreibung der potentiellen Nachteile bei Offenlegung", LabelEN: "Description of potential harm if disclosed"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Vertraulichkeitsklub",
|
||||
TitleEN: "Confidentiality club",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Vorschlag des Personenkreises mit Zugang (max. 1 natürliche Person je Partei + Vertreter)", LabelEN: "Proposed list of persons with access (min. one natural person per party plus representatives)", Rule: "RoP 262A.5"},
|
||||
{LabelDE: "Geheimhaltungsverpflichtung für alle Klub-Mitglieder vorgesehen", LabelEN: "Confidentiality undertaking foreseen for all club members"},
|
||||
{LabelDE: "Ggf. besondere Modalitäten (nur Einsicht vor Ort, keine Kopien)", LabelEN: "Specific modalities (on-site inspection only, no copies) if applicable"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Verfahrensrechtliche Aspekte",
|
||||
TitleEN: "Procedural aspects",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Zeitpunkt des Antrags (so früh wie möglich)", LabelEN: "Timing of the application (as early as possible)"},
|
||||
{LabelDE: "Abstimmung mit Gegenpartei dokumentiert oder Gründe der Nichtabstimmung", LabelEN: "Coordination with opposing party documented or reasons for absence"},
|
||||
{LabelDE: "Geschwärzte und ungeschwärzte Fassungen der Dokumente vorbereitet", LabelEN: "Redacted and unredacted versions of documents prepared"},
|
||||
{LabelDE: "Verhältnismäßigkeit geprüft (Art. 9 Know-how-RL)", LabelEN: "Proportionality considered (Art. 9 Trade Secrets Directive)"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Slug: "upc-representative-registration",
|
||||
TitleDE: "UPC Vertreterregistrierung",
|
||||
TitleEN: "UPC Representative Registration",
|
||||
DescriptionDE: "Unterlagen für die Eintragung als zugelassener Vertreter vor dem UPC.",
|
||||
DescriptionEN: "Documents required to be registered as a qualified representative before the UPC.",
|
||||
Regime: "UPC",
|
||||
CourtDE: "Kanzlei des UPC / Registrar",
|
||||
CourtEN: "UPC Registry",
|
||||
ReferenceDE: "Art. 48 EPGÜ; RoP 286 ff.",
|
||||
ReferenceEN: "Art. 48 UPCA; RoP 286 et seq.",
|
||||
Groups: []ChecklistGroup{
|
||||
{
|
||||
TitleDE: "Rechtsanwälte (Art. 48(1))",
|
||||
TitleEN: "Lawyers (Art. 48(1))",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Nachweis der Zulassung als Rechtsanwalt in einem teilnehmenden Mitgliedstaat", LabelEN: "Proof of authorisation as a lawyer in a contracting member state", NoteDE: "Aktuelle Zulassungsbescheinigung der zuständigen Kammer.", NoteEN: "Recent certificate from the competent bar association."},
|
||||
{LabelDE: "Aktuelle Registerauskunft (nicht älter als 3 Monate)", LabelEN: "Up-to-date bar register extract (not older than 3 months)"},
|
||||
{LabelDE: "Berufshaftpflichtversicherung nachgewiesen", LabelEN: "Professional indemnity insurance evidenced"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Patentanwälte (Art. 48(2))",
|
||||
TitleEN: "European patent attorneys (Art. 48(2))",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Eintragung in der Liste der zugelassenen Vertreter beim EPA", LabelEN: "Entry on the EPO list of professional representatives"},
|
||||
{LabelDE: "EPLC oder gleichwertige Qualifikation (Art. 48(2))", LabelEN: "European Patent Litigation Certificate (EPLC) or equivalent"},
|
||||
{LabelDE: "Nachweis der Qualifikation (Zeugnis / Bescheinigung)", LabelEN: "Evidence of qualification (certificate)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Allgemeine Unterlagen",
|
||||
TitleEN: "General documents",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Ausgefülltes UPC-Registrierungsformular", LabelEN: "Completed UPC registration form"},
|
||||
{LabelDE: "Gültige Ausweiskopie", LabelEN: "Valid identity document copy"},
|
||||
{LabelDE: "Zustellungsadresse (in einem Mitgliedstaat)", LabelEN: "Address for service (in a contracting member state)"},
|
||||
{LabelDE: "CMS-Account erstellt und Starke-Authentifizierung eingerichtet", LabelEN: "CMS account created and strong authentication configured"},
|
||||
{LabelDE: "Bankverbindung für Gebührenzahlungen hinterlegt", LabelEN: "Bank details lodged for fee payments"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Slug: "bpatg-nullity-action",
|
||||
TitleDE: "Nichtigkeitsklage BPatG",
|
||||
TitleEN: "Patent Nullity Action (BPatG)",
|
||||
DescriptionDE: "Formale und inhaltliche Anforderungen an eine Nichtigkeitsklage vor dem Bundespatentgericht.",
|
||||
DescriptionEN: "Formal and substantive requirements for a nullity action before the German Federal Patent Court.",
|
||||
Regime: "DE",
|
||||
CourtDE: "Bundespatentgericht, München",
|
||||
CourtEN: "Federal Patent Court (BPatG), Munich",
|
||||
ReferenceDE: "§§ 81 ff. PatG",
|
||||
ReferenceEN: "Sections 81 et seq. Patent Act (PatG)",
|
||||
Groups: []ChecklistGroup{
|
||||
{
|
||||
TitleDE: "Formale Anforderungen",
|
||||
TitleEN: "Formal requirements",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Bezeichnung der Parteien (Kläger, Beklagter = Patentinhaber)", LabelEN: "Names of the parties (claimant, defendant = patent proprietor)", Rule: "§ 253 ZPO i.V.m. § 99 PatG"},
|
||||
{LabelDE: "Vertretung durch zugelassenen Rechts- oder Patentanwalt", LabelEN: "Representation by admitted attorney-at-law or patent attorney", Rule: "§ 97 PatG"},
|
||||
{LabelDE: "Schriftform und Unterschrift des Bevollmächtigten", LabelEN: "Written form and signature of the representative"},
|
||||
{LabelDE: "Zustellungsbevollmächtigter bei Auslandssitz", LabelEN: "Address for service in Germany if abroad"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Inhaltliche Anforderungen",
|
||||
TitleEN: "Substantive requirements",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Bezeichnung des angegriffenen Patents (Nummer, ggf. Teil)", LabelEN: "Identification of the challenged patent (number, part)", Rule: "§ 81(1) PatG"},
|
||||
{LabelDE: "Antrag auf Nichtigerklärung (ganz oder teilweise)", LabelEN: "Request for full or partial revocation", Rule: "§ 81(1) PatG"},
|
||||
{LabelDE: "Nichtigkeitsgründe konkret benannt", LabelEN: "Specific grounds for invalidity", Rule: "§ 22(1), § 81(1) PatG", NoteDE: "Mangelnde Patentfähigkeit (§§ 1-5), unzureichende Offenbarung (§ 21(1) Nr. 2), widerrechtliche Entnahme, unzulässige Erweiterung.", NoteEN: "Lack of patentability, insufficient disclosure, unlawful usurpation, impermissible extension."},
|
||||
{LabelDE: "Tatsachen und Beweismittel für jeden Nichtigkeitsgrund", LabelEN: "Facts and evidence for each ground"},
|
||||
{LabelDE: "Substantiierte Merkmalsgliederung und Vergleich mit Stand der Technik", LabelEN: "Substantiated feature analysis and comparison with prior art"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Zulässigkeit",
|
||||
TitleEN: "Admissibility",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Keine parallele Einspruchsfrist am EPA offen (Sperrwirkung)", LabelEN: "No pending opposition period at the EPO (blocking effect)", Rule: "§ 81(2) PatG"},
|
||||
{LabelDE: "Klageberechtigung (Popularklage bei PatG ja, außer bei Usurpation)", LabelEN: "Standing to sue (popularis action permitted except for usurpation)", Rule: "§ 81(3) PatG"},
|
||||
{LabelDE: "Streitwert angegeben und begründet", LabelEN: "Value in dispute indicated and reasoned"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Anlagen und Gebühren",
|
||||
TitleEN: "Annexes and fees",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Patentschrift des angegriffenen Patents", LabelEN: "Specification of the challenged patent"},
|
||||
{LabelDE: "Entgegenhaltungen vollständig vorgelegt und ggf. übersetzt", LabelEN: "Prior art documents submitted in full, with translations if needed"},
|
||||
{LabelDE: "Vollmacht", LabelEN: "Power of attorney"},
|
||||
{LabelDE: "Gerichtsgebühr (4,5-facher Gebührensatz PatKostG) gezahlt", LabelEN: "Court fee (4.5x GKG rate, PatKostG) paid", Rule: "GebVerz. PatKostG Nr. 401300"},
|
||||
{LabelDE: "Zahl der Abschriften für Beklagte(n) + 1", LabelEN: "Copies for defendant(s) plus one"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Slug: "epa-opposition",
|
||||
TitleDE: "EPA Einspruch",
|
||||
TitleEN: "EPO Opposition",
|
||||
DescriptionDE: "Formvorschriften und Frist für einen Einspruch gegen ein erteiltes europäisches Patent.",
|
||||
DescriptionEN: "Formal requirements and deadline for an opposition against a granted European patent.",
|
||||
Regime: "EPA",
|
||||
CourtDE: "Europäisches Patentamt, Einspruchsabteilung",
|
||||
CourtEN: "European Patent Office, Opposition Division",
|
||||
DeadlineDE: "9 Monate nach Veröffentlichung des Erteilungshinweises",
|
||||
DeadlineEN: "9 months after publication of the mention of grant",
|
||||
ReferenceDE: "Art. 99 EPÜ; Regel 76 EPÜ",
|
||||
ReferenceEN: "Art. 99 EPC; Rule 76 EPC",
|
||||
Groups: []ChecklistGroup{
|
||||
{
|
||||
TitleDE: "Frist und Einreichung",
|
||||
TitleEN: "Deadline and filing",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "9-Monats-Frist ab B-Schriften-Veröffentlichung eingehalten", LabelEN: "9-month deadline from B-publication met", Rule: "Art. 99(1) EPÜ / EPC", NoteDE: "Nicht verlängerbar; Wiedereinsetzung nur eng begrenzt möglich.", NoteEN: "Not extendable; re-establishment only in narrow circumstances."},
|
||||
{LabelDE: "Einreichung über EPA Online Filing / MyEPO", LabelEN: "Filed via EPO Online Filing / MyEPO"},
|
||||
{LabelDE: "Eingangstag dokumentiert und Einhaltung der Frist geprüft", LabelEN: "Filing receipt saved and deadline compliance verified"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Parteien und Vertretung",
|
||||
TitleEN: "Parties and representation",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Bezeichnung des Einsprechenden (Name, Anschrift, Staat)", LabelEN: "Identification of the opponent (name, address, state)", Rule: "Regel 76(2)(a) EPÜ / EPC"},
|
||||
{LabelDE: "Vertretung durch zugelassenen Vertreter oder Rechtsanwalt", LabelEN: "Representation by professional representative or lawyer", Rule: "Art. 134 EPÜ / EPC"},
|
||||
{LabelDE: "Vollmacht (soweit erforderlich)", LabelEN: "Power of attorney (where required)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Angriff auf das Patent",
|
||||
TitleEN: "Attack on the patent",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Bezeichnung des angegriffenen europäischen Patents (Nummer, Erteilungsdatum)", LabelEN: "Identification of the challenged European patent (number, grant date)", Rule: "Regel 76(2)(b) EPÜ / EPC"},
|
||||
{LabelDE: "Umfang des Einspruchs (vollständig / teilweise, Anspruchsnummern)", LabelEN: "Extent of opposition (full / partial, claim numbers)", Rule: "Regel 76(2)(c) EPÜ / EPC"},
|
||||
{LabelDE: "Einspruchsgründe (Art. 100 EPÜ) konkret genannt", LabelEN: "Grounds of opposition under Art. 100 EPC stated", NoteDE: "a) mangelnde Patentfähigkeit, b) unzureichende Offenbarung, c) unzulässige Erweiterung.", NoteEN: "a) lack of patentability, b) insufficient disclosure, c) impermissible amendment."},
|
||||
{LabelDE: "Tatsachen und Beweismittel je Einspruchsgrund substantiiert", LabelEN: "Substantiated facts and evidence for each ground", Rule: "Regel 76(2)(c) EPÜ / EPC"},
|
||||
},
|
||||
},
|
||||
{
|
||||
TitleDE: "Anlagen und Gebühr",
|
||||
TitleEN: "Annexes and fee",
|
||||
Items: []ChecklistItem{
|
||||
{LabelDE: "Entgegenhaltungen vollständig und (soweit nötig) in Amtssprache", LabelEN: "Prior art fully submitted and, if necessary, translated into an official language"},
|
||||
{LabelDE: "Ggf. Gutachten oder eidesstattliche Erklärungen beigefügt", LabelEN: "Expert opinions or affidavits included where appropriate"},
|
||||
{LabelDE: "Einspruchsgebühr fristgerecht entrichtet", LabelEN: "Opposition fee paid in time", Rule: "Art. 99(1) EPÜ / EPC; Art. 2(1) GebO / RFees"},
|
||||
{LabelDE: "Identifizierung der Sprache (Regel 3 EPÜ) beachtet", LabelEN: "Compliant language of proceedings (Rule 3 EPC)", Rule: "Regel 3 EPÜ / EPC"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func totalItemCount(c Checklist) int {
|
||||
n := 0
|
||||
for _, g := range c.Groups {
|
||||
n += len(g.Items)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func findChecklist(slug string) (Checklist, bool) {
|
||||
for _, c := range checklists {
|
||||
if c.Slug == slug {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return Checklist{}, false
|
||||
}
|
||||
|
||||
func handleChecklistenPage(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "dist/checklisten.html")
|
||||
}
|
||||
|
||||
func handleChecklistDetailPage(w http.ResponseWriter, r *http.Request) {
|
||||
slug := r.PathValue("slug")
|
||||
if _, ok := findChecklist(slug); !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, "dist/checklisten-detail.html")
|
||||
}
|
||||
|
||||
func handleChecklistenAPI(w http.ResponseWriter, r *http.Request) {
|
||||
summaries := make([]ChecklistSummary, 0, len(checklists))
|
||||
for _, c := range checklists {
|
||||
summaries = append(summaries, ChecklistSummary{
|
||||
Slug: c.Slug,
|
||||
TitleDE: c.TitleDE,
|
||||
TitleEN: c.TitleEN,
|
||||
DescriptionDE: c.DescriptionDE,
|
||||
DescriptionEN: c.DescriptionEN,
|
||||
Regime: c.Regime,
|
||||
CourtDE: c.CourtDE,
|
||||
CourtEN: c.CourtEN,
|
||||
ItemCount: totalItemCount(c),
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, summaries)
|
||||
}
|
||||
|
||||
func handleChecklistAPI(w http.ResponseWriter, r *http.Request) {
|
||||
slug := r.PathValue("slug")
|
||||
c, ok := findChecklist(slug)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "Checkliste nicht gefunden."})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, c)
|
||||
}
|
||||
|
||||
func handleChecklistenFeedback(w http.ResponseWriter, r *http.Request) {
|
||||
var feedback ChecklistFeedback
|
||||
if err := json.NewDecoder(r.Body).Decode(&feedback); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage."})
|
||||
return
|
||||
}
|
||||
|
||||
feedback.FeedbackType = strings.TrimSpace(feedback.FeedbackType)
|
||||
feedback.Checklist = strings.TrimSpace(feedback.Checklist)
|
||||
feedback.Message = strings.TrimSpace(feedback.Message)
|
||||
|
||||
if feedback.Message == "" || feedback.FeedbackType == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Nachricht und Art sind erforderlich."})
|
||||
return
|
||||
}
|
||||
|
||||
accessToken := ""
|
||||
email := ""
|
||||
if cookie, err := r.Cookie(auth.SessionCookieName); err == nil {
|
||||
accessToken = cookie.Value
|
||||
email = extractEmailFromJWT(cookie.Value)
|
||||
}
|
||||
|
||||
payload := map[string]string{
|
||||
"feedback_type": feedback.FeedbackType,
|
||||
"checklist": feedback.Checklist,
|
||||
"message": feedback.Message,
|
||||
"submitted_by": email,
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
log.Printf("checklisten feedback marshal error: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Interner Fehler."})
|
||||
return
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("%s/rest/v1/checklisten_feedback", authClient.URL)
|
||||
req2, err := http.NewRequest("POST", endpoint, bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
log.Printf("checklisten feedback request error: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Interner Fehler."})
|
||||
return
|
||||
}
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
req2.Header.Set("apikey", authClient.AnonKey)
|
||||
if accessToken != "" {
|
||||
req2.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
} else {
|
||||
req2.Header.Set("Authorization", "Bearer "+authClient.AnonKey)
|
||||
}
|
||||
req2.Header.Set("Prefer", "return=minimal")
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Do(req2)
|
||||
if err != nil {
|
||||
log.Printf("checklisten feedback supabase error: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Fehler beim Speichern."})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 300 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("checklisten feedback supabase status %d: %s", resp.StatusCode, string(body))
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Fehler beim Speichern."})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"ok": "true"})
|
||||
}
|
||||
@@ -47,6 +47,11 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string) {
|
||||
protected.HandleFunc("GET /api/tools/gebuehrentabellen", handleGebuehrentabellenAPI)
|
||||
protected.HandleFunc("GET /api/tools/gebuehrentabellen/lookup", handleGebuehrentabellenLookup)
|
||||
protected.HandleFunc("POST /api/tools/gebuehrentabellen/feedback", handleGebuehrentabellenFeedback)
|
||||
protected.HandleFunc("GET /checklisten", handleChecklistenPage)
|
||||
protected.HandleFunc("GET /checklisten/{slug}", handleChecklistDetailPage)
|
||||
protected.HandleFunc("GET /api/checklisten", handleChecklistenAPI)
|
||||
protected.HandleFunc("GET /api/checklisten/{slug}", handleChecklistAPI)
|
||||
protected.HandleFunc("POST /api/checklisten/feedback", handleChecklistenFeedback)
|
||||
mux.Handle("/", client.Middleware(protected))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user