Merge: /projects/{id} notfound fix + German DOM/URL rename (t-paliad-038)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()]);
|
||||
});
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">← Zurück zur Ü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ädt…</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ü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ä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ü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ü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ü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ü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ü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ü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ü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ü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">
|
||||
|
||||
Reference in New Issue
Block a user