import { initI18n, onLangChange, t, getLang } from "./i18n"; import { initSidebar } from "./sidebar"; interface ChecklistSummary { slug: string; titleDE: string; titleEN: string; descriptionDE: string; descriptionEN: string; regime: string; courtDE: string; courtEN: string; itemCount: number; origin?: "static" | "authored"; visibility?: string; owner_email?: string; owner_display_name?: string; } interface MyChecklist { id: string; slug: string; owner_id: string; title: string; description: string; regime: string; court: string; reference: string; deadline: string; lang: string; visibility: string; created_at: string; updated_at: string; } interface ChecklistInstance { id: string; template_slug: string; name: string; project_id?: string | null; state: Record; created_by: string; created_at: string; updated_at: string; project_reference?: string | null; project_title?: string | null; } type TabId = "templates" | "mine" | "gallery" | "instances"; const VALID_TABS: TabId[] = ["templates", "mine", "gallery", "instances"]; let allChecklists: ChecklistSummary[] = []; let activeRegime = "all"; let galleryRegime = "all"; let allInstances: ChecklistInstance[] = []; let templatesBySlug: Record = {}; let instancesLoaded = false; let myTemplates: MyChecklist[] = []; let myTemplatesLoaded = false; let galleryLoaded = false; let me: { id: string; email: string } | null = null; let activeTab: TabId = "templates"; function esc(s: string): string { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } function escAttr(s: string): string { return s.replace(/&/g, "&").replace(/"/g, """); } function parseTab(): TabId { const params = new URLSearchParams(window.location.search); const candidate = params.get("tab"); if (candidate && (VALID_TABS as string[]).includes(candidate)) { return candidate as TabId; } return "templates"; } async function loadTemplates() { const resp = await fetch("/api/checklists"); if (!resp.ok) return; allChecklists = await resp.json(); templatesBySlug = {}; for (const tpl of allChecklists) templatesBySlug[tpl.slug] = tpl; renderTemplates(); } function renderTemplates() { const grid = document.getElementById("checklist-grid")!; const isEN = getLang() === "en"; const filtered = activeRegime === "all" ? allChecklists : allChecklists.filter((c) => c.regime === activeRegime); if (filtered.length === 0) { grid.innerHTML = `

${esc(t("checklisten.empty"))}

`; return; } grid.innerHTML = filtered.map((c) => { const title = isEN ? c.titleEN : c.titleDE; const desc = isEN ? c.descriptionEN : c.descriptionDE; const court = isEN ? c.courtEN : c.courtDE; const itemsLabel = isEN ? "items" : "Punkte"; return `
${esc(c.regime)} ${c.itemCount} ${itemsLabel}

${esc(title)}

${esc(desc)}

${esc(court)}

`; }).join(""); } function initFilters() { const container = document.getElementById("checklist-filters")!; container.addEventListener("click", (e) => { const btn = (e.target as HTMLElement).closest(".filter-pill"); if (!btn) return; container.querySelectorAll(".filter-pill").forEach((p) => p.classList.remove("active")); btn.classList.add("active"); activeRegime = btn.dataset.regime ?? "all"; renderTemplates(); }); } async function loadInstances() { if (instancesLoaded) return; instancesLoaded = true; // Templates may not be loaded yet if the user lands directly on // ?tab=instances — fetch in parallel so the join below has names. const [instResp, tplResp] = await Promise.all([ fetch("/api/checklist-instances"), allChecklists.length === 0 ? fetch("/api/checklists") : Promise.resolve(null), ]); if (instResp.ok) { allInstances = (await instResp.json()) ?? []; } else { allInstances = []; } if (tplResp && tplResp.ok) { allChecklists = (await tplResp.json()) ?? []; templatesBySlug = {}; for (const tpl of allChecklists) templatesBySlug[tpl.slug] = tpl; } renderInstances(); } function renderInstances() { const loading = document.getElementById("checklists-instances-loading")!; const empty = document.getElementById("checklists-instances-empty")!; const wrap = document.getElementById("checklists-instances-tablewrap")!; const body = document.getElementById("checklists-instances-body")!; loading.style.display = "none"; if (allInstances.length === 0) { empty.style.display = ""; wrap.style.display = "none"; return; } empty.style.display = "none"; wrap.style.display = ""; const isEN = getLang() === "en"; const fmtDate = (iso: string) => { const d = new Date(iso); if (isNaN(d.getTime())) return ""; return d.toLocaleDateString(isEN ? "en-GB" : "de-DE", { year: "numeric", month: "2-digit", day: "2-digit", }); }; body.innerHTML = allInstances.map((inst) => { const tpl = templatesBySlug[inst.template_slug]; const tplName = tpl ? (isEN ? tpl.titleEN : tpl.titleDE) : inst.template_slug; const total = tpl ? tpl.itemCount : 0; const done = Object.values(inst.state || {}).filter(Boolean).length; const pct = total === 0 ? 0 : Math.round((done / total) * 100); let projectCell: string; if (inst.project_id && inst.project_title) { const ref = inst.project_reference ? esc(inst.project_reference) : ""; const title = esc(inst.project_title); const refPart = ref ? `${ref} ` : ""; projectCell = `${refPart}${title}`; } else { projectCell = `Persönlich`; } return ` ${esc(tplName)} ${esc(inst.name)} ${projectCell}
${done} / ${total}
${esc(fmtDate(inst.created_at))} `; }).join(""); body.querySelectorAll(".checklist-instance-row").forEach((row) => { const id = row.dataset.id!; row.addEventListener("click", (e) => { // Let inner links (project, instance name) handle their own navigation. if ((e.target as HTMLElement).closest("a")) return; window.location.href = `/checklists/instances/${id}`; }); }); } function showTab(tab: TabId, opts: { pushHistory?: boolean } = {}) { activeTab = tab; document.querySelectorAll("#checklists-tabs .entity-tab").forEach((el) => { el.classList.toggle("active", el.dataset.tab === tab); }); document.querySelectorAll(".entity-tab-panel").forEach((el) => { el.style.display = el.id === `tab-${tab}` ? "" : "none"; }); if (opts.pushHistory ?? true) { let newURL = "/checklists"; if (tab === "instances") newURL = "/checklists?tab=instances"; if (tab === "mine") newURL = "/checklists?tab=mine"; if (tab === "gallery") newURL = "/checklists?tab=gallery"; if (window.location.pathname + window.location.search !== newURL) { window.history.replaceState({}, "", newURL); } } if (tab === "instances") { void loadInstances(); } if (tab === "mine") { void loadMyTemplates(); } if (tab === "gallery") { void loadGallery(); } } async function loadGallery(force = false) { if (galleryLoaded && !force) return; galleryLoaded = true; // /api/checklists already returns the merged catalog; the gallery // filter just narrows to non-static + non-owned + non-private. if (allChecklists.length === 0) { await loadTemplates(); } renderGallery(); } function renderGallery() { const loading = document.getElementById("checklists-gallery-loading")!; const empty = document.getElementById("checklists-gallery-empty")!; const grid = document.getElementById("checklists-gallery-grid") as HTMLElement; loading.style.display = "none"; const visible = allChecklists.filter((c) => { if (c.origin !== "authored") return false; if (me && c.owner_email && me.email.toLowerCase() === c.owner_email.toLowerCase()) return false; if (galleryRegime !== "all" && c.regime !== galleryRegime) return false; return true; }); if (visible.length === 0) { empty.style.display = ""; grid.style.display = "none"; return; } empty.style.display = "none"; grid.style.display = ""; const isEN = getLang() === "en"; grid.innerHTML = visible.map((c) => { const title = isEN ? c.titleEN : c.titleDE; const desc = isEN ? c.descriptionEN : c.descriptionDE; const court = isEN ? c.courtEN : c.courtDE; const itemsLabel = isEN ? "items" : "Punkte"; const visKey = `checklisten.mine.visibility.${c.visibility || ""}`; const visLabel = c.visibility ? esc(t(visKey as never) || c.visibility) : ""; const authorLine = c.owner_display_name ? `

${esc(t("checklisten.detail.authored.by").replace("{author}", c.owner_display_name))}

` : ""; return `
${esc(c.regime)} ${c.itemCount} ${itemsLabel}

${esc(title)}

${esc(desc)}

${esc(court)}

${authorLine} ${visLabel ? `${visLabel}` : ""}
`; }).join(""); } function initGalleryFilters() { const container = document.getElementById("checklist-gallery-filters"); if (!container) return; container.addEventListener("click", (e) => { const btn = (e.target as HTMLElement).closest(".filter-pill"); if (!btn) return; container.querySelectorAll(".filter-pill").forEach((p) => p.classList.remove("active")); btn.classList.add("active"); galleryRegime = btn.dataset.regime ?? "all"; renderGallery(); }); } async function loadMe() { try { const resp = await fetch("/api/me"); if (resp.ok) me = await resp.json(); } catch { /* leave me=null */ } } async function loadMyTemplates(force = false) { if (myTemplatesLoaded && !force) return; myTemplatesLoaded = true; const resp = await fetch("/api/checklists/templates/mine"); if (!resp.ok) { myTemplates = []; } else { myTemplates = (await resp.json()) ?? []; } renderMyTemplates(); } function renderMyTemplates() { const loading = document.getElementById("checklists-mine-loading")!; const empty = document.getElementById("checklists-mine-empty")!; const grid = document.getElementById("checklists-mine-grid") as HTMLElement; loading.style.display = "none"; if (myTemplates.length === 0) { empty.style.display = ""; grid.style.display = "none"; return; } empty.style.display = "none"; grid.style.display = ""; grid.innerHTML = myTemplates.map((tpl) => { const visKey = `checklisten.mine.visibility.${tpl.visibility}`; const visLabel = esc(t(visKey as never) || tpl.visibility); const titleSafe = esc(tpl.title); return `
${esc(tpl.regime)} ${visLabel}

${titleSafe}

${esc(tpl.description || "")}

${esc(tpl.court || "")}

Bearbeiten
`; }).join(""); grid.querySelectorAll("button[data-action=delete]").forEach((btn) => { btn.addEventListener("click", async (e) => { e.preventDefault(); const slug = btn.dataset.slug!; const title = btn.dataset.title || slug; const msg = t("checklisten.mine.delete.confirm").replace("{title}", title); if (!window.confirm(msg)) return; const resp = await fetch(`/api/checklists/templates/${encodeURIComponent(slug)}`, { method: "DELETE" }); if (!resp.ok) { window.alert(t("checklisten.mine.delete.error")); return; } await loadMyTemplates(true); }); }); } function initTabs() { document.querySelectorAll("#checklists-tabs .entity-tab").forEach((tab) => { tab.addEventListener("click", (e) => { // Let middle-click / cmd-click open in new tab via the real href. if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); const id = tab.dataset.tab as TabId; if (VALID_TABS.includes(id)) showTab(id); }); }); } document.addEventListener("DOMContentLoaded", () => { initI18n(); initSidebar(); initFilters(); initGalleryFilters(); initTabs(); onLangChange(() => { renderTemplates(); if (instancesLoaded) renderInstances(); if (myTemplatesLoaded) renderMyTemplates(); if (galleryLoaded) renderGallery(); }); void loadMe(); void loadTemplates(); showTab(parseTab(), { pushHistory: false }); });