refactor(rename): frontend TSX + client TS files, fetch URLs, nav hrefs

t-paliad-025 Phase 3 — frontend rename pass:

File renames (git mv, preserving history):
  frontend/src/
    akten.tsx               → projects.tsx
    akten-neu.tsx           → projects-new.tsx
    akten-detail.tsx        → projects-detail.tsx
    fristen.tsx             → deadlines.tsx
    fristen-neu.tsx         → deadlines-new.tsx
    fristen-detail.tsx      → deadlines-detail.tsx
    fristen-kalender.tsx    → deadlines-calendar.tsx
    termine.tsx             → appointments.tsx
    termine-neu.tsx         → appointments-new.tsx
    termine-detail.tsx      → appointments-detail.tsx
    termine-kalender.tsx    → appointments-calendar.tsx
    einstellungen.tsx       → settings.tsx
    checklisten*.tsx        → checklists*.tsx
    gerichte.tsx            → courts.tsx
    glossar.tsx             → glossary.tsx

  frontend/src/client/ — same renames, plus notizen.ts → notes.ts.

Render exports renamed (renderAkten → renderProjects, renderFristen →
renderDeadlines, …). build.ts rewired to new names.

Client-side changes:
* fetch() API paths: /api/projekte → /api/projects, /api/fristen →
  /api/deadlines, /api/termine → /api/appointments, /api/notizen →
  /api/notes, /api/gerichte → /api/courts, /api/glossar → /api/glossary,
  /api/dezernate → /api/departments, /api/parteien → /api/parties,
  /api/checklisten → /api/checklists. Legacy /api/akten aliases removed.
* Navigation href/template strings: /akten → /projects, /fristen →
  /deadlines, /termine → /appointments, /einstellungen → /settings,
  /notizen → /notes, /checklisten → /checklists, /gerichte → /courts,
  /glossar → /glossary. Nested paths /neu → /new, /verlauf → /events,
  /kinder → /children, /kalender → /calendar, /dokumente → /documents.
* Interface names in client TS: Frist → Deadline, Termin → Appointment,
  Notiz → Note, Partei → Party, Akte → Project, ProjektMini → ProjectMini,
  Dezernat → Department, DezernatMitglied → DepartmentMember.
* JSON wire-format keys follow backend: projekt_id → project_id, akte_id
  → project_id, frist_id → deadline_id, termin_id → appointment_id,
  akten_event_id → project_event_id, dezernat_id → department_id,
  termin_type → appointment_type.

Go handlers (projects_pages.go, deadlines_pages.go, appointments_pages.go,
checklists.go, courts.go, glossary.go) serve the correctly-named HTML
files from dist/.

Kept German (user-facing i18n + product names):
* i18n keys/strings (src/client/i18n.ts) — DE labels and their keys
* Product names: fristenrechner, kostenrechner, gebuehrentabellen

Build verified: go build / vet / test clean; bun run build clean;
dist/ contains all 26 English-named HTML pages.
This commit is contained in:
m
2026-04-20 17:44:45 +02:00
parent 49c6bc75ca
commit caf319e7ee
45 changed files with 544 additions and 544 deletions

View File

