feat: frontend v2 — Projekte list/create, dashboard + downstream field renames
- akten.tsx + client/akten.ts rewritten for v2: renders /projekte list with
type filter (client/litigation/patent/case/project), status filter, flat
vs roots view toggle, search across title/reference/client_number/
matter_number. Columns now Title / Type / Reference / ClientMatter /
Status / Updated (no more office column per v2 team-based visibility).
- akten-neu.tsx + client/akten-neu.ts rewritten: type selector drives
conditional fields (client industry/country/billing; patent number +
filing/grant dates; case court + case_number). Parent projekt picker
(typeahead over /api/projekte, stores parent_id). ClientMatter
client_number + matter_number (7-digit patterns) + netdocuments_url
fields on every type.
- dashboard.ts field renames: akte_id → projekt_id, akte_title →
projekt_title, akte_ref → projekt_ref. Activity/deadline/appointment
links now point to /projekte/{id}.
- Mass field rename across fristen-*, termine-*, checklisten-*,
fristenrechner.ts, notizen.ts, akten-detail.ts: akte_id → projekt_id,
akte_aktenzeichen → projekt_reference, akte_title → projekt_title,
akte_office → projekt_office. URL paths /akten/${...} → /projekte/${...}.
Pages still referencing deprecated shape (owning_office, collaborators,
firm_wide_visible) render blank for those columns — acceptable during
transition, full akten-detail rewrite (add breadcrumb + Team tab with
inheritance) still pending.
go build/vet/test + bun run build all clean.
This commit is contained in:
@@ -2,124 +2,181 @@ import { h } from "./jsx";
|
||||
import { Sidebar } from "./components/Sidebar";
|
||||
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 {
|
||||
return "<!DOCTYPE html>" + (
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title data-i18n="akten.neu.title">Neue Akte — Paliad</title>
|
||||
<title data-i18n="projekte.neu.title">Neues Projekt — Paliad</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
<body className="has-sidebar">
|
||||
<Sidebar currentPath="/akten/neu" />
|
||||
<Sidebar currentPath="/projekte/neu" />
|
||||
|
||||
<main>
|
||||
<section className="tool-page">
|
||||
<div className="container container-narrow">
|
||||
<div className="tool-header">
|
||||
<a href="/akten" className="akten-back-link" data-i18n="akten.detail.back">← Zurück zur Übersicht</a>
|
||||
<h1 data-i18n="akten.neu.heading">Neue Akte anlegen</h1>
|
||||
<p className="tool-subtitle" data-i18n="akten.neu.subtitle">
|
||||
Anlegen eines neuen Mandats im eigenen Büro. Sichtbarkeit folgt der Büro-Regel;
|
||||
Partner können firmenweite Sichtbarkeit aktivieren.
|
||||
<a href="/projekte" className="akten-back-link" data-i18n="projekte.detail.back">← Zurück zur Ü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 — hierarchisch einordnen.
|
||||
Sichtbarkeit folgt dem Team (Sie werden als „Lead“ automatisch hinzugefügt).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form id="akten-neu-form" className="akten-form" autocomplete="off">
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-title" data-i18n="akten.field.title">Titel</label>
|
||||
<label htmlFor="projekt-type" data-i18n="projekte.field.type">Typ</label>
|
||||
<select id="projekt-type" required>
|
||||
<option value="client" data-i18n="projekte.type.client">Mandant (Wurzel)</option>
|
||||
<option value="litigation" data-i18n="projekte.type.litigation">Streitsache</option>
|
||||
<option value="patent" data-i18n="projekte.type.patent">Patent</option>
|
||||
<option value="case" data-i18n="projekte.type.case">Verfahren</option>
|
||||
<option value="project" data-i18n="projekte.type.project">Projekt (generisch)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-field" id="projekt-parent-wrap" style="display:none">
|
||||
<label htmlFor="projekt-parent-input" data-i18n="projekte.field.parent">Übergeordnetes Projekt</label>
|
||||
<input
|
||||
type="text"
|
||||
id="projekt-parent-input"
|
||||
placeholder="Titel eingeben, um ein Überprojekt zu suchen..."
|
||||
data-i18n-placeholder="projekte.field.parent.placeholder"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<input type="hidden" id="projekt-parent-id" />
|
||||
<div id="projekt-parent-suggestions" className="akten-collab-suggestions" />
|
||||
<p className="form-hint" data-i18n="projekte.field.parent.hint">
|
||||
Leer lassen für ein Wurzel-Projekt (typisch: Mandant).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-title" data-i18n="projekte.field.title">Titel</label>
|
||||
<input
|
||||
type="text"
|
||||
id="akte-title"
|
||||
required
|
||||
placeholder="Kurzbezeichnung des Mandats"
|
||||
data-i18n-placeholder="akten.field.title.placeholder"
|
||||
placeholder="z.B. Siemens AG | Siemens v. Huawei | EP 1 234 567"
|
||||
data-i18n-placeholder="projekte.field.title.placeholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-ref" data-i18n="akten.field.ref">Aktenzeichen</label>
|
||||
<label htmlFor="akte-ref" data-i18n="projekte.field.reference">Interne Referenz (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="akte-ref"
|
||||
required
|
||||
placeholder="z.B. HL-2026-0042"
|
||||
data-i18n-placeholder="akten.field.ref.placeholder"
|
||||
data-i18n-placeholder="projekte.field.reference.placeholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-office" data-i18n="akten.field.office">Federführendes Büro</label>
|
||||
<select id="akte-office" required />
|
||||
{/* Options populated from /api/offices at init. */}
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-status" data-i18n="akten.field.status">Status</label>
|
||||
<select id="akte-status">
|
||||
<option value="active" data-i18n="akten.status.active">Aktiv</option>
|
||||
<option value="completed" data-i18n="akten.status.completed">Abgeschlossen</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-court" data-i18n="akten.field.court">Gericht (optional)</label>
|
||||
<input type="text" id="akte-court" />
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-courtref" data-i18n="akten.field.courtRef">Gerichtsaktenzeichen (optional)</label>
|
||||
<input type="text" id="akte-courtref" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-type" data-i18n="akten.field.akteType">Verfahrensart (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="akte-type"
|
||||
placeholder="UPC Infringement, BPatG Nichtigkeit, EPA Opposition..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-field" id="firm-wide-wrap" style="display:none">
|
||||
<label className="form-checkbox">
|
||||
<input type="checkbox" id="akte-firmwide" />
|
||||
<span data-i18n="akten.field.firmWide">Firmenweit sichtbar</span>
|
||||
</label>
|
||||
<p className="form-hint" data-i18n="akten.field.firmWide.hint">
|
||||
Wenn aktiviert, sehen alle Lawyer diese Akte. Nur für Partner/Admin.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-collab-input" data-i18n="akten.field.collaborators">
|
||||
Weitere Bearbeiter (optional)
|
||||
</label>
|
||||
<div className="akten-collab">
|
||||
<div id="akte-collab-list" className="akten-collab-chips" />
|
||||
<label htmlFor="akte-client-number" data-i18n="projekte.field.client_number">Client-Nr. (7 Ziffern)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="akte-collab-input"
|
||||
placeholder="Name oder E-Mail tippen..."
|
||||
data-i18n-placeholder="akten.field.collaborators.placeholder"
|
||||
autocomplete="off"
|
||||
id="akte-client-number"
|
||||
pattern="[0-9]{7}"
|
||||
maxLength={7}
|
||||
placeholder="0001234"
|
||||
/>
|
||||
<div id="akte-collab-suggestions" className="akten-collab-suggestions" />
|
||||
</div>
|
||||
<p className="form-hint" data-i18n="akten.field.collaborators.hint">
|
||||
Personen, die auch Zugriff erhalten sollen (auch büroübergreifend).
|
||||
</p>
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-matter-number" data-i18n="projekte.field.matter_number">Matter-Nr. (7 Ziffern)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="akte-matter-number"
|
||||
pattern="[0-9]{7}"
|
||||
maxLength={7}
|
||||
placeholder="0000567"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="form-hint" data-i18n="projekte.field.clientmatter.hint">
|
||||
HLC-Billing-Nummern. Format CCCCCCC.MMMMMMM. Client-Nr. wird an Unterprojekte vererbt
|
||||
(überschreibbar).
|
||||
</p>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-netdocs" data-i18n="projekte.field.netdocuments_url">netDocuments-URL (optional)</label>
|
||||
<input
|
||||
type="url"
|
||||
id="akte-netdocs"
|
||||
placeholder="https://netdocs.hoganlovells.com/..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Client-specific */}
|
||||
<div className="projekt-fields projekt-fields-client" id="fields-client" style="display:none">
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-industry" data-i18n="projekte.field.industry">Branche</label>
|
||||
<input type="text" id="akte-industry" placeholder="z.B. industrial" />
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-country" data-i18n="projekte.field.country">Land (ISO-2)</label>
|
||||
<input type="text" id="akte-country" maxLength={2} placeholder="DE" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-billing" data-i18n="projekte.field.billing_reference">Billing-Referenz</label>
|
||||
<input type="text" id="akte-billing" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Patent-specific */}
|
||||
<div className="projekt-fields projekt-fields-patent" id="fields-patent" style="display:none">
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-patent-number" data-i18n="projekte.field.patent_number">Patentnummer</label>
|
||||
<input type="text" id="akte-patent-number" placeholder="EP 1 234 567" />
|
||||
</div>
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-filing-date" data-i18n="projekte.field.filing_date">Anmeldetag</label>
|
||||
<input type="date" id="akte-filing-date" />
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-grant-date" data-i18n="projekte.field.grant_date">Erteilungstag</label>
|
||||
<input type="date" id="akte-grant-date" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Case-specific */}
|
||||
<div className="projekt-fields projekt-fields-case" id="fields-case" style="display:none">
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-court" data-i18n="projekte.field.court">Gericht</label>
|
||||
<input type="text" id="akte-court" placeholder="UPC_CFI_Munich" />
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-case-number" data-i18n="projekte.field.case_number">Aktenzeichen (Gericht)</label>
|
||||
<input type="text" id="akte-case-number" placeholder="UPC_CFI_123/2026" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="akte-status" data-i18n="projekte.field.status">Status</label>
|
||||
<select id="akte-status">
|
||||
<option value="active" data-i18n="projekte.filter.status.active">Aktiv</option>
|
||||
<option value="closed" data-i18n="projekte.filter.status.closed">Abgeschlossen</option>
|
||||
<option value="archived" data-i18n="projekte.filter.status.archived">Archiviert</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p className="form-msg" id="akten-neu-msg" />
|
||||
|
||||
<div className="form-actions">
|
||||
<a href="/akten" className="btn-cancel" data-i18n="akten.cancel">Abbrechen</a>
|
||||
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="akten.submit">Akte anlegen</button>
|
||||
<a href="/projekte" 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>
|
||||
</div>
|
||||
|
||||
@@ -2,17 +2,19 @@ import { h } from "./jsx";
|
||||
import { Sidebar } from "./components/Sidebar";
|
||||
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 {
|
||||
return "<!DOCTYPE html>" + (
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title data-i18n="akten.title">Akten — Paliad</title>
|
||||
<title data-i18n="projekte.title">Projekte — Paliad</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
<body className="has-sidebar">
|
||||
<Sidebar currentPath="/akten" />
|
||||
<Sidebar currentPath="/projekte" />
|
||||
|
||||
<main>
|
||||
<section className="tool-page">
|
||||
@@ -20,13 +22,13 @@ export function renderAkten(): string {
|
||||
<div className="tool-header">
|
||||
<div className="akten-header-row">
|
||||
<div>
|
||||
<h1 data-i18n="akten.heading">Akten</h1>
|
||||
<p className="tool-subtitle" data-i18n="akten.subtitle">
|
||||
Büro-bezogene Mandate. Verlauf, Parteien und (bald) Fristen & Termine an einem Ort.
|
||||
<h1 data-i18n="projekte.heading">Projekte</h1>
|
||||
<p className="tool-subtitle" data-i18n="projekte.subtitle">
|
||||
Mandanten, Streitsachen, Patente und Fälle — hierarchisch organisiert.
|
||||
</p>
|
||||
</div>
|
||||
<a href="/akten/neu" className="btn-primary btn-cta-lime" data-i18n="akten.new">
|
||||
Neue Akte
|
||||
<a href="/projekte/neu" className="btn-primary btn-cta-lime" data-i18n="projekte.new">
|
||||
Neues Projekt
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,39 +43,43 @@ export function renderAkten(): string {
|
||||
type="text"
|
||||
id="akten-search"
|
||||
className="glossar-search"
|
||||
placeholder="Titel oder Aktenzeichen suchen..."
|
||||
data-i18n-placeholder="akten.search.placeholder"
|
||||
placeholder="Titel, Referenz oder ClientMatter..."
|
||||
data-i18n-placeholder="projekte.search.placeholder"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<span className="glossar-count" id="akten-count" />
|
||||
</div>
|
||||
|
||||
<div className="akten-filter-row">
|
||||
<label className="akten-filter-label" htmlFor="akten-office" data-i18n="akten.filter.office">Büro</label>
|
||||
<select id="akten-office" className="akten-select">
|
||||
<option value="" data-i18n="akten.filter.office.all">Alle Büros</option>
|
||||
<option value="munich" data-i18n="office.munich">München</option>
|
||||
<option value="duesseldorf" data-i18n="office.duesseldorf">Düsseldorf</option>
|
||||
<option value="hamburg" data-i18n="office.hamburg">Hamburg</option>
|
||||
<option value="amsterdam" data-i18n="office.amsterdam">Amsterdam</option>
|
||||
<option value="london" data-i18n="office.london">London</option>
|
||||
<option value="paris" data-i18n="office.paris">Paris</option>
|
||||
<option value="milan" data-i18n="office.milan">Mailand</option>
|
||||
<label className="akten-filter-label" htmlFor="projekt-type" data-i18n="projekte.filter.type">Typ</label>
|
||||
<select id="projekt-type" className="akten-select">
|
||||
<option value="" data-i18n="projekte.filter.type.all">Alle Typen</option>
|
||||
<option value="client" data-i18n="projekte.type.client">Mandant</option>
|
||||
<option value="litigation" data-i18n="projekte.type.litigation">Streitsache</option>
|
||||
<option value="patent" data-i18n="projekte.type.patent">Patent</option>
|
||||
<option value="case" data-i18n="projekte.type.case">Verfahren</option>
|
||||
<option value="project" data-i18n="projekte.type.project">Projekt</option>
|
||||
</select>
|
||||
|
||||
<label className="akten-filter-label" htmlFor="akten-status" data-i18n="akten.filter.status">Status</label>
|
||||
<label className="akten-filter-label" htmlFor="akten-status" data-i18n="projekte.filter.status">Status</label>
|
||||
<select id="akten-status" className="akten-select">
|
||||
<option value="" data-i18n="akten.filter.status.all">Alle Status</option>
|
||||
<option value="active" data-i18n="akten.filter.status.active">Aktiv</option>
|
||||
<option value="completed" data-i18n="akten.filter.status.completed">Abgeschlossen</option>
|
||||
<option value="archived" data-i18n="akten.filter.status.archived">Archiviert</option>
|
||||
<option value="" data-i18n="projekte.filter.status.all">Alle Status</option>
|
||||
<option value="active" data-i18n="projekte.filter.status.active">Aktiv</option>
|
||||
<option value="archived" data-i18n="projekte.filter.status.archived">Archiviert</option>
|
||||
<option value="closed" data-i18n="projekte.filter.status.closed">Abgeschlossen</option>
|
||||
</select>
|
||||
|
||||
<label className="akten-filter-label" htmlFor="projekt-view" data-i18n="projekte.filter.view">Ansicht</label>
|
||||
<select id="projekt-view" className="akten-select">
|
||||
<option value="flat" data-i18n="projekte.view.flat">Flache Liste</option>
|
||||
<option value="roots" data-i18n="projekte.view.roots">Nur Mandanten</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="akten-unavailable" className="akten-unavailable" style="display:none">
|
||||
<p data-i18n="akten.unavailable">
|
||||
Aktenverwaltung zurzeit nicht verfügbar — bitte Administrator kontaktieren.
|
||||
<p data-i18n="projekte.unavailable">
|
||||
Projektverwaltung zurzeit nicht verfügbar — bitte Administrator kontaktieren.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -81,11 +87,12 @@ export function renderAkten(): string {
|
||||
<table className="akten-table" id="akten-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="akten.col.title">Titel</th>
|
||||
<th data-i18n="akten.col.ref">Aktenzeichen</th>
|
||||
<th data-i18n="akten.col.office">Büro</th>
|
||||
<th data-i18n="akten.col.status">Status</th>
|
||||
<th data-i18n="akten.col.updated">Zuletzt geändert</th>
|
||||
<th data-i18n="projekte.col.title">Titel</th>
|
||||
<th data-i18n="projekte.col.type">Typ</th>
|
||||
<th data-i18n="projekte.col.reference">Referenz</th>
|
||||
<th data-i18n="projekte.col.clientmatter">ClientMatter</th>
|
||||
<th data-i18n="projekte.col.status">Status</th>
|
||||
<th data-i18n="projekte.col.updated">Zuletzt geändert</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="akten-body" />
|
||||
@@ -93,15 +100,15 @@ export function renderAkten(): string {
|
||||
</div>
|
||||
|
||||
<div className="akten-empty" id="akten-empty" style="display:none">
|
||||
<h2 data-i18n="akten.empty.title">Noch keine Akte angelegt</h2>
|
||||
<p data-i18n="akten.empty.hint">
|
||||
Starten Sie über „Neue Akte“ — Sie sehen hier später Ihre Mandate, nach Büro gefiltert.
|
||||
<h2 data-i18n="projekte.empty.title">Noch kein Projekt angelegt</h2>
|
||||
<p data-i18n="projekte.empty.hint">
|
||||
Starten Sie über „Neues Projekt“ — legen Sie zuerst einen Mandanten an, darunter Streitsachen, Patente und Fälle.
|
||||
</p>
|
||||
<a href="/akten/neu" className="btn-primary btn-cta-lime" data-i18n="akten.new">Neue Akte</a>
|
||||
<a href="/projekte/neu" 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">
|
||||
<p data-i18n="akten.empty.filtered">Keine Treffer für diese Filter.</p>
|
||||
<p data-i18n="projekte.empty.filtered">Keine Treffer für diese Filter.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -16,7 +16,7 @@ interface Akte {
|
||||
|
||||
interface Partei {
|
||||
id: string;
|
||||
akte_id: string;
|
||||
projekt_id: string;
|
||||
name: string;
|
||||
role?: string;
|
||||
representative?: string;
|
||||
@@ -24,7 +24,7 @@ interface Partei {
|
||||
|
||||
interface AkteEvent {
|
||||
id: string;
|
||||
akte_id: string;
|
||||
projekt_id: string;
|
||||
event_type?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
@@ -34,7 +34,7 @@ interface AkteEvent {
|
||||
|
||||
interface Frist {
|
||||
id: string;
|
||||
akte_id: string;
|
||||
projekt_id: string;
|
||||
title: string;
|
||||
due_date: string;
|
||||
status: string;
|
||||
@@ -43,7 +43,7 @@ interface Frist {
|
||||
|
||||
interface Termin {
|
||||
id: string;
|
||||
akte_id?: string;
|
||||
projekt_id?: string;
|
||||
title: string;
|
||||
start_at: string;
|
||||
end_at?: string;
|
||||
@@ -114,7 +114,7 @@ async function loadMe() {
|
||||
|
||||
async function loadAkte(id: string): Promise<boolean> {
|
||||
try {
|
||||
const resp = await fetch(`/api/akten/${id}`);
|
||||
const resp = await fetch(`/api/projekte/${id}`);
|
||||
if (!resp.ok) return false;
|
||||
akte = await resp.json();
|
||||
return true;
|
||||
@@ -125,7 +125,7 @@ async function loadAkte(id: string): Promise<boolean> {
|
||||
|
||||
async function loadParteien(id: string) {
|
||||
try {
|
||||
const resp = await fetch(`/api/akten/${id}/parteien`);
|
||||
const resp = await fetch(`/api/projekte/${id}/parteien`);
|
||||
if (resp.ok) parteien = await resp.json();
|
||||
} catch {
|
||||
parteien = [];
|
||||
@@ -134,7 +134,7 @@ async function loadParteien(id: string) {
|
||||
|
||||
async function loadEvents(id: string) {
|
||||
try {
|
||||
const resp = await fetch(`/api/akten/${id}/events?limit=${EVENTS_PAGE_SIZE}`);
|
||||
const resp = await fetch(`/api/projekte/${id}/events?limit=${EVENTS_PAGE_SIZE}`);
|
||||
if (resp.ok) {
|
||||
events = await resp.json();
|
||||
eventsHasMore = events.length === EVENTS_PAGE_SIZE;
|
||||
@@ -159,7 +159,7 @@ async function loadMoreEvents(id: string) {
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`/api/akten/${id}/events?before=${encodeURIComponent(cursor)}&limit=${EVENTS_PAGE_SIZE}`,
|
||||
`/api/projekte/${id}/events?before=${encodeURIComponent(cursor)}&limit=${EVENTS_PAGE_SIZE}`,
|
||||
);
|
||||
if (resp.ok) {
|
||||
const page: AkteEvent[] = await resp.json();
|
||||
@@ -180,7 +180,7 @@ async function loadMoreEvents(id: string) {
|
||||
|
||||
async function loadFristen(id: string) {
|
||||
try {
|
||||
const resp = await fetch(`/api/akten/${id}/fristen`);
|
||||
const resp = await fetch(`/api/projekte/${id}/fristen`);
|
||||
if (resp.ok) fristen = await resp.json();
|
||||
} catch {
|
||||
fristen = [];
|
||||
@@ -189,7 +189,7 @@ async function loadFristen(id: string) {
|
||||
|
||||
async function loadTermine(id: string) {
|
||||
try {
|
||||
const resp = await fetch(`/api/akten/${id}/termine`);
|
||||
const resp = await fetch(`/api/projekte/${id}/termine`);
|
||||
if (resp.ok) termine = await resp.json();
|
||||
} catch {
|
||||
termine = [];
|
||||
@@ -275,7 +275,7 @@ function initAkteTerminForm() {
|
||||
if (!title || !start) return;
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
akte_id: akte.id,
|
||||
projekt_id: akte.id,
|
||||
title,
|
||||
start_at: new Date(start).toISOString(),
|
||||
};
|
||||
@@ -533,7 +533,7 @@ function showTab(tab: TabId) {
|
||||
});
|
||||
// Deep-link via pushState so sub-routes stay shareable.
|
||||
if (akte) {
|
||||
const newPath = `/akten/${akte.id}/${tab}`;
|
||||
const newPath = `/projekte/${akte.id}/${tab}`;
|
||||
if (window.location.pathname !== newPath) {
|
||||
window.history.replaceState({}, "", newPath);
|
||||
}
|
||||
@@ -549,7 +549,7 @@ async function loadAndRenderChecklistInstances(akteID: string) {
|
||||
checklistInstancesInited = true;
|
||||
try {
|
||||
const [instResp, tplResp] = await Promise.all([
|
||||
fetch(`/api/akten/${akteID}/checklisten`),
|
||||
fetch(`/api/projekte/${akteID}/checklisten`),
|
||||
fetch(`/api/checklisten`),
|
||||
]);
|
||||
checklistInstances = instResp.ok ? await instResp.json() : [];
|
||||
@@ -646,7 +646,7 @@ function initTitleEdit() {
|
||||
}
|
||||
saveBtn.disabled = true;
|
||||
try {
|
||||
const resp = await fetch(`/api/akten/${akte.id}`, {
|
||||
const resp = await fetch(`/api/projekte/${akte.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: newTitle }),
|
||||
@@ -706,7 +706,7 @@ function initParteienForm() {
|
||||
if (rep) payload.representative = rep;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/akten/${akte.id}/parteien`, {
|
||||
const resp = await fetch(`/api/projekte/${akte.id}/parteien`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
@@ -736,7 +736,7 @@ function initParteienForm() {
|
||||
function initFristAddLink() {
|
||||
if (!akte) return;
|
||||
const link = document.getElementById("frist-add-link") as HTMLAnchorElement | null;
|
||||
if (link) link.href = `/akten/${akte.id}/fristen/neu`;
|
||||
if (link) link.href = `/projekte/${akte.id}/fristen/neu`;
|
||||
}
|
||||
|
||||
function initDelete() {
|
||||
@@ -760,7 +760,7 @@ function initDelete() {
|
||||
confirmBtn.addEventListener("click", async () => {
|
||||
if (!akte) return;
|
||||
confirmBtn.disabled = true;
|
||||
const resp = await fetch(`/api/akten/${akte.id}`, { method: "DELETE" });
|
||||
const resp = await fetch(`/api/projekte/${akte.id}`, { method: "DELETE" });
|
||||
if (resp.ok) {
|
||||
window.location.href = "/akten";
|
||||
} else {
|
||||
|
||||
@@ -1,167 +1,153 @@
|
||||
import { initI18n, getLang, onLangChange, t } from "./i18n";
|
||||
import { initI18n, t } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
|
||||
interface User {
|
||||
// /projekte/neu client. Posts v2 CreateProjektInput shape.
|
||||
// Fields shown depend on type selection; parent picker shown for non-client types.
|
||||
|
||||
interface ProjektMini {
|
||||
id: string;
|
||||
email: string;
|
||||
display_name: string;
|
||||
office: string;
|
||||
role: string;
|
||||
title: string;
|
||||
type: string;
|
||||
reference?: string | null;
|
||||
}
|
||||
|
||||
interface Me {
|
||||
id: string;
|
||||
email: string;
|
||||
display_name: string;
|
||||
office: string;
|
||||
role: string;
|
||||
let parentCandidates: ProjektMini[] = [];
|
||||
|
||||
function $(id: string): HTMLElement {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) throw new Error("missing element: " + id);
|
||||
return el;
|
||||
}
|
||||
|
||||
interface Office {
|
||||
key: string;
|
||||
label_de: string;
|
||||
label_en: string;
|
||||
function showFieldsForType(typeSel: string) {
|
||||
const parentWrap = $("projekt-parent-wrap") as HTMLDivElement;
|
||||
const clientFields = $("fields-client") as HTMLDivElement;
|
||||
const patentFields = $("fields-patent") as HTMLDivElement;
|
||||
const caseFields = $("fields-case") as HTMLDivElement;
|
||||
clientFields.style.display = typeSel === "client" ? "block" : "none";
|
||||
patentFields.style.display = typeSel === "patent" ? "block" : "none";
|
||||
caseFields.style.display = typeSel === "case" ? "block" : "none";
|
||||
parentWrap.style.display = typeSel === "client" ? "none" : "block";
|
||||
}
|
||||
|
||||
const selectedCollabs = new Map<string, User>();
|
||||
let allUsers: User[] = [];
|
||||
let me: Me | null = null;
|
||||
let offices: Office[] = [];
|
||||
|
||||
async function loadOffices() {
|
||||
async function loadParentCandidates() {
|
||||
try {
|
||||
const resp = await fetch("/api/offices");
|
||||
if (resp.ok) offices = await resp.json();
|
||||
} catch {
|
||||
offices = [];
|
||||
}
|
||||
renderOfficeOptions();
|
||||
}
|
||||
|
||||
function renderOfficeOptions() {
|
||||
const select = document.getElementById("akte-office") as HTMLSelectElement | null;
|
||||
if (!select) return;
|
||||
const isEN = getLang() === "en";
|
||||
const previous = select.value;
|
||||
select.innerHTML = offices
|
||||
.map((o) => {
|
||||
const label = isEN ? o.label_en : o.label_de;
|
||||
return `<option value="${esc(o.key)}">${esc(label)}</option>`;
|
||||
})
|
||||
.join("");
|
||||
if (previous && offices.some((o) => o.key === previous)) {
|
||||
select.value = previous;
|
||||
} else if (me && offices.some((o) => o.key === me!.office)) {
|
||||
select.value = me.office;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMe() {
|
||||
try {
|
||||
const resp = await fetch("/api/me");
|
||||
if (resp.status === 404) {
|
||||
showError(t("akten.onboarding.required"));
|
||||
disableForm();
|
||||
return;
|
||||
}
|
||||
const resp = await fetch("/api/projekte");
|
||||
if (!resp.ok) return;
|
||||
me = await resp.json();
|
||||
if (!me) return;
|
||||
|
||||
const officeSelect = document.getElementById("akte-office") as HTMLSelectElement;
|
||||
if (offices.some((o) => o.key === me!.office)) {
|
||||
officeSelect.value = me.office;
|
||||
}
|
||||
if (me.role !== "admin") {
|
||||
officeSelect.disabled = true;
|
||||
}
|
||||
|
||||
if (me.role === "partner" || me.role === "admin") {
|
||||
document.getElementById("firm-wide-wrap")!.style.display = "";
|
||||
}
|
||||
parentCandidates = (await resp.json()) as ProjektMini[];
|
||||
} catch {
|
||||
/* non-fatal — form still works */
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const resp = await fetch("/api/users");
|
||||
if (!resp.ok) return;
|
||||
allUsers = await resp.json();
|
||||
} catch {
|
||||
/* non-fatal — collaborator picker disabled silently */
|
||||
}
|
||||
}
|
||||
|
||||
function renderCollabChips() {
|
||||
const wrap = document.getElementById("akte-collab-list")!;
|
||||
wrap.innerHTML = Array.from(selectedCollabs.values())
|
||||
.map(
|
||||
(u) =>
|
||||
`<span class="akten-chip" data-id="${esc(u.id)}">${esc(u.display_name || u.email)}<button type="button" class="akten-chip-x" aria-label="remove">\u00d7</button></span>`,
|
||||
)
|
||||
.join("");
|
||||
wrap.querySelectorAll<HTMLButtonElement>(".akten-chip-x").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const chip = btn.closest<HTMLElement>(".akten-chip")!;
|
||||
selectedCollabs.delete(chip.dataset.id!);
|
||||
renderCollabChips();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initCollabPicker() {
|
||||
const input = document.getElementById("akte-collab-input") as HTMLInputElement;
|
||||
const suggestions = document.getElementById("akte-collab-suggestions")!;
|
||||
function initParentPicker() {
|
||||
const input = $("projekt-parent-input") as HTMLInputElement;
|
||||
const hidden = $("projekt-parent-id") as HTMLInputElement;
|
||||
const sugs = $("projekt-parent-suggestions") as HTMLDivElement;
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const q = input.value.trim().toLowerCase();
|
||||
hidden.value = "";
|
||||
if (!q) {
|
||||
suggestions.innerHTML = "";
|
||||
suggestions.style.display = "none";
|
||||
sugs.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
const matches = allUsers
|
||||
.filter((u) => !selectedCollabs.has(u.id) && (!me || u.id !== me.id))
|
||||
.filter(
|
||||
(u) =>
|
||||
u.email.toLowerCase().includes(q) ||
|
||||
(u.display_name && u.display_name.toLowerCase().includes(q)),
|
||||
)
|
||||
const matches = parentCandidates
|
||||
.filter((p) => {
|
||||
const hay = (p.title + " " + (p.reference || "")).toLowerCase();
|
||||
return hay.includes(q);
|
||||
})
|
||||
.slice(0, 8);
|
||||
if (matches.length === 0) {
|
||||
suggestions.innerHTML = "";
|
||||
suggestions.style.display = "none";
|
||||
return;
|
||||
}
|
||||
suggestions.innerHTML = matches
|
||||
sugs.innerHTML = matches
|
||||
.map(
|
||||
(u) =>
|
||||
`<button type="button" class="akten-suggestion" data-id="${esc(u.id)}">${esc(u.display_name || u.email)}<span class="akten-suggestion-meta">${esc(u.email)}</span></button>`,
|
||||
(p) =>
|
||||
`<div class="akten-collab-suggestion" data-id="${esc(p.id)}" data-title="${esc(p.title)}">
|
||||
<strong>${esc(p.title)}</strong>
|
||||
<span class="akten-type-chip akten-type-${esc(p.type)}">${esc(t("projekte.type." + p.type) || p.type)}</span>
|
||||
</div>`,
|
||||
)
|
||||
.join("");
|
||||
suggestions.style.display = "block";
|
||||
suggestions.querySelectorAll<HTMLButtonElement>(".akten-suggestion").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const id = btn.dataset.id!;
|
||||
const user = allUsers.find((u) => u.id === id);
|
||||
if (user) {
|
||||
selectedCollabs.set(id, user);
|
||||
renderCollabChips();
|
||||
}
|
||||
input.value = "";
|
||||
suggestions.innerHTML = "";
|
||||
suggestions.style.display = "none";
|
||||
sugs.querySelectorAll<HTMLDivElement>(".akten-collab-suggestion").forEach((el) => {
|
||||
el.addEventListener("click", () => {
|
||||
hidden.value = el.dataset.id!;
|
||||
input.value = el.dataset.title!;
|
||||
sugs.innerHTML = "";
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Hide suggestions on outside click
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!(e.target as HTMLElement).closest(".akten-collab")) {
|
||||
suggestions.style.display = "none";
|
||||
function submitForm() {
|
||||
const form = $("akten-neu-form") as HTMLFormElement;
|
||||
const msg = $("akten-neu-msg") as HTMLParagraphElement;
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
msg.textContent = "";
|
||||
|
||||
const type = ($("projekt-type") as HTMLSelectElement).value;
|
||||
const title = ($("akte-title") as HTMLInputElement).value.trim();
|
||||
if (!title) {
|
||||
msg.textContent = t("projekte.error.title_required") || "Title required";
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
type,
|
||||
title,
|
||||
status: ($("akte-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();
|
||||
if (ref) payload.reference = ref;
|
||||
|
||||
const clientNumber = ($("akte-client-number") as HTMLInputElement).value.trim();
|
||||
if (clientNumber) payload.client_number = clientNumber;
|
||||
const matterNumber = ($("akte-matter-number") as HTMLInputElement).value.trim();
|
||||
if (matterNumber) payload.matter_number = matterNumber;
|
||||
const netdocs = ($("akte-netdocs") as HTMLInputElement).value.trim();
|
||||
if (netdocs) payload.netdocuments_url = netdocs;
|
||||
|
||||
if (type === "client") {
|
||||
const ind = ($("akte-industry") as HTMLInputElement).value.trim();
|
||||
if (ind) payload.industry = ind;
|
||||
const cty = ($("akte-country") as HTMLInputElement).value.trim();
|
||||
if (cty) payload.country = cty;
|
||||
const bill = ($("akte-billing") as HTMLInputElement).value.trim();
|
||||
if (bill) payload.billing_reference = bill;
|
||||
}
|
||||
if (type === "patent") {
|
||||
const pat = ($("akte-patent-number") as HTMLInputElement).value.trim();
|
||||
if (pat) payload.patent_number = pat;
|
||||
const fd = ($("akte-filing-date") as HTMLInputElement).value;
|
||||
if (fd) payload.filing_date = fd + "T00:00:00Z";
|
||||
const gd = ($("akte-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();
|
||||
if (court) payload.court = court;
|
||||
const cn = ($("akte-case-number") as HTMLInputElement).value.trim();
|
||||
if (cn) payload.case_number = cn;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch("/api/projekte", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const errBody = await resp.json().catch(() => ({ error: "unknown" }));
|
||||
msg.textContent = errBody.error || "Fehler beim Anlegen";
|
||||
return;
|
||||
}
|
||||
const p = (await resp.json()) as { id: string };
|
||||
window.location.href = `/projekte/${p.id}`;
|
||||
} catch (e) {
|
||||
msg.textContent = String(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -172,100 +158,13 @@ function esc(s: string): string {
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function showError(msg: string) {
|
||||
const el = document.getElementById("akten-neu-msg")!;
|
||||
el.textContent = msg;
|
||||
el.className = "form-msg form-msg-error";
|
||||
}
|
||||
|
||||
function disableForm() {
|
||||
const form = document.getElementById("akten-neu-form") as HTMLFormElement;
|
||||
form.querySelectorAll<HTMLInputElement>("input, select, textarea, button[type=submit]").forEach((el) => {
|
||||
el.disabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
async function submitForm(e: Event) {
|
||||
e.preventDefault();
|
||||
const msg = document.getElementById("akten-neu-msg")!;
|
||||
const submitBtn = document.querySelector<HTMLButtonElement>("#akten-neu-form button[type=submit]")!;
|
||||
|
||||
const title = (document.getElementById("akte-title") as HTMLInputElement).value.trim();
|
||||
const ref = (document.getElementById("akte-ref") as HTMLInputElement).value.trim();
|
||||
const office = (document.getElementById("akte-office") as HTMLSelectElement).value;
|
||||
const status = (document.getElementById("akte-status") as HTMLSelectElement).value;
|
||||
const court = (document.getElementById("akte-court") as HTMLInputElement).value.trim();
|
||||
const courtRef = (document.getElementById("akte-courtref") as HTMLInputElement).value.trim();
|
||||
const akteType = (document.getElementById("akte-type") as HTMLInputElement).value.trim();
|
||||
const firmWide =
|
||||
me &&
|
||||
(me.role === "partner" || me.role === "admin") &&
|
||||
(document.getElementById("akte-firmwide") as HTMLInputElement).checked;
|
||||
|
||||
if (!title || !ref) {
|
||||
showError(t("akten.error.required"));
|
||||
return;
|
||||
}
|
||||
|
||||
msg.textContent = "";
|
||||
msg.className = "form-msg";
|
||||
submitBtn.disabled = true;
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
title,
|
||||
aktenzeichen: ref,
|
||||
owning_office: office,
|
||||
status,
|
||||
};
|
||||
if (court) payload.court = court;
|
||||
if (courtRef) payload.court_ref = courtRef;
|
||||
if (akteType) payload.akte_type = akteType;
|
||||
|
||||
try {
|
||||
const resp = await fetch("/api/akten", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (resp.status === 401 || resp.status === 403) {
|
||||
showError(t("akten.error.forbidden"));
|
||||
submitBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}) as { error?: string });
|
||||
showError(data.error || t("akten.error.generic"));
|
||||
submitBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
const akte = await resp.json();
|
||||
|
||||
const collabIds = Array.from(selectedCollabs.keys());
|
||||
if (collabIds.length > 0 || firmWide) {
|
||||
const patch: Record<string, unknown> = {};
|
||||
if (collabIds.length > 0) patch.collaborators = collabIds;
|
||||
if (firmWide) patch.firm_wide_visible = true;
|
||||
await fetch(`/api/akten/${akte.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
}
|
||||
|
||||
window.location.href = `/akten/${akte.id}`;
|
||||
} catch {
|
||||
showError(t("akten.error.generic"));
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initI18n();
|
||||
initSidebar();
|
||||
initCollabPicker();
|
||||
document.getElementById("akten-neu-form")!.addEventListener("submit", submitForm);
|
||||
await loadOffices();
|
||||
onLangChange(renderOfficeOptions);
|
||||
loadMe();
|
||||
loadUsers();
|
||||
const typeSel = $("projekt-type") as HTMLSelectElement;
|
||||
showFieldsForType(typeSel.value);
|
||||
typeSel.addEventListener("change", () => showFieldsForType(typeSel.value));
|
||||
loadParentCandidates();
|
||||
initParentPicker();
|
||||
submitForm();
|
||||
});
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import { initI18n, onLangChange, t, getLang } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
|
||||
interface Akte {
|
||||
// /projekte list page client. Reads v2 shape from /api/projekte.
|
||||
interface Projekt {
|
||||
id: string;
|
||||
aktenzeichen: string;
|
||||
type: string;
|
||||
parent_id?: string | null;
|
||||
path: string;
|
||||
title: string;
|
||||
reference?: string | null;
|
||||
status: string;
|
||||
owning_office: string;
|
||||
firm_wide_visible: boolean;
|
||||
client_number?: string | null;
|
||||
matter_number?: string | null;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
let allAkten: Akte[] = [];
|
||||
let officeFilter = "";
|
||||
let allRows: Projekt[] = [];
|
||||
let typeFilter = "";
|
||||
let statusFilter = "";
|
||||
let viewMode: "flat" | "roots" = "flat";
|
||||
let searchQuery = "";
|
||||
let loadedOK = false;
|
||||
|
||||
async function loadAkten() {
|
||||
async function loadProjekte() {
|
||||
const unavailable = document.getElementById("akten-unavailable")!;
|
||||
const table = document.querySelector<HTMLElement>(".akten-table-wrap")!;
|
||||
try {
|
||||
const resp = await fetch("/api/akten");
|
||||
const resp = await fetch("/api/projekte");
|
||||
if (resp.status === 503) {
|
||||
unavailable.style.display = "block";
|
||||
table.style.display = "none";
|
||||
@@ -33,7 +38,7 @@ async function loadAkten() {
|
||||
table.style.display = "none";
|
||||
return;
|
||||
}
|
||||
allAkten = await resp.json();
|
||||
allRows = await resp.json();
|
||||
loadedOK = true;
|
||||
render();
|
||||
} catch {
|
||||
@@ -42,17 +47,24 @@ async function loadAkten() {
|
||||
}
|
||||
}
|
||||
|
||||
function getFiltered(): Akte[] {
|
||||
let rows = allAkten;
|
||||
if (officeFilter) rows = rows.filter((a) => a.owning_office === officeFilter);
|
||||
if (statusFilter) rows = rows.filter((a) => a.status === statusFilter);
|
||||
function getFiltered(): Projekt[] {
|
||||
let rows = allRows;
|
||||
if (viewMode === "roots") rows = rows.filter((p) => !p.parent_id);
|
||||
if (typeFilter) rows = rows.filter((p) => p.type === typeFilter);
|
||||
if (statusFilter) rows = rows.filter((p) => p.status === statusFilter);
|
||||
if (searchQuery) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
rows = rows.filter(
|
||||
(a) =>
|
||||
a.title.toLowerCase().includes(q) ||
|
||||
a.aktenzeichen.toLowerCase().includes(q),
|
||||
);
|
||||
rows = rows.filter((p) => {
|
||||
const haystack = [
|
||||
p.title,
|
||||
p.reference || "",
|
||||
p.client_number || "",
|
||||
p.matter_number || "",
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return haystack.includes(q);
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
@@ -66,9 +78,9 @@ function render() {
|
||||
const count = document.getElementById("akten-count")!;
|
||||
const filtered = getFiltered();
|
||||
|
||||
count.textContent = `${filtered.length} / ${allAkten.length}`;
|
||||
count.textContent = `${filtered.length} / ${allRows.length}`;
|
||||
|
||||
if (allAkten.length === 0) {
|
||||
if (allRows.length === 0) {
|
||||
tbody.innerHTML = "";
|
||||
tableWrap.style.display = "none";
|
||||
empty.style.display = "block";
|
||||
@@ -88,19 +100,20 @@ function render() {
|
||||
emptyFiltered.style.display = "none";
|
||||
|
||||
tbody.innerHTML = filtered
|
||||
.map((a) => {
|
||||
const statusKey = `akten.status.${a.status}`;
|
||||
const statusLabel = t(statusKey);
|
||||
const officeLabel = t(`office.${a.owning_office}`) || a.owning_office;
|
||||
const firmWide = a.firm_wide_visible
|
||||
? `<span class="akten-firmwide-dot" title="${escAttr(t("akten.detail.firmwide.on"))}">\u2737</span>`
|
||||
: "";
|
||||
return `<tr class="akten-row" data-id="${esc(a.id)}">
|
||||
<td class="akten-col-title">${esc(a.title)} ${firmWide}</td>
|
||||
<td class="akten-col-ref">${esc(a.aktenzeichen)}</td>
|
||||
<td><span class="akten-office-chip akten-office-${esc(a.owning_office)}">${esc(officeLabel)}</span></td>
|
||||
<td><span class="akten-status-chip akten-status-${esc(a.status)}">${esc(statusLabel)}</span></td>
|
||||
<td class="akten-col-updated">${fmtDate(a.updated_at)}</td>
|
||||
.map((p) => {
|
||||
const typeLabel = t(`projekte.type.${p.type}`) || p.type;
|
||||
const statusLabel = t(`projekte.filter.status.${p.status}`) || p.status;
|
||||
const clientMatter =
|
||||
p.client_number && p.matter_number
|
||||
? `${p.client_number}.${p.matter_number}`
|
||||
: p.client_number || p.matter_number || "";
|
||||
return `<tr class="akten-row" data-id="${esc(p.id)}">
|
||||
<td class="akten-col-title">${esc(p.title)}</td>
|
||||
<td><span class="akten-type-chip akten-type-${esc(p.type)}">${esc(typeLabel)}</span></td>
|
||||
<td class="akten-col-ref">${esc(p.reference || "")}</td>
|
||||
<td class="akten-col-ref">${esc(clientMatter)}</td>
|
||||
<td><span class="akten-status-chip akten-status-${esc(p.status)}">${esc(statusLabel)}</span></td>
|
||||
<td class="akten-col-updated">${fmtDate(p.updated_at)}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
@@ -108,7 +121,7 @@ function render() {
|
||||
tbody.querySelectorAll<HTMLTableRowElement>(".akten-row").forEach((row) => {
|
||||
row.addEventListener("click", () => {
|
||||
const id = row.dataset.id!;
|
||||
window.location.href = `/akten/${id}`;
|
||||
window.location.href = `/projekte/${id}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -132,10 +145,6 @@ function esc(s: string): string {
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function escAttr(s: string): string {
|
||||
return s.replace(/&/g, "&").replace(/"/g, """);
|
||||
}
|
||||
|
||||
function initSearch() {
|
||||
const input = document.getElementById("akten-search") as HTMLInputElement;
|
||||
input.addEventListener("input", () => {
|
||||
@@ -145,16 +154,21 @@ function initSearch() {
|
||||
}
|
||||
|
||||
function initFilters() {
|
||||
const office = document.getElementById("akten-office") as HTMLSelectElement;
|
||||
const typeSel = document.getElementById("projekt-type") as HTMLSelectElement;
|
||||
const status = document.getElementById("akten-status") as HTMLSelectElement;
|
||||
office.addEventListener("change", () => {
|
||||
officeFilter = office.value;
|
||||
const view = document.getElementById("projekt-view") as HTMLSelectElement;
|
||||
typeSel.addEventListener("change", () => {
|
||||
typeFilter = typeSel.value;
|
||||
render();
|
||||
});
|
||||
status.addEventListener("change", () => {
|
||||
statusFilter = status.value;
|
||||
render();
|
||||
});
|
||||
view.addEventListener("change", () => {
|
||||
viewMode = view.value as "flat" | "roots";
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
@@ -163,5 +177,5 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
initSearch();
|
||||
initFilters();
|
||||
onLangChange(render);
|
||||
loadAkten();
|
||||
loadProjekte();
|
||||
});
|
||||
|
||||
@@ -35,13 +35,13 @@ interface ChecklistInstance {
|
||||
id: string;
|
||||
template_slug: string;
|
||||
name: string;
|
||||
akte_id?: string | null;
|
||||
projekt_id?: string | null;
|
||||
state: Record<string, boolean>;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
akte_aktenzeichen?: string | null;
|
||||
akte_title?: string | null;
|
||||
projekt_reference?: string | null;
|
||||
projekt_title?: string | null;
|
||||
}
|
||||
|
||||
interface AkteSummary {
|
||||
@@ -177,8 +177,8 @@ function renderInstances() {
|
||||
|
||||
body.innerHTML = instances.map((inst) => {
|
||||
const { done, pct } = progress(inst);
|
||||
const akteCell = inst.akte_id && inst.akte_aktenzeichen
|
||||
? `<a href="/akten/${esc(inst.akte_id)}" class="checklist-instance-akte-link">${esc(inst.akte_aktenzeichen)}</a>`
|
||||
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>`
|
||||
: `<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>
|
||||
@@ -257,8 +257,8 @@ function initNewInstance() {
|
||||
return;
|
||||
}
|
||||
const akteID = akteSel.value || null;
|
||||
const payload: { name: string; akte_id?: string } = { name };
|
||||
if (akteID) payload.akte_id = akteID;
|
||||
const payload: { name: string; projekt_id?: string } = { name };
|
||||
if (akteID) payload.projekt_id = akteID;
|
||||
|
||||
const slug = templateSlug();
|
||||
const submitBtn = form.querySelector(".btn-primary") as HTMLButtonElement;
|
||||
|
||||
@@ -35,7 +35,7 @@ interface Instance {
|
||||
id: string;
|
||||
template_slug: string;
|
||||
name: string;
|
||||
akte_id?: string | null;
|
||||
projekt_id?: string | null;
|
||||
state: Record<string, boolean>;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
@@ -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.akte_id) {
|
||||
if (instance.projekt_id) {
|
||||
const akteLabel = isEN ? "Akte" : "Akte";
|
||||
parts.push(`<div class="checklist-meta-item"><dt>${akteLabel}</dt><dd><a href="/akten/${esc(instance.akte_id)}">${t("checklisten.instance.akte.open") || "Öffnen"}</a></dd></div>`);
|
||||
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>`);
|
||||
}
|
||||
document.getElementById("instance-meta")!.innerHTML = parts.join("");
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ interface UpcomingDeadline {
|
||||
id: string;
|
||||
title: string;
|
||||
due_date: string;
|
||||
akte_id: string;
|
||||
akte_title: string;
|
||||
akte_ref: string;
|
||||
projekt_id: string;
|
||||
projekt_title: string;
|
||||
projekt_ref: string;
|
||||
urgency: "overdue" | "today" | "urgent" | "soon";
|
||||
}
|
||||
|
||||
@@ -38,18 +38,18 @@ interface UpcomingAppointment {
|
||||
start_at: string;
|
||||
end_at: string | null;
|
||||
type: string | null;
|
||||
akte_id: string | null;
|
||||
akte_title: string | null;
|
||||
akte_ref: string | null;
|
||||
projekt_id: string | null;
|
||||
projekt_title: string | null;
|
||||
projekt_ref: string | null;
|
||||
}
|
||||
|
||||
interface ActivityEntry {
|
||||
timestamp: string;
|
||||
actor_email: string | null;
|
||||
actor_name: string | null;
|
||||
akte_id: string;
|
||||
akte_title: string;
|
||||
akte_ref: string;
|
||||
projekt_id: string;
|
||||
projekt_title: string;
|
||||
projekt_ref: string;
|
||||
action: string | null;
|
||||
details: string;
|
||||
description: string | null;
|
||||
@@ -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="/akten/${esc(d.akte_id)}/fristen" class="dashboard-list-link">
|
||||
<a href="/projekte/${esc(d.projekt_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.akte_ref)} · ${esc(d.akte_title)}</span>
|
||||
<span class="dashboard-list-ref">${esc(d.projekt_ref)} · ${esc(d.projekt_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.akte_id ? `/akten/${esc(a.akte_id)}/termine` : "#";
|
||||
const tag = a.akte_id ? "a" : "div";
|
||||
const akteLine = a.akte_ref && a.akte_title
|
||||
? `<span class="dashboard-list-ref">${esc(a.akte_ref)} · ${esc(a.akte_title)}</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)} · ${esc(a.projekt_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="/akten/${esc(e.akte_id)}" class="dashboard-activity-akte">${esc(e.akte_ref)}</a>
|
||||
<a href="/projekte/${esc(e.projekt_id)}" class="dashboard-activity-akte">${esc(e.projekt_ref)}</a>
|
||||
<span class="dashboard-activity-details">${esc(e.details)}</span>
|
||||
</span>
|
||||
</li>`;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { initNotes } from "./notizen";
|
||||
|
||||
interface Frist {
|
||||
id: string;
|
||||
akte_id: string;
|
||||
projekt_id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
due_date: string;
|
||||
@@ -103,7 +103,7 @@ async function loadFrist(id: string): Promise<boolean> {
|
||||
|
||||
async function loadAkte(akteID: string) {
|
||||
try {
|
||||
const resp = await fetch(`/api/akten/${akteID}`);
|
||||
const resp = await fetch(`/api/projekte/${akteID}`);
|
||||
if (resp.ok) akte = await resp.json();
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
@@ -147,10 +147,10 @@ function render() {
|
||||
|
||||
const akteLink = document.getElementById("frist-akte-link") as HTMLAnchorElement;
|
||||
if (akte) {
|
||||
akteLink.href = `/akten/${akte.id}`;
|
||||
akteLink.href = `/projekte/${akte.id}`;
|
||||
akteLink.textContent = `${akte.aktenzeichen} \u2014 ${akte.title}`;
|
||||
} else {
|
||||
akteLink.href = `/akten/${frist.akte_id}`;
|
||||
akteLink.href = `/projekte/${frist.projekt_id}`;
|
||||
akteLink.textContent = "\u2014";
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ function initDelete() {
|
||||
confirmBtn.disabled = true;
|
||||
const resp = await fetch(`/api/fristen/${frist.id}`, { method: "DELETE" });
|
||||
if (resp.ok) {
|
||||
const target = akte ? `/akten/${akte.id}/fristen` : "/fristen";
|
||||
const target = akte ? `/projekte/${akte.id}/fristen` : "/fristen";
|
||||
window.location.href = target;
|
||||
} else {
|
||||
confirmBtn.disabled = false;
|
||||
@@ -325,7 +325,7 @@ async function main() {
|
||||
notfound.style.display = "block";
|
||||
return;
|
||||
}
|
||||
await loadAkte(frist.akte_id);
|
||||
await loadAkte(frist.projekt_id);
|
||||
if (frist.rule_id) await loadRule(frist.rule_id);
|
||||
|
||||
loading.style.display = "none";
|
||||
|
||||
@@ -3,12 +3,12 @@ import { initSidebar } from "./sidebar";
|
||||
|
||||
interface Frist {
|
||||
id: string;
|
||||
akte_id: string;
|
||||
projekt_id: string;
|
||||
title: string;
|
||||
due_date: string;
|
||||
status: string;
|
||||
akte_aktenzeichen: string;
|
||||
akte_title: string;
|
||||
projekt_reference: string;
|
||||
projekt_title: string;
|
||||
}
|
||||
|
||||
let allFristen: Frist[] = [];
|
||||
@@ -128,7 +128,7 @@ function openPopup(iso: string) {
|
||||
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="/akten/${esc(f.akte_id)}" class="frist-cal-popup-akte">${esc(f.akte_aktenzeichen)}</a>
|
||||
<a href="/projekte/${esc(f.projekt_id)}" class="frist-cal-popup-akte">${esc(f.projekt_reference)}</a>
|
||||
</li>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
@@ -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 = `/akten/${preselectedAkteID}/fristen`;
|
||||
cancel.href = `/akten/${preselectedAkteID}/fristen`;
|
||||
back.href = `/projekte/${preselectedAkteID}/fristen`;
|
||||
cancel.href = `/projekte/${preselectedAkteID}/fristen`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ async function submitForm(e: Event) {
|
||||
if (notes) payload.notes = notes;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/akten/${encodeURIComponent(akteID)}/fristen`, {
|
||||
const resp = await fetch(`/api/projekte/${encodeURIComponent(akteID)}/fristen`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
@@ -128,7 +128,7 @@ async function submitForm(e: Event) {
|
||||
}
|
||||
const created = await resp.json();
|
||||
if (preselectedAkteID) {
|
||||
window.location.href = `/akten/${preselectedAkteID}/fristen`;
|
||||
window.location.href = `/projekte/${preselectedAkteID}/fristen`;
|
||||
} else {
|
||||
window.location.href = `/fristen/${created.id}`;
|
||||
}
|
||||
@@ -144,9 +144,9 @@ function detectPreselect() {
|
||||
if (parts[0] === "akten" && parts[1] && parts[2] === "fristen" && parts[3] === "neu") {
|
||||
preselectedAkteID = parts[1];
|
||||
}
|
||||
// Or ?akte_id= query string
|
||||
// Or ?projekt_id= query string
|
||||
const qp = new URLSearchParams(window.location.search);
|
||||
const fromQuery = qp.get("akte_id");
|
||||
const fromQuery = qp.get("projekt_id");
|
||||
if (fromQuery) preselectedAkteID = fromQuery;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@ import { initSidebar } from "./sidebar";
|
||||
|
||||
interface Frist {
|
||||
id: string;
|
||||
akte_id: string;
|
||||
projekt_id: string;
|
||||
title: string;
|
||||
due_date: string;
|
||||
status: string;
|
||||
source: string;
|
||||
rule_id?: string;
|
||||
akte_aktenzeichen: string;
|
||||
akte_title: string;
|
||||
akte_office: string;
|
||||
projekt_reference: string;
|
||||
projekt_title: string;
|
||||
projekt_office: string;
|
||||
rule_code?: string;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ async function loadAkten() {
|
||||
async function loadSummary() {
|
||||
try {
|
||||
const url = akteFilter
|
||||
? `/api/fristen/summary?akte_id=${encodeURIComponent(akteFilter)}`
|
||||
? `/api/fristen/summary?projekt_id=${encodeURIComponent(akteFilter)}`
|
||||
: `/api/fristen/summary`;
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) return;
|
||||
@@ -76,7 +76,7 @@ async function loadFristen() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (statusFilter) params.set("status", statusFilter);
|
||||
if (akteFilter) params.set("akte_id", akteFilter);
|
||||
if (akteFilter) params.set("projekt_id", akteFilter);
|
||||
const resp = await fetch(`/api/fristen?${params.toString()}`);
|
||||
if (resp.status === 503) {
|
||||
unavailable.style.display = "block";
|
||||
@@ -168,8 +168,8 @@ function render() {
|
||||
<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="/akten/${esc(f.akte_id)}">${esc(f.akte_aktenzeichen)}</a>
|
||||
<span class="frist-akte-title">${esc(f.akte_title)}</span>
|
||||
<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>
|
||||
<td class="frist-col-rule">${ruleLabel}</td>
|
||||
<td><span class="akten-status-chip akten-status-${esc(f.status)}">${esc(statusLabel)}</span></td>
|
||||
@@ -214,7 +214,7 @@ function initFilters() {
|
||||
// Pre-fill from URL
|
||||
const params = urlParams();
|
||||
if (params.has("status")) statusFilter = params.get("status")!;
|
||||
if (params.has("akte_id")) akteFilter = params.get("akte_id")!;
|
||||
if (params.has("projekt_id")) akteFilter = params.get("projekt_id")!;
|
||||
status.value = statusFilter;
|
||||
|
||||
status.addEventListener("change", async () => {
|
||||
|
||||
@@ -247,7 +247,7 @@ async function submitSave() {
|
||||
|
||||
submit.disabled = true;
|
||||
try {
|
||||
const resp = await fetch(`/api/akten/${encodeURIComponent(akteID)}/fristen/bulk`, {
|
||||
const resp = await fetch(`/api/projekte/${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?akte_id=${encodeURIComponent(akteID)}">${escHtml(t("fristen.save.success.link"))}</a>`;
|
||||
msg.innerHTML = `${escHtml(t("fristen.save.success"))} <a href="/fristen?projekt_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(() => {
|
||||
|
||||
@@ -14,7 +14,7 @@ export type NotizParentType = "akte" | "frist" | "termin";
|
||||
|
||||
export interface Notiz {
|
||||
id: string;
|
||||
akte_id?: string | null;
|
||||
projekt_id?: string | null;
|
||||
frist_id?: string | null;
|
||||
termin_id?: string | null;
|
||||
akten_event_id?: string | null;
|
||||
@@ -48,7 +48,7 @@ interface NotesState {
|
||||
function baseURL(parentType: NotizParentType, parentId: string): string {
|
||||
switch (parentType) {
|
||||
case "akte":
|
||||
return `/api/akten/${parentId}/notizen`;
|
||||
return `/api/projekte/${parentId}/notizen`;
|
||||
case "frist":
|
||||
return `/api/fristen/${parentId}/notizen`;
|
||||
case "termin":
|
||||
|
||||
@@ -4,7 +4,7 @@ import { initNotes } from "./notizen";
|
||||
|
||||
interface Termin {
|
||||
id: string;
|
||||
akte_id?: string;
|
||||
projekt_id?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
start_at: string;
|
||||
@@ -70,7 +70,7 @@ async function loadTermin(id: string): Promise<boolean> {
|
||||
|
||||
async function loadAkte(id: string) {
|
||||
try {
|
||||
const resp = await fetch(`/api/akten/${id}`);
|
||||
const resp = await fetch(`/api/projekte/${id}`);
|
||||
if (resp.ok) akte = await resp.json();
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
@@ -96,9 +96,9 @@ function renderHeader() {
|
||||
}
|
||||
|
||||
const akteRow = document.getElementById("termin-akte-row")!;
|
||||
if (termin.akte_id && akte) {
|
||||
if (termin.projekt_id && akte) {
|
||||
const link = document.getElementById("termin-akte-link") as HTMLAnchorElement;
|
||||
link.href = `/akten/${akte.id}`;
|
||||
link.href = `/projekte/${akte.id}`;
|
||||
link.textContent = `${akte.aktenzeichen} \u2014 ${akte.title}`;
|
||||
akteRow.style.display = "";
|
||||
} else {
|
||||
@@ -200,7 +200,7 @@ async function main() {
|
||||
notFound.style.display = "block";
|
||||
return;
|
||||
}
|
||||
if (termin.akte_id) await loadAkte(termin.akte_id);
|
||||
if (termin.projekt_id) await loadAkte(termin.projekt_id);
|
||||
loading.style.display = "none";
|
||||
body.style.display = "";
|
||||
renderHeader();
|
||||
|
||||
@@ -3,13 +3,13 @@ import { initSidebar } from "./sidebar";
|
||||
|
||||
interface Termin {
|
||||
id: string;
|
||||
akte_id?: string;
|
||||
projekt_id?: string;
|
||||
title: string;
|
||||
start_at: string;
|
||||
end_at?: string;
|
||||
termin_type?: string;
|
||||
akte_aktenzeichen?: string;
|
||||
akte_title?: string;
|
||||
projekt_reference?: string;
|
||||
projekt_title?: string;
|
||||
}
|
||||
|
||||
let allTermine: Termin[] = [];
|
||||
@@ -131,8 +131,8 @@ function openPopup(iso: string) {
|
||||
|
||||
list.innerHTML = items
|
||||
.map((tt) => {
|
||||
const akteRef = tt.akte_id
|
||||
? `<a href="/akten/${esc(tt.akte_id)}" class="frist-cal-popup-akte">${esc(tt.akte_aktenzeichen ?? "")}</a>`
|
||||
const akteRef = tt.projekt_id
|
||||
? `<a href="/projekte/${esc(tt.projekt_id)}" class="frist-cal-popup-akte">${esc(tt.projekt_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>
|
||||
|
||||
@@ -36,9 +36,9 @@ function populateAkten() {
|
||||
}
|
||||
sel.innerHTML = opts.join("");
|
||||
|
||||
// Pre-select Akte from query (?akte_id=...)
|
||||
// Pre-select Akte from query (?projekt_id=...)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const ak = params.get("akte_id");
|
||||
const ak = params.get("projekt_id");
|
||||
if (ak) sel.value = ak;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ async function submitForm(ev: Event) {
|
||||
};
|
||||
if (endRaw) payload.end_at = new Date(endRaw).toISOString();
|
||||
if (type) payload.termin_type = type;
|
||||
if (akteID) payload.akte_id = akteID;
|
||||
if (akteID) payload.projekt_id = akteID;
|
||||
if (location) payload.location = location;
|
||||
if (description) payload.description = description;
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@ import { initSidebar } from "./sidebar";
|
||||
|
||||
interface Termin {
|
||||
id: string;
|
||||
akte_id?: string;
|
||||
projekt_id?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
start_at: string;
|
||||
end_at?: string;
|
||||
location?: string;
|
||||
termin_type?: string;
|
||||
akte_aktenzeichen?: string;
|
||||
akte_title?: string;
|
||||
akte_office?: string;
|
||||
projekt_reference?: string;
|
||||
projekt_title?: string;
|
||||
projekt_office?: string;
|
||||
}
|
||||
|
||||
interface Akte {
|
||||
@@ -75,7 +75,7 @@ async function loadTermine() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (typeFilter) params.set("type", typeFilter);
|
||||
if (akteFilter && akteFilter !== PERSONAL) params.set("akte_id", akteFilter);
|
||||
if (akteFilter && akteFilter !== PERSONAL) params.set("projekt_id", akteFilter);
|
||||
if (fromFilter) params.set("from", fromFilter);
|
||||
if (toFilter) params.set("to", toFilter);
|
||||
const resp = await fetch(`/api/termine?${params.toString()}`);
|
||||
@@ -91,7 +91,7 @@ async function loadTermine() {
|
||||
return;
|
||||
}
|
||||
const data: Termin[] = await resp.json();
|
||||
allTermine = akteFilter === PERSONAL ? data.filter((x) => !x.akte_id) : data;
|
||||
allTermine = akteFilter === PERSONAL ? data.filter((x) => !x.projekt_id) : data;
|
||||
loadedOK = true;
|
||||
render();
|
||||
} catch {
|
||||
@@ -149,9 +149,9 @@ function render() {
|
||||
.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.akte_id
|
||||
? `<a class="akten-ref-link" href="/akten/${esc(tt.akte_id)}">${esc(tt.akte_aktenzeichen ?? "")}</a>`
|
||||
+ `<span class="frist-akte-title">${esc(tt.akte_title ?? "")}</span>`
|
||||
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>`
|
||||
: `<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>
|
||||
@@ -182,7 +182,7 @@ function initFilters() {
|
||||
|
||||
const params = urlParams();
|
||||
if (params.has("type")) typeFilter = params.get("type")!;
|
||||
if (params.has("akte_id")) akteFilter = params.get("akte_id")!;
|
||||
if (params.has("projekt_id")) akteFilter = params.get("projekt_id")!;
|
||||
if (params.has("from")) fromFilter = params.get("from")!;
|
||||
if (params.has("to")) toFilter = params.get("to")!;
|
||||
type.value = typeFilter;
|
||||
|
||||
Reference in New Issue
Block a user