refactor(rename): frontend TSX + client TS files, fetch URLs, nav hrefs

t-paliad-025 Phase 3 — frontend rename pass:

File renames (git mv, preserving history):
  frontend/src/
    akten.tsx               → projects.tsx
    akten-neu.tsx           → projects-new.tsx
    akten-detail.tsx        → projects-detail.tsx
    fristen.tsx             → deadlines.tsx
    fristen-neu.tsx         → deadlines-new.tsx
    fristen-detail.tsx      → deadlines-detail.tsx
    fristen-kalender.tsx    → deadlines-calendar.tsx
    termine.tsx             → appointments.tsx
    termine-neu.tsx         → appointments-new.tsx
    termine-detail.tsx      → appointments-detail.tsx
    termine-kalender.tsx    → appointments-calendar.tsx
    einstellungen.tsx       → settings.tsx
    checklisten*.tsx        → checklists*.tsx
    gerichte.tsx            → courts.tsx
    glossar.tsx             → glossary.tsx

  frontend/src/client/ — same renames, plus notizen.ts → notes.ts.

Render exports renamed (renderAkten → renderProjects, renderFristen →
renderDeadlines, …). build.ts rewired to new names.

Client-side changes:
* fetch() API paths: /api/projekte → /api/projects, /api/fristen →
  /api/deadlines, /api/termine → /api/appointments, /api/notizen →
  /api/notes, /api/gerichte → /api/courts, /api/glossar → /api/glossary,
  /api/dezernate → /api/departments, /api/parteien → /api/parties,
  /api/checklisten → /api/checklists. Legacy /api/akten aliases removed.
* Navigation href/template strings: /akten → /projects, /fristen →
  /deadlines, /termine → /appointments, /einstellungen → /settings,
  /notizen → /notes, /checklisten → /checklists, /gerichte → /courts,
  /glossar → /glossary. Nested paths /neu → /new, /verlauf → /events,
  /kinder → /children, /kalender → /calendar, /dokumente → /documents.
* Interface names in client TS: Frist → Deadline, Termin → Appointment,
  Notiz → Note, Partei → Party, Akte → Project, ProjektMini → ProjectMini,
  Dezernat → Department, DezernatMitglied → DepartmentMember.
* JSON wire-format keys follow backend: projekt_id → project_id, akte_id
  → project_id, frist_id → deadline_id, termin_id → appointment_id,
  akten_event_id → project_event_id, dezernat_id → department_id,
  termin_type → appointment_type.

Go handlers (projects_pages.go, deadlines_pages.go, appointments_pages.go,
checklists.go, courts.go, glossary.go) serve the correctly-named HTML
files from dist/.

Kept German (user-facing i18n + product names):
* i18n keys/strings (src/client/i18n.ts) — DE labels and their keys
* Product names: fristenrechner, kostenrechner, gebuehrentabellen

Build verified: go build / vet / test clean; bun run build clean;
dist/ contains all 26 English-named HTML pages.
This commit is contained in:
m
2026-04-20 17:44:45 +02:00
parent 49c6bc75ca
commit caf319e7ee
45 changed files with 544 additions and 544 deletions

View File

