From cf94f0ca2503aaf126488772c20ee66213a20e9e Mon Sep 17 00:00:00 2001 From: m Date: Sun, 26 Apr 2026 01:04:07 +0200 Subject: [PATCH] fix(projects-detail): /projects/{id} notfound + rename German DOM/URL leftovers (t-paliad-038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the URGENT bug: parseAkteID() in frontend/src/client/projects-detail.ts only accepted /projekte/{id} and /akten/{id} URL prefixes. After t-paliad-025 renamed pages to /projects/{id}, parts[0] === "projects" failed both checks → null id → notfound branch fired before any /api/projects/{id} fetch. The 200 from curl was real; the page just never asked. Fix: parseProjectID() now reads /projects/{id}. Old bookmark tab slugs (verlauf, parteien, fristen, …) are mapped to their English successors so deep links don't silently fall back to the default tab. Bundled cleanup — every per-project subpath the client TS still hit was a 404 because the rename only touched top-level routes. Lockstep rename of URLs, function names, DOM IDs, and the TabId union in projects-detail.ts + projects-detail.tsx: - /api/projects/{id}/parteien|fristen|termine|notizen|checklisten → /parties|deadlines|appointments|notes|checklists - loadParteien/loadFristen/loadTermine/loadAkte/parseAkteID → loadParties/loadDeadlines/loadAppointments/loadProject/parseProjectID (the old loadParteien/loadFristen/loadTermine bodies even assigned to undeclared `parteien`/`fristen`/`termine` — would have thrown ReferenceError as soon as the catch branch ran) - DOM IDs: akten-detail-* → project-detail-*, parteien-* → parties-*, partei-* → party-*, project-fristen-* → project-deadlines-*, project-termin(e)-* → project-appointment(s)-*, project-checklisten-* → project-checklists-*, akten-events-* → project-events-*, kinder-* → children-*, projekt-breadcrumb → project-breadcrumb, frist-add-link → deadline-add-link, termin-add-btn → appointment-add-btn - Tab slugs in URL + data-tab + tab-* IDs: verlauf/kinder/parteien/ fristen/termine/notizen/checklisten → history/children/parties/deadlines/appointments/notes/checklists - frist-add-link href: /projects/{id}/fristen/neu → /projects/{id}/deadlines/new Sweep across the rest of frontend/src/client/: - notes.ts: NotizParentType → NotesParentType, "frist"/"termin" → "deadline"/"appointment", baseURL paths /…/notizen → /…/notes; updated callers in deadlines-detail.ts and appointments-detail.ts. - deadlines-new.ts: undeclared `akten` reference (loadAkten was assigning to a never-declared name) replaced with `projects`; URL /…/fristen → /…/deadlines; path-parsing of /akten/{id}/fristen/neu rewritten as /projects/{id}/deadlines/new; preselectedAkteID → preselectedProjectID; Project.aktenzeichen field (no longer emitted by API) → reference. - fristenrechner.ts: bulk endpoint /…/fristen/bulk → /…/deadlines/bulk; request body { fristen } → { deadlines } (server expects "deadlines" key); ProjectOption interface now uses reference instead of aktenzeichen. - deadlines.ts, appointments.ts, deadlines-detail.ts, appointments-detail.ts, checklists-detail.ts, appointments-new.ts: Project interface field aktenzeichen → reference (the API returns "reference"; the old field rendered as undefined in select options and detail headers). i18n key strings (akten.detail.*, projekte.*, fristen.*, termine.*, checklisten.*, notizen.*) intentionally kept in German per the t-paliad-025 convention. CSS class names (frist-row, akten-table-wrap, termin-dot, etc.) untouched — separate stylistic cleanup. Verified: go build/vet/test clean, bun run build clean, dist HTML + bundled JS contain only the new English IDs (remaining German strings are i18n keys). --- frontend/src/client/appointments-detail.ts | 6 +- frontend/src/client/appointments-new.ts | 4 +- frontend/src/client/appointments.ts | 4 +- frontend/src/client/checklists-detail.ts | 4 +- frontend/src/client/deadlines-detail.ts | 6 +- frontend/src/client/deadlines-new.ts | 43 ++-- frontend/src/client/deadlines.ts | 4 +- frontend/src/client/fristenrechner.ts | 38 +-- frontend/src/client/notes.ts | 24 +- frontend/src/client/projects-detail.ts | 255 ++++++++++++--------- frontend/src/projects-detail.tsx | 144 ++++++------ 11 files changed, 282 insertions(+), 250 deletions(-) diff --git a/frontend/src/client/appointments-detail.ts b/frontend/src/client/appointments-detail.ts index 1235267..09f01aa 100644 --- a/frontend/src/client/appointments-detail.ts +++ b/frontend/src/client/appointments-detail.ts @@ -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); } } diff --git a/frontend/src/client/appointments-new.ts b/frontend/src/client/appointments-new.ts index 4e3076a..e710577 100644 --- a/frontend/src/client/appointments-new.ts +++ b/frontend/src/client/appointments-new.ts @@ -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( - ``, + ``, ); } sel.innerHTML = opts.join(""); diff --git a/frontend/src/client/appointments.ts b/frontend/src/client/appointments.ts index 8cff56e..d6e5235 100644 --- a/frontend/src/client/appointments.ts +++ b/frontend/src/client/appointments.ts @@ -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( - ``, + ``, ); } sel.innerHTML = options.join(""); diff --git a/frontend/src/client/checklists-detail.ts b/frontend/src/client/checklists-detail.ts index d0e1e06..be1f255 100644 --- a/frontend/src/client/checklists-detail.ts +++ b/frontend/src/client/checklists-detail.ts @@ -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); }); } diff --git a/frontend/src/client/deadlines-detail.ts b/frontend/src/client/deadlines-detail.ts index ae5dc11..c12d811 100644 --- a/frontend/src/client/deadlines-detail.ts +++ b/frontend/src/client/deadlines-detail.ts @@ -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); } } diff --git a/frontend/src/client/deadlines-new.ts b/frontend/src/client/deadlines-new.ts index d85bccc..3129d92 100644 --- a/frontend/src/client/deadlines-new.ts +++ b/frontend/src/client/deadlines-new.ts @@ -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"))} ${esc(t("fristen.field.project.empty.link"))}`; return; } const options: string[] = [ - ``, + ``, ]; - 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( - ``, + ``, ); } 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("#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()]); }); diff --git a/frontend/src/client/deadlines.ts b/frontend/src/client/deadlines.ts index b26f531..bc1f6f2 100644 --- a/frontend/src/client/deadlines.ts +++ b/frontend/src/client/deadlines.ts @@ -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( - ``, + ``, ); } sel.innerHTML = options.join(""); diff --git a/frontend/src/client/fristenrechner.ts b/frontend/src/client/fristenrechner.ts index a97b364..41437c7 100644 --- a/frontend/src/client/fristenrechner.ts +++ b/frontend/src/client/fristenrechner.ts @@ -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 { +async function fetchProjects(): Promise { 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) => ``) + sel.innerHTML = projects + .map((p) => ``) .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("#frist-save-list input[type=checkbox]"); - const fristen: Array> = []; + const deadlinesPayload: Array> = []; 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"))} ${escHtml(t("fristen.save.success.link"))}`; + msg.innerHTML = `${escHtml(t("fristen.save.success"))} ${escHtml(t("fristen.save.success.link"))}`; 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(() => { diff --git a/frontend/src/client/notes.ts b/frontend/src/client/notes.ts index c619994..e515b8a 100644 --- a/frontend/src/client/notes.ts +++ b/frontend/src/client/notes.ts @@ -1,5 +1,5 @@ -// Shared polymorphic notes module. Each detail page (akten / fristen / -// termine) renders an empty
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 { 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); diff --git a/frontend/src/client/projects-detail.ts b/frontend/src/client/projects-detail.ts index 5750655..a93dc2a 100644 --- a/frontend/src/client/projects-detail.ts +++ b/frontend/src/client/projects-detail.ts @@ -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 = { + 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 = {}; 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 { +async function loadProject(id: string): Promise { try { const resp = await fetch(`/api/projects/${id}`); if (!resp.ok) return false; @@ -154,12 +185,12 @@ async function loadAkte(id: string): Promise { } } -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 = { @@ -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() { ${fmtDateOnly(f.due_date)} ${esc(f.title)} - \u2014 + — ${esc(statusLabel)} `; }) @@ -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("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() { ${esc(roleLabel)} ${esc(p.representative || "")} - + `; }) .join(""); - tbody.querySelectorAll(".partei-remove").forEach((btn) => { + tbody.querySelectorAll(".party-remove").forEach((btn) => { btn.textContent = t("akten.detail.parteien.remove"); btn.addEventListener("click", async () => { const row = btn.closest("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) => `${esc(a.title)}`, ); parts.push(`${esc(project.title)}`); - el.innerHTML = parts.join(`\u203A`); + el.innerHTML = parts.join(``); } // ----- 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(); diff --git a/frontend/src/projects-detail.tsx b/frontend/src/projects-detail.tsx index e27e47c..0a8e60a 100644 --- a/frontend/src/projects-detail.tsx +++ b/frontend/src/projects-detail.tsx @@ -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 "" + ( @@ -23,17 +23,17 @@ export function renderProjectsDetail(): string {
← Zurück zur Übersicht -