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; reference?: string | null; 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(".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(".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 ` ${fmtDate(f.due_date)} ${esc(f.title)} ${esc(f.project_reference)} ${esc(f.project_title)} ${ruleLabel} ${esc(statusLabel)} `; }) .join(""); tbody.querySelectorAll(".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(".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[] = [ ``, ]; for (const a of allProjects) { options.push( ``, ); } sel.innerHTML = options.join(""); if (akteFilter) sel.value = akteFilter; } function initSummaryCards() { document.querySelectorAll(".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()]); });