@@ -6,24 +6,24 @@ import { renderKostenrechner } from "./src/kostenrechner";
import { renderFristenrechner } from "./src/fristenrechner"; import { renderFristenrechner } from "./src/fristenrechner";
import { renderDownloads } from "./src/downloads"; import { renderDownloads } from "./src/downloads";
import { renderLinks } from "./src/links"; import { renderLinks } from "./src/links";
import { renderGlossar } from "./src/glossar"; import { renderGlossary } from "./src/glossary";
import { renderGebuehrentabellen } from "./src/gebuehrentabellen"; import { renderGebuehrentabellen } from "./src/gebuehrentabellen";
import { renderChecklisten } from "./src/checklisten"; import { renderChecklists } from "./src/checklists";
import { renderChecklistenDetail } from "./src/checklisten-detail"; import { renderChecklistsDetail } from "./src/checklists-detail";
import { renderChecklistenInstance } from "./src/checklisten-instance"; import { renderChecklistsInstance } from "./src/checklists-instance";
import { renderGerichte } from "./src/gerichte"; import { renderCourts } from "./src/courts";
import { renderAkten } from "./src/akten"; import { renderProjects } from "./src/projects";
import { renderAktenNeu } from "./src/akten-neu"; import { renderProjectsNew } from "./src/projects-new";
import { renderAktenDetail } from "./src/akten-detail"; import { renderProjectsDetail } from "./src/projects-detail";
import { renderFristen } from "./src/fristen"; import { renderDeadlines } from "./src/deadlines";
import { renderFristenNeu } from "./src/fristen-neu"; import { renderDeadlinesNew } from "./src/deadlines-new";
import { renderFristenDetail } from "./src/fristen-detail"; import { renderDeadlinesDetail } from "./src/deadlines-detail";
import { renderFristenKalender } from "./src/fristen-kalender"; import { renderDeadlinesCalendar } from "./src/deadlines-calendar";
import { renderTermine } from "./src/termine"; import { renderAppointments } from "./src/appointments";
import { renderTermineNeu } from "./src/termine-neu"; import { renderAppointmentsNew } from "./src/appointments-new";
import { renderTermineDetail } from "./src/termine-detail"; import { renderAppointmentsDetail } from "./src/appointments-detail";
import { renderTermineKalender } from "./src/termine-kalender"; import { renderAppointmentsCalendar } from "./src/appointments-calendar";
import { renderEinstellungen } from "./src/einstellungen"; import { renderSettings } from "./src/settings";
import { renderDashboard } from "./src/dashboard"; import { renderDashboard } from "./src/dashboard";
import { renderOnboarding } from "./src/onboarding"; import { renderOnboarding } from "./src/onboarding";
@@ -43,24 +43,24 @@ async function build() {
join(import.meta.dir, "src/client/fristenrechner.ts"), join(import.meta.dir, "src/client/fristenrechner.ts"),
join(import.meta.dir, "src/client/downloads.ts"), join(import.meta.dir, "src/client/downloads.ts"),
join(import.meta.dir, "src/client/links.ts"), join(import.meta.dir, "src/client/links.ts"),
join(import.meta.dir, "src/client/glossar.ts"), join(import.meta.dir, "src/client/glossary.ts"),
join(import.meta.dir, "src/client/gebuehrentabellen.ts"), join(import.meta.dir, "src/client/gebuehrentabellen.ts"),
join(import.meta.dir, "src/client/checklisten.ts"), join(import.meta.dir, "src/client/checklists.ts"),
join(import.meta.dir, "src/client/checklisten-detail.ts"), join(import.meta.dir, "src/client/checklists-detail.ts"),
join(import.meta.dir, "src/client/checklisten-instance.ts"), join(import.meta.dir, "src/client/checklists-instance.ts"),
join(import.meta.dir, "src/client/gerichte.ts"), join(import.meta.dir, "src/client/courts.ts"),
join(import.meta.dir, "src/client/akten.ts"), join(import.meta.dir, "src/client/projects.ts"),
join(import.meta.dir, "src/client/akten-neu.ts"), join(import.meta.dir, "src/client/projects-new.ts"),
join(import.meta.dir, "src/client/akten-detail.ts"), join(import.meta.dir, "src/client/projects-detail.ts"),
join(import.meta.dir, "src/client/fristen.ts"), join(import.meta.dir, "src/client/deadlines.ts"),
join(import.meta.dir, "src/client/fristen-neu.ts"), join(import.meta.dir, "src/client/deadlines-new.ts"),
join(import.meta.dir, "src/client/fristen-detail.ts"), join(import.meta.dir, "src/client/deadlines-detail.ts"),
join(import.meta.dir, "src/client/fristen-kalender.ts"), join(import.meta.dir, "src/client/deadlines-calendar.ts"),
join(import.meta.dir, "src/client/termine.ts"), join(import.meta.dir, "src/client/appointments.ts"),
join(import.meta.dir, "src/client/termine-neu.ts"), join(import.meta.dir, "src/client/appointments-new.ts"),
join(import.meta.dir, "src/client/termine-detail.ts"), join(import.meta.dir, "src/client/appointments-detail.ts"),
join(import.meta.dir, "src/client/termine-kalender.ts"), join(import.meta.dir, "src/client/appointments-calendar.ts"),
join(import.meta.dir, "src/client/einstellungen.ts"), join(import.meta.dir, "src/client/settings.ts"),
join(import.meta.dir, "src/client/dashboard.ts"), join(import.meta.dir, "src/client/dashboard.ts"),
join(import.meta.dir, "src/client/onboarding.ts"), join(import.meta.dir, "src/client/onboarding.ts"),
], ],
@@ -90,24 +90,24 @@ async function build() {
await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner()); await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner());
await Bun.write(join(DIST, "downloads.html"), renderDownloads()); await Bun.write(join(DIST, "downloads.html"), renderDownloads());
await Bun.write(join(DIST, "links.html"), renderLinks()); await Bun.write(join(DIST, "links.html"), renderLinks());
await Bun.write(join(DIST, "glossar.html"), renderGlossar()); await Bun.write(join(DIST, "glossary.html"), renderGlossary());
await Bun.write(join(DIST, "gebuehrentabellen.html"), renderGebuehrentabellen()); await Bun.write(join(DIST, "gebuehrentabellen.html"), renderGebuehrentabellen());
await Bun.write(join(DIST, "checklisten.html"), renderChecklisten()); await Bun.write(join(DIST, "checklists.html"), renderChecklists());
await Bun.write(join(DIST, "checklisten-detail.html"), renderChecklistenDetail()); await Bun.write(join(DIST, "checklists-detail.html"), renderChecklistsDetail());
await Bun.write(join(DIST, "checklisten-instance.html"), renderChecklistenInstance()); await Bun.write(join(DIST, "checklists-instance.html"), renderChecklistsInstance());
await Bun.write(join(DIST, "gerichte.html"), renderGerichte()); await Bun.write(join(DIST, "courts.html"), renderCourts());
await Bun.write(join(DIST, "akten.html"), renderAkten()); await Bun.write(join(DIST, "projects.html"), renderProjects());
await Bun.write(join(DIST, "akten-neu.html"), renderAktenNeu()); await Bun.write(join(DIST, "projects-new.html"), renderProjectsNew());
await Bun.write(join(DIST, "akten-detail.html"), renderAktenDetail()); await Bun.write(join(DIST, "projects-detail.html"), renderProjectsDetail());
await Bun.write(join(DIST, "fristen.html"), renderFristen()); await Bun.write(join(DIST, "deadlines.html"), renderDeadlines());
await Bun.write(join(DIST, "fristen-neu.html"), renderFristenNeu()); await Bun.write(join(DIST, "deadlines-new.html"), renderDeadlinesNew());
await Bun.write(join(DIST, "fristen-detail.html"), renderFristenDetail()); await Bun.write(join(DIST, "deadlines-detail.html"), renderDeadlinesDetail());
await Bun.write(join(DIST, "fristen-kalender.html"), renderFristenKalender()); await Bun.write(join(DIST, "deadlines-calendar.html"), renderDeadlinesCalendar());
await Bun.write(join(DIST, "termine.html"), renderTermine()); await Bun.write(join(DIST, "appointments.html"), renderAppointments());
await Bun.write(join(DIST, "termine-neu.html"), renderTermineNeu()); await Bun.write(join(DIST, "appointments-new.html"), renderAppointmentsNew());
await Bun.write(join(DIST, "termine-detail.html"), renderTermineDetail()); await Bun.write(join(DIST, "appointments-detail.html"), renderAppointmentsDetail());
await Bun.write(join(DIST, "termine-kalender.html"), renderTermineKalender()); await Bun.write(join(DIST, "appointments-calendar.html"), renderAppointmentsCalendar());
await Bun.write(join(DIST, "einstellungen.html"), renderEinstellungen()); await Bun.write(join(DIST, "settings.html"), renderSettings());
await Bun.write(join(DIST, "dashboard.html"), renderDashboard()); await Bun.write(join(DIST, "dashboard.html"), renderDashboard());
await Bun.write(join(DIST, "onboarding.html"), renderOnboarding()); await Bun.write(join(DIST, "onboarding.html"), renderOnboarding());

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar"; import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export function renderTermineKalender(): string { export function renderAppointmentsCalendar(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -12,7 +12,7 @@ export function renderTermineKalender(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/termine" /> <Sidebar currentPath="/appointments" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
@@ -26,8 +26,8 @@ export function renderTermineKalender(): string {
</p> </p>
</div> </div>
<div className="fristen-header-actions"> <div className="fristen-header-actions">
<a href="/termine" className="btn-secondary" data-i18n="termine.kalender.list">Listenansicht</a> <a href="/appointments" className="btn-secondary" data-i18n="termine.kalender.list">Listenansicht</a>
<a href="/termine/neu" className="btn-primary btn-cta-lime" data-i18n="termine.list.new">Neuer Termin</a> <a href="/appointments/new" className="btn-primary btn-cta-lime" data-i18n="termine.list.new">Neuer Termin</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar"; import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export function renderTermineDetail(): string { export function renderAppointmentsDetail(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -12,12 +12,12 @@ export function renderTermineDetail(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/termine" /> <Sidebar currentPath="/appointments" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
<div className="container container-narrow"> <div className="container container-narrow">
<a href="/termine" className="akten-back-link" data-i18n="termine.detail.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a> <a href="/appointments" className="akten-back-link" data-i18n="termine.detail.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a>
<div id="termin-loading" className="akten-loading" data-i18n="termine.detail.loading">L&auml;dt&hellip;</div> <div id="termin-loading" className="akten-loading" data-i18n="termine.detail.loading">L&auml;dt&hellip;</div>
<div id="termin-not-found" style="display:none" className="akten-empty"> <div id="termin-not-found" style="display:none" className="akten-empty">

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar"; import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export function renderTermineNeu(): string { export function renderAppointmentsNew(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -12,13 +12,13 @@ export function renderTermineNeu(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/termine/neu" /> <Sidebar currentPath="/appointments/new" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
<div className="container container-narrow"> <div className="container container-narrow">
<div className="tool-header"> <div className="tool-header">
<a href="/termine" className="akten-back-link" id="termin-neu-back" data-i18n="termine.neu.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a> <a href="/appointments" className="akten-back-link" id="termin-neu-back" data-i18n="termine.neu.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a>
<h1 data-i18n="termine.neu.heading">Neuer Termin</h1> <h1 data-i18n="termine.neu.heading">Neuer Termin</h1>
<p className="tool-subtitle" data-i18n="termine.neu.subtitle"> <p className="tool-subtitle" data-i18n="termine.neu.subtitle">
Pers&ouml;nlich oder einer Akte zugeordnet. Bei aktiver CalDAV-Synchronisation erscheint der Termin auch im externen Kalender. Pers&ouml;nlich oder einer Akte zugeordnet. Bei aktiver CalDAV-Synchronisation erscheint der Termin auch im externen Kalender.
@@ -80,7 +80,7 @@ export function renderTermineNeu(): string {
<p className="form-msg" id="termin-neu-msg" /> <p className="form-msg" id="termin-neu-msg" />
<div className="form-actions"> <div className="form-actions">
<a href="/termine" id="termin-neu-cancel" className="btn-cancel" data-i18n="termine.neu.cancel">Abbrechen</a> <a href="/appointments" id="termin-neu-cancel" className="btn-cancel" data-i18n="termine.neu.cancel">Abbrechen</a>
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="termine.neu.submit">Termin anlegen</button> <button type="submit" className="btn-primary btn-cta-lime" data-i18n="termine.neu.submit">Termin anlegen</button>
</div> </div>
</form> </form>

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar"; import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export function renderTermine(): string { export function renderAppointments(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -12,7 +12,7 @@ export function renderTermine(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/termine" /> <Sidebar currentPath="/appointments" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
@@ -26,10 +26,10 @@ export function renderTermine(): string {
</p> </p>
</div> </div>
<div className="fristen-header-actions"> <div className="fristen-header-actions">
<a href="/termine/kalender" className="btn-secondary" data-i18n="termine.list.calendar"> <a href="/appointments/calendar" className="btn-secondary" data-i18n="termine.list.calendar">
Kalenderansicht Kalenderansicht
</a> </a>
<a href="/termine/neu" className="btn-primary btn-cta-lime" data-i18n="termine.list.new"> <a href="/appointments/new" className="btn-primary btn-cta-lime" data-i18n="termine.list.new">
Neuer Termin Neuer Termin
</a> </a>
</div> </div>
@@ -105,7 +105,7 @@ export function renderTermine(): string {
<p data-i18n="termine.empty.hint"> <p data-i18n="termine.empty.hint">
Sobald Termine angelegt werden, erscheinen sie hier. Sobald Termine angelegt werden, erscheinen sie hier.
</p> </p>
<a href="/termine/neu" className="btn-primary btn-cta-lime" data-i18n="termine.list.new">Neuer Termin</a> <a href="/appointments/new" className="btn-primary btn-cta-lime" data-i18n="termine.list.new">Neuer Termin</a>
</div> </div>
<div className="akten-empty akten-empty-filtered" id="termine-empty-filtered" style="display:none"> <div className="akten-empty akten-empty-filtered" id="termine-empty-filtered" style="display:none">

View File

@@ -6,7 +6,7 @@ import { Footer } from "./components/Footer";
// instances + CTA to create a new instance. Clicking an instance takes // instances + CTA to create a new instance. Clicking an instance takes
// the user to /checklisten/instances/{id} where the interactive // the user to /checklisten/instances/{id} where the interactive
// checkboxes live. // checkboxes live.
export function renderChecklistenDetail(): string { export function renderChecklistsDetail(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -16,12 +16,12 @@ export function renderChecklistenDetail(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/checklisten" /> <Sidebar currentPath="/checklists" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
<div className="container"> <div className="container">
<a href="/checklisten" className="checklist-back"> <a href="/checklists" className="checklist-back">
<span className="checklist-back-arrow">&larr;</span> <span className="checklist-back-arrow">&larr;</span>
<span data-i18n="checklisten.back">Zur&uuml;ck zur &Uuml;bersicht</span> <span data-i18n="checklisten.back">Zur&uuml;ck zur &Uuml;bersicht</span>
</a> </a>

View File

@@ -5,7 +5,7 @@ import { Footer } from "./components/Footer";
// Interactive instance page. Loads template + instance JSON, renders // Interactive instance page. Loads template + instance JSON, renders
// checkboxes, PATCHes /api/checklist-instances/{id} on every toggle. // checkboxes, PATCHes /api/checklist-instances/{id} on every toggle.
// Reset button POSTs to .../reset. // Reset button POSTs to .../reset.
export function renderChecklistenInstance(): string { export function renderChecklistsInstance(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -15,7 +15,7 @@ export function renderChecklistenInstance(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/checklisten" /> <Sidebar currentPath="/checklists" />
<main> <main>
<section className="tool-page"> <section className="tool-page">

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar"; import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export function renderChecklisten(): string { export function renderChecklists(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -12,7 +12,7 @@ export function renderChecklisten(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/checklisten" /> <Sidebar currentPath="/checklists" />
<main> <main>
<section className="tool-page"> <section className="tool-page">

View File

@@ -1,18 +1,18 @@
import { initI18n, onLangChange, t, getLang } from "./i18n"; import { initI18n, onLangChange, t, getLang } from "./i18n";
import { initSidebar } from "./sidebar"; import { initSidebar } from "./sidebar";
interface Termin { interface Appointment {
id: string; id: string;
projekt_id?: string; project_id?: string;
title: string; title: string;
start_at: string; start_at: string;
end_at?: string; end_at?: string;
termin_type?: string; appointment_type?: string;
projekt_reference?: string; project_reference?: string;
projekt_title?: string; project_title?: string;
} }
let allTermine: Termin[] = []; let allAppointments: Appointment[] = [];
let viewYear = 0; let viewYear = 0;
let viewMonth = 0; let viewMonth = 0;
@@ -37,15 +37,15 @@ async function loadTermine() {
// We could narrow this, but the user typically navigates ±1-2 months // We could narrow this, but the user typically navigates ±1-2 months
// and the dataset is small. // and the dataset is small.
try { try {
const resp = await fetch("/api/termine"); const resp = await fetch("/api/appointments");
if (resp.ok) allTermine = await resp.json(); if (resp.ok) allAppointments = await resp.json();
} catch { } catch {
/* non-fatal */ /* non-fatal */
} }
} }
function termineForDate(iso: string): Termin[] { function appointmentsForDate(iso: string): Appointment[] {
return allTermine.filter((t) => t.start_at.slice(0, 10) === iso); return allAppointments.filter((t) => t.start_at.slice(0, 10) === iso);
} }
function typeClass(t?: string): string { function typeClass(t?: string): string {
@@ -80,12 +80,12 @@ function render() {
} }
for (let day = 1; day <= daysInMonth; day++) { for (let day = 1; day <= daysInMonth; day++) {
const iso = isoDate(viewYear, viewMonth, day); const iso = isoDate(viewYear, viewMonth, day);
const items = termineForDate(iso); const items = appointmentsForDate(iso);
const isToday = iso === todayISO; const isToday = iso === todayISO;
const dots = items const dots = items
.slice(0, 4) .slice(0, 4)
.map((tt) => `<span class="termin-dot ${typeClass(tt.termin_type)}" title="${esc(tt.title)}"></span>`) .map((tt) => `<span class="termin-dot ${typeClass(tt.appointment_type)}" title="${esc(tt.title)}"></span>`)
.join(""); .join("");
const more = items.length > 4 ? `<span class="frist-cal-more">+${items.length - 4}</span>` : ""; const more = items.length > 4 ? `<span class="frist-cal-more">+${items.length - 4}</span>` : "";
@@ -106,7 +106,7 @@ function render() {
const monthStart = isoDate(viewYear, viewMonth, 1); const monthStart = isoDate(viewYear, viewMonth, 1);
const monthEnd = isoDate(viewYear, viewMonth, daysInMonth); const monthEnd = isoDate(viewYear, viewMonth, daysInMonth);
const hasInMonth = allTermine.some((tt) => { const hasInMonth = allAppointments.some((tt) => {
const iso = tt.start_at.slice(0, 10); const iso = tt.start_at.slice(0, 10);
return iso >= monthStart && iso <= monthEnd; return iso >= monthStart && iso <= monthEnd;
}); });
@@ -115,7 +115,7 @@ function render() {
} }
function openPopup(iso: string) { function openPopup(iso: string) {
const items = termineForDate(iso); const items = appointmentsForDate(iso);
if (items.length === 0) return; if (items.length === 0) return;
const popup = document.getElementById("cal-popup")!; const popup = document.getElementById("cal-popup")!;
const dateEl = document.getElementById("cal-popup-date")!; const dateEl = document.getElementById("cal-popup-date")!;
@@ -131,13 +131,13 @@ function openPopup(iso: string) {
list.innerHTML = items list.innerHTML = items
.map((tt) => { .map((tt) => {
const akteRef = tt.projekt_id const akteRef = tt.project_id
? `<a href="/projekte/${esc(tt.projekt_id)}" class="frist-cal-popup-akte">${esc(tt.projekt_reference ?? "")}</a>` ? `<a href="/projects/${esc(tt.project_id)}" class="frist-cal-popup-project">${esc(tt.project_reference ?? "")}</a>`
: `<span class="termin-personal-tag">${esc(t("termine.personal"))}</span>`; : `<span class="termin-personal-tag">${esc(t("termine.personal"))}</span>`;
return `<li class="frist-cal-popup-item"> return `<li class="frist-cal-popup-item">
<span class="termin-dot ${typeClass(tt.termin_type)}"></span> <span class="termin-dot ${typeClass(tt.appointment_type)}"></span>
<span class="frist-cal-popup-time">${esc(fmtTime(tt.start_at))}</span> <span class="frist-cal-popup-time">${esc(fmtTime(tt.start_at))}</span>
<a href="/termine/${esc(tt.id)}" class="frist-cal-popup-title">${esc(tt.title)}</a> <a href="/appointments/${esc(tt.id)}" class="frist-cal-popup-title">${esc(tt.title)}</a>
${akteRef} ${akteRef}
</li>`; </li>`;
}) })

View File

@@ -1,27 +1,27 @@
import { initI18n, t, getLang } from "./i18n"; import { initI18n, t, getLang } from "./i18n";
import { initSidebar } from "./sidebar"; import { initSidebar } from "./sidebar";
import { initNotes } from "./notizen"; import { initNotes } from "./notes";
interface Termin { interface Appointment {
id: string; id: string;
projekt_id?: string; project_id?: string;
title: string; title: string;
description?: string; description?: string;
start_at: string; start_at: string;
end_at?: string; end_at?: string;
location?: string; location?: string;
termin_type?: string; appointment_type?: string;
created_by?: string; created_by?: string;
} }
interface Akte { interface Project {
id: string; id: string;
aktenzeichen: string; aktenzeichen: string;
title: string; title: string;
} }
let termin: Termin | null = null; let termin: Appointment | null = null;
let akte: Akte | null = null; let project: Project | null = null;
function parseTerminID(): string | null { function parseTerminID(): string | null {
const parts = window.location.pathname.split("/").filter(Boolean); const parts = window.location.pathname.split("/").filter(Boolean);
@@ -59,7 +59,7 @@ function esc(s: string): string {
async function loadTermin(id: string): Promise<boolean> { async function loadTermin(id: string): Promise<boolean> {
try { try {
const resp = await fetch(`/api/termine/${id}`); const resp = await fetch(`/api/appointments/${id}`);
if (!resp.ok) return false; if (!resp.ok) return false;
termin = await resp.json(); termin = await resp.json();
return true; return true;
@@ -70,8 +70,8 @@ async function loadTermin(id: string): Promise<boolean> {
async function loadAkte(id: string) { async function loadAkte(id: string) {
try { try {
const resp = await fetch(`/api/projekte/${id}`); const resp = await fetch(`/api/projects/${id}`);
if (resp.ok) akte = await resp.json(); if (resp.ok) project = await resp.json();
} catch { } catch {
/* non-fatal */ /* non-fatal */
} }
@@ -87,19 +87,19 @@ function renderHeader() {
document.getElementById("termin-time-display")!.textContent = time; document.getElementById("termin-time-display")!.textContent = time;
const badge = document.getElementById("termin-type-badge")!; const badge = document.getElementById("termin-type-badge")!;
if (termin.termin_type) { if (termin.appointment_type) {
badge.textContent = t(`termine.type.${termin.termin_type}`) || termin.termin_type; badge.textContent = t(`termine.type.${termin.appointment_type}`) || termin.appointment_type;
badge.className = `termin-type-badge termin-type-${termin.termin_type}`; badge.className = `termin-type-badge termin-type-${termin.appointment_type}`;
badge.style.display = ""; badge.style.display = "";
} else { } else {
badge.style.display = "none"; badge.style.display = "none";
} }
const akteRow = document.getElementById("termin-akte-row")!; const akteRow = document.getElementById("termin-project-row")!;
if (termin.projekt_id && akte) { if (termin.project_id && project) {
const link = document.getElementById("termin-akte-link") as HTMLAnchorElement; const link = document.getElementById("termin-project-link") as HTMLAnchorElement;
link.href = `/projekte/${akte.id}`; link.href = `/projects/${project.id}`;
link.textContent = `${akte.aktenzeichen} \u2014 ${akte.title}`; link.textContent = `${project.aktenzeichen} \u2014 ${project.title}`;
akteRow.style.display = ""; akteRow.style.display = "";
} else { } else {
akteRow.style.display = "none"; akteRow.style.display = "none";
@@ -111,7 +111,7 @@ function fillEditForm() {
(document.getElementById("termin-title-edit") as HTMLInputElement).value = termin.title; (document.getElementById("termin-title-edit") as HTMLInputElement).value = termin.title;
(document.getElementById("termin-start-edit") as HTMLInputElement).value = toLocalInput(termin.start_at); (document.getElementById("termin-start-edit") as HTMLInputElement).value = toLocalInput(termin.start_at);
(document.getElementById("termin-end-edit") as HTMLInputElement).value = toLocalInput(termin.end_at); (document.getElementById("termin-end-edit") as HTMLInputElement).value = toLocalInput(termin.end_at);
(document.getElementById("termin-type-edit") as HTMLSelectElement).value = termin.termin_type ?? ""; (document.getElementById("termin-type-edit") as HTMLSelectElement).value = termin.appointment_type ?? "";
(document.getElementById("termin-location-edit") as HTMLInputElement).value = termin.location ?? ""; (document.getElementById("termin-location-edit") as HTMLInputElement).value = termin.location ?? "";
(document.getElementById("termin-description-edit") as HTMLTextAreaElement).value = termin.description ?? ""; (document.getElementById("termin-description-edit") as HTMLTextAreaElement).value = termin.description ?? "";
} }
@@ -134,14 +134,14 @@ async function saveEdit(ev: Event) {
title, title,
start_at: new Date(startRaw).toISOString(), start_at: new Date(startRaw).toISOString(),
end_at: endRaw ? new Date(endRaw).toISOString() : null, end_at: endRaw ? new Date(endRaw).toISOString() : null,
termin_type: type, appointment_type: type,
location, location,
description, description,
}; };
submitBtn.disabled = true; submitBtn.disabled = true;
try { try {
const resp = await fetch(`/api/termine/${termin.id}`, { const resp = await fetch(`/api/appointments/${termin.id}`, {
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),
@@ -168,9 +168,9 @@ async function deleteTermin() {
if (!termin) return; if (!termin) return;
if (!confirm(t("termine.detail.delete.confirm"))) return; if (!confirm(t("termine.detail.delete.confirm"))) return;
try { try {
const resp = await fetch(`/api/termine/${termin.id}`, { method: "DELETE" }); const resp = await fetch(`/api/appointments/${termin.id}`, { method: "DELETE" });
if (resp.ok || resp.status === 204) { if (resp.ok || resp.status === 204) {
window.location.href = "/termine"; window.location.href = "/appointments";
} else { } else {
const data = await resp.json().catch(() => ({}) as { error?: string }); const data = await resp.json().catch(() => ({}) as { error?: string });
const msg = document.getElementById("termin-edit-msg")!; const msg = document.getElementById("termin-edit-msg")!;
@@ -200,7 +200,7 @@ async function main() {
notFound.style.display = "block"; notFound.style.display = "block";
return; return;
} }
if (termin.projekt_id) await loadAkte(termin.projekt_id); if (termin.project_id) await loadAkte(termin.project_id);
loading.style.display = "none"; loading.style.display = "none";
body.style.display = ""; body.style.display = "";
renderHeader(); renderHeader();

View File

@@ -1,13 +1,13 @@
import { initI18n, t } from "./i18n"; import { initI18n, t } from "./i18n";
import { initSidebar } from "./sidebar"; import { initSidebar } from "./sidebar";
interface Akte { interface Project {
id: string; id: string;
aktenzeichen: string; aktenzeichen: string;
title: string; title: string;
} }
let allAkten: Akte[] = []; let allProjects: Project[] = [];
function esc(s: string): string { function esc(s: string): string {
const d = document.createElement("div"); const d = document.createElement("div");
@@ -17,28 +17,28 @@ function esc(s: string): string {
async function loadAkten() { async function loadAkten() {
try { try {
const resp = await fetch("/api/akten"); const resp = await fetch("/api/projects");
if (resp.ok) allAkten = await resp.json(); if (resp.ok) allProjects = await resp.json();
} catch { } catch {
/* non-fatal */ /* non-fatal */
} }
} }
function populateAkten() { function populateAkten() {
const sel = document.getElementById("termin-akte") as HTMLSelectElement; const sel = document.getElementById("termin-project") as HTMLSelectElement;
const opts: string[] = [ const opts: string[] = [
`<option value="">${esc(t("termine.field.akte.none"))}</option>`, `<option value="">${esc(t("termine.field.project.none"))}</option>`,
]; ];
for (const a of allAkten) { for (const a of allProjects) {
opts.push( opts.push(
`<option value="${esc(a.id)}">${esc(a.aktenzeichen)} \u2014 ${esc(a.title)}</option>`, `<option value="${esc(a.id)}">${esc(a.aktenzeichen)} \u2014 ${esc(a.title)}</option>`,
); );
} }
sel.innerHTML = opts.join(""); sel.innerHTML = opts.join("");
// Pre-select Akte from query (?projekt_id=...) // Pre-select Project from query (?project_id=...)
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const ak = params.get("projekt_id"); const ak = params.get("project_id");
if (ak) sel.value = ak; if (ak) sel.value = ak;
} }
@@ -64,7 +64,7 @@ async function submitForm(ev: Event) {
const startRaw = (document.getElementById("termin-start") as HTMLInputElement).value; const startRaw = (document.getElementById("termin-start") as HTMLInputElement).value;
const endRaw = (document.getElementById("termin-end") as HTMLInputElement).value; const endRaw = (document.getElementById("termin-end") as HTMLInputElement).value;
const type = (document.getElementById("termin-type") as HTMLSelectElement).value; const type = (document.getElementById("termin-type") as HTMLSelectElement).value;
const akteID = (document.getElementById("termin-akte") as HTMLSelectElement).value; const akteID = (document.getElementById("termin-project") as HTMLSelectElement).value;
const location = (document.getElementById("termin-location") as HTMLInputElement).value.trim(); const location = (document.getElementById("termin-location") as HTMLInputElement).value.trim();
const description = (document.getElementById("termin-description") as HTMLTextAreaElement).value.trim(); const description = (document.getElementById("termin-description") as HTMLTextAreaElement).value.trim();
@@ -79,21 +79,21 @@ async function submitForm(ev: Event) {
start_at: new Date(startRaw).toISOString(), start_at: new Date(startRaw).toISOString(),
}; };
if (endRaw) payload.end_at = new Date(endRaw).toISOString(); if (endRaw) payload.end_at = new Date(endRaw).toISOString();
if (type) payload.termin_type = type; if (type) payload.appointment_type = type;
if (akteID) payload.projekt_id = akteID; if (akteID) payload.project_id = akteID;
if (location) payload.location = location; if (location) payload.location = location;
if (description) payload.description = description; if (description) payload.description = description;
submitBtn.disabled = true; submitBtn.disabled = true;
try { try {
const resp = await fetch("/api/termine", { const resp = await fetch("/api/appointments", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
if (resp.ok) { if (resp.ok) {
const created = await resp.json(); const created = await resp.json();
window.location.href = `/termine/${created.id}`; window.location.href = `/appointments/${created.id}`;
return; return;
} }
const data = await resp.json().catch(() => ({}) as { error?: string }); const data = await resp.json().catch(() => ({}) as { error?: string });

View File

@@ -1,21 +1,21 @@
import { initI18n, onLangChange, t, getLang } from "./i18n"; import { initI18n, onLangChange, t, getLang } from "./i18n";
import { initSidebar } from "./sidebar"; import { initSidebar } from "./sidebar";
interface Termin { interface Appointment {
id: string; id: string;
projekt_id?: string; project_id?: string;
title: string; title: string;
description?: string; description?: string;
start_at: string; start_at: string;
end_at?: string; end_at?: string;
location?: string; location?: string;
termin_type?: string; appointment_type?: string;
projekt_reference?: string; project_reference?: string;
projekt_title?: string; project_title?: string;
projekt_office?: string; projekt_office?: string;
} }
interface Akte { interface Project {
id: string; id: string;
aktenzeichen: string; aktenzeichen: string;
title: string; title: string;
@@ -30,8 +30,8 @@ interface Summary {
const PERSONAL = "__personal__"; const PERSONAL = "__personal__";
let allTermine: Termin[] = []; let allAppointments: Appointment[] = [];
let allAkten: Akte[] = []; let allProjects: Project[] = [];
let typeFilter = ""; let typeFilter = "";
let akteFilter = ""; let akteFilter = "";
let fromFilter = ""; let fromFilter = "";
@@ -44,8 +44,8 @@ function urlParams(): URLSearchParams {
async function loadAkten() { async function loadAkten() {
try { try {
const resp = await fetch("/api/akten"); const resp = await fetch("/api/projects");
if (resp.ok) allAkten = await resp.json(); if (resp.ok) allProjects = await resp.json();
} catch { } catch {
/* non-fatal */ /* non-fatal */
} }
@@ -53,7 +53,7 @@ async function loadAkten() {
async function loadSummary() { async function loadSummary() {
try { try {
const resp = await fetch("/api/termine/summary"); const resp = await fetch("/api/appointments/summary");
if (!resp.ok) return; if (!resp.ok) return;
const sum: Summary = await resp.json(); const sum: Summary = await resp.json();
setCount("sum-today", sum.today); setCount("sum-today", sum.today);
@@ -75,10 +75,10 @@ async function loadTermine() {
try { try {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (typeFilter) params.set("type", typeFilter); if (typeFilter) params.set("type", typeFilter);
if (akteFilter && akteFilter !== PERSONAL) params.set("projekt_id", akteFilter); if (akteFilter && akteFilter !== PERSONAL) params.set("project_id", akteFilter);
if (fromFilter) params.set("from", fromFilter); if (fromFilter) params.set("from", fromFilter);
if (toFilter) params.set("to", toFilter); if (toFilter) params.set("to", toFilter);
const resp = await fetch(`/api/termine?${params.toString()}`); const resp = await fetch(`/api/appointments?${params.toString()}`);
if (resp.status === 503) { if (resp.status === 503) {
unavailable.style.display = "block"; unavailable.style.display = "block";
tableWrap.style.display = "none"; tableWrap.style.display = "none";
@@ -90,8 +90,8 @@ async function loadTermine() {
tableWrap.style.display = "none"; tableWrap.style.display = "none";
return; return;
} }
const data: Termin[] = await resp.json(); const data: Appointment[] = await resp.json();
allTermine = akteFilter === PERSONAL ? data.filter((x) => !x.projekt_id) : data; allAppointments = akteFilter === PERSONAL ? data.filter((x) => !x.project_id) : data;
loadedOK = true; loadedOK = true;
render(); render();
} catch { } catch {
@@ -128,7 +128,7 @@ function render() {
const emptyFiltered = document.getElementById("termine-empty-filtered")!; const emptyFiltered = document.getElementById("termine-empty-filtered")!;
const tableWrap = document.querySelector<HTMLElement>(".akten-table-wrap")!; const tableWrap = document.querySelector<HTMLElement>(".akten-table-wrap")!;
if (allTermine.length === 0) { if (allAppointments.length === 0) {
tbody.innerHTML = ""; tbody.innerHTML = "";
tableWrap.style.display = "none"; tableWrap.style.display = "none";
if (!typeFilter && !akteFilter && !fromFilter && !toFilter) { if (!typeFilter && !akteFilter && !fromFilter && !toFilter) {
@@ -145,19 +145,19 @@ function render() {
empty.style.display = "none"; empty.style.display = "none";
emptyFiltered.style.display = "none"; emptyFiltered.style.display = "none";
tbody.innerHTML = allTermine tbody.innerHTML = allAppointments
.map((tt) => { .map((tt) => {
const typeLabel = tt.termin_type ? t(`termine.type.${tt.termin_type}`) || tt.termin_type : ""; const typeLabel = tt.appointment_type ? t(`termine.type.${tt.appointment_type}`) || tt.appointment_type : "";
const typeClass = tt.termin_type ? `termin-type-${tt.termin_type}` : ""; const typeClass = tt.appointment_type ? `termin-type-${tt.appointment_type}` : "";
const akteCell = tt.projekt_id const akteCell = tt.project_id
? `<a class="akten-ref-link" href="/projekte/${esc(tt.projekt_id)}">${esc(tt.projekt_reference ?? "")}</a>` ? `<a class="akten-ref-link" href="/projects/${esc(tt.project_id)}">${esc(tt.project_reference ?? "")}</a>`
+ `<span class="frist-akte-title">${esc(tt.projekt_title ?? "")}</span>` + `<span class="frist-project-title">${esc(tt.project_title ?? "")}</span>`
: `<span class="termin-personal-tag" data-i18n="termine.personal">${esc(t("termine.personal"))}</span>`; : `<span class="termin-personal-tag" data-i18n="termine.personal">${esc(t("termine.personal"))}</span>`;
return `<tr class="frist-row" data-id="${esc(tt.id)}"> return `<tr class="frist-row" data-id="${esc(tt.id)}">
<td class="frist-col-check"><span class="termin-dot ${typeClass}" /></td> <td class="frist-col-check"><span class="termin-dot ${typeClass}" /></td>
<td class="frist-col-due">${esc(fmtDateTime(tt.start_at))}</td> <td class="frist-col-due">${esc(fmtDateTime(tt.start_at))}</td>
<td class="frist-col-title">${esc(tt.title)}</td> <td class="frist-col-title">${esc(tt.title)}</td>
<td class="frist-col-akte">${akteCell}</td> <td class="frist-col-project">${akteCell}</td>
<td>${esc(tt.location ?? "")}</td> <td>${esc(tt.location ?? "")}</td>
<td><span class="termin-type-chip ${typeClass}">${esc(typeLabel)}</span></td> <td><span class="termin-type-chip ${typeClass}">${esc(typeLabel)}</span></td>
</tr>`; </tr>`;
@@ -169,20 +169,20 @@ function render() {
row.addEventListener("click", (e) => { row.addEventListener("click", (e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (target.closest("a")) return; if (target.closest("a")) return;
window.location.href = `/termine/${id}`; window.location.href = `/appointments/${id}`;
}); });
}); });
} }
function initFilters() { function initFilters() {
const type = document.getElementById("termin-filter-type") as HTMLSelectElement; const type = document.getElementById("termin-filter-type") as HTMLSelectElement;
const akte = document.getElementById("termin-filter-akte") as HTMLSelectElement; const project = document.getElementById("termin-filter-project") as HTMLSelectElement;
const from = document.getElementById("termin-filter-from") as HTMLInputElement; const from = document.getElementById("termin-filter-from") as HTMLInputElement;
const to = document.getElementById("termin-filter-to") as HTMLInputElement; const to = document.getElementById("termin-filter-to") as HTMLInputElement;
const params = urlParams(); const params = urlParams();
if (params.has("type")) typeFilter = params.get("type")!; if (params.has("type")) typeFilter = params.get("type")!;
if (params.has("projekt_id")) akteFilter = params.get("projekt_id")!; if (params.has("project_id")) akteFilter = params.get("project_id")!;
if (params.has("from")) fromFilter = params.get("from")!; if (params.has("from")) fromFilter = params.get("from")!;
if (params.has("to")) toFilter = params.get("to")!; if (params.has("to")) toFilter = params.get("to")!;
type.value = typeFilter; type.value = typeFilter;
@@ -193,8 +193,8 @@ function initFilters() {
typeFilter = type.value; typeFilter = type.value;
await Promise.all([loadTermine(), loadSummary()]); await Promise.all([loadTermine(), loadSummary()]);
}); });
akte.addEventListener("change", async () => { project.addEventListener("change", async () => {
akteFilter = akte.value; akteFilter = project.value;
await Promise.all([loadTermine(), loadSummary()]); await Promise.all([loadTermine(), loadSummary()]);
}); });
from.addEventListener("change", async () => { from.addEventListener("change", async () => {
@@ -208,12 +208,12 @@ function initFilters() {
} }
function populateAkteFilter() { function populateAkteFilter() {
const sel = document.getElementById("termin-filter-akte") as HTMLSelectElement; const sel = document.getElementById("termin-filter-project") as HTMLSelectElement;
const options: string[] = [ const options: string[] = [
`<option value="">${esc(t("termine.filter.akte.all"))}</option>`, `<option value="">${esc(t("termine.filter.project.all"))}</option>`,
`<option value="${PERSONAL}">${esc(t("termine.filter.akte.personal"))}</option>`, `<option value="${PERSONAL}">${esc(t("termine.filter.project.personal"))}</option>`,
]; ];
for (const a of allAkten) { for (const a of allProjects) {
options.push( options.push(
`<option value="${esc(a.id)}">${esc(a.aktenzeichen)} \u2014 ${esc(a.title)}</option>`, `<option value="${esc(a.id)}">${esc(a.aktenzeichen)} \u2014 ${esc(a.title)}</option>`,
); );

View File

@@ -35,13 +35,13 @@ interface ChecklistInstance {
id: string; id: string;
template_slug: string; template_slug: string;
name: string; name: string;
projekt_id?: string | null; project_id?: string | null;
state: Record<string, boolean>; state: Record<string, boolean>;
created_by: string; created_by: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
projekt_reference?: string | null; project_reference?: string | null;
projekt_title?: string | null; project_title?: string | null;
} }
interface AkteSummary { interface AkteSummary {
@@ -52,7 +52,7 @@ interface AkteSummary {
let template: Checklist | null = null; let template: Checklist | null = null;
let instances: ChecklistInstance[] = []; let instances: ChecklistInstance[] = [];
let akten: AkteSummary[] = []; let projects: AkteSummary[] = [];
let totalItems = 0; let totalItems = 0;
function esc(s: string): string { function esc(s: string): string {
@@ -69,7 +69,7 @@ function templateSlug(): string {
async function loadTemplate() { async function loadTemplate() {
const slug = templateSlug(); const slug = templateSlug();
const resp = await fetch(`/api/checklisten/${encodeURIComponent(slug)}`); const resp = await fetch(`/api/checklists/${encodeURIComponent(slug)}`);
if (!resp.ok) { if (!resp.ok) {
document.title = "404 — Paliad"; document.title = "404 — Paliad";
document.getElementById("checklist-title")!.textContent = t("checklisten.notfound"); document.getElementById("checklist-title")!.textContent = t("checklisten.notfound");
@@ -85,7 +85,7 @@ async function loadTemplate() {
async function loadInstances() { async function loadInstances() {
const slug = templateSlug(); const slug = templateSlug();
try { try {
const resp = await fetch(`/api/checklisten/${encodeURIComponent(slug)}/instances`); const resp = await fetch(`/api/checklists/${encodeURIComponent(slug)}/instances`);
if (!resp.ok) { if (!resp.ok) {
instances = []; instances = [];
} else { } else {
@@ -99,7 +99,7 @@ async function loadInstances() {
async function loadAkten() { async function loadAkten() {
try { try {
const resp = await fetch("/api/akten"); const resp = await fetch("/api/projects");
if (resp.ok) akten = await resp.json(); if (resp.ok) akten = await resp.json();
} catch { } catch {
akten = []; akten = [];
@@ -121,7 +121,7 @@ function renderHeader() {
document.getElementById("checklist-subtitle")!.textContent = desc; document.getElementById("checklist-subtitle")!.textContent = desc;
const courtLabel = isEN ? "Court / Authority" : "Gericht / Behörde"; const courtLabel = isEN ? "Court / Authority" : "Gericht / Behörde";
const deadlineLabel = isEN ? "Deadline" : "Frist"; const deadlineLabel = isEN ? "Deadline" : "Deadline";
const refLabel = isEN ? "Reference" : "Rechtsgrundlage"; const refLabel = isEN ? "Reference" : "Rechtsgrundlage";
const regimeLabel = isEN ? "Regime" : "Bereich"; const regimeLabel = isEN ? "Regime" : "Bereich";
const itemsLabel = isEN ? "Items" : "Punkte"; const itemsLabel = isEN ? "Items" : "Punkte";
@@ -177,11 +177,11 @@ function renderInstances() {
body.innerHTML = instances.map((inst) => { body.innerHTML = instances.map((inst) => {
const { done, pct } = progress(inst); const { done, pct } = progress(inst);
const akteCell = inst.projekt_id && inst.projekt_reference const akteCell = inst.project_id && inst.project_reference
? `<a href="/projekte/${esc(inst.projekt_id)}" class="checklist-instance-akte-link">${esc(inst.projekt_reference)}</a>` ? `<a href="/projects/${esc(inst.project_id)}" class="checklist-instance-project-link">${esc(inst.project_reference)}</a>`
: `<span class="akten-muted">${personalLabel}</span>`; : `<span class="akten-muted">${personalLabel}</span>`;
return `<tr data-id="${esc(inst.id)}" class="checklist-instance-row"> return `<tr data-id="${esc(inst.id)}" class="checklist-instance-row">
<td><a href="/checklisten/instances/${esc(inst.id)}" class="checklist-instance-name">${esc(inst.name)}</a></td> <td><a href="/checklists/instances/${esc(inst.id)}" class="checklist-instance-name">${esc(inst.name)}</a></td>
<td> <td>
<div class="checklist-progress-inline"> <div class="checklist-progress-inline">
<div class="checklist-progress-bar"> <div class="checklist-progress-bar">
@@ -193,7 +193,7 @@ function renderInstances() {
<td>${akteCell}</td> <td>${akteCell}</td>
<td>${esc(formatDate(inst.created_at))}</td> <td>${esc(formatDate(inst.created_at))}</td>
<td class="checklist-instance-actions"> <td class="checklist-instance-actions">
<a class="btn-small btn-ghost" href="/checklisten/instances/${esc(inst.id)}">${esc(openLabel)}</a> <a class="btn-small btn-ghost" href="/checklists/instances/${esc(inst.id)}">${esc(openLabel)}</a>
<button type="button" class="btn-small btn-ghost btn-delete-instance" data-id="${esc(inst.id)}" data-name="${esc(inst.name)}">${esc(deleteLabel)}</button> <button type="button" class="btn-small btn-ghost btn-delete-instance" data-id="${esc(inst.id)}" data-name="${esc(inst.name)}">${esc(deleteLabel)}</button>
</td> </td>
</tr>`; </tr>`;
@@ -213,7 +213,7 @@ function renderInstances() {
} }
function renderAkteOptions() { function renderAkteOptions() {
const sel = document.getElementById("new-instance-akte") as HTMLSelectElement; const sel = document.getElementById("new-instance-project") as HTMLSelectElement;
if (!sel) return; if (!sel) return;
const none = sel.querySelector('option[value=""]'); const none = sel.querySelector('option[value=""]');
sel.innerHTML = ""; sel.innerHTML = "";
@@ -231,7 +231,7 @@ function initNewInstance() {
const form = document.getElementById("new-instance-form")! as HTMLFormElement; const form = document.getElementById("new-instance-form")! as HTMLFormElement;
const msg = document.getElementById("new-instance-msg")!; const msg = document.getElementById("new-instance-msg")!;
const nameInput = document.getElementById("new-instance-name") as HTMLInputElement; const nameInput = document.getElementById("new-instance-name") as HTMLInputElement;
const akteSel = document.getElementById("new-instance-akte") as HTMLSelectElement; const akteSel = document.getElementById("new-instance-project") as HTMLSelectElement;
const open = () => { const open = () => {
msg.textContent = ""; msg.textContent = "";
@@ -257,14 +257,14 @@ function initNewInstance() {
return; return;
} }
const akteID = akteSel.value || null; const akteID = akteSel.value || null;
const payload: { name: string; projekt_id?: string } = { name }; const payload: { name: string; project_id?: string } = { name };
if (akteID) payload.projekt_id = akteID; if (akteID) payload.project_id = akteID;
const slug = templateSlug(); const slug = templateSlug();
const submitBtn = form.querySelector(".btn-primary") as HTMLButtonElement; const submitBtn = form.querySelector(".btn-primary") as HTMLButtonElement;
submitBtn.disabled = true; submitBtn.disabled = true;
try { try {
const resp = await fetch(`/api/checklisten/${encodeURIComponent(slug)}/instances`, { const resp = await fetch(`/api/checklists/${encodeURIComponent(slug)}/instances`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),
@@ -275,7 +275,7 @@ function initNewInstance() {
return; return;
} }
const created = await resp.json() as ChecklistInstance; const created = await resp.json() as ChecklistInstance;
window.location.href = `/checklisten/instances/${encodeURIComponent(created.id)}`; window.location.href = `/checklists/instances/${encodeURIComponent(created.id)}`;
} catch { } catch {
msg.textContent = t("checklisten.newInstance.error.generic"); msg.textContent = t("checklisten.newInstance.error.generic");
msg.className = "form-msg form-msg-error"; msg.className = "form-msg form-msg-error";
@@ -332,7 +332,7 @@ function initFeedback() {
submitBtn.disabled = true; submitBtn.disabled = true;
try { try {
const resp = await fetch("/api/checklisten/feedback", { const resp = await fetch("/api/checklists/feedback", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),

View File

@@ -35,7 +35,7 @@ interface Instance {
id: string; id: string;
template_slug: string; template_slug: string;
name: string; name: string;
projekt_id?: string | null; project_id?: string | null;
state: Record<string, boolean>; state: Record<string, boolean>;
created_by: string; created_by: string;
created_at: string; created_at: string;
@@ -82,7 +82,7 @@ async function loadInstance(): Promise<boolean> {
} }
async function loadTemplate(slug: string): Promise<boolean> { async function loadTemplate(slug: string): Promise<boolean> {
const resp = await fetch(`/api/checklisten/${encodeURIComponent(slug)}`); const resp = await fetch(`/api/checklists/${encodeURIComponent(slug)}`);
if (!resp.ok) return false; if (!resp.ok) return false;
template = await resp.json(); template = await resp.json();
return true; return true;
@@ -111,7 +111,7 @@ async function bootstrap() {
// Back link goes to the template page. // Back link goes to the template page.
const back = document.getElementById("instance-back") as HTMLAnchorElement; const back = document.getElementById("instance-back") as HTMLAnchorElement;
back.href = `/checklisten/${encodeURIComponent(instance.template_slug)}`; back.href = `/checklists/${encodeURIComponent(instance.template_slug)}`;
renderAll(); renderAll();
} }
@@ -137,7 +137,7 @@ function renderHeader() {
(document.getElementById("instance-template-title") as HTMLElement).textContent = tplTitle; (document.getElementById("instance-template-title") as HTMLElement).textContent = tplTitle;
const courtLabel = isEN ? "Court / Authority" : "Gericht / Behörde"; const courtLabel = isEN ? "Court / Authority" : "Gericht / Behörde";
const deadlineLabel = isEN ? "Deadline" : "Frist"; const deadlineLabel = isEN ? "Deadline" : "Deadline";
const refLabel = isEN ? "Reference" : "Rechtsgrundlage"; const refLabel = isEN ? "Reference" : "Rechtsgrundlage";
const regimeLabel = isEN ? "Regime" : "Bereich"; const regimeLabel = isEN ? "Regime" : "Bereich";
@@ -150,9 +150,9 @@ function renderHeader() {
if (reference) { if (reference) {
parts.push(`<div class="checklist-meta-item"><dt>${refLabel}</dt><dd>${esc(reference)}</dd></div>`); parts.push(`<div class="checklist-meta-item"><dt>${refLabel}</dt><dd>${esc(reference)}</dd></div>`);
} }
if (instance.projekt_id) { if (instance.project_id) {
const akteLabel = isEN ? "Akte" : "Akte"; const akteLabel = isEN ? "Project" : "Project";
parts.push(`<div class="checklist-meta-item"><dt>${akteLabel}</dt><dd><a href="/projekte/${esc(instance.projekt_id)}">${t("checklisten.instance.akte.open") || "Öffnen"}</a></dd></div>`); parts.push(`<div class="checklist-meta-item"><dt>${akteLabel}</dt><dd><a href="/projects/${esc(instance.project_id)}">${t("checklisten.instance.project.open") || "Öffnen"}</a></dd></div>`);
} }
document.getElementById("instance-meta")!.innerHTML = parts.join(""); document.getElementById("instance-meta")!.innerHTML = parts.join("");
} }
@@ -359,7 +359,7 @@ function initFeedback() {
} }
submitBtn.disabled = true; submitBtn.disabled = true;
try { try {
const resp = await fetch("/api/checklisten/feedback", { const resp = await fetch("/api/checklists/feedback", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),

View File

@@ -23,7 +23,7 @@ function esc(s: string): string {
} }
async function load() { async function load() {
const resp = await fetch("/api/checklisten"); const resp = await fetch("/api/checklists");
if (!resp.ok) return; if (!resp.ok) return;
allChecklists = await resp.json(); allChecklists = await resp.json();
render(); render();
@@ -47,7 +47,7 @@ function render() {
const desc = isEN ? c.descriptionEN : c.descriptionDE; const desc = isEN ? c.descriptionEN : c.descriptionDE;
const court = isEN ? c.courtEN : c.courtDE; const court = isEN ? c.courtEN : c.courtDE;
const itemsLabel = isEN ? "items" : "Punkte"; const itemsLabel = isEN ? "items" : "Punkte";
return `<a href="/checklisten/${esc(c.slug)}" class="checklist-card"> return `<a href="/checklists/${esc(c.slug)}" class="checklist-card">
<div class="checklist-card-top"> <div class="checklist-card-top">
<span class="checklist-regime checklist-regime-${esc(c.regime)}">${esc(c.regime)}</span> <span class="checklist-regime checklist-regime-${esc(c.regime)}">${esc(c.regime)}</span>
<span class="checklist-card-count">${c.itemCount} ${itemsLabel}</span> <span class="checklist-card-count">${c.itemCount} ${itemsLabel}</span>

View File

@@ -109,7 +109,7 @@ function countryName(code: string): string {
} }
async function loadCourts() { async function loadCourts() {
const resp = await fetch("/api/gerichte"); const resp = await fetch("/api/courts");
if (!resp.ok) return; if (!resp.ok) return;
const data: ApiResponse = await resp.json(); const data: ApiResponse = await resp.json();
allCourts = data.courts; allCourts = data.courts;
@@ -335,7 +335,7 @@ async function submitFeedback(e: Event) {
submitBtn.disabled = true; submitBtn.disabled = true;
try { try {
const resp = await fetch("/api/gerichte/feedback", { const resp = await fetch("/api/courts/feedback", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),

View File

@@ -26,8 +26,8 @@ interface UpcomingDeadline {
id: string; id: string;
title: string; title: string;
due_date: string; due_date: string;
projekt_id: string; project_id: string;
projekt_title: string; project_title: string;
projekt_ref: string; projekt_ref: string;
urgency: "overdue" | "today" | "urgent" | "soon"; urgency: "overdue" | "today" | "urgent" | "soon";
} }
@@ -38,8 +38,8 @@ interface UpcomingAppointment {
start_at: string; start_at: string;
end_at: string | null; end_at: string | null;
type: string | null; type: string | null;
projekt_id: string | null; project_id: string | null;
projekt_title: string | null; project_title: string | null;
projekt_ref: string | null; projekt_ref: string | null;
} }
@@ -47,8 +47,8 @@ interface ActivityEntry {
timestamp: string; timestamp: string;
actor_email: string | null; actor_email: string | null;
actor_name: string | null; actor_name: string | null;
projekt_id: string; project_id: string;
projekt_title: string; project_title: string;
projekt_ref: string; projekt_ref: string;
action: string | null; action: string | null;
details: string; details: string;
@@ -160,10 +160,10 @@ function renderDeadlines(items: UpcomingDeadline[]): void {
const urgencyClass = `dashboard-urgency-${d.urgency}`; const urgencyClass = `dashboard-urgency-${d.urgency}`;
const urgencyLabel = t(`dashboard.urgency.${d.urgency}`); const urgencyLabel = t(`dashboard.urgency.${d.urgency}`);
return `<li class="dashboard-list-item"> return `<li class="dashboard-list-item">
<a href="/projekte/${esc(d.projekt_id)}/fristen" class="dashboard-list-link"> <a href="/projects/${esc(d.project_id)}/fristen" class="dashboard-list-link">
<div class="dashboard-list-main"> <div class="dashboard-list-main">
<span class="dashboard-list-title">${esc(d.title)}</span> <span class="dashboard-list-title">${esc(d.title)}</span>
<span class="dashboard-list-ref">${esc(d.projekt_ref)} &middot; ${esc(d.projekt_title)}</span> <span class="dashboard-list-ref">${esc(d.projekt_ref)} &middot; ${esc(d.project_title)}</span>
</div> </div>
<div class="dashboard-list-meta"> <div class="dashboard-list-meta">
<span class="dashboard-urgency-badge ${urgencyClass}" title="${escAttr(urgencyLabel)}">${esc(formatRelative(d.due_date))}</span> <span class="dashboard-urgency-badge ${urgencyClass}" title="${escAttr(urgencyLabel)}">${esc(formatRelative(d.due_date))}</span>
@@ -189,10 +189,10 @@ function renderAppointments(items: UpcomingAppointment[]): void {
const dot = a.type const dot = a.type
? `<span class="dashboard-termin-dot dashboard-termin-${esc(a.type)}" aria-hidden="true"></span>` ? `<span class="dashboard-termin-dot dashboard-termin-${esc(a.type)}" aria-hidden="true"></span>`
: `<span class="dashboard-termin-dot" aria-hidden="true"></span>`; : `<span class="dashboard-termin-dot" aria-hidden="true"></span>`;
const href = a.projekt_id ? `/projekte/${esc(a.projekt_id)}/termine` : "#"; const href = a.project_id ? `/projects/${esc(a.project_id)}/termine` : "#";
const tag = a.projekt_id ? "a" : "div"; const tag = a.project_id ? "a" : "div";
const akteLine = a.projekt_ref && a.projekt_title const akteLine = a.projekt_ref && a.project_title
? `<span class="dashboard-list-ref">${esc(a.projekt_ref)} &middot; ${esc(a.projekt_title)}</span>` ? `<span class="dashboard-list-ref">${esc(a.projekt_ref)} &middot; ${esc(a.project_title)}</span>`
: ""; : "";
return `<li class="dashboard-list-item"> return `<li class="dashboard-list-item">
<${tag} href="${href}" class="dashboard-list-link"> <${tag} href="${href}" class="dashboard-list-link">
@@ -230,7 +230,7 @@ function renderActivity(items: ActivityEntry[]): void {
<span class="dashboard-activity-body"> <span class="dashboard-activity-body">
<span class="dashboard-activity-actor">${esc(actor)}</span> <span class="dashboard-activity-actor">${esc(actor)}</span>
<span class="dashboard-activity-action">${esc(actionLabel)}</span> <span class="dashboard-activity-action">${esc(actionLabel)}</span>
<a href="/projekte/${esc(e.projekt_id)}" class="dashboard-activity-akte">${esc(e.projekt_ref)}</a> <a href="/projects/${esc(e.project_id)}" class="dashboard-activity-project">${esc(e.projekt_ref)}</a>
<span class="dashboard-activity-details">${esc(e.details)}</span> <span class="dashboard-activity-details">${esc(e.details)}</span>
</span> </span>
</li>`; </li>`;

View File

@@ -1,17 +1,17 @@
import { initI18n, onLangChange, t, getLang } from "./i18n"; import { initI18n, onLangChange, t, getLang } from "./i18n";
import { initSidebar } from "./sidebar"; import { initSidebar } from "./sidebar";
interface Frist { interface Deadline {
id: string; id: string;
projekt_id: string; project_id: string;
title: string; title: string;
due_date: string; due_date: string;
status: string; status: string;
projekt_reference: string; project_reference: string;
projekt_title: string; project_title: string;
} }
let allFristen: Frist[] = []; let allDeadlines: Deadline[] = [];
let viewYear = 0; let viewYear = 0;
let viewMonth = 0; // 0-11 let viewMonth = 0; // 0-11
@@ -39,14 +39,14 @@ function urgencyClass(due: string, status: string): string {
async function loadFristen() { async function loadFristen() {
try { try {
// Load all (open + completed) — calendar shows everything for context. // Load all (open + completed) — calendar shows everything for context.
const resp = await fetch("/api/fristen?status=all"); const resp = await fetch("/api/deadlines?status=all");
if (resp.ok) allFristen = await resp.json(); if (resp.ok) allFristen = await resp.json();
} catch { } catch {
/* non-fatal */ /* non-fatal */
} }
} }
function fristenForDate(iso: string): Frist[] { function deadlinesForDate(iso: string): Deadline[] {
return allFristen.filter((f) => f.due_date.slice(0, 10) === iso); return allFristen.filter((f) => f.due_date.slice(0, 10) === iso);
} }
@@ -73,7 +73,7 @@ function render() {
} }
for (let day = 1; day <= daysInMonth; day++) { for (let day = 1; day <= daysInMonth; day++) {
const iso = isoDate(viewYear, viewMonth, day); const iso = isoDate(viewYear, viewMonth, day);
const fristen = fristenForDate(iso); const fristen = deadlinesForDate(iso);
const isToday = iso === todayISO; const isToday = iso === todayISO;
const dots = fristen const dots = fristen
@@ -108,7 +108,7 @@ function render() {
} }
function openPopup(iso: string) { function openPopup(iso: string) {
const fristen = fristenForDate(iso); const fristen = deadlinesForDate(iso);
if (fristen.length === 0) return; if (fristen.length === 0) return;
const popup = document.getElementById("cal-popup")!; const popup = document.getElementById("cal-popup")!;
const dateEl = document.getElementById("cal-popup-date")!; const dateEl = document.getElementById("cal-popup-date")!;
@@ -127,8 +127,8 @@ function openPopup(iso: string) {
const cls = urgencyClass(f.due_date, f.status); const cls = urgencyClass(f.due_date, f.status);
return `<li class="frist-cal-popup-item"> return `<li class="frist-cal-popup-item">
<span class="frist-cal-dot ${cls}"></span> <span class="frist-cal-dot ${cls}"></span>
<a href="/fristen/${esc(f.id)}" class="frist-cal-popup-title">${esc(f.title)}</a> <a href="/deadlines/${esc(f.id)}" class="frist-cal-popup-title">${esc(f.title)}</a>
<a href="/projekte/${esc(f.projekt_id)}" class="frist-cal-popup-akte">${esc(f.projekt_reference)}</a> <a href="/projects/${esc(f.project_id)}" class="frist-cal-popup-project">${esc(f.project_reference)}</a>
</li>`; </li>`;
}) })
.join(""); .join("");

View File

@@ -1,10 +1,10 @@
import { initI18n, onLangChange, t, getLang } from "./i18n"; import { initI18n, onLangChange, t, getLang } from "./i18n";
import { initSidebar } from "./sidebar"; import { initSidebar } from "./sidebar";
import { initNotes } from "./notizen"; import { initNotes } from "./notes";
interface Frist { interface Deadline {
id: string; id: string;
projekt_id: string; project_id: string;
title: string; title: string;
description?: string; description?: string;
due_date: string; due_date: string;
@@ -16,7 +16,7 @@ interface Frist {
completed_at?: string; completed_at?: string;
} }
interface Akte { interface Project {
id: string; id: string;
aktenzeichen: string; aktenzeichen: string;
title: string; title: string;
@@ -34,8 +34,8 @@ interface Me {
role: string; role: string;
} }
let frist: Frist | null = null; let deadline: Deadline | null = null;
let akte: Akte | null = null; let project: Project | null = null;
let rule: DeadlineRule | null = null; let rule: DeadlineRule | null = null;
let me: Me | null = null; let me: Me | null = null;
@@ -92,7 +92,7 @@ function urgencyClass(due: string, status: string): string {
async function loadFrist(id: string): Promise<boolean> { async function loadFrist(id: string): Promise<boolean> {
try { try {
const resp = await fetch(`/api/fristen/${id}`); const resp = await fetch(`/api/deadlines/${id}`);
if (!resp.ok) return false; if (!resp.ok) return false;
frist = await resp.json(); frist = await resp.json();
return true; return true;
@@ -103,8 +103,8 @@ async function loadFrist(id: string): Promise<boolean> {
async function loadAkte(akteID: string) { async function loadAkte(akteID: string) {
try { try {
const resp = await fetch(`/api/projekte/${akteID}`); const resp = await fetch(`/api/projects/${akteID}`);
if (resp.ok) akte = await resp.json(); if (resp.ok) project = await resp.json();
} catch { } catch {
/* non-fatal */ /* non-fatal */
} }
@@ -145,12 +145,12 @@ function render() {
statusChip.className = `akten-status-chip akten-status-${frist.status}`; statusChip.className = `akten-status-chip akten-status-${frist.status}`;
statusChip.textContent = t(`fristen.status.${frist.status}`) || frist.status; statusChip.textContent = t(`fristen.status.${frist.status}`) || frist.status;
const akteLink = document.getElementById("frist-akte-link") as HTMLAnchorElement; const akteLink = document.getElementById("frist-project-link") as HTMLAnchorElement;
if (akte) { if (project) {
akteLink.href = `/projekte/${akte.id}`; akteLink.href = `/projects/${project.id}`;
akteLink.textContent = `${akte.aktenzeichen} \u2014 ${akte.title}`; akteLink.textContent = `${project.aktenzeichen} \u2014 ${project.title}`;
} else { } else {
akteLink.href = `/projekte/${frist.projekt_id}`; akteLink.href = `/projects/${frist.project_id}`;
akteLink.textContent = "\u2014"; akteLink.textContent = "\u2014";
} }
@@ -241,7 +241,7 @@ function initEdit() {
if (!newTitle || !newDue) return; if (!newTitle || !newDue) return;
saveBtn.disabled = true; saveBtn.disabled = true;
try { try {
const resp = await fetch(`/api/fristen/${frist.id}`, { const resp = await fetch(`/api/deadlines/${frist.id}`, {
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: newTitle, due_date: newDue, notes: newNotes }), body: JSON.stringify({ title: newTitle, due_date: newDue, notes: newNotes }),
@@ -263,7 +263,7 @@ function initComplete() {
if (!frist || frist.status === "completed") return; if (!frist || frist.status === "completed") return;
btn.disabled = true; btn.disabled = true;
try { try {
const resp = await fetch(`/api/fristen/${frist.id}/complete`, { method: "PATCH" }); const resp = await fetch(`/api/deadlines/${frist.id}/complete`, { method: "PATCH" });
if (resp.ok) { if (resp.ok) {
frist = await resp.json(); frist = await resp.json();
render(); render();
@@ -297,9 +297,9 @@ function initDelete() {
confirmBtn.addEventListener("click", async () => { confirmBtn.addEventListener("click", async () => {
if (!frist) return; if (!frist) return;
confirmBtn.disabled = true; confirmBtn.disabled = true;
const resp = await fetch(`/api/fristen/${frist.id}`, { method: "DELETE" }); const resp = await fetch(`/api/deadlines/${frist.id}`, { method: "DELETE" });
if (resp.ok) { if (resp.ok) {
const target = akte ? `/projekte/${akte.id}/fristen` : "/fristen"; const target = project ? `/projects/${project.id}/fristen` : "/deadlines";
window.location.href = target; window.location.href = target;
} else { } else {
confirmBtn.disabled = false; confirmBtn.disabled = false;
@@ -325,7 +325,7 @@ async function main() {
notfound.style.display = "block"; notfound.style.display = "block";
return; return;
} }
await loadAkte(frist.projekt_id); await loadAkte(frist.project_id);
if (frist.rule_id) await loadRule(frist.rule_id); if (frist.rule_id) await loadRule(frist.rule_id);
loading.style.display = "none"; loading.style.display = "none";

View File

@@ -1,7 +1,7 @@
import { initI18n, t } from "./i18n"; import { initI18n, t } from "./i18n";
import { initSidebar } from "./sidebar"; import { initSidebar } from "./sidebar";
interface Akte { interface Project {
id: string; id: string;
aktenzeichen: string; aktenzeichen: string;
title: string; title: string;
@@ -30,19 +30,19 @@ function showError(msg: string) {
} }
async function loadAkten() { async function loadAkten() {
const sel = document.getElementById("frist-akte") as HTMLSelectElement; const sel = document.getElementById("frist-project") as HTMLSelectElement;
const hint = document.getElementById("frist-akte-empty-hint")!; const hint = document.getElementById("frist-project-empty-hint")!;
try { try {
const resp = await fetch("/api/akten"); const resp = await fetch("/api/projects");
if (!resp.ok) return; if (!resp.ok) return;
const akten: Akte[] = await resp.json(); const projects: Project[] = await resp.json();
if (akten.length === 0) { if (akten.length === 0) {
hint.style.display = ""; hint.style.display = "";
hint.innerHTML = `${esc(t("fristen.field.akte.empty"))} <a href="/akten/neu">${esc(t("fristen.field.akte.empty.link"))}</a>`; hint.innerHTML = `${esc(t("fristen.field.project.empty"))} <a href="/projects/new">${esc(t("fristen.field.project.empty.link"))}</a>`;
return; return;
} }
const options: string[] = [ const options: string[] = [
`<option value="" disabled${preselectedAkteID ? "" : " selected"} data-i18n="fristen.field.akte.choose">${esc(t("fristen.field.akte.choose"))}</option>`, `<option value="" disabled${preselectedAkteID ? "" : " selected"} data-i18n="fristen.field.project.choose">${esc(t("fristen.field.project.choose"))}</option>`,
]; ];
for (const a of akten) { for (const a of akten) {
const isSelected = preselectedAkteID === a.id ? " selected" : ""; const isSelected = preselectedAkteID === a.id ? " selected" : "";
@@ -81,8 +81,8 @@ function initBackLinks() {
if (preselectedAkteID) { if (preselectedAkteID) {
const back = document.getElementById("frist-neu-back") as HTMLAnchorElement; const back = document.getElementById("frist-neu-back") as HTMLAnchorElement;
const cancel = document.getElementById("frist-neu-cancel") as HTMLAnchorElement; const cancel = document.getElementById("frist-neu-cancel") as HTMLAnchorElement;
back.href = `/projekte/${preselectedAkteID}/fristen`; back.href = `/projects/${preselectedAkteID}/fristen`;
cancel.href = `/projekte/${preselectedAkteID}/fristen`; cancel.href = `/projects/${preselectedAkteID}/fristen`;
} }
} }
@@ -91,7 +91,7 @@ async function submitForm(e: Event) {
const submitBtn = document.querySelector<HTMLButtonElement>("#frist-neu-form button[type=submit]")!; const submitBtn = document.querySelector<HTMLButtonElement>("#frist-neu-form button[type=submit]")!;
const msg = document.getElementById("frist-neu-msg")!; const msg = document.getElementById("frist-neu-msg")!;
const akteID = (document.getElementById("frist-akte") as HTMLSelectElement).value; const akteID = (document.getElementById("frist-project") as HTMLSelectElement).value;
const title = (document.getElementById("frist-title") as HTMLInputElement).value.trim(); const title = (document.getElementById("frist-title") as HTMLInputElement).value.trim();
const due = (document.getElementById("frist-due") as HTMLInputElement).value; const due = (document.getElementById("frist-due") as HTMLInputElement).value;
const ruleID = (document.getElementById("frist-rule") as HTMLSelectElement).value; const ruleID = (document.getElementById("frist-rule") as HTMLSelectElement).value;
@@ -115,7 +115,7 @@ async function submitForm(e: Event) {
if (notes) payload.notes = notes; if (notes) payload.notes = notes;
try { try {
const resp = await fetch(`/api/projekte/${encodeURIComponent(akteID)}/fristen`, { const resp = await fetch(`/api/projects/${encodeURIComponent(akteID)}/fristen`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),
@@ -128,9 +128,9 @@ async function submitForm(e: Event) {
} }
const created = await resp.json(); const created = await resp.json();
if (preselectedAkteID) { if (preselectedAkteID) {
window.location.href = `/projekte/${preselectedAkteID}/fristen`; window.location.href = `/projects/${preselectedAkteID}/fristen`;
} else { } else {
window.location.href = `/fristen/${created.id}`; window.location.href = `/deadlines/${created.id}`;
} }
} catch { } catch {
showError(t("fristen.error.generic")); showError(t("fristen.error.generic"));
@@ -139,14 +139,14 @@ async function submitForm(e: Event) {
} }
function detectPreselect() { function detectPreselect() {
// Path /akten/{id}/fristen/neu pre-selects that akte. // Path /akten/{id}/fristen/neu pre-selects that project.
const parts = window.location.pathname.split("/").filter(Boolean); const parts = window.location.pathname.split("/").filter(Boolean);
if (parts[0] === "akten" && parts[1] && parts[2] === "fristen" && parts[3] === "neu") { if (parts[0] === "akten" && parts[1] && parts[2] === "fristen" && parts[3] === "neu") {
preselectedAkteID = parts[1]; preselectedAkteID = parts[1];
} }
// Or ?projekt_id= query string // Or ?project_id= query string
const qp = new URLSearchParams(window.location.search); const qp = new URLSearchParams(window.location.search);
const fromQuery = qp.get("projekt_id"); const fromQuery = qp.get("project_id");
if (fromQuery) preselectedAkteID = fromQuery; if (fromQuery) preselectedAkteID = fromQuery;
} }

View File

@@ -1,21 +1,21 @@
import { initI18n, onLangChange, t, getLang } from "./i18n"; import { initI18n, onLangChange, t, getLang } from "./i18n";
import { initSidebar } from "./sidebar"; import { initSidebar } from "./sidebar";
interface Frist { interface Deadline {
id: string; id: string;
projekt_id: string; project_id: string;
title: string; title: string;
due_date: string; due_date: string;
status: string; status: string;
source: string; source: string;
rule_id?: string; rule_id?: string;
projekt_reference: string; project_reference: string;
projekt_title: string; project_title: string;
projekt_office: string; projekt_office: string;
rule_code?: string; rule_code?: string;
} }
interface Akte { interface Project {
id: string; id: string;
aktenzeichen: string; aktenzeichen: string;
title: string; title: string;
@@ -29,8 +29,8 @@ interface Summary {
total: number; total: number;
} }
let allFristen: Frist[] = []; let allDeadlines: Deadline[] = [];
let allAkten: Akte[] = []; let allProjects: Project[] = [];
let statusFilter = "pending"; let statusFilter = "pending";
let akteFilter = ""; let akteFilter = "";
let loadedOK = false; let loadedOK = false;
@@ -41,8 +41,8 @@ function urlParams(): URLSearchParams {
async function loadAkten() { async function loadAkten() {
try { try {
const resp = await fetch("/api/akten"); const resp = await fetch("/api/projects");
if (resp.ok) allAkten = await resp.json(); if (resp.ok) allProjects = await resp.json();
} catch { } catch {
/* non-fatal */ /* non-fatal */
} }
@@ -51,8 +51,8 @@ async function loadAkten() {
async function loadSummary() { async function loadSummary() {
try { try {
const url = akteFilter const url = akteFilter
? `/api/fristen/summary?projekt_id=${encodeURIComponent(akteFilter)}` ? `/api/deadlines/summary?project_id=${encodeURIComponent(akteFilter)}`
: `/api/fristen/summary`; : `/api/deadlines/summary`;
const resp = await fetch(url); const resp = await fetch(url);
if (!resp.ok) return; if (!resp.ok) return;
const sum: Summary = await resp.json(); const sum: Summary = await resp.json();
@@ -76,8 +76,8 @@ async function loadFristen() {
try { try {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (statusFilter) params.set("status", statusFilter); if (statusFilter) params.set("status", statusFilter);
if (akteFilter) params.set("projekt_id", akteFilter); if (akteFilter) params.set("project_id", akteFilter);
const resp = await fetch(`/api/fristen?${params.toString()}`); const resp = await fetch(`/api/deadlines?${params.toString()}`);
if (resp.status === 503) { if (resp.status === 503) {
unavailable.style.display = "block"; unavailable.style.display = "block";
tableWrap.style.display = "none"; tableWrap.style.display = "none";
@@ -167,9 +167,9 @@ function render() {
</td> </td>
<td class="frist-col-due ${urgency}"><span class="frist-due-dot"></span>${fmtDate(f.due_date)}</td> <td class="frist-col-due ${urgency}"><span class="frist-due-dot"></span>${fmtDate(f.due_date)}</td>
<td class="frist-col-title ${titleClass}">${esc(f.title)}</td> <td class="frist-col-title ${titleClass}">${esc(f.title)}</td>
<td class="frist-col-akte"> <td class="frist-col-project">
<a class="akten-ref-link" href="/projekte/${esc(f.projekt_id)}">${esc(f.projekt_reference)}</a> <a class="akten-ref-link" href="/projects/${esc(f.project_id)}">${esc(f.project_reference)}</a>
<span class="frist-akte-title">${esc(f.projekt_title)}</span> <span class="frist-project-title">${esc(f.project_title)}</span>
</td> </td>
<td class="frist-col-rule">${ruleLabel}</td> <td class="frist-col-rule">${ruleLabel}</td>
<td><span class="akten-status-chip akten-status-${esc(f.status)}">${esc(statusLabel)}</span></td> <td><span class="akten-status-chip akten-status-${esc(f.status)}">${esc(statusLabel)}</span></td>
@@ -183,7 +183,7 @@ function render() {
// Don't navigate if clicking the checkbox or a link // Don't navigate if clicking the checkbox or a link
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (target.closest(".frist-complete-cb") || target.closest("a")) return; if (target.closest(".frist-complete-cb") || target.closest("a")) return;
window.location.href = `/fristen/${id}`; window.location.href = `/deadlines/${id}`;
}); });
const cb = row.querySelector<HTMLInputElement>(".frist-complete-cb"); const cb = row.querySelector<HTMLInputElement>(".frist-complete-cb");
if (cb) { if (cb) {
@@ -191,7 +191,7 @@ function render() {
if (!cb.checked) return; if (!cb.checked) return;
cb.disabled = true; cb.disabled = true;
try { try {
const resp = await fetch(`/api/fristen/${id}/complete`, { method: "PATCH" }); const resp = await fetch(`/api/deadlines/${id}/complete`, { method: "PATCH" });
if (resp.ok) { if (resp.ok) {
await Promise.all([loadFristen(), loadSummary()]); await Promise.all([loadFristen(), loadSummary()]);
} else { } else {
@@ -209,31 +209,31 @@ function render() {
function initFilters() { function initFilters() {
const status = document.getElementById("frist-filter-status") as HTMLSelectElement; const status = document.getElementById("frist-filter-status") as HTMLSelectElement;
const akte = document.getElementById("frist-filter-akte") as HTMLSelectElement; const project = document.getElementById("frist-filter-project") as HTMLSelectElement;
// Pre-fill from URL // Pre-fill from URL
const params = urlParams(); const params = urlParams();
if (params.has("status")) statusFilter = params.get("status")!; if (params.has("status")) statusFilter = params.get("status")!;
if (params.has("projekt_id")) akteFilter = params.get("projekt_id")!; if (params.has("project_id")) akteFilter = params.get("project_id")!;
status.value = statusFilter; status.value = statusFilter;
status.addEventListener("change", async () => { status.addEventListener("change", async () => {
statusFilter = status.value; statusFilter = status.value;
await Promise.all([loadFristen(), loadSummary()]); await Promise.all([loadFristen(), loadSummary()]);
}); });
akte.addEventListener("change", async () => { project.addEventListener("change", async () => {
akteFilter = akte.value; akteFilter = project.value;
await Promise.all([loadFristen(), loadSummary()]); await Promise.all([loadFristen(), loadSummary()]);
}); });
} }
function populateAkteFilter() { function populateAkteFilter() {
const sel = document.getElementById("frist-filter-akte") as HTMLSelectElement; const sel = document.getElementById("frist-filter-project") as HTMLSelectElement;
// Keep the first "all" option, then append sorted Akten. // Keep the first "all" option, then append sorted Akten.
const options: string[] = [ const options: string[] = [
`<option value="" data-i18n="fristen.filter.akte.all">${esc(t("fristen.filter.akte.all"))}</option>`, `<option value="" data-i18n="fristen.filter.project.all">${esc(t("fristen.filter.project.all"))}</option>`,
]; ];
for (const a of allAkten) { for (const a of allProjects) {
options.push( options.push(
`<option value="${esc(a.id)}">${esc(a.aktenzeichen)} \u2014 ${esc(a.title)}</option>`, `<option value="${esc(a.id)}">${esc(a.aktenzeichen)} \u2014 ${esc(a.title)}</option>`,
); );

View File

@@ -121,7 +121,7 @@ function escHtml(s: string): string {
async function fetchAkten(): Promise<AkteOption[]> { async function fetchAkten(): Promise<AkteOption[]> {
try { try {
const resp = await fetch("/api/akten"); const resp = await fetch("/api/projects");
if (!resp.ok) return []; if (!resp.ok) return [];
return (await resp.json()) as AkteOption[]; return (await resp.json()) as AkteOption[];
} catch { } catch {
@@ -142,11 +142,11 @@ function ensureSaveModal() {
<button class="modal-close" id="frist-save-modal-close" type="button">&times;</button> <button class="modal-close" id="frist-save-modal-close" type="button">&times;</button>
</div> </div>
<div class="form-field"> <div class="form-field">
<label for="frist-save-akte" data-i18n="fristen.save.modal.akte">${escHtml(t("fristen.save.modal.akte"))}</label> <label for="frist-save-project" data-i18n="fristen.save.modal.project">${escHtml(t("fristen.save.modal.project"))}</label>
<select id="frist-save-akte"></select> <select id="frist-save-project"></select>
<p class="form-hint" id="frist-save-no-akten" style="display:none"> <p class="form-hint" id="frist-save-no-akten" style="display:none">
<span data-i18n="fristen.save.modal.no_akten">${escHtml(t("fristen.save.modal.no_akten"))}</span> <span data-i18n="fristen.save.modal.no_akten">${escHtml(t("fristen.save.modal.no_akten"))}</span>
<a href="/akten/neu" data-i18n="fristen.save.modal.no_akten.link">${escHtml(t("fristen.save.modal.no_akten.link"))}</a> <a href="/projects/new" data-i18n="fristen.save.modal.no_akten.link">${escHtml(t("fristen.save.modal.no_akten.link"))}</a>
</p> </p>
</div> </div>
<div class="form-field"> <div class="form-field">
@@ -179,7 +179,7 @@ async function openSaveModal() {
if (!lastResponse) return; if (!lastResponse) return;
ensureSaveModal(); ensureSaveModal();
const akten = await fetchAkten(); const akten = await fetchAkten();
const sel = document.getElementById("frist-save-akte") as HTMLSelectElement; const sel = document.getElementById("frist-save-project") as HTMLSelectElement;
const noAkten = document.getElementById("frist-save-no-akten")!; const noAkten = document.getElementById("frist-save-no-akten")!;
const submit = document.getElementById("frist-save-submit") as HTMLButtonElement; const submit = document.getElementById("frist-save-submit") as HTMLButtonElement;
@@ -221,7 +221,7 @@ async function openSaveModal() {
async function submitSave() { async function submitSave() {
if (!lastResponse) return; if (!lastResponse) return;
const sel = document.getElementById("frist-save-akte") as HTMLSelectElement; const sel = document.getElementById("frist-save-project") as HTMLSelectElement;
const akteID = sel.value; const akteID = sel.value;
const submit = document.getElementById("frist-save-submit") as HTMLButtonElement; const submit = document.getElementById("frist-save-submit") as HTMLButtonElement;
const msg = document.getElementById("frist-save-msg")!; const msg = document.getElementById("frist-save-msg")!;
@@ -247,7 +247,7 @@ async function submitSave() {
submit.disabled = true; submit.disabled = true;
try { try {
const resp = await fetch(`/api/projekte/${encodeURIComponent(akteID)}/fristen/bulk`, { const resp = await fetch(`/api/projects/${encodeURIComponent(akteID)}/fristen/bulk`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fristen }), body: JSON.stringify({ fristen }),
@@ -259,7 +259,7 @@ async function submitSave() {
submit.disabled = false; submit.disabled = false;
return; return;
} }
msg.innerHTML = `${escHtml(t("fristen.save.success"))} <a href="/fristen?projekt_id=${encodeURIComponent(akteID)}">${escHtml(t("fristen.save.success.link"))}</a>`; msg.innerHTML = `${escHtml(t("fristen.save.success"))} <a href="/deadlines?project_id=${encodeURIComponent(akteID)}">${escHtml(t("fristen.save.success.link"))}</a>`;
msg.className = "form-msg form-msg-ok"; msg.className = "form-msg form-msg-ok";
// Re-enable after a short delay so user can read it; modal stays open with the link. // Re-enable after a short delay so user can read it; modal stays open with the link.
setTimeout(() => { setTimeout(() => {
@@ -384,7 +384,7 @@ document.addEventListener("DOMContentLoaded", () => {
// Print button // Print button
document.getElementById("fristen-print-btn")!.addEventListener("click", () => window.print()); document.getElementById("fristen-print-btn")!.addEventListener("click", () => window.print());
// Save-to-Akte CTA (Phase E) // Save-to-Project CTA (Phase E)
const saveBtn = document.getElementById("fristen-save-cta"); const saveBtn = document.getElementById("fristen-save-cta");
if (saveBtn) saveBtn.addEventListener("click", openSaveModal); if (saveBtn) saveBtn.addEventListener("click", openSaveModal);
}); });

View File

@@ -15,7 +15,7 @@ let searchQuery = "";
const ICON_FEEDBACK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>'; const ICON_FEEDBACK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
async function loadTerms() { async function loadTerms() {
const resp = await fetch("/api/glossar"); const resp = await fetch("/api/glossary");
if (!resp.ok) return; if (!resp.ok) return;
allTerms = await resp.json(); allTerms = await resp.json();
render(); render();
@@ -162,7 +162,7 @@ async function submitSuggestion(e: Event) {
submitBtn.disabled = true; submitBtn.disabled = true;
try { try {
const resp = await fetch("/api/glossar/suggest", { const resp = await fetch("/api/glossary/suggest", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),

View File

@@ -10,14 +10,14 @@
import { t, getLang } from "./i18n"; import { t, getLang } from "./i18n";
export type NotizParentType = "akte" | "frist" | "termin"; export type NotizParentType = "project" | "frist" | "termin";
export interface Notiz { export interface Note {
id: string; id: string;
projekt_id?: string | null; project_id?: string | null;
frist_id?: string | null; deadline_id?: string | null;
termin_id?: string | null; appointment_id?: string | null;
akten_event_id?: string | null; project_event_id?: string | null;
content: string; content: string;
created_by?: string | null; created_by?: string | null;
created_at: string; created_at: string;
@@ -35,7 +35,7 @@ interface NotesState {
parentType: NotizParentType; parentType: NotizParentType;
parentId: string; parentId: string;
me: Me | null; me: Me | null;
notes: Notiz[]; notes: Note[];
listEl: HTMLElement; listEl: HTMLElement;
emptyEl: HTMLElement; emptyEl: HTMLElement;
formEl: HTMLFormElement; formEl: HTMLFormElement;
@@ -47,12 +47,12 @@ interface NotesState {
function baseURL(parentType: NotizParentType, parentId: string): string { function baseURL(parentType: NotizParentType, parentId: string): string {
switch (parentType) { switch (parentType) {
case "akte": case "project":
return `/api/projekte/${parentId}/notizen`; return `/api/projects/${parentId}/notizen`;
case "frist": case "frist":
return `/api/fristen/${parentId}/notizen`; return `/api/deadlines/${parentId}/notizen`;
case "termin": case "termin":
return `/api/termine/${parentId}/notizen`; return `/api/appointments/${parentId}/notizen`;
} }
} }
@@ -62,7 +62,7 @@ function esc(s: string): string {
return d.innerHTML; return d.innerHTML;
} }
function authorLabel(n: Notiz): string { function authorLabel(n: Note): string {
return n.author_name || n.author_email || t("notizen.unknown_author"); return n.author_name || n.author_email || t("notizen.unknown_author");
} }
@@ -93,12 +93,12 @@ function fmtRelative(iso: string): string {
}); });
} }
function canEdit(state: NotesState, n: Notiz): boolean { function canEdit(state: NotesState, n: Note): boolean {
if (!state.me) return false; if (!state.me) return false;
return n.created_by === state.me.id; return n.created_by === state.me.id;
} }
function canDelete(state: NotesState, n: Notiz): boolean { function canDelete(state: NotesState, n: Note): boolean {
if (!state.me) return false; if (!state.me) return false;
if (n.created_by === state.me.id) return true; if (n.created_by === state.me.id) return true;
return state.me.role === "partner" || state.me.role === "admin"; return state.me.role === "partner" || state.me.role === "admin";
@@ -116,7 +116,7 @@ function render(state: NotesState) {
} }
} }
function noteCard(state: NotesState, n: Notiz): HTMLElement { function noteCard(state: NotesState, n: Note): HTMLElement {
const card = document.createElement("li"); const card = document.createElement("li");
card.className = "notiz-card"; card.className = "notiz-card";
card.dataset.id = n.id; card.dataset.id = n.id;
@@ -213,7 +213,7 @@ async function loadNotes(state: NotesState) {
render(state); render(state);
return; return;
} }
const data = (await resp.json()) as Notiz[]; const data = (await resp.json()) as Note[];
state.notes = Array.isArray(data) ? data : []; state.notes = Array.isArray(data) ? data : [];
} catch { } catch {
state.notes = []; state.notes = [];
@@ -252,7 +252,7 @@ async function submitForm(ev: Event, state: NotesState) {
async function addNote(state: NotesState, content: string) { async function addNote(state: NotesState, content: string) {
// Optimistic add: insert a placeholder, replace with server response on success. // Optimistic add: insert a placeholder, replace with server response on success.
const tempID = `temp-${Date.now()}`; const tempID = `temp-${Date.now()}`;
const placeholder: Notiz = { const placeholder: Note = {
id: tempID, id: tempID,
content, content,
created_by: state.me?.id ?? null, created_by: state.me?.id ?? null,
@@ -280,7 +280,7 @@ async function addNote(state: NotesState, content: string) {
showMsg(state, "error", data.error || t("notizen.error.generic")); showMsg(state, "error", data.error || t("notizen.error.generic"));
return; return;
} }
const saved = (await resp.json()) as Notiz; const saved = (await resp.json()) as Note;
state.notes = state.notes.map((n) => (n.id === tempID ? saved : n)); state.notes = state.notes.map((n) => (n.id === tempID ? saved : n));
render(state); render(state);
} catch { } catch {
@@ -291,7 +291,7 @@ async function addNote(state: NotesState, content: string) {
} }
} }
function startEdit(state: NotesState, n: Notiz) { function startEdit(state: NotesState, n: Note) {
state.editingID = n.id; state.editingID = n.id;
state.textareaEl.value = n.content; state.textareaEl.value = n.content;
state.textareaEl.focus(); state.textareaEl.focus();
@@ -329,7 +329,7 @@ async function saveEdit(state: NotesState, id: string, content: string) {
cancelEdit(state); cancelEdit(state);
try { try {
const resp = await fetch(`/api/notizen/${id}`, { const resp = await fetch(`/api/notes/${id}`, {
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }), body: JSON.stringify({ content }),
@@ -341,7 +341,7 @@ async function saveEdit(state: NotesState, id: string, content: string) {
showMsg(state, "error", data.error || t("notizen.error.generic")); showMsg(state, "error", data.error || t("notizen.error.generic"));
return; return;
} }
const saved = (await resp.json()) as Notiz; const saved = (await resp.json()) as Note;
state.notes = state.notes.map((n) => (n.id === id ? saved : n)); state.notes = state.notes.map((n) => (n.id === id ? saved : n));
render(state); render(state);
} catch { } catch {
@@ -351,13 +351,13 @@ async function saveEdit(state: NotesState, id: string, content: string) {
} }
} }
async function deleteNote(state: NotesState, n: Notiz) { async function deleteNote(state: NotesState, n: Note) {
if (!confirm(t("notizen.delete.confirm"))) return; if (!confirm(t("notizen.delete.confirm"))) return;
const prev = state.notes; const prev = state.notes;
state.notes = state.notes.filter((x) => x.id !== n.id); state.notes = state.notes.filter((x) => x.id !== n.id);
render(state); render(state);
try { try {
const resp = await fetch(`/api/notizen/${n.id}`, { method: "DELETE" }); const resp = await fetch(`/api/notes/${n.id}`, { method: "DELETE" });
if (!resp.ok && resp.status !== 204) { if (!resp.ok && resp.status !== 204) {
state.notes = prev; state.notes = prev;
render(state); render(state);

View File

@@ -1,8 +1,8 @@
import { initI18n, onLangChange, t, getLang } from "./i18n"; import { initI18n, onLangChange, t, getLang } from "./i18n";
import { initSidebar } from "./sidebar"; import { initSidebar } from "./sidebar";
import { initNotes } from "./notizen"; import { initNotes } from "./notes";
interface Akte { interface Project {
id: string; id: string;
type: string; type: string;
parent_id?: string | null; parent_id?: string | null;
@@ -21,7 +21,7 @@ interface Akte {
interface ProjektTeamMember { interface ProjektTeamMember {
id: string; id: string;
projekt_id: string; project_id: string;
user_id: string; user_id: string;
role: string; role: string;
inherited: boolean; inherited: boolean;
@@ -32,7 +32,7 @@ interface ProjektTeamMember {
inherited_from_title?: string | null; inherited_from_title?: string | null;
} }
interface ProjektMini { interface ProjectMini {
id: string; id: string;
type: string; type: string;
title: string; title: string;
@@ -40,9 +40,9 @@ interface ProjektMini {
status: string; status: string;
} }
interface Partei { interface Party {
id: string; id: string;
projekt_id: string; project_id: string;
name: string; name: string;
role?: string; role?: string;
representative?: string; representative?: string;
@@ -50,7 +50,7 @@ interface Partei {
interface AkteEvent { interface AkteEvent {
id: string; id: string;
projekt_id: string; project_id: string;
event_type?: string; event_type?: string;
title: string; title: string;
description?: string; description?: string;
@@ -58,23 +58,23 @@ interface AkteEvent {
created_by?: string; created_by?: string;
} }
interface Frist { interface Deadline {
id: string; id: string;
projekt_id: string; project_id: string;
title: string; title: string;
due_date: string; due_date: string;
status: string; status: string;
rule_id?: string; rule_id?: string;
} }
interface Termin { interface Appointment {
id: string; id: string;
projekt_id?: string; project_id?: string;
title: string; title: string;
start_at: string; start_at: string;
end_at?: string; end_at?: string;
location?: string; location?: string;
termin_type?: string; appointment_type?: string;
} }
interface Me { interface Me {
@@ -105,14 +105,14 @@ interface ChecklistTemplateSummary {
let checklistInstances: ChecklistInstanceSummary[] = []; let checklistInstances: ChecklistInstanceSummary[] = [];
let checklistTemplates: Record<string, ChecklistTemplateSummary> = {}; let checklistTemplates: Record<string, ChecklistTemplateSummary> = {};
let akte: Akte | null = null; let project: Project | null = null;
let me: Me | null = null; let me: Me | null = null;
let parteien: Partei[] = []; let parties: Party[] = [];
let events: AkteEvent[] = []; let events: AkteEvent[] = [];
let fristen: Frist[] = []; let deadlines: Deadline[] = [];
let termine: Termin[] = []; let appointments: Appointment[] = [];
let ancestors: ProjektMini[] = []; let ancestors: ProjectMini[] = [];
let children: ProjektMini[] = []; let children: ProjectMini[] = [];
let teamMembers: ProjektTeamMember[] = []; let teamMembers: ProjektTeamMember[] = [];
let userOptions: { id: string; display_name: string; email: string }[] = []; let userOptions: { id: string; display_name: string; email: string }[] = [];
@@ -145,9 +145,9 @@ async function loadMe() {
async function loadAkte(id: string): Promise<boolean> { async function loadAkte(id: string): Promise<boolean> {
try { try {
const resp = await fetch(`/api/projekte/${id}`); const resp = await fetch(`/api/projects/${id}`);
if (!resp.ok) return false; if (!resp.ok) return false;
akte = await resp.json(); project = await resp.json();
return true; return true;
} catch { } catch {
return false; return false;
@@ -156,7 +156,7 @@ async function loadAkte(id: string): Promise<boolean> {
async function loadParteien(id: string) { async function loadParteien(id: string) {
try { try {
const resp = await fetch(`/api/projekte/${id}/parteien`); const resp = await fetch(`/api/projects/${id}/parteien`);
if (resp.ok) parteien = await resp.json(); if (resp.ok) parteien = await resp.json();
} catch { } catch {
parteien = []; parteien = [];
@@ -165,7 +165,7 @@ async function loadParteien(id: string) {
async function loadEvents(id: string) { async function loadEvents(id: string) {
try { try {
const resp = await fetch(`/api/projekte/${id}/events?limit=${EVENTS_PAGE_SIZE}`); const resp = await fetch(`/api/projects/${id}/events?limit=${EVENTS_PAGE_SIZE}`);
if (resp.ok) { if (resp.ok) {
events = await resp.json(); events = await resp.json();
eventsHasMore = events.length === EVENTS_PAGE_SIZE; eventsHasMore = events.length === EVENTS_PAGE_SIZE;
@@ -190,7 +190,7 @@ async function loadMoreEvents(id: string) {
} }
try { try {
const resp = await fetch( const resp = await fetch(
`/api/projekte/${id}/events?before=${encodeURIComponent(cursor)}&limit=${EVENTS_PAGE_SIZE}`, `/api/projects/${id}/events?before=${encodeURIComponent(cursor)}&limit=${EVENTS_PAGE_SIZE}`,
); );
if (resp.ok) { if (resp.ok) {
const page: AkteEvent[] = await resp.json(); const page: AkteEvent[] = await resp.json();
@@ -211,7 +211,7 @@ async function loadMoreEvents(id: string) {
async function loadFristen(id: string) { async function loadFristen(id: string) {
try { try {
const resp = await fetch(`/api/projekte/${id}/fristen`); const resp = await fetch(`/api/projects/${id}/fristen`);
if (resp.ok) fristen = await resp.json(); if (resp.ok) fristen = await resp.json();
} catch { } catch {
fristen = []; fristen = [];
@@ -220,7 +220,7 @@ async function loadFristen(id: string) {
async function loadTermine(id: string) { async function loadTermine(id: string) {
try { try {
const resp = await fetch(`/api/projekte/${id}/termine`); const resp = await fetch(`/api/projects/${id}/termine`);
if (resp.ok) termine = await resp.json(); if (resp.ok) termine = await resp.json();
} catch { } catch {
termine = []; termine = [];
@@ -242,10 +242,10 @@ function fmtDateTimeLocal(iso: string): string {
} }
} }
function renderTermine() { function renderAppointments() {
const tbody = document.getElementById("akte-termine-body"); const tbody = document.getElementById("project-termine-body");
const empty = document.getElementById("akte-termine-empty"); const empty = document.getElementById("project-termine-empty");
const wrap = document.getElementById("akte-termine-tablewrap"); const wrap = document.getElementById("project-termine-tablewrap");
if (!tbody || !empty || !wrap) return; if (!tbody || !empty || !wrap) return;
if (termine.length === 0) { if (termine.length === 0) {
tbody.innerHTML = ""; tbody.innerHTML = "";
@@ -257,8 +257,8 @@ function renderTermine() {
empty.style.display = "none"; empty.style.display = "none";
tbody.innerHTML = termine tbody.innerHTML = termine
.map((tt) => { .map((tt) => {
const typeLabel = tt.termin_type ? t(`termine.type.${tt.termin_type}`) || tt.termin_type : ""; const typeLabel = tt.appointment_type ? t(`termine.type.${tt.appointment_type}`) || tt.appointment_type : "";
const typeClass = tt.termin_type ? `termin-type-${tt.termin_type}` : ""; const typeClass = tt.appointment_type ? `termin-type-${tt.appointment_type}` : "";
return `<tr class="termin-row" data-id="${esc(tt.id)}"> return `<tr class="termin-row" data-id="${esc(tt.id)}">
<td class="frist-col-check"><span class="termin-dot ${typeClass}" /></td> <td class="frist-col-check"><span class="termin-dot ${typeClass}" /></td>
<td>${esc(fmtDateTimeLocal(tt.start_at))}</td> <td>${esc(fmtDateTimeLocal(tt.start_at))}</td>
@@ -271,22 +271,22 @@ function renderTermine() {
tbody.querySelectorAll<HTMLTableRowElement>(".termin-row").forEach((row) => { tbody.querySelectorAll<HTMLTableRowElement>(".termin-row").forEach((row) => {
const id = row.dataset.id!; const id = row.dataset.id!;
row.addEventListener("click", () => { row.addEventListener("click", () => {
window.location.href = `/termine/${id}`; window.location.href = `/appointments/${id}`;
}); });
}); });
} }
function initAkteTerminForm() { function initAkteTerminForm() {
const addBtn = document.getElementById("termin-add-btn") as HTMLButtonElement | null; const addBtn = document.getElementById("termin-add-btn") as HTMLButtonElement | null;
const form = document.getElementById("akte-termin-form") as HTMLFormElement | null; const form = document.getElementById("project-termin-form") as HTMLFormElement | null;
const cancelBtn = document.getElementById("akte-termin-cancel") as HTMLButtonElement | null; const cancelBtn = document.getElementById("project-termin-cancel") as HTMLButtonElement | null;
const msg = document.getElementById("akte-termin-msg"); const msg = document.getElementById("project-termin-msg");
if (!addBtn || !form || !cancelBtn || !msg) return; if (!addBtn || !form || !cancelBtn || !msg) return;
addBtn.addEventListener("click", () => { addBtn.addEventListener("click", () => {
form.style.display = ""; form.style.display = "";
addBtn.style.display = "none"; addBtn.style.display = "none";
(document.getElementById("akte-termin-title") as HTMLInputElement).focus(); (document.getElementById("project-termin-title") as HTMLInputElement).focus();
}); });
cancelBtn.addEventListener("click", () => { cancelBtn.addEventListener("click", () => {
form.reset(); form.reset();
@@ -297,28 +297,28 @@ function initAkteTerminForm() {
form.addEventListener("submit", async (e) => { form.addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
if (!akte) return; if (!project) return;
const title = (document.getElementById("akte-termin-title") as HTMLInputElement).value.trim(); const title = (document.getElementById("project-termin-title") as HTMLInputElement).value.trim();
const start = (document.getElementById("akte-termin-start") as HTMLInputElement).value; const start = (document.getElementById("project-termin-start") as HTMLInputElement).value;
const end = (document.getElementById("akte-termin-end") as HTMLInputElement).value; const end = (document.getElementById("project-termin-end") as HTMLInputElement).value;
const type = (document.getElementById("akte-termin-type") as HTMLSelectElement).value; const type = (document.getElementById("project-termin-type") as HTMLSelectElement).value;
const location = (document.getElementById("akte-termin-location") as HTMLInputElement).value.trim(); const location = (document.getElementById("project-termin-location") as HTMLInputElement).value.trim();
if (!title || !start) return; if (!title || !start) return;
const payload: Record<string, unknown> = { const payload: Record<string, unknown> = {
projekt_id: akte.id, project_id: project.id,
title, title,
start_at: new Date(start).toISOString(), start_at: new Date(start).toISOString(),
}; };
if (end) payload.end_at = new Date(end).toISOString(); if (end) payload.end_at = new Date(end).toISOString();
if (type) payload.termin_type = type; if (type) payload.appointment_type = type;
if (location) payload.location = location; if (location) payload.location = location;
msg.textContent = ""; msg.textContent = "";
const submitBtn = form.querySelector<HTMLButtonElement>("button[type=submit]")!; const submitBtn = form.querySelector<HTMLButtonElement>("button[type=submit]")!;
submitBtn.disabled = true; submitBtn.disabled = true;
try { try {
const resp = await fetch("/api/termine", { const resp = await fetch("/api/appointments", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),
@@ -327,9 +327,9 @@ function initAkteTerminForm() {
form.reset(); form.reset();
form.style.display = "none"; form.style.display = "none";
addBtn.style.display = ""; addBtn.style.display = "";
await loadTermine(akte.id); await loadTermine(project.id);
renderTermine(); renderAppointments();
await loadEvents(akte.id); await loadEvents(project.id);
renderEvents(); renderEvents();
} else { } else {
const data = await resp.json().catch(() => ({}) as { error?: string }); const data = await resp.json().catch(() => ({}) as { error?: string });
@@ -369,10 +369,10 @@ function urgencyClass(due: string, status: string): string {
return "frist-urgency-later"; return "frist-urgency-later";
} }
function renderFristen() { function renderDeadlines() {
const tbody = document.getElementById("akte-fristen-body"); const tbody = document.getElementById("project-fristen-body");
const empty = document.getElementById("akte-fristen-empty"); const empty = document.getElementById("project-fristen-empty");
const wrap = document.getElementById("akte-fristen-tablewrap"); const wrap = document.getElementById("project-fristen-tablewrap");
if (!tbody || !empty || !wrap) return; if (!tbody || !empty || !wrap) return;
if (fristen.length === 0) { if (fristen.length === 0) {
tbody.innerHTML = ""; tbody.innerHTML = "";
@@ -407,18 +407,18 @@ function renderFristen() {
row.addEventListener("click", (e) => { row.addEventListener("click", (e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (target.closest(".frist-complete-cb")) return; if (target.closest(".frist-complete-cb")) return;
window.location.href = `/fristen/${id}`; window.location.href = `/deadlines/${id}`;
}); });
const cb = row.querySelector<HTMLInputElement>(".frist-complete-cb"); const cb = row.querySelector<HTMLInputElement>(".frist-complete-cb");
if (cb) { if (cb) {
cb.addEventListener("change", async () => { cb.addEventListener("change", async () => {
if (!cb.checked || !akte) return; if (!cb.checked || !project) return;
cb.disabled = true; cb.disabled = true;
const resp = await fetch(`/api/fristen/${id}/complete`, { method: "PATCH" }); const resp = await fetch(`/api/deadlines/${id}/complete`, { method: "PATCH" });
if (resp.ok) { if (resp.ok) {
await loadFristen(akte.id); await loadFristen(project.id);
renderFristen(); renderDeadlines();
await loadEvents(akte.id); await loadEvents(project.id);
renderEvents(); renderEvents();
} else { } else {
cb.checked = false; cb.checked = false;
@@ -455,14 +455,14 @@ function fmtDateTime(iso: string): string {
} }
function renderHeader() { function renderHeader() {
if (!akte) return; if (!project) return;
(document.getElementById("akte-title-display") as HTMLElement).textContent = akte.title; (document.getElementById("project-title-display") as HTMLElement).textContent = project.title;
(document.getElementById("akte-title-edit") as HTMLInputElement).value = akte.title; (document.getElementById("project-title-edit") as HTMLInputElement).value = project.title;
(document.getElementById("akte-ref-display") as HTMLElement).textContent = akte.reference || ""; (document.getElementById("project-ref-display") as HTMLElement).textContent = project.reference || "";
const descDisplay = document.getElementById("akte-description-display") as HTMLElement; const descDisplay = document.getElementById("project-description-display") as HTMLElement;
const descEdit = document.getElementById("akte-description-edit") as HTMLTextAreaElement; const descEdit = document.getElementById("project-description-edit") as HTMLTextAreaElement;
const description = (akte as Akte & { description?: string | null }).description ?? ""; const description = (project as Project & { description?: string | null }).description ?? "";
descDisplay.textContent = description; descDisplay.textContent = description;
descEdit.value = description; descEdit.value = description;
const descWrap = document.querySelector<HTMLElement>(".akten-detail-description"); const descWrap = document.querySelector<HTMLElement>(".akten-detail-description");
@@ -472,21 +472,21 @@ function renderHeader() {
descWrap.dataset.empty = description ? "0" : "1"; descWrap.dataset.empty = description ? "0" : "1";
} }
const typeChip = document.getElementById("akte-type-chip")!; const typeChip = document.getElementById("project-type-chip")!;
typeChip.className = `akten-type-chip akten-type-${akte.type}`; typeChip.className = `akten-type-chip akten-type-${project.type}`;
typeChip.textContent = t(`projekte.type.${akte.type}`) || akte.type; typeChip.textContent = t(`projekte.type.${project.type}`) || project.type;
// ClientMatter display. If the projekt itself has no client_number, walk // ClientMatter display. If the projekt itself has no client_number, walk
// up the ancestor chain to find an inherited one. // up the ancestor chain to find an inherited one.
const cm = document.getElementById("akte-clientmatter")!; const cm = document.getElementById("project-clientmatter")!;
const effectiveClient = akte.client_number || inheritedClientNumber(); const effectiveClient = project.client_number || inheritedClientNumber();
const effectiveMatter = akte.matter_number || ""; const effectiveMatter = project.matter_number || "";
if (effectiveClient || effectiveMatter) { if (effectiveClient || effectiveMatter) {
cm.textContent = cm.textContent =
effectiveClient && effectiveMatter effectiveClient && effectiveMatter
? `${effectiveClient}.${effectiveMatter}` ? `${effectiveClient}.${effectiveMatter}`
: effectiveClient || effectiveMatter; : effectiveClient || effectiveMatter;
if (!akte.client_number && effectiveClient) { if (!project.client_number && effectiveClient) {
cm.classList.add("akten-ref-inherited"); cm.classList.add("akten-ref-inherited");
cm.title = t("projekte.detail.clientmatter.inherited") || "inherited"; cm.title = t("projekte.detail.clientmatter.inherited") || "inherited";
} else { } else {
@@ -497,20 +497,20 @@ function renderHeader() {
cm.textContent = ""; cm.textContent = "";
} }
const statusChip = document.getElementById("akte-status-chip")!; const statusChip = document.getElementById("project-status-chip")!;
statusChip.className = `akten-status-chip akten-status-${akte.status}`; statusChip.className = `akten-status-chip akten-status-${project.status}`;
statusChip.textContent = t(`projekte.filter.status.${akte.status}`) || akte.status; statusChip.textContent = t(`projekte.filter.status.${project.status}`) || project.status;
const netdocs = document.getElementById("akte-netdocs") as HTMLAnchorElement; const netdocs = document.getElementById("project-netdocs") as HTMLAnchorElement;
if (akte.netdocuments_url) { if (project.netdocuments_url) {
netdocs.href = akte.netdocuments_url; netdocs.href = project.netdocuments_url;
netdocs.style.display = ""; netdocs.style.display = "";
} else { } else {
netdocs.style.display = "none"; netdocs.style.display = "none";
} }
// Delete visibility: partner/admin only // Delete visibility: partner/admin only
const deleteWrap = document.getElementById("akte-delete-wrap")!; const deleteWrap = document.getElementById("project-delete-wrap")!;
if (me && (me.role === "partner" || me.role === "admin")) { if (me && (me.role === "partner" || me.role === "admin")) {
deleteWrap.style.display = ""; deleteWrap.style.display = "";
} else { } else {
@@ -547,7 +547,7 @@ function initEventsLoadMore() {
const btn = document.getElementById("akten-events-loadmore"); const btn = document.getElementById("akten-events-loadmore");
if (!btn) return; if (!btn) return;
btn.addEventListener("click", () => { btn.addEventListener("click", () => {
if (akte) void loadMoreEvents(akte.id); if (project) void loadMoreEvents(project.id);
}); });
} }
@@ -583,9 +583,9 @@ function renderParteien() {
const row = btn.closest<HTMLTableRowElement>("tr")!; const row = btn.closest<HTMLTableRowElement>("tr")!;
const id = row.dataset.id!; const id = row.dataset.id!;
if (!confirm(t("akten.detail.parteien.remove.confirm"))) return; if (!confirm(t("akten.detail.parteien.remove.confirm"))) return;
const resp = await fetch(`/api/parteien/${id}`, { method: "DELETE" }); const resp = await fetch(`/api/parties/${id}`, { method: "DELETE" });
if (resp.ok && akte) { if (resp.ok && project) {
await loadParteien(akte.id); await loadParteien(project.id);
renderParteien(); renderParteien();
} }
}); });
@@ -600,14 +600,14 @@ function showTab(tab: TabId) {
el.style.display = el.id === `tab-${tab}` ? "" : "none"; el.style.display = el.id === `tab-${tab}` ? "" : "none";
}); });
// Deep-link via pushState so sub-routes stay shareable. // Deep-link via pushState so sub-routes stay shareable.
if (akte) { if (project) {
const newPath = `/projekte/${akte.id}/${tab}`; const newPath = `/projects/${project.id}/${tab}`;
if (window.location.pathname !== newPath) { if (window.location.pathname !== newPath) {
window.history.replaceState({}, "", newPath); window.history.replaceState({}, "", newPath);
} }
} }
if (tab === "checklisten" && akte) { if (tab === "checklisten" && project) {
void loadAndRenderChecklistInstances(akte.id); void loadAndRenderChecklistInstances(project.id);
} }
} }
@@ -617,8 +617,8 @@ async function loadAndRenderChecklistInstances(akteID: string) {
checklistInstancesInited = true; checklistInstancesInited = true;
try { try {
const [instResp, tplResp] = await Promise.all([ const [instResp, tplResp] = await Promise.all([
fetch(`/api/projekte/${akteID}/checklisten`), fetch(`/api/projects/${akteID}/checklisten`),
fetch(`/api/checklisten`), fetch(`/api/checklists`),
]); ]);
checklistInstances = instResp.ok ? await instResp.json() : []; checklistInstances = instResp.ok ? await instResp.json() : [];
const templates = tplResp.ok ? await tplResp.json() as ChecklistTemplateSummary[] : []; const templates = tplResp.ok ? await tplResp.json() as ChecklistTemplateSummary[] : [];
@@ -631,9 +631,9 @@ async function loadAndRenderChecklistInstances(akteID: string) {
} }
function renderChecklistInstances() { function renderChecklistInstances() {
const body = document.getElementById("akte-checklisten-body"); const body = document.getElementById("project-checklisten-body");
const empty = document.getElementById("akte-checklisten-empty"); const empty = document.getElementById("project-checklisten-empty");
const wrap = document.getElementById("akte-checklisten-tablewrap"); const wrap = document.getElementById("project-checklisten-tablewrap");
if (!body || !empty || !wrap) return; if (!body || !empty || !wrap) return;
if (checklistInstances.length === 0) { if (checklistInstances.length === 0) {
@@ -661,7 +661,7 @@ function renderChecklistInstances() {
const pct = total === 0 ? 0 : Math.round((done / total) * 100); const pct = total === 0 ? 0 : Math.round((done / total) * 100);
return `<tr> return `<tr>
<td>${escapeHtml(tplName)}</td> <td>${escapeHtml(tplName)}</td>
<td><a href="/checklisten/instances/${escapeHtml(inst.id)}" class="checklist-instance-name">${escapeHtml(inst.name)}</a></td> <td><a href="/checklists/instances/${escapeHtml(inst.id)}" class="checklist-instance-name">${escapeHtml(inst.name)}</a></td>
<td> <td>
<div class="checklist-progress-inline"> <div class="checklist-progress-inline">
<div class="checklist-progress-bar"> <div class="checklist-progress-bar">
@@ -691,12 +691,12 @@ function initTabs() {
} }
function initTitleEdit() { function initTitleEdit() {
const display = document.getElementById("akte-title-display")!; const display = document.getElementById("project-title-display")!;
const editInput = document.getElementById("akte-title-edit") as HTMLInputElement; const editInput = document.getElementById("project-title-edit") as HTMLInputElement;
const descDisplay = document.getElementById("akte-description-display") as HTMLElement; const descDisplay = document.getElementById("project-description-display") as HTMLElement;
const descEdit = document.getElementById("akte-description-edit") as HTMLTextAreaElement; const descEdit = document.getElementById("project-description-edit") as HTMLTextAreaElement;
const editBtn = document.getElementById("akte-edit-btn") as HTMLButtonElement; const editBtn = document.getElementById("project-edit-btn") as HTMLButtonElement;
const saveBtn = document.getElementById("akte-save-btn") as HTMLButtonElement; const saveBtn = document.getElementById("project-save-btn") as HTMLButtonElement;
editBtn.addEventListener("click", () => { editBtn.addEventListener("click", () => {
display.style.display = "none"; display.style.display = "none";
@@ -710,11 +710,11 @@ function initTitleEdit() {
}); });
saveBtn.addEventListener("click", async () => { saveBtn.addEventListener("click", async () => {
if (!akte) return; if (!project) return;
const newTitle = editInput.value.trim(); const newTitle = editInput.value.trim();
const newDesc = descEdit.value.trim(); const newDesc = descEdit.value.trim();
const oldDesc = (akte as Akte & { description?: string | null }).description ?? ""; const oldDesc = (project as Project & { description?: string | null }).description ?? "";
const titleUnchanged = !newTitle || newTitle === akte.title; const titleUnchanged = !newTitle || newTitle === project.title;
const descUnchanged = newDesc === oldDesc; const descUnchanged = newDesc === oldDesc;
if (titleUnchanged && descUnchanged) { if (titleUnchanged && descUnchanged) {
cancelEdit(); cancelEdit();
@@ -725,15 +725,15 @@ function initTitleEdit() {
const body: Record<string, unknown> = {}; const body: Record<string, unknown> = {};
if (!titleUnchanged) body.title = newTitle; if (!titleUnchanged) body.title = newTitle;
if (!descUnchanged) body.description = newDesc; if (!descUnchanged) body.description = newDesc;
const resp = await fetch(`/api/projekte/${akte.id}`, { const resp = await fetch(`/api/projects/${project.id}`, {
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
if (resp.ok) { if (resp.ok) {
akte = await resp.json(); project = await resp.json();
renderHeader(); renderHeader();
if (akte) await loadEvents(akte.id); if (project) await loadEvents(project.id);
renderEvents(); renderEvents();
} }
} finally { } finally {
@@ -773,7 +773,7 @@ function initParteienForm() {
form.addEventListener("submit", async (e) => { form.addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
if (!akte) return; if (!project) return;
const name = (document.getElementById("partei-name") as HTMLInputElement).value.trim(); const name = (document.getElementById("partei-name") as HTMLInputElement).value.trim();
const role = (document.getElementById("partei-role") as HTMLSelectElement).value; const role = (document.getElementById("partei-role") as HTMLSelectElement).value;
const rep = (document.getElementById("partei-rep") as HTMLInputElement).value.trim(); const rep = (document.getElementById("partei-rep") as HTMLInputElement).value.trim();
@@ -787,7 +787,7 @@ function initParteienForm() {
if (rep) payload.representative = rep; if (rep) payload.representative = rep;
try { try {
const resp = await fetch(`/api/projekte/${akte.id}/parteien`, { const resp = await fetch(`/api/projects/${project.id}/parteien`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),
@@ -796,9 +796,9 @@ function initParteienForm() {
form.reset(); form.reset();
form.style.display = "none"; form.style.display = "none";
addBtn.style.display = ""; addBtn.style.display = "";
await loadParteien(akte.id); await loadParteien(project.id);
renderParteien(); renderParteien();
await loadEvents(akte.id); await loadEvents(project.id);
renderEvents(); renderEvents();
} else { } else {
const data = await resp.json().catch(() => ({}) as { error?: string }); const data = await resp.json().catch(() => ({}) as { error?: string });
@@ -815,13 +815,13 @@ function initParteienForm() {
} }
function initFristAddLink() { function initFristAddLink() {
if (!akte) return; if (!project) return;
const link = document.getElementById("frist-add-link") as HTMLAnchorElement | null; const link = document.getElementById("frist-add-link") as HTMLAnchorElement | null;
if (link) link.href = `/projekte/${akte.id}/fristen/neu`; if (link) link.href = `/projects/${project.id}/fristen/neu`;
} }
function initDelete() { function initDelete() {
const btn = document.getElementById("akte-delete-btn")!; const btn = document.getElementById("project-delete-btn")!;
const modal = document.getElementById("delete-modal")!; const modal = document.getElementById("delete-modal")!;
const close = document.getElementById("delete-modal-close")!; const close = document.getElementById("delete-modal-close")!;
const cancel = document.getElementById("delete-modal-cancel")!; const cancel = document.getElementById("delete-modal-cancel")!;
@@ -839,11 +839,11 @@ function initDelete() {
if (e.target === e.currentTarget) closeModal(); if (e.target === e.currentTarget) closeModal();
}); });
confirmBtn.addEventListener("click", async () => { confirmBtn.addEventListener("click", async () => {
if (!akte) return; if (!project) return;
confirmBtn.disabled = true; confirmBtn.disabled = true;
const resp = await fetch(`/api/projekte/${akte.id}`, { method: "DELETE" }); const resp = await fetch(`/api/projects/${project.id}`, { method: "DELETE" });
if (resp.ok) { if (resp.ok) {
window.location.href = "/projekte"; window.location.href = "/projects";
} else { } else {
confirmBtn.disabled = false; confirmBtn.disabled = false;
closeModal(); closeModal();
@@ -865,7 +865,7 @@ async function main() {
await loadMe(); await loadMe();
const ok = await loadAkte(id); const ok = await loadAkte(id);
if (!ok || !akte) { if (!ok || !project) {
loading.style.display = "none"; loading.style.display = "none";
notfound.style.display = "block"; notfound.style.display = "block";
return; return;
@@ -888,8 +888,8 @@ async function main() {
renderBreadcrumb(); renderBreadcrumb();
renderParteien(); renderParteien();
renderEvents(); renderEvents();
renderFristen(); renderDeadlines();
renderTermine(); renderAppointments();
renderChildren(); renderChildren();
renderTeam(); renderTeam();
initFristAddLink(); initFristAddLink();
@@ -911,7 +911,7 @@ function inheritedClientNumber(): string | null {
// Walks ancestor chain (root → parent) and returns the nearest non-null // Walks ancestor chain (root → parent) and returns the nearest non-null
// client_number for display when the projekt itself has none. // client_number for display when the projekt itself has none.
for (let i = ancestors.length - 1; i >= 0; i--) { for (let i = ancestors.length - 1; i >= 0; i--) {
const a = ancestors[i] as ProjektMini & { client_number?: string | null }; const a = ancestors[i] as ProjectMini & { client_number?: string | null };
if (a.client_number) return a.client_number; if (a.client_number) return a.client_number;
} }
return null; return null;
@@ -919,22 +919,22 @@ function inheritedClientNumber(): string | null {
async function loadAncestors(id: string) { async function loadAncestors(id: string) {
try { try {
const resp = await fetch(`/api/projekte/${id}/ancestors`); const resp = await fetch(`/api/projects/${id}/ancestors`);
if (resp.ok) ancestors = (await resp.json()) as ProjektMini[]; if (resp.ok) ancestors = (await resp.json()) as ProjectMini[];
} catch { } catch {
ancestors = []; ancestors = [];
} }
} }
function renderBreadcrumb() { function renderBreadcrumb() {
if (!akte) return; if (!project) return;
const el = document.getElementById("projekt-breadcrumb"); const el = document.getElementById("projekt-breadcrumb");
if (!el) return; if (!el) return;
const parts: string[] = ancestors.map( const parts: string[] = ancestors.map(
(a) => (a) =>
`<a href="/projekte/${esc(a.id)}" class="projekt-crumb">${esc(a.title)}</a>`, `<a href="/projects/${esc(a.id)}" class="projekt-crumb">${esc(a.title)}</a>`,
); );
parts.push(`<span class="projekt-crumb projekt-crumb-current">${esc(akte.title)}</span>`); parts.push(`<span class="projekt-crumb projekt-crumb-current">${esc(project.title)}</span>`);
el.innerHTML = parts.join(`<span class="projekt-crumb-sep">\u203A</span>`); el.innerHTML = parts.join(`<span class="projekt-crumb-sep">\u203A</span>`);
} }
@@ -942,8 +942,8 @@ function renderBreadcrumb() {
async function loadChildren(id: string) { async function loadChildren(id: string) {
try { try {
const resp = await fetch(`/api/projekte/${id}/kinder`); const resp = await fetch(`/api/projects/${id}/children`);
if (resp.ok) children = (await resp.json()) as ProjektMini[]; if (resp.ok) children = (await resp.json()) as ProjectMini[];
} catch { } catch {
children = []; children = [];
} }
@@ -961,7 +961,7 @@ function renderChildren() {
list.innerHTML = children list.innerHTML = children
.map( .map(
(c) => `<li class="projekt-child-item"> (c) => `<li class="projekt-child-item">
<a href="/projekte/${esc(c.id)}" class="projekt-child-link"> <a href="/projects/${esc(c.id)}" class="projekt-child-link">
<span class="akten-type-chip akten-type-${esc(c.type)}">${esc(t("projekte.type." + c.type) || c.type)}</span> <span class="akten-type-chip akten-type-${esc(c.type)}">${esc(t("projekte.type." + c.type) || c.type)}</span>
<span class="projekt-child-title">${esc(c.title)}</span> <span class="projekt-child-title">${esc(c.title)}</span>
${c.reference ? `<span class="projekt-child-ref">${esc(c.reference)}</span>` : ""} ${c.reference ? `<span class="projekt-child-ref">${esc(c.reference)}</span>` : ""}
@@ -974,16 +974,16 @@ function renderChildren() {
function initChildAddLink() { function initChildAddLink() {
const link = document.getElementById("child-add-link") as HTMLAnchorElement | null; const link = document.getElementById("child-add-link") as HTMLAnchorElement | null;
if (!link || !akte) return; if (!link || !project) return;
// Pre-fill parent_id for the create form via query param. // Pre-fill parent_id for the create form via query param.
link.href = `/projekte/neu?parent=${encodeURIComponent(akte.id)}`; link.href = `/projects/neu?parent=${encodeURIComponent(project.id)}`;
} }
// ----- Team tab ----------------------------------------------------------- // ----- Team tab -----------------------------------------------------------
async function loadTeam(id: string) { async function loadTeam(id: string) {
try { try {
const resp = await fetch(`/api/projekte/${id}/team`); const resp = await fetch(`/api/projects/${id}/team`);
if (resp.ok) teamMembers = (await resp.json()) as ProjektTeamMember[]; if (resp.ok) teamMembers = (await resp.json()) as ProjektTeamMember[];
} catch { } catch {
teamMembers = []; teamMembers = [];
@@ -1032,15 +1032,15 @@ function renderTeam() {
body.querySelectorAll<HTMLButtonElement>(".team-remove-btn").forEach((btn) => { body.querySelectorAll<HTMLButtonElement>(".team-remove-btn").forEach((btn) => {
btn.addEventListener("click", async () => { btn.addEventListener("click", async () => {
if (!akte) return; if (!project) return;
const userID = btn.dataset.userId!; const userID = btn.dataset.userId!;
if (!window.confirm(t("projekte.detail.team.confirm_remove") || "Mitglied entfernen?")) return; if (!window.confirm(t("projekte.detail.team.confirm_remove") || "Mitglied entfernen?")) return;
const resp = await fetch( const resp = await fetch(
`/api/projekte/${akte.id}/team/${encodeURIComponent(userID)}`, `/api/projects/${project.id}/team/${encodeURIComponent(userID)}`,
{ method: "DELETE" }, { method: "DELETE" },
); );
if (resp.ok) { if (resp.ok) {
await loadTeam(akte.id); await loadTeam(project.id);
renderTeam(); renderTeam();
} }
}); });
@@ -1112,7 +1112,7 @@ function initTeamForm(id: string) {
msg.textContent = t("projekte.detail.team.error.user_required") || "Benutzer ausw\u00e4hlen"; msg.textContent = t("projekte.detail.team.error.user_required") || "Benutzer ausw\u00e4hlen";
return; return;
} }
const resp = await fetch(`/api/projekte/${id}/team`, { const resp = await fetch(`/api/projects/${id}/team`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: hidden.value, role: role.value }), body: JSON.stringify({ user_id: hidden.value, role: role.value }),
@@ -1141,7 +1141,7 @@ function initNotesContainer(akteID: string) {
const container = document.getElementById("notes-container"); const container = document.getElementById("notes-container");
if (!container) return; if (!container) return;
container.setAttribute("data-parent-id", akteID); container.setAttribute("data-parent-id", akteID);
void initNotes(container as HTMLElement, "akte", akteID); void initNotes(container as HTMLElement, "project", akteID);
notesInited = true; notesInited = true;
} }
@@ -1153,8 +1153,8 @@ document.addEventListener("DOMContentLoaded", () => {
renderBreadcrumb(); renderBreadcrumb();
renderEvents(); renderEvents();
renderParteien(); renderParteien();
renderFristen(); renderDeadlines();
renderTermine(); renderAppointments();
renderChildren(); renderChildren();
renderTeam(); renderTeam();
}); });

View File

@@ -4,14 +4,14 @@ import { initSidebar } from "./sidebar";
// /projekte/neu client. Posts v2 CreateProjektInput shape. // /projekte/neu client. Posts v2 CreateProjektInput shape.
// Fields shown depend on type selection; parent picker shown for non-client types. // Fields shown depend on type selection; parent picker shown for non-client types.
interface ProjektMini { interface ProjectMini {
id: string; id: string;
title: string; title: string;
type: string; type: string;
reference?: string | null; reference?: string | null;
} }
let parentCandidates: ProjektMini[] = []; let parentCandidates: ProjectMini[] = [];
function $(id: string): HTMLElement { function $(id: string): HTMLElement {
const el = document.getElementById(id); const el = document.getElementById(id);
@@ -32,9 +32,9 @@ function showFieldsForType(typeSel: string) {
async function loadParentCandidates() { async function loadParentCandidates() {
try { try {
const resp = await fetch("/api/projekte"); const resp = await fetch("/api/projects");
if (!resp.ok) return; if (!resp.ok) return;
parentCandidates = (await resp.json()) as ProjektMini[]; parentCandidates = (await resp.json()) as ProjectMini[];
} catch { } catch {
// ignore // ignore
} }
@@ -85,7 +85,7 @@ function submitForm() {
msg.textContent = ""; msg.textContent = "";
const type = ($("projekt-type") as HTMLSelectElement).value; const type = ($("projekt-type") as HTMLSelectElement).value;
const title = ($("akte-title") as HTMLInputElement).value.trim(); const title = ($("project-title") as HTMLInputElement).value.trim();
if (!title) { if (!title) {
msg.textContent = t("projekte.error.title_required") || "Title required"; msg.textContent = t("projekte.error.title_required") || "Title required";
return; return;
@@ -94,48 +94,48 @@ function submitForm() {
const payload: Record<string, unknown> = { const payload: Record<string, unknown> = {
type, type,
title, title,
status: ($("akte-status") as HTMLSelectElement).value, status: ($("project-status") as HTMLSelectElement).value,
}; };
const parentID = ($("projekt-parent-id") as HTMLInputElement).value; const parentID = ($("projekt-parent-id") as HTMLInputElement).value;
if (type !== "client" && parentID) payload.parent_id = parentID; if (type !== "client" && parentID) payload.parent_id = parentID;
const ref = ($("akte-ref") as HTMLInputElement).value.trim(); const ref = ($("project-ref") as HTMLInputElement).value.trim();
if (ref) payload.reference = ref; if (ref) payload.reference = ref;
const clientNumber = ($("akte-client-number") as HTMLInputElement).value.trim(); const clientNumber = ($("project-client-number") as HTMLInputElement).value.trim();
if (clientNumber) payload.client_number = clientNumber; if (clientNumber) payload.client_number = clientNumber;
const matterNumber = ($("akte-matter-number") as HTMLInputElement).value.trim(); const matterNumber = ($("project-matter-number") as HTMLInputElement).value.trim();
if (matterNumber) payload.matter_number = matterNumber; if (matterNumber) payload.matter_number = matterNumber;
const netdocs = ($("akte-netdocs") as HTMLInputElement).value.trim(); const netdocs = ($("project-netdocs") as HTMLInputElement).value.trim();
if (netdocs) payload.netdocuments_url = netdocs; if (netdocs) payload.netdocuments_url = netdocs;
if (type === "client") { if (type === "client") {
const ind = ($("akte-industry") as HTMLInputElement).value.trim(); const ind = ($("project-industry") as HTMLInputElement).value.trim();
if (ind) payload.industry = ind; if (ind) payload.industry = ind;
const cty = ($("akte-country") as HTMLInputElement).value.trim(); const cty = ($("project-country") as HTMLInputElement).value.trim();
if (cty) payload.country = cty; if (cty) payload.country = cty;
} }
const desc = ($("akte-description") as HTMLTextAreaElement).value.trim(); const desc = ($("project-description") as HTMLTextAreaElement).value.trim();
if (desc) payload.description = desc; if (desc) payload.description = desc;
if (type === "patent") { if (type === "patent") {
const pat = ($("akte-patent-number") as HTMLInputElement).value.trim(); const pat = ($("project-patent-number") as HTMLInputElement).value.trim();
if (pat) payload.patent_number = pat; if (pat) payload.patent_number = pat;
const fd = ($("akte-filing-date") as HTMLInputElement).value; const fd = ($("project-filing-date") as HTMLInputElement).value;
if (fd) payload.filing_date = fd + "T00:00:00Z"; if (fd) payload.filing_date = fd + "T00:00:00Z";
const gd = ($("akte-grant-date") as HTMLInputElement).value; const gd = ($("project-grant-date") as HTMLInputElement).value;
if (gd) payload.grant_date = gd + "T00:00:00Z"; if (gd) payload.grant_date = gd + "T00:00:00Z";
} }
if (type === "case") { if (type === "case") {
const court = ($("akte-court") as HTMLInputElement).value.trim(); const court = ($("project-court") as HTMLInputElement).value.trim();
if (court) payload.court = court; if (court) payload.court = court;
const cn = ($("akte-case-number") as HTMLInputElement).value.trim(); const cn = ($("project-case-number") as HTMLInputElement).value.trim();
if (cn) payload.case_number = cn; if (cn) payload.case_number = cn;
} }
try { try {
const resp = await fetch("/api/projekte", { const resp = await fetch("/api/projects", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload), body: JSON.stringify(payload),
@@ -146,7 +146,7 @@ function submitForm() {
return; return;
} }
const p = (await resp.json()) as { id: string }; const p = (await resp.json()) as { id: string };
window.location.href = `/projekte/${p.id}`; window.location.href = `/projects/${p.id}`;
} catch (e) { } catch (e) {
msg.textContent = String(e); msg.textContent = String(e);
} }
@@ -164,9 +164,9 @@ async function applyParentFromQueryString() {
const parentID = qs.get("parent"); const parentID = qs.get("parent");
if (!parentID) return; if (!parentID) return;
try { try {
const resp = await fetch(`/api/projekte/${encodeURIComponent(parentID)}`); const resp = await fetch(`/api/projects/${encodeURIComponent(parentID)}`);
if (!resp.ok) return; if (!resp.ok) return;
const p = (await resp.json()) as ProjektMini; const p = (await resp.json()) as ProjectMini;
($("projekt-parent-id") as HTMLInputElement).value = p.id; ($("projekt-parent-id") as HTMLInputElement).value = p.id;
($("projekt-parent-input") as HTMLInputElement).value = p.title; ($("projekt-parent-input") as HTMLInputElement).value = p.title;
// Default to 'case' under a non-root parent; user can override. // Default to 'case' under a non-root parent; user can override.

View File

@@ -1,8 +1,8 @@
import { initI18n, onLangChange, t, getLang } from "./i18n"; import { initI18n, onLangChange, t, getLang } from "./i18n";
import { initSidebar } from "./sidebar"; import { initSidebar } from "./sidebar";
// /projekte list page client. Reads v2 shape from /api/projekte. // /projekte list page client. Reads v2 shape from /api/projects.
interface Projekt { interface Project {
id: string; id: string;
type: string; type: string;
parent_id?: string | null; parent_id?: string | null;
@@ -15,7 +15,7 @@ interface Projekt {
updated_at: string; updated_at: string;
} }
let allRows: Projekt[] = []; let allRows: Project[] = [];
let typeFilter = ""; let typeFilter = "";
let statusFilter = ""; let statusFilter = "";
let viewMode: "flat" | "tree" | "roots" = "flat"; let viewMode: "flat" | "tree" | "roots" = "flat";
@@ -26,7 +26,7 @@ async function loadProjekte() {
const unavailable = document.getElementById("akten-unavailable")!; const unavailable = document.getElementById("akten-unavailable")!;
const table = document.querySelector<HTMLElement>(".akten-table-wrap")!; const table = document.querySelector<HTMLElement>(".akten-table-wrap")!;
try { try {
const resp = await fetch("/api/projekte"); const resp = await fetch("/api/projects");
if (resp.status === 503) { if (resp.status === 503) {
unavailable.style.display = "block"; unavailable.style.display = "block";
table.style.display = "none"; table.style.display = "none";
@@ -47,7 +47,7 @@ async function loadProjekte() {
} }
} }
function getFiltered(): Projekt[] { function getFiltered(): Project[] {
let rows = allRows; let rows = allRows;
if (viewMode === "roots") rows = rows.filter((p) => !p.parent_id); if (viewMode === "roots") rows = rows.filter((p) => !p.parent_id);
if (typeFilter) rows = rows.filter((p) => p.type === typeFilter); if (typeFilter) rows = rows.filter((p) => p.type === typeFilter);
@@ -142,7 +142,7 @@ function render() {
tbody.querySelectorAll<HTMLTableRowElement>(".akten-row").forEach((row) => { tbody.querySelectorAll<HTMLTableRowElement>(".akten-row").forEach((row) => {
row.addEventListener("click", () => { row.addEventListener("click", () => {
const id = row.dataset.id!; const id = row.dataset.id!;
window.location.href = `/projekte/${id}`; window.location.href = `/projects/${id}`;
}); });
}); });
} }

View File

@@ -522,7 +522,7 @@ async function deleteCalDAVConfig() {
// --- Dezernat tab ----------------------------------------------------------- // --- Dezernat tab -----------------------------------------------------------
interface Dezernat { interface Department {
id: string; id: string;
name: string; name: string;
lead_user_id?: string | null; lead_user_id?: string | null;
@@ -555,7 +555,7 @@ async function loadDezernatTab(): Promise<void> {
} }
try { try {
const resp = await fetch("/api/dezernate"); const resp = await fetch("/api/departments");
if (!resp.ok) return; if (!resp.ok) return;
allDezernate = (await resp.json()) as Dezernat[]; allDezernate = (await resp.json()) as Dezernat[];
} catch { } catch {
@@ -601,7 +601,7 @@ async function renderMyDezernat(): Promise<void> {
const myDezernate: Dezernat[] = []; const myDezernate: Dezernat[] = [];
for (const d of allDezernate) { for (const d of allDezernate) {
try { try {
const resp = await fetch(`/api/dezernate/${encodeURIComponent(d.id)}/members`); const resp = await fetch(`/api/departments/${encodeURIComponent(d.id)}/members`);
if (!resp.ok) continue; if (!resp.ok) continue;
const members = (await resp.json()) as DezernatMember[]; const members = (await resp.json()) as DezernatMember[];
if (members.some((m) => m.user_id === me!.id)) { if (members.some((m) => m.user_id === me!.id)) {
@@ -619,7 +619,7 @@ async function renderMyDezernat(): Promise<void> {
const parts: string[] = []; const parts: string[] = [];
for (const d of myDezernate) { for (const d of myDezernate) {
const resp = await fetch(`/api/dezernate/${encodeURIComponent(d.id)}/members`); const resp = await fetch(`/api/departments/${encodeURIComponent(d.id)}/members`);
const members = resp.ok ? ((await resp.json()) as DezernatMember[]) : []; const members = resp.ok ? ((await resp.json()) as DezernatMember[]) : [];
const leadName = members.find((m) => m.user_id === d.lead_user_id)?.display_name; const leadName = members.find((m) => m.user_id === d.lead_user_id)?.display_name;
parts.push(`<div class="dezernat-card"> parts.push(`<div class="dezernat-card">
@@ -695,7 +695,7 @@ function renderDezernatAdminTable(): void {
btn.addEventListener("click", async () => { btn.addEventListener("click", async () => {
const id = btn.dataset.id!; const id = btn.dataset.id!;
if (!window.confirm(t("dezernat.confirm_delete") || "Dezernat wirklich l\u00f6schen?")) return; if (!window.confirm(t("dezernat.confirm_delete") || "Dezernat wirklich l\u00f6schen?")) return;
const resp = await fetch(`/api/dezernate/${encodeURIComponent(id)}`, { method: "DELETE" }); const resp = await fetch(`/api/departments/${encodeURIComponent(id)}`, { method: "DELETE" });
if (resp.ok) { if (resp.ok) {
allDezernate = allDezernate.filter((d) => d.id !== id); allDezernate = allDezernate.filter((d) => d.id !== id);
renderDezernatAdminTable(); renderDezernatAdminTable();
@@ -725,7 +725,7 @@ async function loadAndRenderDezernatMembers(dezernatID: string): Promise<void> {
const ul = document.getElementById(`dezernat-members-${dezernatID}`); const ul = document.getElementById(`dezernat-members-${dezernatID}`);
if (!ul) return; if (!ul) return;
try { try {
const resp = await fetch(`/api/dezernate/${encodeURIComponent(dezernatID)}/members`); const resp = await fetch(`/api/departments/${encodeURIComponent(dezernatID)}/members`);
if (!resp.ok) return; if (!resp.ok) return;
const members = (await resp.json()) as DezernatMember[]; const members = (await resp.json()) as DezernatMember[];
if (!members.length) { if (!members.length) {
@@ -746,7 +746,7 @@ async function loadAndRenderDezernatMembers(dezernatID: string): Promise<void> {
const uid = btn.dataset.user!; const uid = btn.dataset.user!;
if (!window.confirm(t("dezernat.confirm_remove") || "Mitglied entfernen?")) return; if (!window.confirm(t("dezernat.confirm_remove") || "Mitglied entfernen?")) return;
const r = await fetch( const r = await fetch(
`/api/dezernate/${encodeURIComponent(did)}/members/${encodeURIComponent(uid)}`, `/api/departments/${encodeURIComponent(did)}/members/${encodeURIComponent(uid)}`,
{ method: "DELETE" }, { method: "DELETE" },
); );
if (r.ok) { if (r.ok) {
@@ -806,7 +806,7 @@ function wireDezernatAddForm(dezernatID: string): void {
msg.textContent = t("dezernat.error.user_required") || "Benutzer ausw\u00e4hlen"; msg.textContent = t("dezernat.error.user_required") || "Benutzer ausw\u00e4hlen";
return; return;
} }
const resp = await fetch(`/api/dezernate/${encodeURIComponent(dezernatID)}/members`, { const resp = await fetch(`/api/departments/${encodeURIComponent(dezernatID)}/members`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: hidden.value }), body: JSON.stringify({ user_id: hidden.value }),
@@ -839,7 +839,7 @@ async function submitNewDezernat(e: Event): Promise<void> {
return; return;
} }
try { try {
const resp = await fetch("/api/dezernate", { const resp = await fetch("/api/departments", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, office }), body: JSON.stringify({ name, office }),

View File

@@ -25,7 +25,7 @@ interface SidebarProps {
function navItem(href: string, icon: string, i18nKey: string, label: string, currentPath: string): string { function navItem(href: string, icon: string, i18nKey: string, label: string, currentPath: string): string {
// "Active" is true for the item whose href is a prefix of currentPath. // "Active" is true for the item whose href is a prefix of currentPath.
// That way sub-routes like /projekte/{id}/verlauf keep the /projekte entry lit. // That way sub-routes like /projekte/{id}/events keep the /projekte entry lit.
// /akten and /akten/* are kept as legacy aliases and also highlight /projekte. // /akten and /akten/* are kept as legacy aliases and also highlight /projekte.
const active = const active =
href === currentPath || href === currentPath ||
@@ -78,9 +78,9 @@ export function Sidebar({ currentPath }: SidebarProps): string {
)} )}
{group("nav.group.arbeit", "Arbeit", {group("nav.group.arbeit", "Arbeit",
navItem("/projekte", ICON_FOLDER, "nav.projekte", "Projekte", currentPath) + navItem("/projects", ICON_FOLDER, "nav.projekte", "Projekte", currentPath) +
navItem("/fristen", ICON_CLOCK, "nav.fristen", "Fristen", currentPath) + navItem("/deadlines", ICON_CLOCK, "nav.fristen", "Fristen", currentPath) +
navItem("/termine", ICON_CALENDAR, "nav.termine", "Termine", currentPath), navItem("/appointments", ICON_CALENDAR, "nav.termine", "Termine", currentPath),
)} )}
{group("nav.group.werkzeuge", "Werkzeuge", {group("nav.group.werkzeuge", "Werkzeuge",
@@ -90,9 +90,9 @@ export function Sidebar({ currentPath }: SidebarProps): string {
)} )}
{group("nav.group.wissen", "Wissen", {group("nav.group.wissen", "Wissen",
navItem("/checklisten", ICON_CHECK, "nav.checklisten", "Checklisten", currentPath) + navItem("/checklists", ICON_CHECK, "nav.checklisten", "Checklisten", currentPath) +
navItem("/glossar", ICON_BOOK, "nav.glossar", "Glossar", currentPath) + navItem("/glossary", ICON_BOOK, "nav.glossar", "Glossar", currentPath) +
navItem("/gerichte", ICON_BUILDING, "nav.gerichte", "Gerichte", currentPath), navItem("/courts", ICON_BUILDING, "nav.gerichte", "Gerichte", currentPath),
)} )}
{group("nav.group.ressourcen", "Ressourcen", {group("nav.group.ressourcen", "Ressourcen",
@@ -101,7 +101,7 @@ export function Sidebar({ currentPath }: SidebarProps): string {
)} )}
{group("nav.group.einstellungen", "Einstellungen", {group("nav.group.einstellungen", "Einstellungen",
navItem("/einstellungen", ICON_GEAR, "nav.einstellungen", "Einstellungen", currentPath), navItem("/settings", ICON_GEAR, "nav.einstellungen", "Einstellungen", currentPath),
)} )}
</nav> </nav>

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar"; import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export function renderGerichte(): string { export function renderCourts(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -12,7 +12,7 @@ export function renderGerichte(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/gerichte" /> <Sidebar currentPath="/courts" />
<main> <main>
<section className="tool-page"> <section className="tool-page">

View File

@@ -56,19 +56,19 @@ export function renderDashboard(): string {
Fristen auf einen Blick Fristen auf einen Blick
</h2> </h2>
<div className="dashboard-summary-grid"> <div className="dashboard-summary-grid">
<a href="/fristen?status=overdue" className="dashboard-card dashboard-card-red" id="dashboard-card-overdue"> <a href="/deadlines?status=overdue" className="dashboard-card dashboard-card-red" id="dashboard-card-overdue">
<div className="dashboard-card-count" id="dashboard-count-overdue">0</div> <div className="dashboard-card-count" id="dashboard-count-overdue">0</div>
<div className="dashboard-card-label" data-i18n="dashboard.summary.overdue">&Uuml;berf&auml;llig</div> <div className="dashboard-card-label" data-i18n="dashboard.summary.overdue">&Uuml;berf&auml;llig</div>
</a> </a>
<a href="/fristen?status=this_week" className="dashboard-card dashboard-card-amber" id="dashboard-card-thisweek"> <a href="/deadlines?status=this_week" className="dashboard-card dashboard-card-amber" id="dashboard-card-thisweek">
<div className="dashboard-card-count" id="dashboard-count-this-week">0</div> <div className="dashboard-card-count" id="dashboard-count-this-week">0</div>
<div className="dashboard-card-label" data-i18n="dashboard.summary.this_week">Diese Woche</div> <div className="dashboard-card-label" data-i18n="dashboard.summary.this_week">Diese Woche</div>
</a> </a>
<a href="/fristen?status=upcoming" className="dashboard-card dashboard-card-green" id="dashboard-card-upcoming"> <a href="/deadlines?status=upcoming" className="dashboard-card dashboard-card-green" id="dashboard-card-upcoming">
<div className="dashboard-card-count" id="dashboard-count-upcoming">0</div> <div className="dashboard-card-count" id="dashboard-count-upcoming">0</div>
<div className="dashboard-card-label" data-i18n="dashboard.summary.upcoming">Kommend</div> <div className="dashboard-card-label" data-i18n="dashboard.summary.upcoming">Kommend</div>
</a> </a>
<a href="/fristen?status=completed" className="dashboard-card dashboard-card-done" id="dashboard-card-completed"> <a href="/deadlines?status=completed" className="dashboard-card dashboard-card-done" id="dashboard-card-completed">
<div className="dashboard-card-count" id="dashboard-count-completed">0</div> <div className="dashboard-card-count" id="dashboard-count-completed">0</div>
<div className="dashboard-card-label" data-i18n="dashboard.summary.completed">Abgeschlossen (7&#8239;T.)</div> <div className="dashboard-card-label" data-i18n="dashboard.summary.completed">Abgeschlossen (7&#8239;T.)</div>
</a> </a>
@@ -77,7 +77,7 @@ export function renderDashboard(): string {
{/* Matter summary card */} {/* Matter summary card */}
<section className="dashboard-matters"> <section className="dashboard-matters">
<a href="/akten" className="dashboard-matter-card"> <a href="/projects" className="dashboard-matter-card">
<div className="dashboard-matter-header"> <div className="dashboard-matter-header">
<h3 data-i18n="dashboard.matters.heading">Meine Akten</h3> <h3 data-i18n="dashboard.matters.heading">Meine Akten</h3>
<span className="dashboard-matter-arrow" aria-hidden="true">&rarr;</span> <span className="dashboard-matter-arrow" aria-hidden="true">&rarr;</span>

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar"; import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export function renderFristenKalender(): string { export function renderDeadlinesCalendar(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -12,7 +12,7 @@ export function renderFristenKalender(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/fristen" /> <Sidebar currentPath="/deadlines" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
@@ -26,8 +26,8 @@ export function renderFristenKalender(): string {
</p> </p>
</div> </div>
<div className="fristen-header-actions"> <div className="fristen-header-actions">
<a href="/fristen" className="btn-secondary" data-i18n="fristen.kalender.list">Listenansicht</a> <a href="/deadlines" className="btn-secondary" data-i18n="fristen.kalender.list">Listenansicht</a>
<a href="/fristen/neu" className="btn-primary btn-cta-lime" data-i18n="fristen.list.new">Neue Frist</a> <a href="/deadlines/new" className="btn-primary btn-cta-lime" data-i18n="fristen.list.new">Neue Frist</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar"; import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export function renderFristenDetail(): string { export function renderDeadlinesDetail(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -12,12 +12,12 @@ export function renderFristenDetail(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/fristen" /> <Sidebar currentPath="/deadlines" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
<div className="container"> <div className="container">
<a href="/fristen" className="akten-back-link" data-i18n="fristen.detail.back">&larr; Zur&uuml;ck zur Fristen&uuml;bersicht</a> <a href="/deadlines" className="akten-back-link" data-i18n="fristen.detail.back">&larr; Zur&uuml;ck zur Fristen&uuml;bersicht</a>
<div id="frist-loading" className="akten-loading"> <div id="frist-loading" className="akten-loading">
<p data-i18n="fristen.detail.loading">L&auml;dt&hellip;</p> <p data-i18n="fristen.detail.loading">L&auml;dt&hellip;</p>

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar"; import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export function renderFristenNeu(): string { export function renderDeadlinesNew(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -12,13 +12,13 @@ export function renderFristenNeu(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/fristen/neu" /> <Sidebar currentPath="/deadlines/new" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
<div className="container container-narrow"> <div className="container container-narrow">
<div className="tool-header"> <div className="tool-header">
<a href="/fristen" className="akten-back-link" id="frist-neu-back" data-i18n="fristen.neu.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a> <a href="/deadlines" className="akten-back-link" id="frist-neu-back" data-i18n="fristen.neu.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a>
<h1 data-i18n="fristen.neu.heading">Neue Frist anlegen</h1> <h1 data-i18n="fristen.neu.heading">Neue Frist anlegen</h1>
<p className="tool-subtitle" data-i18n="fristen.neu.subtitle"> <p className="tool-subtitle" data-i18n="fristen.neu.subtitle">
Eine persistente Frist an einer Akte. Sichtbar f&uuml;r alle Personen, die die Akte sehen k&ouml;nnen. Eine persistente Frist an einer Akte. Sichtbar f&uuml;r alle Personen, die die Akte sehen k&ouml;nnen.
@@ -69,7 +69,7 @@ export function renderFristenNeu(): string {
<p className="form-msg" id="frist-neu-msg" /> <p className="form-msg" id="frist-neu-msg" />
<div className="form-actions"> <div className="form-actions">
<a href="/fristen" id="frist-neu-cancel" className="btn-cancel" data-i18n="fristen.neu.cancel">Abbrechen</a> <a href="/deadlines" id="frist-neu-cancel" className="btn-cancel" data-i18n="fristen.neu.cancel">Abbrechen</a>
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="fristen.neu.submit">Frist anlegen</button> <button type="submit" className="btn-primary btn-cta-lime" data-i18n="fristen.neu.submit">Frist anlegen</button>
</div> </div>
</form> </form>

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar"; import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export function renderFristen(): string { export function renderDeadlines(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -12,7 +12,7 @@ export function renderFristen(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/fristen" /> <Sidebar currentPath="/deadlines" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
@@ -26,10 +26,10 @@ export function renderFristen(): string {
</p> </p>
</div> </div>
<div className="fristen-header-actions"> <div className="fristen-header-actions">
<a href="/fristen/kalender" className="btn-secondary" data-i18n="fristen.list.calendar"> <a href="/deadlines/calendar" className="btn-secondary" data-i18n="fristen.list.calendar">
Kalenderansicht Kalenderansicht
</a> </a>
<a href="/fristen/neu" className="btn-primary btn-cta-lime" data-i18n="fristen.list.new"> <a href="/deadlines/new" className="btn-primary btn-cta-lime" data-i18n="fristen.list.new">
Neue Frist Neue Frist
</a> </a>
</div> </div>
@@ -105,7 +105,7 @@ export function renderFristen(): string {
<p data-i18n="fristen.empty.hint"> <p data-i18n="fristen.empty.hint">
Sobald Fristen angelegt oder aus dem Fristenrechner &uuml;bernommen werden, erscheinen sie hier. Sobald Fristen angelegt oder aus dem Fristenrechner &uuml;bernommen werden, erscheinen sie hier.
</p> </p>
<a href="/fristen/neu" className="btn-primary btn-cta-lime" data-i18n="fristen.list.new">Neue Frist</a> <a href="/deadlines/new" className="btn-primary btn-cta-lime" data-i18n="fristen.list.new">Neue Frist</a>
</div> </div>
<div className="akten-empty akten-empty-filtered" id="fristen-empty-filtered" style="display:none"> <div className="akten-empty akten-empty-filtered" id="fristen-empty-filtered" style="display:none">

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar"; import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export function renderGlossar(): string { export function renderGlossary(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -12,7 +12,7 @@ export function renderGlossar(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/glossar" /> <Sidebar currentPath="/glossary" />
<main> <main>
<section className="tool-page"> <section className="tool-page">

View File

@@ -75,7 +75,7 @@ export function renderIndex(): string {
<p data-i18n="index.deadline.desc">Berechnung von Verfahrensfristen f&uuml;r UPC-, deutsche und EPA-Verfahren mit Feiertags-Anpassung.</p> <p data-i18n="index.deadline.desc">Berechnung von Verfahrensfristen f&uuml;r UPC-, deutsche und EPA-Verfahren mit Feiertags-Anpassung.</p>
</a> </a>
<a href="/glossar" className="card card-link"> <a href="/glossary" className="card card-link">
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_GLOSSAR }} /> <div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_GLOSSAR }} />
<h2 data-i18n="index.glossar.title">Patentglossar</h2> <h2 data-i18n="index.glossar.title">Patentglossar</h2>
<p data-i18n="index.glossar.desc">Zweisprachiges DE/EN-Glossar der wichtigsten Begriffe im Patentrecht. Durchsuchbar nach Kategorien.</p> <p data-i18n="index.glossar.desc">Zweisprachiges DE/EN-Glossar der wichtigsten Begriffe im Patentrecht. Durchsuchbar nach Kategorien.</p>
@@ -87,13 +87,13 @@ export function renderIndex(): string {
<p data-i18n="index.gebuehren.desc">Interaktive Geb&uuml;hrentabellen f&uuml;r GKG, RVG, UPC, EPA und PatKostG. Streitwert eingeben, Geb&uuml;hr ablesen.</p> <p data-i18n="index.gebuehren.desc">Interaktive Geb&uuml;hrentabellen f&uuml;r GKG, RVG, UPC, EPA und PatKostG. Streitwert eingeben, Geb&uuml;hr ablesen.</p>
</a> </a>
<a href="/checklisten" className="card card-link"> <a href="/checklists" className="card card-link">
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_CHECK }} /> <div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_CHECK }} />
<h2 data-i18n="index.checklisten.title">Checklisten</h2> <h2 data-i18n="index.checklisten.title">Checklisten</h2>
<p data-i18n="index.checklisten.desc">Interaktive Checklisten f&uuml;r UPC-, DE- und EPA-Verfahren. Fortschritt wird lokal gespeichert.</p> <p data-i18n="index.checklisten.desc">Interaktive Checklisten f&uuml;r UPC-, DE- und EPA-Verfahren. Fortschritt wird lokal gespeichert.</p>
</a> </a>
<a href="/gerichte" className="card card-link"> <a href="/courts" className="card card-link">
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_BUILDING }} /> <div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_BUILDING }} />
<h2 data-i18n="index.gerichte.title">Gerichtsverzeichnis</h2> <h2 data-i18n="index.gerichte.title">Gerichtsverzeichnis</h2>
<p data-i18n="index.gerichte.desc">Gerichte, UPC-Kammern und Patent&auml;mter auf einen Blick &mdash; mit Adressen, Einreichungshinweisen und Sprachen.</p> <p data-i18n="index.gerichte.desc">Gerichte, UPC-Kammern und Patent&auml;mter auf einen Blick &mdash; mit Adressen, Einreichungshinweisen und Sprachen.</p>

View File

@@ -6,7 +6,7 @@ import { Footer } from "./components/Footer";
// compatibility; DOM + labels are v2 (reference not aktenzeichen, type chip, // compatibility; DOM + labels are v2 (reference not aktenzeichen, type chip,
// breadcrumb, Team tab with inheritance badges, children section, // breadcrumb, Team tab with inheritance badges, children section,
// ClientMatter + netDocuments display). // ClientMatter + netDocuments display).
export function renderAktenDetail(): string { export function renderProjectsDetail(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -16,12 +16,12 @@ export function renderAktenDetail(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/projekte" /> <Sidebar currentPath="/projects" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
<div className="container"> <div className="container">
<a href="/projekte" className="akten-back-link" data-i18n="projekte.detail.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a> <a href="/projects" className="akten-back-link" data-i18n="projekte.detail.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a>
<nav className="projekt-breadcrumb" id="projekt-breadcrumb" aria-label="Breadcrumb" /> <nav className="projekt-breadcrumb" id="projekt-breadcrumb" aria-label="Breadcrumb" />
@@ -145,7 +145,7 @@ export function renderAktenDetail(): string {
{/* Untergeordnet (children tree) */} {/* Untergeordnet (children tree) */}
<section className="akten-tab-panel" id="tab-kinder" style="display:none"> <section className="akten-tab-panel" id="tab-kinder" style="display:none">
<div className="akten-parteien-controls"> <div className="akten-parteien-controls">
<a id="child-add-link" className="btn-primary btn-cta-lime btn-small" href="/projekte/neu" data-i18n="projekte.detail.kinder.add"> <a id="child-add-link" className="btn-primary btn-cta-lime btn-small" href="/projects/new" data-i18n="projekte.detail.kinder.add">
Untervorhaben anlegen Untervorhaben anlegen
</a> </a>
</div> </div>
@@ -321,7 +321,7 @@ export function renderAktenDetail(): string {
</table> </table>
</div> </div>
<p className="tool-subtitle akten-checklisten-hint" data-i18n="projekte.detail.checklisten.hint"> <p className="tool-subtitle akten-checklisten-hint" data-i18n="projekte.detail.checklisten.hint">
Instanzen werden auf der Vorlagen-Seite unter <a href="/checklisten">Checklisten</a> angelegt. Instanzen werden auf der Vorlagen-Seite unter <a href="/checklists">Checklisten</a> angelegt.
</p> </p>
</section> </section>

View File

@@ -4,7 +4,7 @@ import { Footer } from "./components/Footer";
// "Neues Projekt" form (v2). Rendered at /projekte/neu. Supports five types; // "Neues Projekt" form (v2). Rendered at /projekte/neu. Supports five types;
// fields show/hide based on the selected type via client TS. // fields show/hide based on the selected type via client TS.
export function renderAktenNeu(): string { export function renderProjectsNew(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -14,13 +14,13 @@ export function renderAktenNeu(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/projekte/neu" /> <Sidebar currentPath="/projects/new" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
<div className="container container-narrow"> <div className="container container-narrow">
<div className="tool-header"> <div className="tool-header">
<a href="/projekte" className="akten-back-link" data-i18n="projekte.detail.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a> <a href="/projects" className="akten-back-link" data-i18n="projekte.detail.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a>
<h1 data-i18n="projekte.neu.heading">Neues Projekt anlegen</h1> <h1 data-i18n="projekte.neu.heading">Neues Projekt anlegen</h1>
<p className="tool-subtitle" data-i18n="projekte.neu.subtitle"> <p className="tool-subtitle" data-i18n="projekte.neu.subtitle">
Mandant, Streitsache, Patent, Verfahren oder generisches Projekt &mdash; hierarchisch einordnen. Mandant, Streitsache, Patent, Verfahren oder generisches Projekt &mdash; hierarchisch einordnen.
@@ -176,7 +176,7 @@ export function renderAktenNeu(): string {
<p className="form-msg" id="akten-neu-msg" /> <p className="form-msg" id="akten-neu-msg" />
<div className="form-actions"> <div className="form-actions">
<a href="/projekte" className="btn-cancel" data-i18n="projekte.cancel">Abbrechen</a> <a href="/projects" className="btn-cancel" data-i18n="projekte.cancel">Abbrechen</a>
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="projekte.submit">Projekt anlegen</button> <button type="submit" className="btn-primary btn-cta-lime" data-i18n="projekte.submit">Projekt anlegen</button>
</div> </div>
</form> </form>

View File

@@ -4,7 +4,7 @@ import { Footer } from "./components/Footer";
// Renders the /projekte list page. File + export name stays `Akten` for build // Renders the /projekte list page. File + export name stays `Akten` for build
// pipeline compatibility; labels + data bindings are v2 (t-paliad-024). // pipeline compatibility; labels + data bindings are v2 (t-paliad-024).
export function renderAkten(): string { export function renderProjects(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -14,7 +14,7 @@ export function renderAkten(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/projekte" /> <Sidebar currentPath="/projects" />
<main> <main>
<section className="tool-page"> <section className="tool-page">
@@ -27,7 +27,7 @@ export function renderAkten(): string {
Mandanten, Streitsachen, Patente und F&auml;lle &mdash; hierarchisch organisiert. Mandanten, Streitsachen, Patente und F&auml;lle &mdash; hierarchisch organisiert.
</p> </p>
</div> </div>
<a href="/projekte/neu" className="btn-primary btn-cta-lime" data-i18n="projekte.new"> <a href="/projects/new" className="btn-primary btn-cta-lime" data-i18n="projekte.new">
Neues Projekt Neues Projekt
</a> </a>
</div> </div>
@@ -105,7 +105,7 @@ export function renderAkten(): string {
<p data-i18n="projekte.empty.hint"> <p data-i18n="projekte.empty.hint">
Starten Sie &uuml;ber &bdquo;Neues Projekt&ldquo; &mdash; legen Sie zuerst einen Mandanten an, darunter Streitsachen, Patente und F&auml;lle. Starten Sie &uuml;ber &bdquo;Neues Projekt&ldquo; &mdash; legen Sie zuerst einen Mandanten an, darunter Streitsachen, Patente und F&auml;lle.
</p> </p>
<a href="/projekte/neu" className="btn-primary btn-cta-lime" data-i18n="projekte.new">Neues Projekt</a> <a href="/projects/new" className="btn-primary btn-cta-lime" data-i18n="projekte.new">Neues Projekt</a>
</div> </div>
<div className="akten-empty akten-empty-filtered" id="akten-empty-filtered" style="display:none"> <div className="akten-empty akten-empty-filtered" id="akten-empty-filtered" style="display:none">

View File

@@ -6,7 +6,7 @@ import { Footer } from "./components/Footer";
// — keep the structure additive so future sections (keys, API tokens, etc.) // — keep the structure additive so future sections (keys, API tokens, etc.)
// only need one more <a class="akten-tab"> and one more <section> below. // only need one more <a class="akten-tab"> and one more <section> below.
// Tab switching is client-side from ?tab=<name>; the default tab is profil. // Tab switching is client-side from ?tab=<name>; the default tab is profil.
export function renderEinstellungen(): string { export function renderSettings(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
<html lang="de"> <html lang="de">
<head> <head>
@@ -16,7 +16,7 @@ export function renderEinstellungen(): string {
<link rel="stylesheet" href="/assets/global.css" /> <link rel="stylesheet" href="/assets/global.css" />
</head> </head>
<body className="has-sidebar"> <body className="has-sidebar">
<Sidebar currentPath="/einstellungen" /> <Sidebar currentPath="/settings" />
<main> <main>
<section className="tool-page"> <section className="tool-page">

View File

@@ -12,7 +12,7 @@ func handleAppointmentsListPage(w http.ResponseWriter, r *http.Request) {
} }
func handleAppointmentsNewPage(w http.ResponseWriter, r *http.Request) { func handleAppointmentsNewPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/appointments-neu.html") http.ServeFile(w, r, "dist/appointments-new.html")
} }
func handleAppointmentsDetailPage(w http.ResponseWriter, r *http.Request) { func handleAppointmentsDetailPage(w http.ResponseWriter, r *http.Request) {
@@ -20,7 +20,7 @@ func handleAppointmentsDetailPage(w http.ResponseWriter, r *http.Request) {
} }
func handleAppointmentsCalendarPage(w http.ResponseWriter, r *http.Request) { func handleAppointmentsCalendarPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/appointments-kalender.html") http.ServeFile(w, r, "dist/appointments-calendar.html")
} }
// handleSettingsPage serves the unified settings page with tabs for // handleSettingsPage serves the unified settings page with tabs for

View File

@@ -12,7 +12,7 @@ func handleDeadlinesListPage(w http.ResponseWriter, r *http.Request) {
} }
func handleDeadlinesNewPage(w http.ResponseWriter, r *http.Request) { func handleDeadlinesNewPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/deadlines-neu.html") http.ServeFile(w, r, "dist/deadlines-new.html")
} }
func handleDeadlinesDetailPage(w http.ResponseWriter, r *http.Request) { func handleDeadlinesDetailPage(w http.ResponseWriter, r *http.Request) {
@@ -20,5 +20,5 @@ func handleDeadlinesDetailPage(w http.ResponseWriter, r *http.Request) {
} }
func handleDeadlinesCalendarPage(w http.ResponseWriter, r *http.Request) { func handleDeadlinesCalendarPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/deadlines-kalender.html") http.ServeFile(w, r, "dist/deadlines-calendar.html")
} }

View File

@@ -10,7 +10,7 @@ import (
// Fristenrechner page handler: serves the static HTML. No DB dependency. // Fristenrechner page handler: serves the static HTML. No DB dependency.
func handleFristenrechnerPage(w http.ResponseWriter, r *http.Request) { func handleFristenrechnerPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/deadlinesrechner.html") http.ServeFile(w, r, "dist/fristenrechner.html")
} }
// POST /api/tools/fristenrechner — calculate the UI timeline for a proceeding. // POST /api/tools/fristenrechner — calculate the UI timeline for a proceeding.

View File

@@ -14,13 +14,13 @@ import "net/http"
// a "Coming Soon — Phase X" panel in the client until later phases land. // a "Coming Soon — Phase X" panel in the client until later phases land.
func handleProjectsListPage(w http.ResponseWriter, r *http.Request) { func handleProjectsListPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/akten.html") http.ServeFile(w, r, "dist/projects.html")
} }
func handleProjectsNewPage(w http.ResponseWriter, r *http.Request) { func handleProjectsNewPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/akten-neu.html") http.ServeFile(w, r, "dist/projects-new.html")
} }
func handleProjectsDetailPage(w http.ResponseWriter, r *http.Request) { func handleProjectsDetailPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/akten-detail.html") http.ServeFile(w, r, "dist/projects-detail.html")
} }