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

View File

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

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer";
export function renderTermineDetail(): string {
export function renderAppointmentsDetail(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -12,12 +12,12 @@ export function renderTermineDetail(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/termine" />
<Sidebar currentPath="/appointments" />
<main>
<section className="tool-page">
<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-not-found" style="display:none" className="akten-empty">

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer";
export function renderTermineNeu(): string {
export function renderAppointmentsNew(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -12,13 +12,13 @@ export function renderTermineNeu(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/termine/neu" />
<Sidebar currentPath="/appointments/new" />
<main>
<section className="tool-page">
<div className="container container-narrow">
<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>
<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.
@@ -80,7 +80,7 @@ export function renderTermineNeu(): string {
<p className="form-msg" id="termin-neu-msg" />
<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>
</div>
</form>

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer";
export function renderTermine(): string {
export function renderAppointments(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -12,7 +12,7 @@ export function renderTermine(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/termine" />
<Sidebar currentPath="/appointments" />
<main>
<section className="tool-page">
@@ -26,10 +26,10 @@ export function renderTermine(): string {
</p>
</div>
<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
</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
</a>
</div>
@@ -105,7 +105,7 @@ export function renderTermine(): string {
<p data-i18n="termine.empty.hint">
Sobald Termine angelegt werden, erscheinen sie hier.
</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 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
// the user to /checklisten/instances/{id} where the interactive
// checkboxes live.
export function renderChecklistenDetail(): string {
export function renderChecklistsDetail(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -16,12 +16,12 @@ export function renderChecklistenDetail(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/checklisten" />
<Sidebar currentPath="/checklists" />
<main>
<section className="tool-page">
<div className="container">
<a href="/checklisten" className="checklist-back">
<a href="/checklists" className="checklist-back">
<span className="checklist-back-arrow">&larr;</span>
<span data-i18n="checklisten.back">Zur&uuml;ck zur &Uuml;bersicht</span>
</a>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,8 +26,8 @@ interface UpcomingDeadline {
id: string;
title: string;
due_date: string;
projekt_id: string;
projekt_title: string;
project_id: string;
project_title: string;
projekt_ref: string;
urgency: "overdue" | "today" | "urgent" | "soon";
}
@@ -38,8 +38,8 @@ interface UpcomingAppointment {
start_at: string;
end_at: string | null;
type: string | null;
projekt_id: string | null;
projekt_title: string | null;
project_id: string | null;
project_title: string | null;
projekt_ref: string | null;
}
@@ -47,8 +47,8 @@ interface ActivityEntry {
timestamp: string;
actor_email: string | null;
actor_name: string | null;
projekt_id: string;
projekt_title: string;
project_id: string;
project_title: string;
projekt_ref: string;
action: string | null;
details: string;
@@ -160,10 +160,10 @@ function renderDeadlines(items: UpcomingDeadline[]): void {
const urgencyClass = `dashboard-urgency-${d.urgency}`;
const urgencyLabel = t(`dashboard.urgency.${d.urgency}`);
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">
<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 class="dashboard-list-meta">
<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
? `<span class="dashboard-termin-dot dashboard-termin-${esc(a.type)}" 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 tag = a.projekt_id ? "a" : "div";
const akteLine = a.projekt_ref && a.projekt_title
? `<span class="dashboard-list-ref">${esc(a.projekt_ref)} &middot; ${esc(a.projekt_title)}</span>`
const href = a.project_id ? `/projects/${esc(a.project_id)}/termine` : "#";
const tag = a.project_id ? "a" : "div";
const akteLine = a.projekt_ref && a.project_title
? `<span class="dashboard-list-ref">${esc(a.projekt_ref)} &middot; ${esc(a.project_title)}</span>`
: "";
return `<li class="dashboard-list-item">
<${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-actor">${esc(actor)}</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>
</li>`;

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { initI18n, t } from "./i18n";
import { initSidebar } from "./sidebar";
interface Akte {
interface Project {
id: string;
aktenzeichen: string;
title: string;
@@ -30,19 +30,19 @@ function showError(msg: string) {
}
async function loadAkten() {
const sel = document.getElementById("frist-akte") as HTMLSelectElement;
const hint = document.getElementById("frist-akte-empty-hint")!;
const sel = document.getElementById("frist-project") as HTMLSelectElement;
const hint = document.getElementById("frist-project-empty-hint")!;
try {
const resp = await fetch("/api/akten");
const resp = await fetch("/api/projects");
if (!resp.ok) return;
const akten: Akte[] = await resp.json();
const projects: Project[] = await resp.json();
if (akten.length === 0) {
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;
}
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) {
const isSelected = preselectedAkteID === a.id ? " selected" : "";
@@ -81,8 +81,8 @@ function initBackLinks() {
if (preselectedAkteID) {
const back = document.getElementById("frist-neu-back") as HTMLAnchorElement;
const cancel = document.getElementById("frist-neu-cancel") as HTMLAnchorElement;
back.href = `/projekte/${preselectedAkteID}/fristen`;
cancel.href = `/projekte/${preselectedAkteID}/fristen`;
back.href = `/projects/${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 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 due = (document.getElementById("frist-due") as HTMLInputElement).value;
const ruleID = (document.getElementById("frist-rule") as HTMLSelectElement).value;
@@ -115,7 +115,7 @@ async function submitForm(e: Event) {
if (notes) payload.notes = notes;
try {
const resp = await fetch(`/api/projekte/${encodeURIComponent(akteID)}/fristen`, {
const resp = await fetch(`/api/projects/${encodeURIComponent(akteID)}/fristen`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
@@ -128,9 +128,9 @@ async function submitForm(e: Event) {
}
const created = await resp.json();
if (preselectedAkteID) {
window.location.href = `/projekte/${preselectedAkteID}/fristen`;
window.location.href = `/projects/${preselectedAkteID}/fristen`;
} else {
window.location.href = `/fristen/${created.id}`;
window.location.href = `/deadlines/${created.id}`;
}
} catch {
showError(t("fristen.error.generic"));
@@ -139,14 +139,14 @@ async function submitForm(e: Event) {
}
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);
if (parts[0] === "akten" && parts[1] && parts[2] === "fristen" && parts[3] === "neu") {
preselectedAkteID = parts[1];
}
// Or ?projekt_id= query string
// Or ?project_id= query string
const qp = new URLSearchParams(window.location.search);
const fromQuery = qp.get("projekt_id");
const fromQuery = qp.get("project_id");
if (fromQuery) preselectedAkteID = fromQuery;
}

View File

@@ -1,21 +1,21 @@
import { initI18n, onLangChange, t, getLang } from "./i18n";
import { initSidebar } from "./sidebar";
interface Frist {
interface Deadline {
id: string;
projekt_id: string;
project_id: string;
title: string;
due_date: string;
status: string;
source: string;
rule_id?: string;
projekt_reference: string;
projekt_title: string;
project_reference: string;
project_title: string;
projekt_office: string;
rule_code?: string;
}
interface Akte {
interface Project {
id: string;
aktenzeichen: string;
title: string;
@@ -29,8 +29,8 @@ interface Summary {
total: number;
}
let allFristen: Frist[] = [];
let allAkten: Akte[] = [];
let allDeadlines: Deadline[] = [];
let allProjects: Project[] = [];
let statusFilter = "pending";
let akteFilter = "";
let loadedOK = false;
@@ -41,8 +41,8 @@ function urlParams(): URLSearchParams {
async function loadAkten() {
try {
const resp = await fetch("/api/akten");
if (resp.ok) allAkten = await resp.json();
const resp = await fetch("/api/projects");
if (resp.ok) allProjects = await resp.json();
} catch {
/* non-fatal */
}
@@ -51,8 +51,8 @@ async function loadAkten() {
async function loadSummary() {
try {
const url = akteFilter
? `/api/fristen/summary?projekt_id=${encodeURIComponent(akteFilter)}`
: `/api/fristen/summary`;
? `/api/deadlines/summary?project_id=${encodeURIComponent(akteFilter)}`
: `/api/deadlines/summary`;
const resp = await fetch(url);
if (!resp.ok) return;
const sum: Summary = await resp.json();
@@ -76,8 +76,8 @@ async function loadFristen() {
try {
const params = new URLSearchParams();
if (statusFilter) params.set("status", statusFilter);
if (akteFilter) params.set("projekt_id", akteFilter);
const resp = await fetch(`/api/fristen?${params.toString()}`);
if (akteFilter) params.set("project_id", akteFilter);
const resp = await fetch(`/api/deadlines?${params.toString()}`);
if (resp.status === 503) {
unavailable.style.display = "block";
tableWrap.style.display = "none";
@@ -167,9 +167,9 @@ function render() {
</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-akte">
<a class="akten-ref-link" href="/projekte/${esc(f.projekt_id)}">${esc(f.projekt_reference)}</a>
<span class="frist-akte-title">${esc(f.projekt_title)}</span>
<td class="frist-col-project">
<a class="akten-ref-link" href="/projects/${esc(f.project_id)}">${esc(f.project_reference)}</a>
<span class="frist-project-title">${esc(f.project_title)}</span>
</td>
<td class="frist-col-rule">${ruleLabel}</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
const target = e.target as HTMLElement;
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");
if (cb) {
@@ -191,7 +191,7 @@ function render() {
if (!cb.checked) return;
cb.disabled = true;
try {
const resp = await fetch(`/api/fristen/${id}/complete`, { method: "PATCH" });
const resp = await fetch(`/api/deadlines/${id}/complete`, { method: "PATCH" });
if (resp.ok) {
await Promise.all([loadFristen(), loadSummary()]);
} else {
@@ -209,31 +209,31 @@ function render() {
function initFilters() {
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
const params = urlParams();
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.addEventListener("change", async () => {
statusFilter = status.value;
await Promise.all([loadFristen(), loadSummary()]);
});
akte.addEventListener("change", async () => {
akteFilter = akte.value;
project.addEventListener("change", async () => {
akteFilter = project.value;
await Promise.all([loadFristen(), loadSummary()]);
});
}
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.
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(
`<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[]> {
try {
const resp = await fetch("/api/akten");
const resp = await fetch("/api/projects");
if (!resp.ok) return [];
return (await resp.json()) as AkteOption[];
} catch {
@@ -142,11 +142,11 @@ function ensureSaveModal() {
<button class="modal-close" id="frist-save-modal-close" type="button">&times;</button>
</div>
<div class="form-field">
<label for="frist-save-akte" data-i18n="fristen.save.modal.akte">${escHtml(t("fristen.save.modal.akte"))}</label>
<select id="frist-save-akte"></select>
<label for="frist-save-project" data-i18n="fristen.save.modal.project">${escHtml(t("fristen.save.modal.project"))}</label>
<select id="frist-save-project"></select>
<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>
<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>
</div>
<div class="form-field">
@@ -179,7 +179,7 @@ async function openSaveModal() {
if (!lastResponse) return;
ensureSaveModal();
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 submit = document.getElementById("frist-save-submit") as HTMLButtonElement;
@@ -221,7 +221,7 @@ async function openSaveModal() {
async function submitSave() {
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 submit = document.getElementById("frist-save-submit") as HTMLButtonElement;
const msg = document.getElementById("frist-save-msg")!;
@@ -247,7 +247,7 @@ async function submitSave() {
submit.disabled = true;
try {
const resp = await fetch(`/api/projekte/${encodeURIComponent(akteID)}/fristen/bulk`, {
const resp = await fetch(`/api/projects/${encodeURIComponent(akteID)}/fristen/bulk`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fristen }),
@@ -259,7 +259,7 @@ async function submitSave() {
submit.disabled = false;
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";
// Re-enable after a short delay so user can read it; modal stays open with the link.
setTimeout(() => {
@@ -384,7 +384,7 @@ document.addEventListener("DOMContentLoaded", () => {
// Print button
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");
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>';
async function loadTerms() {
const resp = await fetch("/api/glossar");
const resp = await fetch("/api/glossary");
if (!resp.ok) return;
allTerms = await resp.json();
render();
@@ -162,7 +162,7 @@ async function submitSuggestion(e: Event) {
submitBtn.disabled = true;
try {
const resp = await fetch("/api/glossar/suggest", {
const resp = await fetch("/api/glossary/suggest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),

View File

@@ -10,14 +10,14 @@
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;
projekt_id?: string | null;
frist_id?: string | null;
termin_id?: string | null;
akten_event_id?: string | null;
project_id?: string | null;
deadline_id?: string | null;
appointment_id?: string | null;
project_event_id?: string | null;
content: string;
created_by?: string | null;
created_at: string;
@@ -35,7 +35,7 @@ interface NotesState {
parentType: NotizParentType;
parentId: string;
me: Me | null;
notes: Notiz[];
notes: Note[];
listEl: HTMLElement;
emptyEl: HTMLElement;
formEl: HTMLFormElement;
@@ -47,12 +47,12 @@ interface NotesState {
function baseURL(parentType: NotizParentType, parentId: string): string {
switch (parentType) {
case "akte":
return `/api/projekte/${parentId}/notizen`;
case "project":
return `/api/projects/${parentId}/notizen`;
case "frist":
return `/api/fristen/${parentId}/notizen`;
return `/api/deadlines/${parentId}/notizen`;
case "termin":
return `/api/termine/${parentId}/notizen`;
return `/api/appointments/${parentId}/notizen`;
}
}
@@ -62,7 +62,7 @@ function esc(s: string): string {
return d.innerHTML;
}
function authorLabel(n: Notiz): string {
function authorLabel(n: Note): string {
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;
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 (n.created_by === state.me.id) return true;
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");
card.className = "notiz-card";
card.dataset.id = n.id;
@@ -213,7 +213,7 @@ async function loadNotes(state: NotesState) {
render(state);
return;
}
const data = (await resp.json()) as Notiz[];
const data = (await resp.json()) as Note[];
state.notes = Array.isArray(data) ? data : [];
} catch {
state.notes = [];
@@ -252,7 +252,7 @@ async function submitForm(ev: Event, state: NotesState) {
async function addNote(state: NotesState, content: string) {
// Optimistic add: insert a placeholder, replace with server response on success.
const tempID = `temp-${Date.now()}`;
const placeholder: Notiz = {
const placeholder: Note = {
id: tempID,
content,
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"));
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));
render(state);
} 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.textareaEl.value = n.content;
state.textareaEl.focus();
@@ -329,7 +329,7 @@ async function saveEdit(state: NotesState, id: string, content: string) {
cancelEdit(state);
try {
const resp = await fetch(`/api/notizen/${id}`, {
const resp = await fetch(`/api/notes/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
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"));
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));
render(state);
} 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;
const prev = state.notes;
state.notes = state.notes.filter((x) => x.id !== n.id);
render(state);
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) {
state.notes = prev;
render(state);

View File

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

View File

@@ -4,14 +4,14 @@ import { initSidebar } from "./sidebar";
// /projekte/neu client. Posts v2 CreateProjektInput shape.
// Fields shown depend on type selection; parent picker shown for non-client types.
interface ProjektMini {
interface ProjectMini {
id: string;
title: string;
type: string;
reference?: string | null;
}
let parentCandidates: ProjektMini[] = [];
let parentCandidates: ProjectMini[] = [];
function $(id: string): HTMLElement {
const el = document.getElementById(id);
@@ -32,9 +32,9 @@ function showFieldsForType(typeSel: string) {
async function loadParentCandidates() {
try {
const resp = await fetch("/api/projekte");
const resp = await fetch("/api/projects");
if (!resp.ok) return;
parentCandidates = (await resp.json()) as ProjektMini[];
parentCandidates = (await resp.json()) as ProjectMini[];
} catch {
// ignore
}
@@ -85,7 +85,7 @@ function submitForm() {
msg.textContent = "";
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) {
msg.textContent = t("projekte.error.title_required") || "Title required";
return;
@@ -94,48 +94,48 @@ function submitForm() {
const payload: Record<string, unknown> = {
type,
title,
status: ($("akte-status") as HTMLSelectElement).value,
status: ($("project-status") as HTMLSelectElement).value,
};
const parentID = ($("projekt-parent-id") as HTMLInputElement).value;
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;
const clientNumber = ($("akte-client-number") as HTMLInputElement).value.trim();
const clientNumber = ($("project-client-number") as HTMLInputElement).value.trim();
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;
const netdocs = ($("akte-netdocs") as HTMLInputElement).value.trim();
const netdocs = ($("project-netdocs") as HTMLInputElement).value.trim();
if (netdocs) payload.netdocuments_url = netdocs;
if (type === "client") {
const ind = ($("akte-industry") as HTMLInputElement).value.trim();
const ind = ($("project-industry") as HTMLInputElement).value.trim();
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;
}
const desc = ($("akte-description") as HTMLTextAreaElement).value.trim();
const desc = ($("project-description") as HTMLTextAreaElement).value.trim();
if (desc) payload.description = desc;
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;
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";
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 (type === "case") {
const court = ($("akte-court") as HTMLInputElement).value.trim();
const court = ($("project-court") as HTMLInputElement).value.trim();
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;
}
try {
const resp = await fetch("/api/projekte", {
const resp = await fetch("/api/projects", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
@@ -146,7 +146,7 @@ function submitForm() {
return;
}
const p = (await resp.json()) as { id: string };
window.location.href = `/projekte/${p.id}`;
window.location.href = `/projects/${p.id}`;
} catch (e) {
msg.textContent = String(e);
}
@@ -164,9 +164,9 @@ async function applyParentFromQueryString() {
const parentID = qs.get("parent");
if (!parentID) return;
try {
const resp = await fetch(`/api/projekte/${encodeURIComponent(parentID)}`);
const resp = await fetch(`/api/projects/${encodeURIComponent(parentID)}`);
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-input") as HTMLInputElement).value = p.title;
// 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 { initSidebar } from "./sidebar";
// /projekte list page client. Reads v2 shape from /api/projekte.
interface Projekt {
// /projekte list page client. Reads v2 shape from /api/projects.
interface Project {
id: string;
type: string;
parent_id?: string | null;
@@ -15,7 +15,7 @@ interface Projekt {
updated_at: string;
}
let allRows: Projekt[] = [];
let allRows: Project[] = [];
let typeFilter = "";
let statusFilter = "";
let viewMode: "flat" | "tree" | "roots" = "flat";
@@ -26,7 +26,7 @@ async function loadProjekte() {
const unavailable = document.getElementById("akten-unavailable")!;
const table = document.querySelector<HTMLElement>(".akten-table-wrap")!;
try {
const resp = await fetch("/api/projekte");
const resp = await fetch("/api/projects");
if (resp.status === 503) {
unavailable.style.display = "block";
table.style.display = "none";
@@ -47,7 +47,7 @@ async function loadProjekte() {
}
}
function getFiltered(): Projekt[] {
function getFiltered(): Project[] {
let rows = allRows;
if (viewMode === "roots") rows = rows.filter((p) => !p.parent_id);
if (typeFilter) rows = rows.filter((p) => p.type === typeFilter);
@@ -142,7 +142,7 @@ function render() {
tbody.querySelectorAll<HTMLTableRowElement>(".akten-row").forEach((row) => {
row.addEventListener("click", () => {
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 -----------------------------------------------------------
interface Dezernat {
interface Department {
id: string;
name: string;
lead_user_id?: string | null;
@@ -555,7 +555,7 @@ async function loadDezernatTab(): Promise<void> {
}
try {
const resp = await fetch("/api/dezernate");
const resp = await fetch("/api/departments");
if (!resp.ok) return;
allDezernate = (await resp.json()) as Dezernat[];
} catch {
@@ -601,7 +601,7 @@ async function renderMyDezernat(): Promise<void> {
const myDezernate: Dezernat[] = [];
for (const d of allDezernate) {
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;
const members = (await resp.json()) as DezernatMember[];
if (members.some((m) => m.user_id === me!.id)) {
@@ -619,7 +619,7 @@ async function renderMyDezernat(): Promise<void> {
const parts: string[] = [];
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 leadName = members.find((m) => m.user_id === d.lead_user_id)?.display_name;
parts.push(`<div class="dezernat-card">
@@ -695,7 +695,7 @@ function renderDezernatAdminTable(): void {
btn.addEventListener("click", async () => {
const id = btn.dataset.id!;
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) {
allDezernate = allDezernate.filter((d) => d.id !== id);
renderDezernatAdminTable();
@@ -725,7 +725,7 @@ async function loadAndRenderDezernatMembers(dezernatID: string): Promise<void> {
const ul = document.getElementById(`dezernat-members-${dezernatID}`);
if (!ul) return;
try {
const resp = await fetch(`/api/dezernate/${encodeURIComponent(dezernatID)}/members`);
const resp = await fetch(`/api/departments/${encodeURIComponent(dezernatID)}/members`);
if (!resp.ok) return;
const members = (await resp.json()) as DezernatMember[];
if (!members.length) {
@@ -746,7 +746,7 @@ async function loadAndRenderDezernatMembers(dezernatID: string): Promise<void> {
const uid = btn.dataset.user!;
if (!window.confirm(t("dezernat.confirm_remove") || "Mitglied entfernen?")) return;
const r = await fetch(
`/api/dezernate/${encodeURIComponent(did)}/members/${encodeURIComponent(uid)}`,
`/api/departments/${encodeURIComponent(did)}/members/${encodeURIComponent(uid)}`,
{ method: "DELETE" },
);
if (r.ok) {
@@ -806,7 +806,7 @@ function wireDezernatAddForm(dezernatID: string): void {
msg.textContent = t("dezernat.error.user_required") || "Benutzer ausw\u00e4hlen";
return;
}
const resp = await fetch(`/api/dezernate/${encodeURIComponent(dezernatID)}/members`, {
const resp = await fetch(`/api/departments/${encodeURIComponent(dezernatID)}/members`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: hidden.value }),
@@ -839,7 +839,7 @@ async function submitNewDezernat(e: Event): Promise<void> {
return;
}
try {
const resp = await fetch("/api/dezernate", {
const resp = await fetch("/api/departments", {
method: "POST",
headers: { "Content-Type": "application/json" },
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 {
// "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.
const active =
href === currentPath ||
@@ -78,9 +78,9 @@ export function Sidebar({ currentPath }: SidebarProps): string {
)}
{group("nav.group.arbeit", "Arbeit",
navItem("/projekte", ICON_FOLDER, "nav.projekte", "Projekte", currentPath) +
navItem("/fristen", ICON_CLOCK, "nav.fristen", "Fristen", currentPath) +
navItem("/termine", ICON_CALENDAR, "nav.termine", "Termine", currentPath),
navItem("/projects", ICON_FOLDER, "nav.projekte", "Projekte", currentPath) +
navItem("/deadlines", ICON_CLOCK, "nav.fristen", "Fristen", currentPath) +
navItem("/appointments", ICON_CALENDAR, "nav.termine", "Termine", currentPath),
)}
{group("nav.group.werkzeuge", "Werkzeuge",
@@ -90,9 +90,9 @@ export function Sidebar({ currentPath }: SidebarProps): string {
)}
{group("nav.group.wissen", "Wissen",
navItem("/checklisten", ICON_CHECK, "nav.checklisten", "Checklisten", currentPath) +
navItem("/glossar", ICON_BOOK, "nav.glossar", "Glossar", currentPath) +
navItem("/gerichte", ICON_BUILDING, "nav.gerichte", "Gerichte", currentPath),
navItem("/checklists", ICON_CHECK, "nav.checklisten", "Checklisten", currentPath) +
navItem("/glossary", ICON_BOOK, "nav.glossar", "Glossar", currentPath) +
navItem("/courts", ICON_BUILDING, "nav.gerichte", "Gerichte", currentPath),
)}
{group("nav.group.ressourcen", "Ressourcen",
@@ -101,7 +101,7 @@ export function Sidebar({ currentPath }: SidebarProps): string {
)}
{group("nav.group.einstellungen", "Einstellungen",
navItem("/einstellungen", ICON_GEAR, "nav.einstellungen", "Einstellungen", currentPath),
navItem("/settings", ICON_GEAR, "nav.einstellungen", "Einstellungen", currentPath),
)}
</nav>

View File

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

View File

@@ -56,19 +56,19 @@ export function renderDashboard(): string {
Fristen auf einen Blick
</h2>
<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-label" data-i18n="dashboard.summary.overdue">&Uuml;berf&auml;llig</div>
</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-label" data-i18n="dashboard.summary.this_week">Diese Woche</div>
</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-label" data-i18n="dashboard.summary.upcoming">Kommend</div>
</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-label" data-i18n="dashboard.summary.completed">Abgeschlossen (7&#8239;T.)</div>
</a>
@@ -77,7 +77,7 @@ export function renderDashboard(): string {
{/* Matter summary card */}
<section className="dashboard-matters">
<a href="/akten" className="dashboard-matter-card">
<a href="/projects" className="dashboard-matter-card">
<div className="dashboard-matter-header">
<h3 data-i18n="dashboard.matters.heading">Meine Akten</h3>
<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 { Footer } from "./components/Footer";
export function renderFristenKalender(): string {
export function renderDeadlinesCalendar(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -12,7 +12,7 @@ export function renderFristenKalender(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/fristen" />
<Sidebar currentPath="/deadlines" />
<main>
<section className="tool-page">
@@ -26,8 +26,8 @@ export function renderFristenKalender(): string {
</p>
</div>
<div className="fristen-header-actions">
<a href="/fristen" 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" className="btn-secondary" data-i18n="fristen.kalender.list">Listenansicht</a>
<a href="/deadlines/new" className="btn-primary btn-cta-lime" data-i18n="fristen.list.new">Neue Frist</a>
</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer";
export function renderFristenDetail(): string {
export function renderDeadlinesDetail(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -12,12 +12,12 @@ export function renderFristenDetail(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/fristen" />
<Sidebar currentPath="/deadlines" />
<main>
<section className="tool-page">
<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">
<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 { Footer } from "./components/Footer";
export function renderFristenNeu(): string {
export function renderDeadlinesNew(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -12,13 +12,13 @@ export function renderFristenNeu(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/fristen/neu" />
<Sidebar currentPath="/deadlines/new" />
<main>
<section className="tool-page">
<div className="container container-narrow">
<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>
<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.
@@ -69,7 +69,7 @@ export function renderFristenNeu(): string {
<p className="form-msg" id="frist-neu-msg" />
<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>
</div>
</form>

View File

@@ -2,7 +2,7 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer";
export function renderFristen(): string {
export function renderDeadlines(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -12,7 +12,7 @@ export function renderFristen(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/fristen" />
<Sidebar currentPath="/deadlines" />
<main>
<section className="tool-page">
@@ -26,10 +26,10 @@ export function renderFristen(): string {
</p>
</div>
<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
</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
</a>
</div>
@@ -105,7 +105,7 @@ export function renderFristen(): string {
<p data-i18n="fristen.empty.hint">
Sobald Fristen angelegt oder aus dem Fristenrechner &uuml;bernommen werden, erscheinen sie hier.
</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 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 { Footer } from "./components/Footer";
export function renderGlossar(): string {
export function renderGlossary(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -12,7 +12,7 @@ export function renderGlossar(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/glossar" />
<Sidebar currentPath="/glossary" />
<main>
<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>
</a>
<a href="/glossar" className="card card-link">
<a href="/glossary" className="card card-link">
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_GLOSSAR }} />
<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>
@@ -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>
</a>
<a href="/checklisten" className="card card-link">
<a href="/checklists" className="card card-link">
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_CHECK }} />
<h2 data-i18n="index.checklisten.title">Checklisten</h2>
<p data-i18n="index.checklisten.desc">Interaktive Checklisten f&uuml;r UPC-, DE- und EPA-Verfahren. Fortschritt wird lokal gespeichert.</p>
</a>
<a href="/gerichte" className="card card-link">
<a href="/courts" className="card card-link">
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_BUILDING }} />
<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>

View File

@@ -6,7 +6,7 @@ import { Footer } from "./components/Footer";
// compatibility; DOM + labels are v2 (reference not aktenzeichen, type chip,
// breadcrumb, Team tab with inheritance badges, children section,
// ClientMatter + netDocuments display).
export function renderAktenDetail(): string {
export function renderProjectsDetail(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -16,12 +16,12 @@ export function renderAktenDetail(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/projekte" />
<Sidebar currentPath="/projects" />
<main>
<section className="tool-page">
<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" />
@@ -145,7 +145,7 @@ export function renderAktenDetail(): string {
{/* Untergeordnet (children tree) */}
<section className="akten-tab-panel" id="tab-kinder" style="display:none">
<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
</a>
</div>
@@ -321,7 +321,7 @@ export function renderAktenDetail(): string {
</table>
</div>
<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>
</section>

View File

@@ -4,7 +4,7 @@ import { Footer } from "./components/Footer";
// "Neues Projekt" form (v2). Rendered at /projekte/neu. Supports five types;
// fields show/hide based on the selected type via client TS.
export function renderAktenNeu(): string {
export function renderProjectsNew(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -14,13 +14,13 @@ export function renderAktenNeu(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/projekte/neu" />
<Sidebar currentPath="/projects/new" />
<main>
<section className="tool-page">
<div className="container container-narrow">
<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>
<p className="tool-subtitle" data-i18n="projekte.neu.subtitle">
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" />
<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>
</div>
</form>

View File

@@ -4,7 +4,7 @@ import { Footer } from "./components/Footer";
// Renders the /projekte list page. File + export name stays `Akten` for build
// pipeline compatibility; labels + data bindings are v2 (t-paliad-024).
export function renderAkten(): string {
export function renderProjects(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -14,7 +14,7 @@ export function renderAkten(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/projekte" />
<Sidebar currentPath="/projects" />
<main>
<section className="tool-page">
@@ -27,7 +27,7 @@ export function renderAkten(): string {
Mandanten, Streitsachen, Patente und F&auml;lle &mdash; hierarchisch organisiert.
</p>
</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
</a>
</div>
@@ -105,7 +105,7 @@ export function renderAkten(): string {
<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.
</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 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.)
// 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.
export function renderEinstellungen(): string {
export function renderSettings(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
@@ -16,7 +16,7 @@ export function renderEinstellungen(): string {
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/einstellungen" />
<Sidebar currentPath="/settings" />
<main>
<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) {
http.ServeFile(w, r, "dist/appointments-neu.html")
http.ServeFile(w, r, "dist/appointments-new.html")
}
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) {
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

View File

@@ -12,7 +12,7 @@ func handleDeadlinesListPage(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) {
@@ -20,5 +20,5 @@ func handleDeadlinesDetailPage(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.
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.

View File

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