/* Key register — printable reports. Everything renders to plain HTML so the browser's own Print / Save as PDF produces the document. No PDF library, nothing to keep up to date. */ "use strict"; let S = null, view = "out", whoId = null; const $out = document.getElementById("out"); const $view = document.getElementById("view"); const $who = document.getElementById("who"); const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); const fmt = (d) => { if (!d) return "—"; const [y, m, dd] = d.split("-"); return `${dd} ${T._months[+m - 1]} ${y}`; }; const days = (a, b) => Math.round((new Date(b) - new Date(a)) / 864e5); const person = (id) => S.people.find((p) => p.id === +id); const door = (id) => S.doors.find((d) => d.id === +id); const cab = (id) => S.cabinets.find((c) => c.id === id) || { name: "—", full_name: "—" }; const doorName = (i) => (i.door_id ? door(i.door_id).name : i.zone); const isLate = (i) => i.loan && i.loan.due_date && i.loan.due_date < S.today; /* Hook order — the audit sheet is meant to be read down the cabinet. */ const byHook = (a, b) => (a.hook || "").localeCompare(b.hook || "", undefined, { numeric: true }) || a.tag.localeCompare(b.tag, undefined, { numeric: true }); function head(title, sub) { const now = new Date(); return `

${esc(T.brand)}

${esc(title)}

${esc(T.repGenerated(fmt(S.today), now.toTimeString().slice(0, 5)))}${ sub ? " · " + esc(sub) : ""}

`; } const stat = (n, label) => `
${n}${esc(label)}
`; /* ---------------------------------------------------------------- issued */ function viewOut(onlyLate) { const out = S.items.filter((i) => i.status === "out" && (!onlyLate || isLate(i))); const byPerson = {}; out.forEach((i) => (byPerson[i.loan.person_id] = byPerson[i.loan.person_id] || []).push(i)); Object.values(byPerson).forEach((l) => l.sort(byHook)); const ids = Object.keys(byPerson).sort((a, b) => person(a).name.localeCompare(person(b).name, "pt")); if (!out.length) return head(onlyLate ? T.repOverdue : T.repOut) + `

${esc(T.repNothing)}

`; return head(onlyLate ? T.repOverdue : T.repOut) + `
${stat(out.length, T.stKeysOut)} ${stat(ids.length, T.stPeople)} ${stat(S.items.filter(isLate).length, T.stOverdue)}
` + ids.map((pid) => { const p = person(pid), list = byPerson[pid]; return `

${esc(p.name)}

${esc(p.dept)}${p.role ? " · " + esc(p.role) : ""}${ p.phone ? " · " + esc(p.phone) : ""}

${list.map((i) => { const late = isLate(i); return ``; }).join("")}
${esc(T.cTag)}${esc(T.cDoor)}${esc(T.cBuilding)} ${esc(T.cCabinet)}${esc(T.cOut)}${esc(T.cDue)}
${esc(i.tag)} ${esc(doorName(i))} ${esc(i.building)} ${esc(cab(i.cabinet_id).name)} ${fmt(i.loan.out_date)} ${i.loan.due_date ? fmt(i.loan.due_date) : "—"}${ late ? ` (+${days(i.loan.due_date, S.today)})` : ""}
`; }).join("") + signOff(); } /* ------------------------------------------------- receipt for one person */ async function viewPerson(id) { const p = person(id); if (!p) return head(T.repReceiptTitle) + `

${esc(T.repChoosePerson)}

`; let rows = []; try { rows = await fetch("/api/report/person/" + id).then((r) => r.json()); } catch (e) { /* offline */ } if (!rows.length) return head(T.repReceiptTitle, p.name) + `

${esc(T.repNothing)}

`; return head(T.repReceiptTitle, `${p.name}${p.dept ? " · " + p.dept : ""}`) + `

${esc(T.repReceiptIntro(rows.length))}

${rows.map((h) => { const late = h.due_date && h.due_date < S.today; return ``; }).join("")}
${esc(T.cTag)}${esc(T.cDoor)}${esc(T.cBuilding)} ${esc(T.cCabinet)}${esc(T.cOut)}${esc(T.cDue)}
${esc(h.tag)}${esc(h.door || "")} ${esc(h.building)}${esc(cab(h.cabinet_id).name)} ${fmt(h.out_date)} ${h.due_date ? fmt(h.due_date) : "—"}
${esc(T.signature)}
${rows.filter((h) => h.signature).map((h) => `
${esc(h.tag)} · ${esc(T.repSignedOn(fmt(h.out_date)))}
`).join("") || `

${esc(T.repNoSig)}

`} ${signOff(p.name)}`; } /* ------------------------------------------------------------- inventory */ function viewInventory() { const live = S.items.filter((i) => i.status !== "retired"); const groups = {}; S.cabinets.forEach((c) => { const list = live.filter((i) => i.cabinet_id === c.id).sort(byHook); if (list.length) groups[c.id] = list; }); const statusWord = (i) => (T.statuses && T.statuses[i.status]) || i.status; return head(T.repInventory) + `
${stat(S.items.length, T.stTotal)} ${stat(S.items.filter((i) => i.status === "in").length, T.stInCab)} ${stat(S.items.filter((i) => i.status === "out").length, T.stKeysOut)} ${stat(S.items.filter((i) => i.status === "reserve").length, T.stReserve)}
` + S.cabinets.filter((c) => groups[c.id]).map((c) => `

${esc(c.full_name)}

${esc(c.note || "")}

${groups[c.id].map((i) => ``).join("")}
${esc(T.cTag)}${esc(T.cDoor)}${esc(T.cBuilding)} ${esc(T.cHook)}${esc(T.cStatus)}${esc(T.cPerson)}
${esc(i.tag)}${esc(doorName(i))} ${esc(i.building)}${esc(i.hook)} ${esc(statusWord(i))} ${i.loan && i.status === "out" ? esc(person(i.loan.person_id).name) : ""}
`).join(""); } /* ------------------------------------------- audit sheet, one per cabinet */ function viewAudit() { const cabs = whoId && whoId !== "ALL" ? S.cabinets.filter((c) => c.id === whoId) : S.cabinets; return head(T.repAudit) + `

${esc(T.repAuditIntro)}

` + cabs.map((c) => { const list = S.items.filter((i) => i.cabinet_id === c.id && i.status !== "retired").sort(byHook); if (!list.length) return ""; return `

${esc(c.full_name)}

${esc(c.note || "")}

${list.map((i) => ``).join("")}
${esc(T.cCheck)}${esc(T.cTag)} ${esc(T.cHook)}${esc(T.cDoor)} ${esc(T.cStatus)}${esc(T.cPerson)}
${esc(i.tag)}${esc(i.hook)} ${esc(doorName(i))} ${esc((T.statuses && T.statuses[i.status]) || i.status)} ${i.loan && i.status === "out" ? esc(person(i.loan.person_id).name) : ""}
`; }).join("") + `
${esc(T.repAuditBy)}
${esc(T.repDate)}
${esc(T.repNotes)}
`; } function signOff(holder) { return `
${holder ? `
${esc(T.repHolder)} — ${esc(holder)}
` : ""}
${esc(T.repResponsible)}
${esc(T.repDate)}
`; } /* ------------------------------------------------------------------ shell */ async function render() { $out.innerHTML = ""; const isPerson = view === "person"; const isAudit = view === "audit"; $who.style.display = isPerson || isAudit ? "" : "none"; if (isPerson) { $who.innerHTML = `` + S.people.map((p) => ``).join(""); $out.innerHTML = await viewPerson(whoId); } else if (isAudit) { $who.innerHTML = `` + S.cabinets.map((c) => ``).join(""); $out.innerHTML = viewAudit(); } else if (view === "overdue") { $out.innerHTML = viewOut(true); } else if (view === "inventory") { $out.innerHTML = viewInventory(); } else { $out.innerHTML = viewOut(false); } document.title = `${T.brand} — ${$view.selectedOptions[0].textContent}`; } (async () => { try { S = await fetch("/api/state").then((r) => r.json()); } catch (e) { $out.innerHTML = `

${esc(T ? T.noServer : "offline")}

`; return; } setLang(S.lang); document.getElementById("printbtn").textContent = T.printPdf; document.getElementById("backlink").title = T.back; $view.innerHTML = [["out", T.repOut], ["overdue", T.repOverdue], ["person", T.repPerson], ["inventory", T.repInventory], ["audit", T.repAudit]] .map(([v, l]) => ``).join(""); /* deep links: /report?view=person&id=3 */ const q = new URLSearchParams(location.search); if (q.get("view")) view = q.get("view"); if (q.get("id")) whoId = q.get("id"); $view.value = view; $view.onchange = () => { view = $view.value; whoId = view === "audit" ? "ALL" : null; render(); }; $who.onchange = () => { whoId = $who.value; render(); }; document.getElementById("printbtn").onclick = () => window.print(); await render(); })();