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:
265
frontend/src/client/deadlines.ts
Normal file
265
frontend/src/client/deadlines.ts
Normal 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) : "—";
|
||||
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()]);
|
||||
});
|
||||
Reference in New Issue
Block a user