@@ -0,0 +1,265 @@
import { initI18n, onLangChange, t, getLang } from "./i18n";
import { initSidebar } from "./sidebar";
interface Deadline {
id: string;
project_id: string;
title: string;
due_date: string;
status: string;
source: string;
rule_id?: string;
project_reference: string;
project_title: string;
projekt_office: string;
rule_code?: string;
}
interface Project {
id: string;
aktenzeichen: string;
title: string;
}
interface Summary {
overdue: number;
this_week: number;
upcoming: number;
completed: number;
total: number;
}
let allDeadlines: Deadline[] = [];
let allProjects: Project[] = [];
let statusFilter = "pending";
let akteFilter = "";
let loadedOK = false;
function urlParams(): URLSearchParams {
return new URLSearchParams(window.location.search);
}
async function loadAkten() {
try {
const resp = await fetch("/api/projects");
if (resp.ok) allProjects = await resp.json();
} catch {
/* non-fatal */
}
}
async function loadSummary() {
try {
const url = akteFilter
? `/api/deadlines/summary?project_id=${encodeURIComponent(akteFilter)}`
: `/api/deadlines/summary`;
const resp = await fetch(url);
if (!resp.ok) return;
const sum: Summary = await resp.json();
setCount("sum-overdue", sum.overdue);
setCount("sum-week", sum.this_week);
setCount("sum-upcoming", sum.upcoming);
setCount("sum-completed", sum.completed);
} catch {
/* non-fatal */
}
}
function setCount(id: string, n: number) {
const el = document.getElementById(id);
if (el) el.textContent = String(n);
}
async function loadFristen() {
const unavailable = document.getElementById("fristen-unavailable")!;
const tableWrap = document.querySelector<HTMLElement>(".akten-table-wrap")!;
try {
const params = new URLSearchParams();
if (statusFilter) params.set("status", statusFilter);
if (akteFilter) params.set("project_id", akteFilter);
const resp = await fetch(`/api/deadlines?${params.toString()}`);
if (resp.status === 503) {
unavailable.style.display = "block";
tableWrap.style.display = "none";
document.getElementById("fristen-empty")!.style.display = "none";
return;
}
if (!resp.ok) {
unavailable.style.display = "block";
tableWrap.style.display = "none";
return;
}
allFristen = await resp.json();
loadedOK = true;
render();
} catch {
unavailable.style.display = "block";
tableWrap.style.display = "none";
}
}
function urgencyClass(due: string, status: string): string {
if (status === "completed") return "frist-urgency-done";
const today = new Date();
today.setHours(0, 0, 0, 0);
const d = new Date(due + "T00:00:00");
const diffDays = Math.floor((d.getTime() - today.getTime()) / 86400000);
if (diffDays < 0) return "frist-urgency-overdue";
if (diffDays <= 7) return "frist-urgency-soon";
return "frist-urgency-later";
}
function fmtDate(iso: string): string {
try {
const d = new Date(iso + "T00:00:00");
return d.toLocaleDateString(getLang() === "de" ? "de-DE" : "en-GB", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
} catch {
return iso;
}
}
function esc(s: string): string {
const d = document.createElement("div");
d.textContent = s;
return d.innerHTML;
}
function render() {
if (!loadedOK) return;
const tbody = document.getElementById("fristen-body")!;
const empty = document.getElementById("fristen-empty")!;
const emptyFiltered = document.getElementById("fristen-empty-filtered")!;
const tableWrap = document.querySelector<HTMLElement>(".akten-table-wrap")!;
if (allFristen.length === 0) {
tbody.innerHTML = "";
tableWrap.style.display = "none";
if (statusFilter === "all" && !akteFilter) {
empty.style.display = "block";
emptyFiltered.style.display = "none";
} else {
empty.style.display = "none";
emptyFiltered.style.display = "block";
}
return;
}
tableWrap.style.display = "";
empty.style.display = "none";
emptyFiltered.style.display = "none";
tbody.innerHTML = allFristen
.map((f) => {
const urgency = urgencyClass(f.due_date, f.status);
const statusLabel = t(`fristen.status.${f.status}`) || f.status;
const ruleLabel = f.rule_code ? esc(f.rule_code) : "&mdash;";
const checked = f.status === "completed" ? "checked" : "";
const disabled = f.status === "completed" ? "disabled" : "";
const titleClass = f.status === "completed" ? "frist-title-done" : "";
return `<tr class="frist-row" data-id="${esc(f.id)}">
<td class="frist-col-check">
<input type="checkbox" class="frist-complete-cb" ${checked} ${disabled}
aria-label="${esc(t("fristen.complete.action"))}" />
</td>
<td class="frist-col-due ${urgency}"><span class="frist-due-dot"></span>${fmtDate(f.due_date)}</td>
<td class="frist-col-title ${titleClass}">${esc(f.title)}</td>
<td class="frist-col-project">
<a class="akten-ref-link" href="/projects/${esc(f.project_id)}">${esc(f.project_reference)}</a>
<span class="frist-project-title">${esc(f.project_title)}</span>
</td>
<td class="frist-col-rule">${ruleLabel}</td>
<td><span class="akten-status-chip akten-status-${esc(f.status)}">${esc(statusLabel)}</span></td>
</tr>`;
})
.join("");
tbody.querySelectorAll<HTMLTableRowElement>(".frist-row").forEach((row) => {
const id = row.dataset.id!;
row.addEventListener("click", (e) => {
// Don't navigate if clicking the checkbox or a link
const target = e.target as HTMLElement;
if (target.closest(".frist-complete-cb") || target.closest("a")) return;
window.location.href = `/deadlines/${id}`;
});
const cb = row.querySelector<HTMLInputElement>(".frist-complete-cb");
if (cb) {
cb.addEventListener("change", async () => {
if (!cb.checked) return;
cb.disabled = true;
try {
const resp = await fetch(`/api/deadlines/${id}/complete`, { method: "PATCH" });
if (resp.ok) {
await Promise.all([loadFristen(), loadSummary()]);
} else {
cb.checked = false;
cb.disabled = false;
}
} catch {
cb.checked = false;
cb.disabled = false;
}
});
}
});
}
function initFilters() {
const status = document.getElementById("frist-filter-status") as HTMLSelectElement;
const project = document.getElementById("frist-filter-project") as HTMLSelectElement;
// Pre-fill from URL
const params = urlParams();
if (params.has("status")) statusFilter = params.get("status")!;
if (params.has("project_id")) akteFilter = params.get("project_id")!;
status.value = statusFilter;
status.addEventListener("change", async () => {
statusFilter = status.value;
await Promise.all([loadFristen(), loadSummary()]);
});
project.addEventListener("change", async () => {
akteFilter = project.value;
await Promise.all([loadFristen(), loadSummary()]);
});
}
function populateAkteFilter() {
const sel = document.getElementById("frist-filter-project") as HTMLSelectElement;
// Keep the first "all" option, then append sorted Akten.
const options: string[] = [
`<option value="" data-i18n="fristen.filter.project.all">${esc(t("fristen.filter.project.all"))}</option>`,
];
for (const a of allProjects) {
options.push(
`<option value="${esc(a.id)}">${esc(a.aktenzeichen)} \u2014 ${esc(a.title)}</option>`,
);
}
sel.innerHTML = options.join("");
if (akteFilter) sel.value = akteFilter;
}
function initSummaryCards() {
document.querySelectorAll<HTMLButtonElement>(".frist-summary-card").forEach((card) => {
card.addEventListener("click", async () => {
const newStatus = card.dataset.status!;
statusFilter = newStatus;
(document.getElementById("frist-filter-status") as HTMLSelectElement).value = newStatus;
await Promise.all([loadFristen(), loadSummary()]);
});
});
}
document.addEventListener("DOMContentLoaded", async () => {
initI18n();
initSidebar();
initFilters();
initSummaryCards();
onLangChange(render);
await loadAkten();
populateAkteFilter();
await Promise.all([loadFristen(), loadSummary()]);
});