Files
paliad/frontend/src/fristenrechner.tsx
m 1df1bc7e40 feat(fristenrechner/inbox-chip): persisted forum pre-filter on /tools/fristenrechner
Adds a slim chip strip above the pathway fork on /tools/fristenrechner
so the user can pick the inbox channel they typically work in (CMS for
UPC, beA / Posteingang for national-DE). Three behaviours stack:

  - URL ?inbox=cms|bea|posteingang  per-visit override; lets a colleague
                                    share a CMS-narrowed link without
                                    flipping the recipient's saved pref.
  - /api/me forum_pref              the user's persisted default,
                                    fetched on hydrate when no URL.
  - unset                            picker shows all four groups.

Click behaviour: write URL → apply filter (hide non-matching
.proceeding-group via the new data-forum attributes) → PATCH /api/me.
The "Alle" chip clears both URL and the saved pref. EPA / DPMA fall
out under cms / bea / posteingang; users still reach those via B2
search or by clearing the chip.

Frontend pieces:

  - frontend/src/fristenrechner.tsx — new .fristen-inbox-bar markup
    above the pathway fork; data-forum attributes on each
    .proceeding-group so the filter knows which to hide.
  - frontend/src/client/fristenrechner.ts — initInboxFilter() hydrates
    from URL → /api/me, wires chip clicks (write URL, apply filter,
    PATCH /api/me opportunistically), restores on popstate.
  - frontend/src/client/i18n.ts — 6 new keys (deadlines.inbox.*) DE+EN.
  - frontend/src/i18n-keys.ts — codegen picked up the new keys.
  - frontend/src/styles/global.css — .fristen-inbox-bar /
    .fristen-inbox-chip / --active / --clear styles, all bound to the
    existing --color-* / --color-accent token palette.

The chip writes "" to forum_pref to clear (matching the
EscalationContactID convention from the previous slice). The B2 forum
filter (the 10-bucket finer-grained chip set further down the page)
stays untouched and orthogonal — this slice is the page-top coarse
pre-filter only.

Refs m/paliad#15.
2026-05-08 16:31:31 +02:00

467 lines
27 KiB
TypeScript

