feat(procedures): U0 skeleton — /tools/procedures page shell (m/paliad#151)

First slice of the unified procedural-events tool train. Ships only the
page chrome — route, sidebar/header, filter strip with search box, four
entry-mode tabs (Verfahren wählen / Direkt suchen / Geführt / Aus Akte),
and the host containers later slices mount their UI into. No data wiring.

Per m's decisions (design §11.5): URL is English (/tools/procedures, not
/tools/verfahren); all four tabs visible from boot (not a single-default
landing); search box lives in the top filter strip and will compose with
chip filters once U1+ wire them.

U1 fills #procedures-panel-search (Mode A), U2 fills -wizard (Mode B),
U3 fills -proceeding + #procedures-output-tree (Verfahrensablauf), U4
hard-cuts /tools/fristenrechner and /tools/verfahrensablauf to 301
redirects and drops the legacy pages.
This commit is contained in:
mAi
2026-05-27 20:19:15 +02:00
parent 66b08813c4
commit 60907e7153
7 changed files with 335 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ import { renderLogin } from "./src/login";
import { renderKostenrechner } from "./src/kostenrechner";
import { renderFristenrechner } from "./src/fristenrechner";
import { renderVerfahrensablauf } from "./src/verfahrensablauf";
import { renderProcedures } from "./src/procedures";
import { renderDownloads } from "./src/downloads";
import { renderLinks } from "./src/links";
import { renderGlossary } from "./src/glossary";
@@ -243,6 +244,7 @@ async function build() {
join(import.meta.dir, "src/client/kostenrechner.ts"),
join(import.meta.dir, "src/client/fristenrechner.ts"),
join(import.meta.dir, "src/client/verfahrensablauf.ts"),
join(import.meta.dir, "src/client/procedures.ts"),
join(import.meta.dir, "src/client/downloads.ts"),
join(import.meta.dir, "src/client/links.ts"),
join(import.meta.dir, "src/client/glossary.ts"),
@@ -371,6 +373,7 @@ async function build() {
await Bun.write(join(DIST, "kostenrechner.html"), renderKostenrechner());
await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner());
await Bun.write(join(DIST, "verfahrensablauf.html"), renderVerfahrensablauf());
await Bun.write(join(DIST, "procedures.html"), renderProcedures());
await Bun.write(join(DIST, "downloads.html"), renderDownloads());
await Bun.write(join(DIST, "links.html"), renderLinks());
await Bun.write(join(DIST, "glossary.html"), renderGlossary());

View File

@@ -205,6 +205,25 @@ const translations: Record<Lang, Record<string, string>> = {
"tools.verfahrensablauf.heading": "Verfahrensablauf",
"tools.verfahrensablauf.subtitle": "Typischen Verfahrensablauf einsehen \u2014 Verfahrensart w\u00e4hlen, Datum optional setzen.",
// Unified procedural-events tool (m/paliad#151, U0 skeleton)
"procedures.title": "Verfahren & Fristen \u2014 Paliad",
"procedures.heading": "Verfahren & Fristen",
"procedures.subtitle": "Verfahrensablauf, Fristenrechner und gef\u00fchrte Suche in einem Tool.",
"procedures.filter.search.placeholder": "Klageerhebung, Hinweisbeschluss, oral hearing\u2026",
"procedures.filter.axis.forum": "Forum:",
"procedures.filter.axis.proc": "Verfahren:",
"procedures.filter.axis.kind": "Ereignisart:",
"procedures.filter.axis.party": "Partei:",
"procedures.tab.proceeding": "Verfahren w\u00e4hlen",
"procedures.tab.search": "Direkt suchen",
"procedures.tab.wizard": "Gef\u00fchrt",
"procedures.tab.akte": "Aus Akte",
"procedures.panel.proceeding.placeholder": "Verfahrenswahl folgt in U3 \u2014 das \u00dcbersichts-Baumdiagramm wird hier eingebettet.",
"procedures.panel.search.placeholder": "Direktsuche folgt in U1.",
"procedures.panel.wizard.placeholder": "Gef\u00fchrter Einstieg folgt in U2.",
"procedures.panel.akte.placeholder": "Akten-Einstieg folgt in einem sp\u00e4teren Slice.",
"nav.procedures": "Verfahren & Fristen",
"deadlines.step1": "Verfahrensart w\u00e4hlen",
"deadlines.step2": "Ausgangsdatum eingeben",
"deadlines.step2.perspective": "Perspektive und Datum",
@@ -3399,6 +3418,25 @@ const translations: Record<Lang, Record<string, string>> = {
"tools.verfahrensablauf.heading": "Procedure Roadmap",
"tools.verfahrensablauf.subtitle": "Browse the typical proceeding shape \u2014 pick a proceeding type, optionally set a trigger date.",
// Unified procedural-events tool (m/paliad#151, U0 skeleton)
"procedures.title": "Procedures & Deadlines \u2014 Paliad",
"procedures.heading": "Procedures & Deadlines",
"procedures.subtitle": "Procedure roadmap, deadline calculator, and guided search in one tool.",
"procedures.filter.search.placeholder": "Statement of claim, hearing notice, m\u00fcndliche Verhandlung\u2026",
"procedures.filter.axis.forum": "Forum:",
"procedures.filter.axis.proc": "Proceeding:",
"procedures.filter.axis.kind": "Event kind:",
"procedures.filter.axis.party": "Party:",
"procedures.tab.proceeding": "Pick proceeding",
"procedures.tab.search": "Direct search",
"procedures.tab.wizard": "Guided",
"procedures.tab.akte": "From matter",
"procedures.panel.proceeding.placeholder": "Pick-proceeding view ships in U3 \u2014 the overview tree mounts here.",
"procedures.panel.search.placeholder": "Direct search ships in U1.",
"procedures.panel.wizard.placeholder": "Guided entry ships in U2.",
"procedures.panel.akte.placeholder": "Matter entry ships in a later slice.",
"nav.procedures": "Procedures & Deadlines",
"deadlines.step1": "Select Proceeding Type",
"deadlines.step2": "Enter Trigger Date",
"deadlines.step2.perspective": "Perspective and Date",

View File

@@ -0,0 +1,72 @@
// /tools/procedures client — U0 skeleton boot
// (m/paliad#151, docs/design-unified-procedural-events-tool-2026-05-27.md).
//
// U0 owns:
// - Tab switching across the four entry modes (proceeding / search /
// wizard / akte). URL-driven via ?mode=<name>; cold open lands on
// "proceeding" per design §11.5.Q3.
// - Showing exactly one panel at a time.
//
// Later slices wire data:
// U1 mounts Mode A into #procedures-panel-search.
// U2 mounts Mode B into #procedures-panel-wizard.
// U3 mounts Verfahrensablauf into #procedures-panel-proceeding +
// #procedures-output-tree.
// The search box in the top filter strip and the chip filters in
// #procedures-filter-chips-* are also wired in U1+ as each slice
// adds its dimension-aware behaviour.
import { initI18n } from "./i18n";
import { initSidebar } from "./sidebar";
type ProceduresTab = "proceeding" | "search" | "wizard" | "akte";
const TABS: ProceduresTab[] = ["proceeding", "search", "wizard", "akte"];
function readTabFromUrl(): ProceduresTab {
const params = new URLSearchParams(window.location.search);
const raw = params.get("mode");
if (raw && (TABS as string[]).includes(raw)) return raw as ProceduresTab;
return "proceeding";
}
function writeTabToUrl(tab: ProceduresTab): void {
const url = new URL(window.location.href);
if (tab === "proceeding") {
url.searchParams.delete("mode");
} else {
url.searchParams.set("mode", tab);
}
history.replaceState(null, "", url.pathname + url.search + url.hash);
}
function showTab(tab: ProceduresTab): void {
for (const t of TABS) {
const btn = document.getElementById(`procedures-tab-${t}`);
const panel = document.getElementById(`procedures-panel-${t}`);
const active = t === tab;
if (btn) {
btn.classList.toggle("is-active", active);
btn.setAttribute("aria-selected", active ? "true" : "false");
}
if (panel) panel.hidden = !active;
}
}
function wireTabs(): void {
for (const t of TABS) {
const btn = document.getElementById(`procedures-tab-${t}`);
if (!btn) continue;
btn.addEventListener("click", () => {
showTab(t);
writeTabToUrl(t);
});
}
}
document.addEventListener("DOMContentLoaded", () => {
initI18n();
initSidebar();
showTab(readTabFromUrl());
wireTabs();
});

View File

@@ -2088,6 +2088,7 @@ export type I18nKey =
| "nav.logout"
| "nav.neuigkeiten"
| "nav.paliadin"
| "nav.procedures"
| "nav.projekte"
| "nav.soon.tooltip"
| "nav.submissions"
@@ -2204,6 +2205,22 @@ export type I18nKey =
| "partner_unit.members_label"
| "partner_unit.none"
| "partner_unit.subtitle"
| "procedures.filter.axis.forum"
| "procedures.filter.axis.kind"
| "procedures.filter.axis.party"
| "procedures.filter.axis.proc"
| "procedures.filter.search.placeholder"
| "procedures.heading"
| "procedures.panel.akte.placeholder"
| "procedures.panel.proceeding.placeholder"
| "procedures.panel.search.placeholder"
| "procedures.panel.wizard.placeholder"
| "procedures.subtitle"
| "procedures.tab.akte"
| "procedures.tab.proceeding"
| "procedures.tab.search"
| "procedures.tab.wizard"
| "procedures.title"
| "project.instance_level.appeal"
| "project.instance_level.cassation"
| "project.instance_level.first"

193
frontend/src/procedures.tsx Normal file
View File

@@ -0,0 +1,193 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { PaliadinWidget } from "./components/PaliadinWidget";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
import { PWAHead } from "./components/PWAHead";
// U0 — Skeleton for the unified procedural-events tool
// (m/paliad#151, design docs/design-unified-procedural-events-tool-2026-05-27.md).
//
// Folds /tools/fristenrechner (Mode A + Mode B + result) and
// /tools/verfahrensablauf into a single page at /tools/procedures. Each
// later slice fills one of the four entry tabs:
//
// U1 — Direkt suchen (Mode A search)
// U2 — Geführt (Mode B wizard)
// U3 — Verfahren (Verfahrensablauf tree + 3-way detail filter)
// U4 — Hard-cut 301 (drop legacy pages, redirect URLs)
//
// This file ships only the page chrome — sidebar, header, filter strip
// with search box, four entry-mode tabs, and the host containers the
// later slices mount their UI into. No data wiring.
export function renderProcedures(): string {
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="procedures.title">Verfahren &amp; Fristen &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar page-procedures">
<Sidebar currentPath="/tools/procedures" />
<BottomNav currentPath="/tools/procedures" />
<main>
<section className="tool-page">
<div className="container">
<div className="tool-header">
<h1 data-i18n="procedures.heading">Verfahren &amp; Fristen</h1>
<p className="tool-subtitle" data-i18n="procedures.subtitle">
Verfahrensablauf, Fristenrechner und ger&uuml;hrte Suche in einem Tool.
</p>
</div>
{/* Shared filter strip — search box + four chip groups
(forum / proceeding / event_kind / party). Lives at the
top of the page so every entry tab and output mode reads
the same active filter set (design §4 + m's Q3
divergence: search composes with chip filters). U0
ships the markup only; chip hydration + search wiring
arrive with U1-U3. */}
<section className="procedures-filter-strip" aria-label="Filter">
<div className="procedures-filter-search">
<svg className="procedures-filter-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="procedures-search-input"
className="procedures-filter-search-input"
autocomplete="off"
spellcheck="false"
data-i18n-placeholder="procedures.filter.search.placeholder"
placeholder="Klageerhebung, Hinweisbeschluss, oral hearing&hellip;"
/>
</div>
<div className="procedures-filter-chips" id="procedures-filter-chips">
<div className="procedures-filter-chip-row" data-axis="forum">
<span className="procedures-filter-axis-label" data-i18n="procedures.filter.axis.forum">Forum:</span>
<div className="procedures-filter-chip-host" id="procedures-filter-chips-forum"></div>
</div>
<div className="procedures-filter-chip-row" data-axis="proc">
<span className="procedures-filter-axis-label" data-i18n="procedures.filter.axis.proc">Verfahren:</span>
<div className="procedures-filter-chip-host" id="procedures-filter-chips-proc"></div>
</div>
<div className="procedures-filter-chip-row" data-axis="kind">
<span className="procedures-filter-axis-label" data-i18n="procedures.filter.axis.kind">Ereignisart:</span>
<div className="procedures-filter-chip-host" id="procedures-filter-chips-kind"></div>
</div>
<div className="procedures-filter-chip-row" data-axis="party">
<span className="procedures-filter-axis-label" data-i18n="procedures.filter.axis.party">Partei:</span>
<div className="procedures-filter-chip-host" id="procedures-filter-chips-party"></div>
</div>
</div>
</section>
{/* Entry-mode tab strip — all four tabs visible from boot
(m's Q3 divergence). The active tab is URL-driven
(?mode=proceeding|search|wizard|akte); cold open lands
on "proceeding" per design §11.5.Q3. */}
<nav className="procedures-tabs" role="tablist" aria-label="Einstieg">
<button type="button"
className="procedures-tab is-active"
role="tab"
aria-selected="true"
data-tab="proceeding"
id="procedures-tab-proceeding">
<span className="procedures-tab-icon" aria-hidden="true">&#128218;</span>
<span className="procedures-tab-label" data-i18n="procedures.tab.proceeding">Verfahren w&auml;hlen</span>
</button>
<button type="button"
className="procedures-tab"
role="tab"
aria-selected="false"
data-tab="search"
id="procedures-tab-search">
<span className="procedures-tab-icon" aria-hidden="true">&#9889;</span>
<span className="procedures-tab-label" data-i18n="procedures.tab.search">Direkt suchen</span>
</button>
<button type="button"
className="procedures-tab"
role="tab"
aria-selected="false"
data-tab="wizard"
id="procedures-tab-wizard">
<span className="procedures-tab-icon" aria-hidden="true">&#129517;</span>
<span className="procedures-tab-label" data-i18n="procedures.tab.wizard">Gef&uuml;hrt</span>
</button>
<button type="button"
className="procedures-tab"
role="tab"
aria-selected="false"
data-tab="akte"
id="procedures-tab-akte">
<span className="procedures-tab-icon" aria-hidden="true">&#128193;</span>
<span className="procedures-tab-label" data-i18n="procedures.tab.akte">Aus Akte</span>
</button>
</nav>
{/* Per-tab content hosts. Only one is visible at a time —
procedures.ts toggles `hidden` on the inactive ones.
Each later slice fills the corresponding host. */}
<section className="procedures-panel" id="procedures-panel-proceeding" role="tabpanel"
aria-labelledby="procedures-tab-proceeding">
<div className="procedures-panel-placeholder" data-i18n="procedures.panel.proceeding.placeholder">
Verfahrenswahl folgt in U3 &mdash; das &Uuml;bersichts-Baumdiagramm wird hier eingebettet.
</div>
</section>
<section className="procedures-panel" id="procedures-panel-search" role="tabpanel"
aria-labelledby="procedures-tab-search" hidden>
<div className="procedures-panel-placeholder" data-i18n="procedures.panel.search.placeholder">
Direktsuche folgt in U1.
</div>
</section>
<section className="procedures-panel" id="procedures-panel-wizard" role="tabpanel"
aria-labelledby="procedures-tab-wizard" hidden>
<div className="procedures-panel-placeholder" data-i18n="procedures.panel.wizard.placeholder">
Gef&uuml;hrter Einstieg folgt in U2.
</div>
</section>
<section className="procedures-panel" id="procedures-panel-akte" role="tabpanel"
aria-labelledby="procedures-tab-akte" hidden>
<div className="procedures-panel-placeholder" data-i18n="procedures.panel.akte.placeholder">
Akten-Einstieg folgt in einem sp&auml;teren Slice.
</div>
</section>
{/* Tree output host. Slice U3 mounts the Verfahrensablauf
tree here; U0 leaves it empty + hidden so the
tab placeholders are the only thing visible. */}
<section className="procedures-output procedures-output-tree" id="procedures-output-tree"
aria-label="Tree output" hidden></section>
{/* Linear-drawer host. Inline drawer expanding beneath a
tree card (design §8 — desktop) AND the standalone
linear follow-up view that Mode A / Mode B land on
after locking a trigger event (design §3.2). U1
switches it on. */}
<section className="procedures-output procedures-output-linear" id="procedures-output-linear"
aria-label="Linear output" hidden></section>
</div>
</section>
</main>
<Footer />
<PaliadinWidget />
<script src="/assets/procedures.js"></script>
</body>
</html>
);
}

View File

@@ -36,6 +36,17 @@ func handleVerfahrensablaufPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/verfahrensablauf.html")
}
// Unified procedural-events tool page (m/paliad#151, design
// docs/design-unified-procedural-events-tool-2026-05-27.md). Consolidates
// Fristenrechner Mode A + Mode B + result + Verfahrensablauf into a
// single surface at /tools/procedures. No DB dependency — the page
// itself is static HTML; per-tab data flows over the existing
// /api/tools/fristenrechner/* endpoints. Slice U4 turns the legacy
// /tools/fristenrechner + /tools/verfahrensablauf into 301 redirects.
func handleProceduresPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/procedures.html")
}
// POST /api/tools/fristenrechner — calculate the UI timeline for a proceeding.
//
// Phase C: routes through FristenrechnerService which pulls rules from

View File

@@ -305,6 +305,7 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc
protected.HandleFunc("POST /api/tools/kostenrechner", handleKostenrechnerAPI)
protected.HandleFunc("GET /tools/fristenrechner", handleFristenrechnerPage)
protected.HandleFunc("GET /tools/verfahrensablauf", handleVerfahrensablaufPage)
protected.HandleFunc("GET /tools/procedures", handleProceduresPage)
protected.HandleFunc("POST /api/tools/fristenrechner", handleFristenrechnerAPI)
protected.HandleFunc("POST /api/tools/fristenrechner/calculate-rule", handleFristenrechnerCalculateRule)
protected.HandleFunc("GET /api/tools/proceeding-types", handleProceedingTypes)