Merge: /projects/{id} notfound fix + German DOM/URL rename (t-paliad-038)

This commit is contained in:
m
2026-04-26 01:06:13 +02:00
11 changed files with 282 additions and 250 deletions

View File

@@ -16,7 +16,7 @@ interface Appointment {
interface Project {
id: string;
aktenzeichen: string;
reference?: string | null;
title: string;
}
@@ -99,7 +99,7 @@ function renderHeader() {
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}`;
link.textContent = `${project.reference || ""} \u2014 ${project.title}`;
akteRow.style.display = "";
} else {
akteRow.style.display = "none";
@@ -212,7 +212,7 @@ async function main() {
const notes = document.getElementById("notes-container");
if (notes) {
notes.setAttribute("data-parent-id", id);
void initNotes(notes as HTMLElement, "termin", id);
void initNotes(notes as HTMLElement, "appointment", id);
}
}

View File

@@ -3,7 +3,7 @@ import { initSidebar } from "./sidebar";
interface Project {
id: string;
aktenzeichen: string;
reference?: string | null;
title: string;
}
@@ -31,7 +31,7 @@ function populateAkten() {
];
for (const a of allProjects) {
opts.push(
`<option value="${esc(a.id)}">${esc(a.aktenzeichen)} \u2014 ${esc(a.title)}</option>`,
`<option value="${esc(a.id)}">${esc(a.reference || "")} \u2014 ${esc(a.title)}</option>`,
);
}
sel.innerHTML = opts.join("");

View File

@@ -17,7 +17,7 @@ interface Appointment {
interface Project {
id: string;
aktenzeichen: string;
reference?: string | null;
title: string;
}
@@ -215,7 +215,7 @@ function populateAkteFilter() {
];
for (const a of allProjects) {
options.push(
`<option value="${esc(a.id)}">${esc(a.aktenzeichen)} \u2014 ${esc(a.title)}</option>`,
`<option value="${esc(a.id)}">${esc(a.reference || "")} \u2014 ${esc(a.title)}</option>`,
);
}
sel.innerHTML = options.join("");

View File

@@ -46,7 +46,7 @@ interface ChecklistInstance {
interface AkteSummary {
id: string;
aktenzeichen: string;
reference?: string | null;
title: string;
}
@@ -221,7 +221,7 @@ function renderAkteOptions() {
akten.forEach((a) => {
const opt = document.createElement("option");
opt.value = a.id;
opt.textContent = `${a.aktenzeichen}${a.title}`;
opt.textContent = `${a.reference || ""}${a.title}`;
sel.appendChild(opt);
});
}

View File

@@ -18,7 +18,7 @@ interface Deadline {
interface Project {
id: string;
aktenzeichen: string;
reference?: string | null;
title: string;
}
@@ -148,7 +148,7 @@ function render() {
const akteLink = document.getElementById("frist-project-link") as HTMLAnchorElement;
if (project) {
akteLink.href = `/projects/${project.id}`;
akteLink.textContent = `${project.aktenzeichen} \u2014 ${project.title}`;
akteLink.textContent = `${project.reference || ""} \u2014 ${project.title}`;
} else {
akteLink.href = `/projects/${frist.project_id}`;
akteLink.textContent = "\u2014";
@@ -338,7 +338,7 @@ async function main() {
const notes = document.getElementById("notes-container");
if (notes) {
notes.setAttribute("data-parent-id", id);
void initNotes(notes as HTMLElement, "frist", id);
void initNotes(notes as HTMLElement, "deadline", id);
}
}

View File

@@ -3,7 +3,7 @@ import { initSidebar } from "./sidebar";
interface Project {
id: string;
aktenzeichen: string;
reference?: string | null;
title: string;
}
@@ -15,7 +15,7 @@ interface DeadlineRule {
rule_code?: string;
}
let preselectedAkteID = "";
let preselectedProjectID = "";
function esc(s: string): string {
const d = document.createElement("div");
@@ -29,25 +29,26 @@ function showError(msg: string) {
el.className = "form-msg form-msg-error";
}
async function loadAkten() {
async function loadProjects() {
const sel = document.getElementById("frist-project") as HTMLSelectElement;
const hint = document.getElementById("frist-project-empty-hint")!;
try {
const resp = await fetch("/api/projects");
if (!resp.ok) return;
const projects: Project[] = await resp.json();
if (akten.length === 0) {
if (projects.length === 0) {
hint.style.display = "";
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.project.choose">${esc(t("fristen.field.project.choose"))}</option>`,
`<option value="" disabled${preselectedProjectID ? "" : " 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" : "";
for (const p of projects) {
const isSelected = preselectedProjectID === p.id ? " selected" : "";
const ref = p.reference || "";
options.push(
`<option value="${esc(a.id)}"${isSelected}>${esc(a.aktenzeichen)} \u2014 ${esc(a.title)}</option>`,
`<option value="${esc(p.id)}"${isSelected}>${esc(ref)} \u2014 ${esc(p.title)}</option>`,
);
}
sel.innerHTML = options.join("");
@@ -78,11 +79,11 @@ async function loadRules() {
}
function initBackLinks() {
if (preselectedAkteID) {
if (preselectedProjectID) {
const back = document.getElementById("frist-neu-back") as HTMLAnchorElement;
const cancel = document.getElementById("frist-neu-cancel") as HTMLAnchorElement;
back.href = `/projects/${preselectedAkteID}/fristen`;
cancel.href = `/projects/${preselectedAkteID}/fristen`;
back.href = `/projects/${preselectedProjectID}/deadlines`;
cancel.href = `/projects/${preselectedProjectID}/deadlines`;
}
}
@@ -91,13 +92,13 @@ 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-project") as HTMLSelectElement).value;
const projectID = (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;
const notes = (document.getElementById("frist-notes") as HTMLTextAreaElement).value.trim();
if (!akteID || !title || !due) {
if (!projectID || !title || !due) {
showError(t("fristen.error.required"));
return;
}
@@ -115,7 +116,7 @@ async function submitForm(e: Event) {
if (notes) payload.notes = notes;
try {
const resp = await fetch(`/api/projects/${encodeURIComponent(akteID)}/fristen`, {
const resp = await fetch(`/api/projects/${encodeURIComponent(projectID)}/deadlines`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
@@ -127,8 +128,8 @@ async function submitForm(e: Event) {
return;
}
const created = await resp.json();
if (preselectedAkteID) {
window.location.href = `/projects/${preselectedAkteID}/fristen`;
if (preselectedProjectID) {
window.location.href = `/projects/${preselectedProjectID}/deadlines`;
} else {
window.location.href = `/deadlines/${created.id}`;
}
@@ -139,15 +140,15 @@ async function submitForm(e: Event) {
}
function detectPreselect() {
// Path /akten/{id}/fristen/neu pre-selects that project.
// Path /projects/{id}/deadlines/new 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];
if (parts[0] === "projects" && parts[1] && parts[2] === "deadlines" && parts[3] === "new") {
preselectedProjectID = parts[1];
}
// Or ?project_id= query string
const qp = new URLSearchParams(window.location.search);
const fromQuery = qp.get("project_id");
if (fromQuery) preselectedAkteID = fromQuery;
if (fromQuery) preselectedProjectID = fromQuery;
}
document.addEventListener("DOMContentLoaded", async () => {
@@ -159,5 +160,5 @@ document.addEventListener("DOMContentLoaded", async () => {
// Default due to today
const dueInput = document.getElementById("frist-due") as HTMLInputElement;
if (!dueInput.value) dueInput.value = new Date().toISOString().split("T")[0];
await Promise.all([loadAkten(), loadRules()]);
await Promise.all([loadProjects(), loadRules()]);
});

View File

@@ -17,7 +17,7 @@ interface Deadline {
interface Project {
id: string;
aktenzeichen: string;
reference?: string | null;
title: string;
}
@@ -235,7 +235,7 @@ function populateAkteFilter() {
];
for (const a of allProjects) {
options.push(
`<option value="${esc(a.id)}">${esc(a.aktenzeichen)} \u2014 ${esc(a.title)}</option>`,
`<option value="${esc(a.id)}">${esc(a.reference || "")} \u2014 ${esc(a.title)}</option>`,
);
}
sel.innerHTML = options.join("");

View File

@@ -103,9 +103,9 @@ async function calculate() {
}
}
interface AkteOption {
interface ProjectOption {
id: string;
aktenzeichen: string;
reference?: string | null;
title: string;
}
@@ -119,11 +119,11 @@ function escHtml(s: string): string {
return d.innerHTML;
}
async function fetchAkten(): Promise<AkteOption[]> {
async function fetchProjects(): Promise<ProjectOption[]> {
try {
const resp = await fetch("/api/projects");
if (!resp.ok) return [];
return (await resp.json()) as AkteOption[];
return (await resp.json()) as ProjectOption[];
} catch {
return [];
}
@@ -178,21 +178,21 @@ function closeSaveModal() {
async function openSaveModal() {
if (!lastResponse) return;
ensureSaveModal();
const akten = await fetchAkten();
const projects = await fetchProjects();
const sel = document.getElementById("frist-save-project") as HTMLSelectElement;
const noAkten = document.getElementById("frist-save-no-akten")!;
const noProjects = document.getElementById("frist-save-no-akten")!;
const submit = document.getElementById("frist-save-submit") as HTMLButtonElement;
if (akten.length === 0) {
if (projects.length === 0) {
sel.style.display = "none";
noAkten.style.display = "";
noProjects.style.display = "";
submit.disabled = true;
} else {
sel.style.display = "";
noAkten.style.display = "none";
noProjects.style.display = "none";
submit.disabled = false;
sel.innerHTML = akten
.map((a) => `<option value="${escAttr(a.id)}">${escHtml(a.aktenzeichen)} \u2014 ${escHtml(a.title)}</option>`)
sel.innerHTML = projects
.map((p) => `<option value="${escAttr(p.id)}">${escHtml(p.reference || "")} \u2014 ${escHtml(p.title)}</option>`)
.join("");
}
@@ -222,20 +222,20 @@ async function openSaveModal() {
async function submitSave() {
if (!lastResponse) return;
const sel = document.getElementById("frist-save-project") as HTMLSelectElement;
const akteID = sel.value;
const projectID = sel.value;
const submit = document.getElementById("frist-save-submit") as HTMLButtonElement;
const msg = document.getElementById("frist-save-msg")!;
if (!akteID) return;
if (!projectID) return;
const checks = document.querySelectorAll<HTMLInputElement>("#frist-save-list input[type=checkbox]");
const fristen: Array<Record<string, unknown>> = [];
const deadlinesPayload: Array<Record<string, unknown>> = [];
checks.forEach((cb) => {
if (!cb.checked || cb.disabled) return;
const idx = Number(cb.dataset.idx);
const dl = lastResponse!.deadlines[idx];
if (!dl || !dl.dueDate) return;
const dlName = getLang() === "en" ? dl.nameEN : dl.name;
fristen.push({
deadlinesPayload.push({
title: dl.ruleRef ? `${dl.ruleRef} \u2014 ${dlName}` : dlName,
due_date: dl.dueDate,
original_due_date: dl.originalDate || undefined,
@@ -243,14 +243,14 @@ async function submitSave() {
notes: dl.notes || undefined,
});
});
if (fristen.length === 0) return;
if (deadlinesPayload.length === 0) return;
submit.disabled = true;
try {
const resp = await fetch(`/api/projects/${encodeURIComponent(akteID)}/fristen/bulk`, {
const resp = await fetch(`/api/projects/${encodeURIComponent(projectID)}/deadlines/bulk`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fristen }),
body: JSON.stringify({ deadlines: deadlinesPayload }),
});
if (!resp.ok) {
const data = await resp.json().catch(() => ({}) as { error?: string });
@@ -259,7 +259,7 @@ async function submitSave() {
submit.disabled = false;
return;
}
msg.innerHTML = `${escHtml(t("fristen.save.success"))} <a href="/deadlines?project_id=${encodeURIComponent(akteID)}">${escHtml(t("fristen.save.success.link"))}</a>`;
msg.innerHTML = `${escHtml(t("fristen.save.success"))} <a href="/deadlines?project_id=${encodeURIComponent(projectID)}">${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(() => {

View File

@@ -1,5 +1,5 @@
// Shared polymorphic notes module. Each detail page (akten / fristen /
// termine) renders an empty <div id="notes-container" data-parent-type=…
// Shared polymorphic notes module. Each detail page (project / deadline /
// appointment) renders an empty <div id="notes-container" data-parent-type=…
// data-parent-id=…> and loads this script. initNotes(container) attaches
// the "Add note" form + list rendering; the API base URL is picked from
// the parent type.
@@ -10,7 +10,7 @@
import { t, getLang } from "./i18n";
export type NotizParentType = "project" | "frist" | "termin";
export type NotesParentType = "project" | "deadline" | "appointment";
export interface Note {
id: string;
@@ -32,7 +32,7 @@ interface Me {
}
interface NotesState {
parentType: NotizParentType;
parentType: NotesParentType;
parentId: string;
me: Me | null;
notes: Note[];
@@ -45,14 +45,14 @@ interface NotesState {
editingID: string | null;
}
function baseURL(parentType: NotizParentType, parentId: string): string {
function baseURL(parentType: NotesParentType, parentId: string): string {
switch (parentType) {
case "project":
return `/api/projects/${parentId}/notizen`;
case "frist":
return `/api/deadlines/${parentId}/notizen`;
case "termin":
return `/api/appointments/${parentId}/notizen`;
return `/api/projects/${parentId}/notes`;
case "deadline":
return `/api/deadlines/${parentId}/notes`;
case "appointment":
return `/api/appointments/${parentId}/notes`;
}
}
@@ -431,7 +431,7 @@ function buildUI(container: HTMLElement, state: NotesState) {
export async function initNotes(
container: HTMLElement,
parentType: NotizParentType,
parentType: NotesParentType,
parentId: string,
): Promise<void> {
const state: NotesState = {
@@ -459,7 +459,7 @@ export async function initNotes(
export function autoInitNotes() {
const el = document.getElementById("notes-container");
if (!el) return;
const parentType = el.getAttribute("data-parent-type") as NotizParentType | null;
const parentType = el.getAttribute("data-parent-type") as NotesParentType | null;
const parentId = el.getAttribute("data-parent-id");
if (!parentType || !parentId) return;
void initNotes(el, parentType, parentId);

View File

@@ -19,7 +19,7 @@ interface Project {
created_at: string;
}
interface ProjektTeamMember {
interface ProjectTeamMember {
id: string;
project_id: string;
user_id: string;
@@ -48,7 +48,7 @@ interface Party {
representative?: string;
}
interface AkteEvent {
interface ProjectEvent {
id: string;
project_id: string;
event_type?: string;
@@ -83,9 +83,39 @@ interface Me {
office: string;
}
type TabId = "verlauf" | "team" | "kinder" | "parteien" | "fristen" | "termine" | "notizen" | "checklisten";
type TabId =
| "history"
| "team"
| "children"
| "parties"
| "deadlines"
| "appointments"
| "notes"
| "checklists";
const VALID_TABS: TabId[] = ["verlauf", "team", "kinder", "parteien", "fristen", "termine", "notizen", "checklisten"];
const VALID_TABS: TabId[] = [
"history",
"team",
"children",
"parties",
"deadlines",
"appointments",
"notes",
"checklists",
];
// Legacy German tab slugs that may appear in bookmarked URLs after the
// rename. Mapped to their English successors so old links still land on the
// right tab instead of silently falling back to "history".
const LEGACY_TAB_ALIASES: Record<string, TabId> = {
verlauf: "history",
kinder: "children",
parteien: "parties",
fristen: "deadlines",
termine: "appointments",
notizen: "notes",
checklisten: "checklists",
};
interface ChecklistInstanceSummary {
id: string;
@@ -108,30 +138,31 @@ let checklistTemplates: Record<string, ChecklistTemplateSummary> = {};
let project: Project | null = null;
let me: Me | null = null;
let parties: Party[] = [];
let events: AkteEvent[] = [];
let events: ProjectEvent[] = [];
let deadlines: Deadline[] = [];
let appointments: Appointment[] = [];
let ancestors: ProjectMini[] = [];
let children: ProjectMini[] = [];
let teamMembers: ProjektTeamMember[] = [];
let teamMembers: ProjectTeamMember[] = [];
let userOptions: { id: string; display_name: string; email: string }[] = [];
const EVENTS_PAGE_SIZE = 50;
let eventsHasMore = false;
let eventsLoadingMore = false;
function parseAkteID(): string | null {
// Accepts /projekte/{id} (new) and /akten/{id} (legacy).
function parseProjectID(): string | null {
const parts = window.location.pathname.split("/").filter(Boolean);
if ((parts[0] !== "projekte" && parts[0] !== "akten") || !parts[1]) return null;
if (parts[0] !== "projects" || !parts[1]) return null;
return parts[1];
}
function parseTab(): TabId {
const parts = window.location.pathname.split("/").filter(Boolean);
const candidate = parts[2] as TabId | undefined;
if (candidate && VALID_TABS.includes(candidate)) return candidate;
return "verlauf";
const candidate = parts[2];
if (!candidate) return "history";
if ((VALID_TABS as string[]).includes(candidate)) return candidate as TabId;
if (LEGACY_TAB_ALIASES[candidate]) return LEGACY_TAB_ALIASES[candidate];
return "history";
}
async function loadMe() {
@@ -143,7 +174,7 @@ async function loadMe() {
}
}
async function loadAkte(id: string): Promise<boolean> {
async function loadProject(id: string): Promise<boolean> {
try {
const resp = await fetch(`/api/projects/${id}`);
if (!resp.ok) return false;
@@ -154,12 +185,12 @@ async function loadAkte(id: string): Promise<boolean> {
}
}
async function loadParteien(id: string) {
async function loadParties(id: string) {
try {
const resp = await fetch(`/api/projects/${id}/parteien`);
if (resp.ok) parteien = await resp.json();
const resp = await fetch(`/api/projects/${id}/parties`);
if (resp.ok) parties = await resp.json();
} catch {
parteien = [];
parties = [];
}
}
@@ -182,7 +213,7 @@ async function loadEvents(id: string) {
async function loadMoreEvents(id: string) {
if (eventsLoadingMore || !eventsHasMore || events.length === 0) return;
const cursor = events[events.length - 1].id;
const btn = document.getElementById("akten-events-loadmore") as HTMLButtonElement | null;
const btn = document.getElementById("project-events-loadmore") as HTMLButtonElement | null;
eventsLoadingMore = true;
if (btn) {
btn.disabled = true;
@@ -193,7 +224,7 @@ async function loadMoreEvents(id: string) {
`/api/projects/${id}/events?before=${encodeURIComponent(cursor)}&limit=${EVENTS_PAGE_SIZE}`,
);
if (resp.ok) {
const page: AkteEvent[] = await resp.json();
const page: ProjectEvent[] = await resp.json();
events = events.concat(page);
eventsHasMore = page.length === EVENTS_PAGE_SIZE;
}
@@ -209,21 +240,21 @@ async function loadMoreEvents(id: string) {
}
}
async function loadFristen(id: string) {
async function loadDeadlines(id: string) {
try {
const resp = await fetch(`/api/projects/${id}/fristen`);
if (resp.ok) fristen = await resp.json();
const resp = await fetch(`/api/projects/${id}/deadlines`);
if (resp.ok) deadlines = await resp.json();
} catch {
fristen = [];
deadlines = [];
}
}
async function loadTermine(id: string) {
async function loadAppointments(id: string) {
try {
const resp = await fetch(`/api/projects/${id}/termine`);
if (resp.ok) termine = await resp.json();
const resp = await fetch(`/api/projects/${id}/appointments`);
if (resp.ok) appointments = await resp.json();
} catch {
termine = [];
appointments = [];
}
}
@@ -243,11 +274,11 @@ function fmtDateTimeLocal(iso: string): string {
}
function renderAppointments() {
const tbody = document.getElementById("project-termine-body");
const empty = document.getElementById("project-termine-empty");
const wrap = document.getElementById("project-termine-tablewrap");
const tbody = document.getElementById("project-appointments-body");
const empty = document.getElementById("project-appointments-empty");
const wrap = document.getElementById("project-appointments-tablewrap");
if (!tbody || !empty || !wrap) return;
if (termine.length === 0) {
if (appointments.length === 0) {
tbody.innerHTML = "";
wrap.style.display = "none";
empty.style.display = "block";
@@ -255,7 +286,7 @@ function renderAppointments() {
}
wrap.style.display = "";
empty.style.display = "none";
tbody.innerHTML = termine
tbody.innerHTML = appointments
.map((tt) => {
const typeLabel = tt.appointment_type ? t(`termine.type.${tt.appointment_type}`) || tt.appointment_type : "";
const typeClass = tt.appointment_type ? `termin-type-${tt.appointment_type}` : "";
@@ -276,17 +307,17 @@ function renderAppointments() {
});
}
function initAkteTerminForm() {
const addBtn = document.getElementById("termin-add-btn") as HTMLButtonElement | null;
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");
function initProjectAppointmentForm() {
const addBtn = document.getElementById("appointment-add-btn") as HTMLButtonElement | null;
const form = document.getElementById("project-appointment-form") as HTMLFormElement | null;
const cancelBtn = document.getElementById("project-appointment-cancel") as HTMLButtonElement | null;
const msg = document.getElementById("project-appointment-msg");
if (!addBtn || !form || !cancelBtn || !msg) return;
addBtn.addEventListener("click", () => {
form.style.display = "";
addBtn.style.display = "none";
(document.getElementById("project-termin-title") as HTMLInputElement).focus();
(document.getElementById("project-appointment-title") as HTMLInputElement).focus();
});
cancelBtn.addEventListener("click", () => {
form.reset();
@@ -298,11 +329,11 @@ function initAkteTerminForm() {
form.addEventListener("submit", async (e) => {
e.preventDefault();
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();
const title = (document.getElementById("project-appointment-title") as HTMLInputElement).value.trim();
const start = (document.getElementById("project-appointment-start") as HTMLInputElement).value;
const end = (document.getElementById("project-appointment-end") as HTMLInputElement).value;
const type = (document.getElementById("project-appointment-type") as HTMLSelectElement).value;
const location = (document.getElementById("project-appointment-location") as HTMLInputElement).value.trim();
if (!title || !start) return;
const payload: Record<string, unknown> = {
@@ -327,7 +358,7 @@ function initAkteTerminForm() {
form.reset();
form.style.display = "none";
addBtn.style.display = "";
await loadTermine(project.id);
await loadAppointments(project.id);
renderAppointments();
await loadEvents(project.id);
renderEvents();
@@ -370,11 +401,11 @@ function urgencyClass(due: string, status: string): string {
}
function renderDeadlines() {
const tbody = document.getElementById("project-fristen-body");
const empty = document.getElementById("project-fristen-empty");
const wrap = document.getElementById("project-fristen-tablewrap");
const tbody = document.getElementById("project-deadlines-body");
const empty = document.getElementById("project-deadlines-empty");
const wrap = document.getElementById("project-deadlines-tablewrap");
if (!tbody || !empty || !wrap) return;
if (fristen.length === 0) {
if (deadlines.length === 0) {
tbody.innerHTML = "";
wrap.style.display = "none";
empty.style.display = "block";
@@ -382,7 +413,7 @@ function renderDeadlines() {
}
wrap.style.display = "";
empty.style.display = "none";
tbody.innerHTML = fristen
tbody.innerHTML = deadlines
.map((f) => {
const urgency = urgencyClass(f.due_date, f.status);
const statusLabel = t(`fristen.status.${f.status}`) || f.status;
@@ -396,7 +427,7 @@ function renderDeadlines() {
</td>
<td class="frist-col-due ${urgency}"><span class="frist-due-dot"></span>${fmtDateOnly(f.due_date)}</td>
<td class="frist-col-title ${titleClass}">${esc(f.title)}</td>
<td class="frist-col-rule">\u2014</td>
<td class="frist-col-rule"></td>
<td><span class="akten-status-chip akten-status-${esc(f.status)}">${esc(statusLabel)}</span></td>
</tr>`;
})
@@ -416,7 +447,7 @@ function renderDeadlines() {
cb.disabled = true;
const resp = await fetch(`/api/deadlines/${id}/complete`, { method: "PATCH" });
if (resp.ok) {
await loadFristen(project.id);
await loadDeadlines(project.id);
renderDeadlines();
await loadEvents(project.id);
renderEvents();
@@ -476,7 +507,7 @@ function renderHeader() {
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
// ClientMatter display. If the project itself has no client_number, walk
// up the ancestor chain to find an inherited one.
const cm = document.getElementById("project-clientmatter")!;
const effectiveClient = project.client_number || inheritedClientNumber();
@@ -519,9 +550,9 @@ function renderHeader() {
}
function renderEvents() {
const list = document.getElementById("akten-events-list")!;
const empty = document.getElementById("akten-events-empty")!;
const moreWrap = document.getElementById("akten-events-loadmore-wrap");
const list = document.getElementById("project-events-list")!;
const empty = document.getElementById("project-events-empty")!;
const moreWrap = document.getElementById("project-events-loadmore-wrap");
if (events.length === 0) {
list.innerHTML = "";
empty.style.display = "block";
@@ -544,18 +575,18 @@ function renderEvents() {
}
function initEventsLoadMore() {
const btn = document.getElementById("akten-events-loadmore");
const btn = document.getElementById("project-events-loadmore");
if (!btn) return;
btn.addEventListener("click", () => {
if (project) void loadMoreEvents(project.id);
});
}
function renderParteien() {
const tbody = document.getElementById("parteien-body")!;
const empty = document.getElementById("parteien-empty")!;
function renderParties() {
const tbody = document.getElementById("parties-body")!;
const empty = document.getElementById("parties-empty")!;
const tableWrap = tbody.closest<HTMLElement>("table")!;
if (parteien.length === 0) {
if (parties.length === 0) {
tbody.innerHTML = "";
tableWrap.style.display = "none";
empty.style.display = "block";
@@ -563,7 +594,7 @@ function renderParteien() {
}
tableWrap.style.display = "";
empty.style.display = "none";
tbody.innerHTML = parteien
tbody.innerHTML = parties
.map((p) => {
const roleKey = p.role ? `akten.detail.parteien.role.${p.role}` : "";
const roleLabel = p.role ? t(roleKey) || p.role : "";
@@ -572,12 +603,12 @@ function renderParteien() {
<td>${esc(roleLabel)}</td>
<td>${esc(p.representative || "")}</td>
<td class="akten-col-actions">
<button type="button" class="btn-link-danger partei-remove" data-i18n="akten.detail.parteien.remove">Entfernen</button>
<button type="button" class="btn-link-danger party-remove" data-i18n="akten.detail.parteien.remove">Entfernen</button>
</td>
</tr>`;
})
.join("");
tbody.querySelectorAll<HTMLButtonElement>(".partei-remove").forEach((btn) => {
tbody.querySelectorAll<HTMLButtonElement>(".party-remove").forEach((btn) => {
btn.textContent = t("akten.detail.parteien.remove");
btn.addEventListener("click", async () => {
const row = btn.closest<HTMLTableRowElement>("tr")!;
@@ -585,8 +616,8 @@ function renderParteien() {
if (!confirm(t("akten.detail.parteien.remove.confirm"))) return;
const resp = await fetch(`/api/parties/${id}`, { method: "DELETE" });
if (resp.ok && project) {
await loadParteien(project.id);
renderParteien();
await loadParties(project.id);
renderParties();
}
});
});
@@ -606,18 +637,18 @@ function showTab(tab: TabId) {
window.history.replaceState({}, "", newPath);
}
}
if (tab === "checklisten" && project) {
if (tab === "checklists" && project) {
void loadAndRenderChecklistInstances(project.id);
}
}
let checklistInstancesInited = false;
async function loadAndRenderChecklistInstances(akteID: string) {
async function loadAndRenderChecklistInstances(projectID: string) {
if (checklistInstancesInited) return;
checklistInstancesInited = true;
try {
const [instResp, tplResp] = await Promise.all([
fetch(`/api/projects/${akteID}/checklisten`),
fetch(`/api/projects/${projectID}/checklists`),
fetch(`/api/checklists`),
]);
checklistInstances = instResp.ok ? await instResp.json() : [];
@@ -631,9 +662,9 @@ async function loadAndRenderChecklistInstances(akteID: string) {
}
function renderChecklistInstances() {
const body = document.getElementById("project-checklisten-body");
const empty = document.getElementById("project-checklisten-empty");
const wrap = document.getElementById("project-checklisten-tablewrap");
const body = document.getElementById("project-checklists-body");
const empty = document.getElementById("project-checklists-empty");
const wrap = document.getElementById("project-checklists-tablewrap");
if (!body || !empty || !wrap) return;
if (checklistInstances.length === 0) {
@@ -752,16 +783,16 @@ function initTitleEdit() {
}
}
function initParteienForm() {
const addBtn = document.getElementById("partei-add-btn") as HTMLButtonElement;
const form = document.getElementById("partei-form") as HTMLFormElement;
const cancelBtn = document.getElementById("partei-cancel") as HTMLButtonElement;
const msg = document.getElementById("partei-msg")!;
function initPartiesForm() {
const addBtn = document.getElementById("party-add-btn") as HTMLButtonElement;
const form = document.getElementById("party-form") as HTMLFormElement;
const cancelBtn = document.getElementById("party-cancel") as HTMLButtonElement;
const msg = document.getElementById("party-msg")!;
addBtn.addEventListener("click", () => {
form.style.display = "";
addBtn.style.display = "none";
(document.getElementById("partei-name") as HTMLInputElement).focus();
(document.getElementById("party-name") as HTMLInputElement).focus();
});
cancelBtn.addEventListener("click", () => {
@@ -774,9 +805,9 @@ function initParteienForm() {
form.addEventListener("submit", async (e) => {
e.preventDefault();
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();
const name = (document.getElementById("party-name") as HTMLInputElement).value.trim();
const role = (document.getElementById("party-role") as HTMLSelectElement).value;
const rep = (document.getElementById("party-rep") as HTMLInputElement).value.trim();
if (!name) return;
msg.textContent = "";
@@ -787,7 +818,7 @@ function initParteienForm() {
if (rep) payload.representative = rep;
try {
const resp = await fetch(`/api/projects/${project.id}/parteien`, {
const resp = await fetch(`/api/projects/${project.id}/parties`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
@@ -796,8 +827,8 @@ function initParteienForm() {
form.reset();
form.style.display = "none";
addBtn.style.display = "";
await loadParteien(project.id);
renderParteien();
await loadParties(project.id);
renderParties();
await loadEvents(project.id);
renderEvents();
} else {
@@ -814,10 +845,10 @@ function initParteienForm() {
});
}
function initFristAddLink() {
function initDeadlineAddLink() {
if (!project) return;
const link = document.getElementById("frist-add-link") as HTMLAnchorElement | null;
if (link) link.href = `/projects/${project.id}/fristen/neu`;
const link = document.getElementById("deadline-add-link") as HTMLAnchorElement | null;
if (link) link.href = `/projects/${project.id}/deadlines/new`;
}
function initDelete() {
@@ -852,10 +883,10 @@ function initDelete() {
}
async function main() {
const id = parseAkteID();
const loading = document.getElementById("akten-detail-loading")!;
const notfound = document.getElementById("akten-detail-notfound")!;
const body = document.getElementById("akten-detail-body")!;
const id = parseProjectID();
const loading = document.getElementById("project-detail-loading")!;
const notfound = document.getElementById("project-detail-notfound")!;
const body = document.getElementById("project-detail-body")!;
if (!id) {
loading.style.display = "none";
@@ -864,7 +895,7 @@ async function main() {
}
await loadMe();
const ok = await loadAkte(id);
const ok = await loadProject(id);
if (!ok || !project) {
loading.style.display = "none";
notfound.style.display = "block";
@@ -872,10 +903,10 @@ async function main() {
}
await Promise.all([
loadParteien(id),
loadParties(id),
loadEvents(id),
loadFristen(id),
loadTermine(id),
loadDeadlines(id),
loadAppointments(id),
loadAncestors(id),
loadChildren(id),
loadTeam(id),
@@ -886,18 +917,18 @@ async function main() {
body.style.display = "";
renderHeader();
renderBreadcrumb();
renderParteien();
renderParties();
renderEvents();
renderDeadlines();
renderAppointments();
renderChildren();
renderTeam();
initFristAddLink();
initDeadlineAddLink();
initChildAddLink();
initTabs();
initTitleEdit();
initParteienForm();
initAkteTerminForm();
initPartiesForm();
initProjectAppointmentForm();
initTeamForm(id);
initDelete();
initEventsLoadMore();
@@ -909,7 +940,7 @@ async function main() {
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.
// client_number for display when the project itself has none.
for (let i = ancestors.length - 1; i >= 0; i--) {
const a = ancestors[i] as ProjectMini & { client_number?: string | null };
if (a.client_number) return a.client_number;
@@ -928,14 +959,14 @@ async function loadAncestors(id: string) {
function renderBreadcrumb() {
if (!project) return;
const el = document.getElementById("projekt-breadcrumb");
const el = document.getElementById("project-breadcrumb");
if (!el) return;
const parts: string[] = ancestors.map(
(a) =>
`<a href="/projects/${esc(a.id)}" class="projekt-crumb">${esc(a.title)}</a>`,
);
parts.push(`<span class="projekt-crumb projekt-crumb-current">${esc(project.title)}</span>`);
el.innerHTML = parts.join(`<span class="projekt-crumb-sep">\u203A</span>`);
el.innerHTML = parts.join(`<span class="projekt-crumb-sep"></span>`);
}
// ----- Children -----------------------------------------------------------
@@ -950,8 +981,8 @@ async function loadChildren(id: string) {
}
function renderChildren() {
const list = document.getElementById("kinder-list")!;
const empty = document.getElementById("kinder-empty")!;
const list = document.getElementById("children-list")!;
const empty = document.getElementById("children-empty")!;
if (!children.length) {
list.innerHTML = "";
empty.style.display = "";
@@ -976,7 +1007,7 @@ function initChildAddLink() {
const link = document.getElementById("child-add-link") as HTMLAnchorElement | null;
if (!link || !project) return;
// Pre-fill parent_id for the create form via query param.
link.href = `/projects/neu?parent=${encodeURIComponent(project.id)}`;
link.href = `/projects/new?parent=${encodeURIComponent(project.id)}`;
}
// ----- Team tab -----------------------------------------------------------
@@ -984,7 +1015,7 @@ function initChildAddLink() {
async function loadTeam(id: string) {
try {
const resp = await fetch(`/api/projects/${id}/team`);
if (resp.ok) teamMembers = (await resp.json()) as ProjektTeamMember[];
if (resp.ok) teamMembers = (await resp.json()) as ProjectTeamMember[];
} catch {
teamMembers = [];
}
@@ -1047,7 +1078,7 @@ function renderTeam() {
});
}
function canRemoveTeamMember(m: ProjektTeamMember): boolean {
function canRemoveTeamMember(m: ProjectTeamMember): boolean {
if (!me) return false;
if (m.user_id === me.id) return true;
return me.role === "partner" || me.role === "admin";
@@ -1109,7 +1140,7 @@ function initTeamForm(id: string) {
e.preventDefault();
msg.textContent = "";
if (!hidden.value) {
msg.textContent = t("projekte.detail.team.error.user_required") || "Benutzer ausw\u00e4hlen";
msg.textContent = t("projekte.detail.team.error.user_required") || "Benutzer auswählen";
return;
}
const resp = await fetch(`/api/projects/${id}/team`, {
@@ -1132,16 +1163,16 @@ function initTeamForm(id: string) {
});
}
// initNotesContainer hooks the shared Notes module into the Akten detail's
// initNotesContainer hooks the shared Notes module into the project detail's
// Notizen tab. Called once per page load — the list lazy-fetches so other
// tabs aren't slowed down by the notes query on initial render.
let notesInited = false;
function initNotesContainer(akteID: string) {
function initNotesContainer(projectID: string) {
if (notesInited) return;
const container = document.getElementById("notes-container");
if (!container) return;
container.setAttribute("data-parent-id", akteID);
void initNotes(container as HTMLElement, "project", akteID);
container.setAttribute("data-parent-id", projectID);
void initNotes(container as HTMLElement, "project", projectID);
notesInited = true;
}
@@ -1152,7 +1183,7 @@ document.addEventListener("DOMContentLoaded", () => {
renderHeader();
renderBreadcrumb();
renderEvents();
renderParteien();
renderParties();
renderDeadlines();
renderAppointments();
renderChildren();

View File

@@ -2,10 +2,10 @@ import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer";
// Projekt detail shell (v2). File name + export kept for build-pipeline
// compatibility; DOM + labels are v2 (reference not aktenzeichen, type chip,
// breadcrumb, Team tab with inheritance badges, children section,
// ClientMatter + netDocuments display).
// Project detail shell (v2). DOM IDs use the English `project-*` /
// `parties-*` / `deadlines-*` / `appointments-*` / `notes-*` / `checklists-*`
// naming. The client TS in client/projects-detail.ts queries these IDs in
// lockstep — keep names in sync.
export function renderProjectsDetail(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
@@ -23,17 +23,17 @@ export function renderProjectsDetail(): string {
<div className="container">
<a href="/projects" className="akten-back-link" data-i18n="projekte.detail.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a>
<nav className="projekt-breadcrumb" id="projekt-breadcrumb" aria-label="Breadcrumb" />
<nav className="projekt-breadcrumb" id="project-breadcrumb" aria-label="Breadcrumb" />
<div id="akten-detail-loading" className="akten-loading">
<div id="project-detail-loading" className="akten-loading">
<p data-i18n="projekte.detail.loading">L&auml;dt&hellip;</p>
</div>
<div id="akten-detail-notfound" className="akten-empty" style="display:none">
<div id="project-detail-notfound" className="akten-empty" style="display:none">
<p data-i18n="projekte.detail.notfound">Projekt nicht gefunden oder keine Berechtigung.</p>
</div>
<div id="akten-detail-body" style="display:none">
<div id="project-detail-body" style="display:none">
<header className="akten-detail-header">
<div className="akten-detail-title-row">
<div className="akten-detail-title-col">
@@ -66,24 +66,24 @@ export function renderProjectsDetail(): string {
</div>
<nav className="akten-tabs" id="project-tabs">
<a className="akten-tab" data-tab="verlauf" href="#" data-i18n="projekte.detail.tab.verlauf">Verlauf</a>
<a className="akten-tab" data-tab="history" href="#" data-i18n="projekte.detail.tab.verlauf">Verlauf</a>
<a className="akten-tab" data-tab="team" href="#" data-i18n="projekte.detail.tab.team">Team</a>
<a className="akten-tab" data-tab="kinder" href="#" data-i18n="projekte.detail.tab.kinder">Untergeordnet</a>
<a className="akten-tab" data-tab="parteien" href="#" data-i18n="projekte.detail.tab.parteien">Parteien</a>
<a className="akten-tab" data-tab="fristen" href="#" data-i18n="projekte.detail.tab.fristen">Fristen</a>
<a className="akten-tab" data-tab="termine" href="#" data-i18n="projekte.detail.tab.termine">Termine</a>
<a className="akten-tab" data-tab="notizen" href="#" data-i18n="projekte.detail.tab.notizen">Notizen</a>
<a className="akten-tab" data-tab="checklisten" href="#" data-i18n="projekte.detail.tab.checklisten">Checklisten</a>
<a className="akten-tab" data-tab="children" href="#" data-i18n="projekte.detail.tab.kinder">Untergeordnet</a>
<a className="akten-tab" data-tab="parties" href="#" data-i18n="projekte.detail.tab.parteien">Parteien</a>
<a className="akten-tab" data-tab="deadlines" href="#" data-i18n="projekte.detail.tab.fristen">Fristen</a>
<a className="akten-tab" data-tab="appointments" href="#" data-i18n="projekte.detail.tab.termine">Termine</a>
<a className="akten-tab" data-tab="notes" href="#" data-i18n="projekte.detail.tab.notizen">Notizen</a>
<a className="akten-tab" data-tab="checklists" href="#" data-i18n="projekte.detail.tab.checklisten">Checklisten</a>
</nav>
{/* Verlauf */}
<section className="akten-tab-panel" id="tab-verlauf">
<ul className="akten-events" id="akten-events-list" />
<p className="akten-events-empty" id="akten-events-empty" style="display:none" data-i18n="projekte.detail.verlauf.empty">
{/* History (Verlauf) */}
<section className="akten-tab-panel" id="tab-history">
<ul className="akten-events" id="project-events-list" />
<p className="akten-events-empty" id="project-events-empty" style="display:none" data-i18n="projekte.detail.verlauf.empty">
Noch keine Ereignisse aufgezeichnet.
</p>
<div className="akten-events-loadmore" id="akten-events-loadmore-wrap" style="display:none">
<button type="button" className="btn-secondary" id="akten-events-loadmore" data-i18n="projekte.detail.verlauf.loadMore">
<div className="akten-events-loadmore" id="project-events-loadmore-wrap" style="display:none">
<button type="button" className="btn-secondary" id="project-events-loadmore" data-i18n="projekte.detail.verlauf.loadMore">
Mehr laden
</button>
</div>
@@ -142,36 +142,36 @@ export function renderProjectsDetail(): string {
</p>
</section>
{/* Untergeordnet (children tree) */}
<section className="akten-tab-panel" id="tab-kinder" style="display:none">
{/* Children (Untergeordnet) */}
<section className="akten-tab-panel" id="tab-children" style="display:none">
<div className="akten-parteien-controls">
<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>
<ul id="kinder-list" className="projekt-children-list" />
<p className="akten-events-empty" id="kinder-empty" style="display:none" data-i18n="projekte.detail.kinder.empty">
<ul id="children-list" className="projekt-children-list" />
<p className="akten-events-empty" id="children-empty" style="display:none" data-i18n="projekte.detail.kinder.empty">
Keine untergeordneten Projekte.
</p>
</section>
{/* Parteien */}
<section className="akten-tab-panel" id="tab-parteien" style="display:none">
{/* Parties (Parteien) */}
<section className="akten-tab-panel" id="tab-parties" style="display:none">
<div className="akten-parteien-controls">
<button id="partei-add-btn" className="btn-primary btn-cta-lime btn-small" type="button" data-i18n="projekte.detail.parteien.add">
<button id="party-add-btn" className="btn-primary btn-cta-lime btn-small" type="button" data-i18n="projekte.detail.parteien.add">
Partei hinzuf&uuml;gen
</button>
</div>
<form id="partei-form" className="akten-form akten-partei-form" style="display:none" autocomplete="off">
<form id="party-form" className="akten-form akten-partei-form" style="display:none" autocomplete="off">
<div className="form-field-row">
<div className="form-field">
<label htmlFor="partei-name" data-i18n="projekte.detail.parteien.form.name">Name</label>
<input type="text" id="partei-name" required />
<label htmlFor="party-name" data-i18n="projekte.detail.parteien.form.name">Name</label>
<input type="text" id="party-name" required />
</div>
<div className="form-field">
<label htmlFor="partei-role" data-i18n="projekte.detail.parteien.form.role">Rolle</label>
<select id="partei-role">
<label htmlFor="party-role" data-i18n="projekte.detail.parteien.form.role">Rolle</label>
<select id="party-role">
<option value="claimant" data-i18n="projekte.detail.parteien.role.claimant">Kl&auml;ger</option>
<option value="defendant" data-i18n="projekte.detail.parteien.role.defendant">Beklagter</option>
<option value="thirdparty" data-i18n="projekte.detail.parteien.role.thirdparty">Streitverk&uuml;ndeter / Drittpartei</option>
@@ -179,14 +179,14 @@ export function renderProjectsDetail(): string {
</div>
</div>
<div className="form-field">
<label htmlFor="partei-rep" data-i18n="projekte.detail.parteien.form.rep">Vertreter (optional)</label>
<input type="text" id="partei-rep" />
<label htmlFor="party-rep" data-i18n="projekte.detail.parteien.form.rep">Vertreter (optional)</label>
<input type="text" id="party-rep" />
</div>
<div className="form-actions">
<button type="button" className="btn-cancel" id="partei-cancel" data-i18n="projekte.detail.parteien.form.cancel">Abbrechen</button>
<button type="button" className="btn-cancel" id="party-cancel" data-i18n="projekte.detail.parteien.form.cancel">Abbrechen</button>
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="projekte.detail.parteien.form.submit">Hinzuf&uuml;gen</button>
</div>
<p className="form-msg" id="partei-msg" />
<p className="form-msg" id="party-msg" />
</form>
<table className="akten-parteien-table">
@@ -198,22 +198,22 @@ export function renderProjectsDetail(): string {
<th />
</tr>
</thead>
<tbody id="parteien-body" />
<tbody id="parties-body" />
</table>
<p className="akten-events-empty" id="parteien-empty" style="display:none" data-i18n="projekte.detail.parteien.empty">
<p className="akten-events-empty" id="parties-empty" style="display:none" data-i18n="projekte.detail.parteien.empty">
Noch keine Parteien eingetragen.
</p>
</section>
{/* Fristen */}
<section className="akten-tab-panel" id="tab-fristen" style="display:none">
{/* Deadlines (Fristen) */}
<section className="akten-tab-panel" id="tab-deadlines" style="display:none">
<div className="akten-parteien-controls">
<a id="frist-add-link" className="btn-primary btn-cta-lime btn-small" data-i18n="projekte.detail.fristen.add" href="#">
<a id="deadline-add-link" className="btn-primary btn-cta-lime btn-small" data-i18n="projekte.detail.fristen.add" href="#">
Frist hinzuf&uuml;gen
</a>
</div>
<div className="akten-table-wrap" id="project-fristen-tablewrap">
<div className="akten-table-wrap" id="project-deadlines-tablewrap">
<table className="akten-table fristen-table">
<thead>
<tr>
@@ -224,31 +224,31 @@ export function renderProjectsDetail(): string {
<th data-i18n="fristen.col.status">Status</th>
</tr>
</thead>
<tbody id="project-fristen-body" />
<tbody id="project-deadlines-body" />
</table>
</div>
<p className="akten-events-empty" id="project-fristen-empty" style="display:none" data-i18n="projekte.detail.fristen.empty">
<p className="akten-events-empty" id="project-deadlines-empty" style="display:none" data-i18n="projekte.detail.fristen.empty">
F&uuml;r dieses Projekt sind noch keine Fristen erfasst.
</p>
</section>
{/* Termine */}
<section className="akten-tab-panel" id="tab-termine" style="display:none">
{/* Appointments (Termine) */}
<section className="akten-tab-panel" id="tab-appointments" style="display:none">
<div className="akten-parteien-controls">
<button type="button" id="termin-add-btn" className="btn-primary btn-cta-lime btn-small" data-i18n="projekte.detail.termine.add">
<button type="button" id="appointment-add-btn" className="btn-primary btn-cta-lime btn-small" data-i18n="projekte.detail.termine.add">
Termin hinzuf&uuml;gen
</button>
</div>
<form id="project-termin-form" className="akten-partei-form" style="display:none">
<form id="project-appointment-form" className="akten-partei-form" style="display:none">
<div className="form-field-row">
<div className="form-field">
<label htmlFor="project-termin-title" data-i18n="termine.field.title">Titel</label>
<input type="text" id="project-termin-title" required />
<label htmlFor="project-appointment-title" data-i18n="termine.field.title">Titel</label>
<input type="text" id="project-appointment-title" required />
</div>
<div className="form-field">
<label htmlFor="project-termin-type" data-i18n="termine.field.type">Typ</label>
<select id="project-termin-type">
<label htmlFor="project-appointment-type" data-i18n="termine.field.type">Typ</label>
<select id="project-appointment-type">
<option value="" data-i18n="termine.field.type.none">Kein Typ</option>
<option value="hearing" data-i18n="termine.type.hearing">Verhandlung</option>
<option value="meeting" data-i18n="termine.type.meeting">Besprechung</option>
@@ -259,26 +259,26 @@ export function renderProjectsDetail(): string {
</div>
<div className="form-field-row">
<div className="form-field">
<label htmlFor="project-termin-start" data-i18n="termine.field.start">Beginn</label>
<input type="datetime-local" id="project-termin-start" required />
<label htmlFor="project-appointment-start" data-i18n="termine.field.start">Beginn</label>
<input type="datetime-local" id="project-appointment-start" required />
</div>
<div className="form-field">
<label htmlFor="project-termin-end" data-i18n="termine.field.end">Ende (optional)</label>
<input type="datetime-local" id="project-termin-end" />
<label htmlFor="project-appointment-end" data-i18n="termine.field.end">Ende (optional)</label>
<input type="datetime-local" id="project-appointment-end" />
</div>
</div>
<div className="form-field">
<label htmlFor="project-termin-location" data-i18n="termine.field.location">Ort</label>
<input type="text" id="project-termin-location" />
<label htmlFor="project-appointment-location" data-i18n="termine.field.location">Ort</label>
<input type="text" id="project-appointment-location" />
</div>
<div className="form-actions">
<button type="button" className="btn-cancel" id="project-termin-cancel" data-i18n="projekte.detail.termine.form.cancel">Abbrechen</button>
<button type="button" className="btn-cancel" id="project-appointment-cancel" data-i18n="projekte.detail.termine.form.cancel">Abbrechen</button>
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="projekte.detail.termine.form.submit">Hinzuf&uuml;gen</button>
</div>
<p className="form-msg" id="project-termin-msg" />
<p className="form-msg" id="project-appointment-msg" />
</form>
<div className="akten-table-wrap" id="project-termine-tablewrap">
<div className="akten-table-wrap" id="project-appointments-tablewrap">
<table className="akten-table fristen-table">
<thead>
<tr>
@@ -289,25 +289,25 @@ export function renderProjectsDetail(): string {
<th data-i18n="termine.col.type">Typ</th>
</tr>
</thead>
<tbody id="project-termine-body" />
<tbody id="project-appointments-body" />
</table>
</div>
<p className="akten-events-empty" id="project-termine-empty" style="display:none" data-i18n="projekte.detail.termine.empty">
<p className="akten-events-empty" id="project-appointments-empty" style="display:none" data-i18n="projekte.detail.termine.empty">
F&uuml;r dieses Projekt sind noch keine Termine erfasst.
</p>
</section>
{/* Notizen */}
<section className="akten-tab-panel" id="tab-notizen" style="display:none">
<div id="notes-container" className="notiz-container" data-parent-type="projekt" />
{/* Notes (Notizen) */}
<section className="akten-tab-panel" id="tab-notes" style="display:none">
<div id="notes-container" className="notiz-container" data-parent-type="project" />
</section>
{/* Checklisten */}
<section className="akten-tab-panel" id="tab-checklisten" style="display:none">
<p id="project-checklisten-empty" className="akten-events-empty" style="display:none" data-i18n="projekte.detail.checklisten.empty">
{/* Checklists (Checklisten) */}
<section className="akten-tab-panel" id="tab-checklists" style="display:none">
<p id="project-checklists-empty" className="akten-events-empty" style="display:none" data-i18n="projekte.detail.checklisten.empty">
F&uuml;r dieses Projekt sind noch keine Checklisten-Instanzen erfasst.
</p>
<div className="akten-table-wrap" id="project-checklisten-tablewrap" style="display:none">
<div className="akten-table-wrap" id="project-checklists-tablewrap" style="display:none">
<table className="akten-table">
<thead>
<tr>
@@ -317,7 +317,7 @@ export function renderProjectsDetail(): string {
<th data-i18n="projekte.detail.checklisten.col.created">Angelegt</th>
</tr>
</thead>
<tbody id="project-checklisten-body" />
<tbody id="project-checklists-body" />
</table>
</div>
<p className="tool-subtitle akten-checklisten-hint" data-i18n="projekte.detail.checklisten.hint">