import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
import { PWAHead } from "./components/PWAHead";
interface ProceedingDef {
code: string;
i18nKey: string;
name: string;
}
function proceedingBtn(p: ProceedingDef): string {
return (
<button type="button" className="proceeding-btn" data-code={p.code}>
<strong data-i18n={p.i18nKey}>{p.name}</strong>
</button>
);
}
// Quick-pick chip definition. Each chip targets ONE deadline_concepts
// slug — clicking sets the search query to the concept's name in the
// active language so trigram search lands on the right concept card.
// Single source of truth for both fork-shortcut and B2-search-bar
// chip rows. Dedup invariant: no two chips share a slug. Label flips
// per language via the chip wiring in client/fristenrechner.ts.
interface QuickChip {
slug: string;
name_de: string;
name_en: string;
}
const QUICK_CHIPS: QuickChip[] = [
{ slug: "statement-of-defence", name_de: "Klageerwiderung", name_en: "Statement of Defence" },
{ slug: "notice-of-appeal", name_de: "Berufungsschrift", name_en: "Notice of Appeal" },
{ slug: "opposition", name_de: "Einspruchsfrist", name_en: "Opposition" },
{ slug: "reply-to-defence", name_de: "Replik", name_en: "Reply to Defence" },
{ slug: "nichtzulassungsbeschwerde", name_de: "Nichtzulassungsbeschwerde", name_en: "Non-admission Appeal (NZB)" },
{ slug: "application-for-determination-of-damages",name_de: "Antrag auf Schadensbemessung", name_en: "Application for Determination of Damages" },
{ slug: "wiedereinsetzung", name_de: "Wiedereinsetzung", name_en: "Re-establishment of Rights" },
];
function quickChip(c: QuickChip): string {
return (
<button type="button" className="fristen-search-chip"
data-chip-slug={c.slug}
data-chip-name-de={c.name_de}
data-chip-name-en={c.name_en}
data-q={c.name_de}>
{c.name_de}
</button>
);
}
const UPC_TYPES: ProceedingDef[] = [
{ code: "UPC_INF", i18nKey: "deadlines.upc_inf", name: "Verletzungsverfahren" },
{ code: "UPC_REV", i18nKey: "deadlines.upc_rev", name: "Nichtigkeitsklage" },
{ code: "UPC_PI", i18nKey: "deadlines.upc_pi", name: "Einstw. Ma\u00dfnahmen" },
{ code: "UPC_APP", i18nKey: "deadlines.upc_app", name: "Berufung" },
{ code: "UPC_DAMAGES", i18nKey: "deadlines.upc_damages", name: "Schadensbemessung" },
{ code: "UPC_DISCOVERY", i18nKey: "deadlines.upc_discovery", name: "Bucheinsicht" },
{ code: "UPC_COST_APPEAL", i18nKey: "deadlines.upc_cost_appeal", name: "Berufung Kosten" },
{ code: "UPC_APP_ORDERS", i18nKey: "deadlines.upc_app_orders", name: "Berufung Anordnungen" },
];
const DE_TYPES: ProceedingDef[] = [
{ code: "DE_INF", i18nKey: "deadlines.de_inf", name: "Verletzungsklage (LG)" },
{ code: "DE_INF_OLG", i18nKey: "deadlines.de_inf_olg", name: "Berufung OLG" },
{ code: "DE_INF_BGH", i18nKey: "deadlines.de_inf_bgh", name: "Revision/NZB BGH" },
{ code: "DE_NULL", i18nKey: "deadlines.de_null", name: "Nichtigkeitsverfahren" },
{ code: "DE_NULL_BGH", i18nKey: "deadlines.de_null_bgh", name: "Berufung BGH (Nichtigk.)" },
];
const EPA_TYPES: ProceedingDef[] = [
{ code: "EPA_OPP", i18nKey: "deadlines.epa_opp", name: "Einspruchsverfahren" },
{ code: "EPA_APP", i18nKey: "deadlines.epa_app", name: "Beschwerdeverfahren" },
{ code: "EP_GRANT", i18nKey: "deadlines.ep_grant", name: "EP-Erteilungsverfahren" },
];
const DPMA_TYPES: ProceedingDef[] = [
{ code: "DPMA_OPP", i18nKey: "deadlines.dpma_opp", name: "Einspruch DPMA" },
{ code: "DPMA_BPATG_BESCHWERDE", i18nKey: "deadlines.dpma_bpatg_beschwerde", name: "Beschwerde BPatG (DPMA)" },
{ code: "DPMA_BGH_RB", i18nKey: "deadlines.dpma_bgh_rb", name: "Rechtsbeschwerde BGH" },
];
export function renderFristenrechner(): string {
const today = new Date().toISOString().split("T")[0];
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#BFF355" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<PWAHead />
<title data-i18n="deadlines.title">Fristenrechner &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/tools/fristenrechner" />
<BottomNav currentPath="/tools/fristenrechner" />
<main>
<section className="tool-page">
<div className="container">
<div className="tool-header">
<h1 data-i18n="deadlines.heading">Fristenrechner</h1>
<p className="tool-subtitle" data-i18n="deadlines.subtitle">
Berechnung von Verfahrensfristen f&uuml;r UPC-, deutsche und EPA-Verfahren.
</p>
</div>
{/* m/paliad#15 inbox-channel chip: persists the user's typical
forum (CMS = UPC; beA / Posteingang = national-DE) via
PATCH /api/me. URL ?inbox= overrides per-visit so a
colleague can share a CMS-narrowed link without flipping
anyone's saved preference. Hidden EPA / DPMA shortcuts
stay reachable via the picker when no chip is active. */}
<div className="fristen-inbox-bar" id="fristen-inbox-bar" role="group" aria-label="Inbox channel">
<span className="fristen-inbox-bar-label" data-i18n="deadlines.inbox.label">Wo kam es an?</span>
<div className="fristen-inbox-chips">
<button type="button" className="fristen-inbox-chip" data-inbox="cms"
data-i18n-title="deadlines.inbox.cms.title" title="UPC &mdash; &uuml;ber CMS">
CMS
</button>
<button type="button" className="fristen-inbox-chip" data-inbox="bea"
data-i18n-title="deadlines.inbox.bea.title" title="Nationale Verfahren &mdash; &uuml;ber beA">
beA
</button>
<button type="button" className="fristen-inbox-chip" data-inbox="posteingang"
data-i18n-title="deadlines.inbox.posteingang.title" title="Nationale Verfahren &mdash; Postzustellung">
<span data-i18n="deadlines.inbox.posteingang">Posteingang</span>
</button>
<button type="button" className="fristen-inbox-chip fristen-inbox-chip--clear" data-inbox-clear>
<span data-i18n="deadlines.inbox.all">Alle</span>
</button>
</div>
</div>
{/* v3 landing fork (t-paliad-133) — visible by default, hidden once
the user picks a pathway. URL ?path= drives visibility. */}
<div className="fristen-pathway-fork" id="fristen-pathway-fork" role="group" aria-label="Pathway selector">
<h2 className="fristen-pathway-fork-heading" data-i18n="deadlines.pathway.fork.heading">Was m&ouml;chten Sie tun?</h2>
<div className="fristen-pathway-fork-cards">
<button type="button" className="fristen-pathway-card" data-path="a" id="fristen-pathway-a-cta">
<span className="fristen-pathway-card-icon" aria-hidden="true">&#128214;</span>
<span className="fristen-pathway-card-title" data-i18n="deadlines.pathway.a.title">Verfahrensablauf informieren</span>
<span className="fristen-pathway-card-desc" data-i18n="deadlines.pathway.a.desc">
Verfahrenstyp w&auml;hlen und alle dazugeh&ouml;rigen Fristen auf einer Zeitleiste sehen.
</span>
</button>
<button type="button" className="fristen-pathway-card" data-path="b" id="fristen-pathway-b-cta">
<span className="fristen-pathway-card-icon" aria-hidden="true">&#128197;</span>
<span className="fristen-pathway-card-title" data-i18n="deadlines.pathway.b.title">Frist eintragen aufgrund Ereignis</span>
<span className="fristen-pathway-card-desc" data-i18n="deadlines.pathway.b.desc">
Ein Ereignis ist eingetreten &mdash; ich brauche die richtige Frist f&uuml;r meine Akte.
</span>
</button>
</div>
<div className="fristen-pathway-fork-shortcut">
<div className="fristen-pathway-fork-shortcut-label" data-i18n="deadlines.pathway.shortcut.label">
oder direkt zu einer Frist springen:
</div>
<div className="fristen-search-chips" id="fristen-fork-chips" role="group" aria-label="Schnellzugriff">
{QUICK_CHIPS.map((c) => quickChip(c))}
</div>
</div>
</div>
{/* Pathway B container — search bar relocates here from the page top.
Mode toggle (B1 tree / B2 filter) sits above the panels.
Hidden until ?path=b. */}
<div className="fristen-pathway-shell" id="fristen-pathway-b" data-path="b" hidden>
<button type="button" className="fristen-pathway-back" id="fristen-pathway-b-back">
<span aria-hidden="true">&larr;</span>{" "}
<span data-i18n="deadlines.pathway.back">zur&uuml;ck zur Auswahl</span>
</button>
<h2 className="fristen-pathway-heading">
<span aria-hidden="true">&#128197;</span>{" "}
<span data-i18n="deadlines.pathway.b.title">Frist eintragen aufgrund Ereignis</span>
</h2>
<div className="fristen-mode-toggle" role="radiogroup" aria-label="B1/B2 mode">
<label className="fristen-mode-toggle-option">
<input type="radio" name="fristen-b-mode" value="tree" id="fristen-b-mode-tree" />
<span data-i18n="deadlines.pathway.b.mode.tree">Schritt-f&uuml;r-Schritt (Entscheidungsbaum)</span>
</label>
<label className="fristen-mode-toggle-option">
<input type="radio" name="fristen-b-mode" value="filter" id="fristen-b-mode-filter" />
<span data-i18n="deadlines.pathway.b.mode.filter">Filter / Suche</span>
</label>
</div>
{/* B1 panel — decision tree above + concept-card results below.
fristen-b1-cascade hosts the breadcrumb / question / button row.
fristen-b1-results hosts the narrowing concept-card list,
populated by runB1Search() in fristenrechner.ts. The cards
reuse renderConceptCard() (B2's card shape). */}
<div className="fristen-b1-panel" id="fristen-b1-panel" data-mode="tree" hidden>
<div className="fristen-b1-cascade" id="fristen-b1-cascade"></div>
<div className="fristen-b1-results" id="fristen-b1-results" aria-live="polite"></div>
</div>
{/* B2 panel — search bar + chips + concept-card results.
The search input + chips + results host live here so
fristenrechner.ts can drive both Phase D (today) and the
B1↔B2 state-share in Phase D (forum filter). */}
<div className="fristen-b2-panel" id="fristen-b2-panel" data-mode="filter">
<div className="fristen-search">
<label htmlFor="fristen-search-input" className="visually-hidden" data-i18n="deadlines.search.label">Frist suchen</label>
<div className="fristen-search-row">
<svg className="fristen-search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="7"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input
type="search"
id="fristen-search-input"
className="fristen-search-input"
autocomplete="off"
spellcheck="false"
data-i18n-placeholder="deadlines.search.placeholder"
placeholder="Klageerwiderung, RoP 23, § 82, Wiedereinsetzung&hellip;"
/>
<button type="button" id="fristen-search-clear" className="fristen-search-clear" aria-label="Suche leeren" data-i18n-aria-label="deadlines.search.clear" hidden>
&times;
</button>
</div>
<div className="fristen-search-chips" id="fristen-search-chips" role="group" aria-label="Schnellzugriff">
<span className="fristen-search-chips-label" data-i18n="deadlines.search.chips.label">Schnellzugriff:</span>
{QUICK_CHIPS.map((c) => quickChip(c))}
</div>
{/* Forum filter row — populated by Phase D. */}
<div className="fristen-forum-filter" id="fristen-forum-filter" hidden>
<span className="fristen-forum-filter-label" data-i18n="deadlines.filter.forum.label">Gericht / System:</span>
<div className="fristen-forum-chips" id="fristen-forum-chips"></div>
</div>
<div id="fristen-search-results" className="fristen-search-results" aria-live="polite"></div>
</div>
</div>
</div>
{/* Pathway A container — wraps the existing wizard.
Hidden until ?path=a. */}
<div className="fristen-pathway-shell" id="fristen-pathway-a" data-path="a" hidden>
<button type="button" className="fristen-pathway-back" id="fristen-pathway-a-back">
<span aria-hidden="true">&larr;</span>{" "}
<span data-i18n="deadlines.pathway.back">zur&uuml;ck zur Auswahl</span>
</button>
<h2 className="fristen-pathway-heading">
<span aria-hidden="true">&#128214;</span>{" "}
<span data-i18n="deadlines.pathway.a.title">Verfahrensablauf informieren</span>
</h2>
{/* v3: legacy mode tabs retired (m's spec lock §10 Q1, 2026-05-05).
Pathway A is Verfahrensablauf-only; trigger-event drill-in
surfaces via concept-card pills with ?path=a&trigger=N URL,
which resurfaces mode-event-panel programmatically below. */}
<div className="fristen-wizard mode-panel" id="mode-procedure-panel" data-mode="procedure" role="tabpanel">
<div className="wizard-step" id="step-1">
<h3 className="wizard-step-label">
<span className="step-number">1</span>
<span data-i18n="deadlines.step1">Verfahrensart w&auml;hlen</span>
</h3>
<div className="proceeding-group" data-forum="upc">
<h4 data-i18n="deadlines.upc">UPC</h4>
<div className="proceeding-btns">
{UPC_TYPES.map((p) => proceedingBtn(p))}
</div>
</div>
<div className="proceeding-group" data-forum="de">
<h4 data-i18n="deadlines.de">Deutsche Gerichte</h4>
<div className="proceeding-btns">
{DE_TYPES.map((p) => proceedingBtn(p))}
</div>
</div>
<div className="proceeding-group" data-forum="epa">
<h4 data-i18n="deadlines.epa">EPA</h4>
<div className="proceeding-btns">
{EPA_TYPES.map((p) => proceedingBtn(p))}
</div>
</div>
<div className="proceeding-group" data-forum="dpma">
<h4 data-i18n="deadlines.dpma">DPMA</h4>
<div className="proceeding-btns">
{DPMA_TYPES.map((p) => proceedingBtn(p))}
</div>
</div>
</div>
<div className="wizard-step" id="step-2" style="display:none">
<h3 className="wizard-step-label">
<span className="step-number">2</span>
<span data-i18n="deadlines.step2">Ausgangsdatum eingeben</span>
</h3>
<div className="date-input-group">
<div className="date-field-row">
<label htmlFor="trigger-event" className="date-label" data-i18n="deadlines.trigger.event">Ausl&ouml;sendes Ereignis:</label>
<span id="trigger-event" className="trigger-event-name">&mdash;</span>
</div>
<div className="date-field-row">
<label htmlFor="trigger-date" className="date-label" data-i18n="deadlines.trigger.date">Datum:</label>
<input type="date" id="trigger-date" className="date-input" value={today} />
</div>
<div className="date-field-row" id="court-picker-row" style="display:none">
<label htmlFor="court-picker" className="date-label" data-i18n="deadlines.court.label">Gericht:</label>
<select id="court-picker" className="date-input"></select>
</div>
<div className="date-field-row" id="priority-date-row" style="display:none">
<label htmlFor="priority-date" className="date-label" data-i18n="deadlines.priority.date">Priorit&auml;tstag (optional):</label>
<input type="date" id="priority-date" className="date-input" />
</div>
<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&auml;nderung (R.30)</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&auml;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>
<button type="button" id="calculate-btn" className="calculate-btn" data-i18n="deadlines.calculate">
Fristen berechnen
</button>
</div>
</div>
<div className="wizard-step" id="step-3" style="display:none">
<h3 className="wizard-step-label">
<span className="step-number">3</span>
<span data-i18n="deadlines.step3">Ergebnis</span>
</h3>
<div className="fristen-view-toggle" id="fristen-view-toggle" role="radiogroup" aria-label="Ansicht">
<span className="fristen-view-label" data-i18n="deadlines.view.label">Ansicht:</span>
<label className="fristen-view-option">
<input type="radio" name="fristen-view" value="timeline" checked />
<span data-i18n="deadlines.view.timeline">Zeitstrahl</span>
</label>
<label className="fristen-view-option">
<input type="radio" name="fristen-view" value="columns" />
<span data-i18n="deadlines.view.columns">Spalten</span>
</label>
</div>
<div id="timeline-container">
</div>
<div className="fristen-result-actions">
<button type="button" id="fristen-save-cta" className="btn-primary btn-cta-lime" style="display:none" data-i18n="deadlines.save.cta">
Als Frist(en) speichern
</button>
<button type="button" id="fristen-print-btn" className="print-btn" style="display:none">
<svg className="print-btn-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="6 9 6 2 18 2 18 9"></polyline>
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path>
<rect x="6" y="14" width="12" height="8"></rect>
</svg>
<span data-i18n="deadlines.print">Drucken</span>
</button>
</div>
</div>
<button type="button" id="reset-btn" className="reset-btn" style="display:none" data-i18n="deadlines.reset">
&larr; Neu berechnen
</button>
</div>
<div className="fristen-wizard mode-panel" id="mode-event-panel" data-mode="event" role="tabpanel" hidden>
<div className="wizard-step" id="event-step-1">
<h3 className="wizard-step-label">
<span className="step-number">1</span>
<span data-i18n="deadlines.event.step1">Trigger-Ereignis w&auml;hlen</span>
</h3>
<p className="wizard-step-hint" data-i18n="deadlines.event.step1.hint">
Welches Ereignis ist eingetreten? (z.B. Klageerhebung, Entscheidung des EPA, Zustellung einer Verf&uuml;gung)
</p>
<div className="event-picker-row">
<label htmlFor="event-search" className="visually-hidden" data-i18n="deadlines.event.search.label">Trigger-Ereignis suchen</label>
<input
type="search"
id="event-search"
className="event-search-input"
autocomplete="off"
data-i18n-placeholder="deadlines.event.search.placeholder"
placeholder="Tippe, um zu suchen&hellip;"
/>
<ul id="event-list" className="event-list" role="listbox" aria-label="Trigger-Ereignisse"></ul>
</div>
</div>
<div className="wizard-step" id="event-step-2" style="display:none">
<h3 className="wizard-step-label">
<span className="step-number">2</span>
<span data-i18n="deadlines.event.step2">Datum des Ereignisses</span>
</h3>
<div className="date-input-group">
<div className="date-field-row">
<label className="date-label" data-i18n="deadlines.event.selected">Gew&auml;hltes Ereignis:</label>
<span id="event-selected-name" className="trigger-event-name">&mdash;</span>
</div>
<div className="date-field-row">
<label htmlFor="event-date" className="date-label" data-i18n="deadlines.event.date">Eintrittsdatum:</label>
<input type="date" id="event-date" className="date-input" value={today} />
</div>
<button type="button" id="event-calculate-btn" className="calculate-btn" data-i18n="deadlines.event.calculate">
Folgefristen berechnen
</button>
</div>
</div>
<div className="wizard-step" id="event-step-3" style="display:none">
<h3 className="wizard-step-label">
<span className="step-number">3</span>
<span data-i18n="deadlines.event.step3">Folgefristen</span>
</h3>
<div id="event-results-container"></div>
<div className="fristen-result-actions">
<button type="button" id="event-print-btn" className="print-btn" style="display:none">
<svg className="print-btn-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="6 9 6 2 18 2 18 9"></polyline>
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path>
<rect x="6" y="14" width="12" height="8"></rect>
</svg>
<span data-i18n="deadlines.print">Drucken</span>
</button>
</div>
</div>
<button type="button" id="event-reset-btn" className="reset-btn" style="display:none" data-i18n="deadlines.reset">
&larr; Neu berechnen
</button>
</div>
</div>{/* /pathway-a */}
</div>
</section>
</main>
<Footer />
<script src="/assets/fristenrechner.js"></script>
</body>
</html>
);
}