/* Key register — admin panel */ "use strict"; let S = null, page = "items", q = ""; const $body = document.getElementById("body"); const $sheet = document.getElementById("sheet"); const $scrim = document.getElementById("scrim"); const $toast = document.getElementById("toast"); const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); const cab = (id) => S.cabinets.find((c) => c.id === id) || { name: "—", full_name: "Unassigned" }; const door = (id) => S.doors.find((d) => d.id === +id); let tt; function toast(m, err) { $toast.textContent = m; $toast.className = "toast on" + (err ? " err" : ""); clearTimeout(tt); tt = setTimeout(() => ($toast.className = "toast"), 3000); } async function api(url, opts) { const r = await fetch(url, opts); const b = await r.json().catch(() => ({})); if (!r.ok) throw new Error(b.error || r.statusText); return b; } async function load() { try { S = await api("/api/state"); setLang(S.lang); chrome(); render(); } catch (e) { $body.innerHTML = `
${esc(T.noServer)}
${esc(e.message)}
`; } } function openSheet(h) { $sheet.innerHTML = '
' + h; $sheet.classList.add("on"); $scrim.classList.add("on"); $sheet.scrollTop = 0; } function closeSheet() { $sheet.classList.remove("on"); $scrim.classList.remove("on"); } $scrim.onclick = closeSheet; document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeSheet(); }); const val = (id) => (document.getElementById(id) || {}).value || ""; const checked = (id) => !!(document.getElementById(id) || {}).checked; async function post(url, data, msg) { try { const r = await api(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }); closeSheet(); await load(); toast(r.note || msg || T.tSaved); } catch (e) { toast(errMsg(e.message), true); } } async function del(url, msg) { try { const r = await api(url, { method: "DELETE" }); closeSheet(); await load(); toast(r.note || msg || T.tRemoved); } catch (e) { toast(errMsg(e.message), true); } } /* ------------------------------------------------------------- keys & tags */ function pageItems() { const term = q.trim().toLowerCase(); const list = S.items.filter((i) => !term || (i.tag + " " + (i.door_id ? door(i.door_id).name : i.zone) + " " + i.building + " " + i.notes).toLowerCase().includes(term)); $body.innerHTML = `

${T.aItems}

${T.aItemsHelp(S.items.length)}

${list.map((i) => ``).join("") || ``}
${T.aTag}${T.aDoor}${T.aCabinet}${T.aHook}${T.aStatus}
${esc(i.tag)}${i.is_master ? ' M' : ""}
${esc((S.kinds.find((k) => k.id === i.kind) || {}).name || i.kind)}
${esc(i.door_id ? door(i.door_id).name : i.zone)}
${esc(i.building)}
${esc(cab(i.cabinet_id).name)} ${esc(i.hook)} ${esc((T.statuses && T.statuses[i.status]) || i.status)}
${T.aNothing}
`; const el = document.getElementById("q"); el.oninput = (e) => { q = e.target.value; pageItems(); const f = document.getElementById("q"); f.focus(); f.setSelectionRange(q.length, q.length); }; } function sheetItem(id) { const i = id ? S.items.find((x) => x.id === +id) : { tag: "", kind: "key", door_id: "", zone: "", cabinet_id: "", hook: "", is_master: 0, uid: "", status: "in", notes: "" }; openSheet(`

${id ? T.aEdit + " " + esc(i.tag) : T.aAddItem.replace("+ ", "")}

${T.vKey} / ${T.vTag}

${T.aTag}
${T.fType}
${T.fZoneKeys} ${T.fZoneTags}
${T.aCabinet}
${T.aHook}
${T.aStatus}
${T.fChipUid}
${T.fPermNote}
${id ? `

${T.retireNote}

` : ""}`); document.getElementById("save").onclick = () => post("/api/admin/items", { id: id || undefined, tag: val("f_tag").trim(), kind: val("f_kind"), door_id: val("f_door") ? +val("f_door") : null, zone: val("f_zone").trim(), cabinet_id: val("f_cab") || null, hook: val("f_hook").trim(), is_master: checked("f_master") ? 1 : 0, uid: val("f_uid").trim(), status: val("f_status"), notes: val("f_notes").trim(), }); if (id) document.getElementById("rm").onclick = () => del("/api/admin/items/" + id); } /* -------------------------------------------------------------------- doors */ function pageDoors() { const groups = {}; S.doors.forEach((d) => (groups[d.building] = groups[d.building] || []).push(d)); $body.innerHTML = `

${T.aDoors}

${T.aDoorsHelp(S.doors.length)}

${Object.entries(groups).map(([b, list]) => `

${esc(b)}

${list.map((d) => ``).join("")}
${T.aNo}${T.aDoors}${T.aLock}${T.aCopies}
${esc(d.num)}${esc(d.name)} ${esc(d.lock_group)} ${S.items.filter((i) => i.door_id === d.id).length}
`).join("") || `
${T.aNothing}
`}`; } function sheetDoor(id) { const d = id ? door(id) : { num: "", building: "", name: "", lock_group: "", notes: "" }; const buildings = [...new Set(S.doors.map((x) => x.building))]; openSheet(`

${id ? T.aEdit + " " + esc(d.num) : T.aAddDoor.replace("+ ", "")}

${T.fNum}
${T.fLockGroup}
${T.fBuilding} ${buildings.map((b) => ` ${T.fDoorName} ${T.fNote}
${id ? `` : ""}

${T.lockGroupNote}

`); document.getElementById("save").onclick = () => post("/api/admin/doors", { id: id || undefined, num: val("f_num").trim(), building: val("f_building").trim(), name: val("f_name").trim(), lock_group: val("f_group").trim(), notes: val("f_notes").trim(), }); if (id) document.getElementById("rm").onclick = () => del("/api/admin/doors/" + id); } /* ------------------------------------------------------------------- people */ function pagePeople() { $body.innerHTML = `

${T.aPeople}

${T.aPeopleHelp}

${S.people.map((p) => { const held = S.items.filter((i) => i.loan && i.loan.person_id === p.id && i.status === "out").length; return ``; }).join("") || ``}
${T.aName}${T.aDept}${T.aHolding}
${esc(p.name)}${p.active ? "" : ` ${T.inactive}`}
${esc(p.role)}
${esc(p.dept)}${held || "—"}
${T.aNothing}
`; } function sheetPerson(id) { const p = id ? S.people.find((x) => x.id === +id) : { name: "", dept: "", role: "", phone: "", email: "", active: 1 }; const held = id ? S.items.filter((i) => i.loan && i.loan.person_id === +id && i.status === "out") : []; openSheet(`

${id ? T.aEdit : T.aAddPerson.replace("+ ", "")}

${T.aName}
${T.aDept}
${T.fFunction}
${T.fPhone}
${T.fEmail}
${held.length ? `
${esc(T.holdsWarn(held.length, held.map((i) => i.tag).join(", ")))}
` : ""}
${id ? `

${T.peopleNote}

` : ""}`); document.getElementById("save").onclick = () => post("/api/admin/people", { id: id || undefined, name: val("f_name").trim(), dept: val("f_dept").trim(), role: val("f_role").trim(), phone: val("f_phone").trim(), email: val("f_email").trim(), active: checked("f_active") ? 1 : 0, }); if (id) document.getElementById("rm").onclick = () => del("/api/admin/people/" + id); } /* ----------------------------------------------------------------- cabinets */ function pageCabinets() { $body.innerHTML = `

${T.aCabinets}

${T.aCabinetsHelp}

${S.cabinets.map((c) => `

${esc(c.full_name)}

${esc(c.id)}${c.off_site ? " · " + T.aOffSite : ""}

${esc(c.note)}

${T.aItemsCount(S.items.filter((i) => i.cabinet_id === c.id).length)}

`).join("") || `
${T.aNothing}
`}`; } function sheetCabinet(id) { const c = id ? S.cabinets.find((x) => x.id === id) : { id: "", name: "", full_name: "", note: "", off_site: 0, sort: S.cabinets.length + 1 }; openSheet(`

${id ? T.aEdit : T.aAddCabinet.replace("+ ", "")}

${T.fShortCode}
${T.fShortName}
${T.fFullName} ${T.fWhereIs}

${T.offSiteNote}

${T.fSort}

${T.fSortHelp}

${id ? `` : ""}`); document.getElementById("save").onclick = () => post("/api/admin/cabinets", { id: val("f_id").trim().toUpperCase(), name: val("f_name").trim(), full_name: val("f_full").trim() || val("f_name").trim(), note: val("f_note").trim(), off_site: checked("f_off") ? 1 : 0, sort: +val("f_sort") || 0, }); if (id) document.getElementById("rm").onclick = () => del("/api/admin/cabinets/" + id); } /* -------------------------------------------------------------------- types */ const SHAPE_CSS = { circle: "border-radius:50%", rounded: "border-radius:5px", square: "border-radius:0" }; function pageKinds() { $body.innerHTML = `

${T.aKinds}

${T.aKindsHelp}

${S.kinds.map((k) => { const n = S.items.filter((i) => i.kind === k.id).length; return `
${esc(k.name)}
${esc(k.id)} · ${esc(k.colour)}
${T.kindInUse(n)}
`; }).join("")}`; } function sheetKind(id) { const k = id ? S.kinds.find((x) => x.id === id) : { id: "", name: "", shape: "circle", colour: "#2E6E60", sort: S.kinds.length + 1 }; const used = id ? S.items.filter((i) => i.kind === id).length : 0; openSheet(`

${id ? T.aEdit : T.aAddKind.replace("+ ", "")}

${T.fKindCode}
${T.fKindName}
${T.fShape}
${T.fColour}
${T.fSort}

${T.kindNote}

${used ? `
${T.kindInUse(used)}
` : ""}
${id ? `` : ""}`); document.getElementById("save").onclick = () => post("/api/admin/kinds", { id: val("f_id"), name: val("f_name").trim(), shape: val("f_shape"), colour: val("f_colour"), sort: +val("f_sort") || 0, }); if (id) document.getElementById("rm").onclick = () => del("/api/admin/kinds/" + id); } /* --------------------------------------------------------- import / backup */ let impKind = "cabinets"; // the first step, not the last const IMP_SPEC = { items: { url: "/api/admin/import", req: "tag, door, building", opt: "lock_group, cabinet, hook, master, status, kind, notes", ex: "tag,door,building,lock_group,cabinet,hook\n12-A,Sacristia,Igreja,B,SAC,SAC·R1·03\n12-B,Sacristia,Igreja,B,SEC,SEC·R2·01" }, doors: { url: "/api/admin/import/doors", req: "num, building, name", opt: "lock_group, notes", ex: "num,building,name,lock_group\n01,Igreja,Entrada principal,A\n02,Igreja,Sacristia,B" }, people: { url: "/api/admin/import/people", req: "name", opt: "dept, role, phone, email, active", ex: "name,dept,role,phone\nMaria Fonseca,Secretaria,Secretária,910000000\nJoão Ribeiro,Manutenção,Sacristão," }, cabinets: { url: "/api/admin/import/cabinets", req: "id, name", opt: "full_name, note, off_site, sort", ex: "id,name,full_name,note,off_site\nSAC,Sacristia,Armário da sacristia,Atrás da porta,0\nCASA,Reserva,Conjunto de reserva,Caixa selada fora,1" }, }; function pageData() { const spec = IMP_SPEC[impKind]; $body.innerHTML = `

${T.impTitle}

${T.impWhat}

${T.impOrder}

${[["cabinets", T.impCabinets], ["doors", T.impDoors], ["people", T.impPeople], ["items", T.impItems]] .map(([k, l], n) => ``).join("")}

${esc(spec.req)}${spec.opt ? " · " + esc(spec.opt) : ""}

${T.expTitle}

${T.expHelp}

${T.expBtn} ${T.reports}

${T.bkTitle}

${T.bkHelp}

${T.bkBtn}

${T.wipeTitle}

${T.wipeWarn}
`; document.getElementById("imp").onclick = async () => { const csv = document.getElementById("csv").value; if (!csv.trim()) return toast(T.impNothing, true); try { const r = await api(spec.url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ csv }) }); /* the combined importer reports items/doors/cabinets, the single-table ones report created/updated — show whichever came back */ const body = r.items !== undefined ? T.impResult(r.items, r.doors, r.cabinets) : T.impResult2(r.created, r.updated); document.getElementById("impres").innerHTML = `
${body}${ r.skipped && r.skipped.length ? T.impSkipped(r.skipped.length, esc(r.skipped.slice(0, 12).join(", ")) + (r.skipped.length > 12 ? "…" : "")) : ""}
`; await load(); toast(T.imported); } catch (e) { toast(errMsg(e.message), true); } }; document.getElementById("wipebtn").onclick = async () => { try { await api("/api/admin/wipe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ confirm: document.getElementById("wipe").value }) }); await load(); toast(T.wipeDone); } catch (e) { toast(errMsg(e.message), true); } }; } /* -------------------------------------------------------------------- audit */ async function pageAudit() { let rows = []; try { rows = await api("/api/admin/audit"); } catch (e) { /* non-fatal */ } $body.innerHTML = `

${T.auditTitle}

${T.auditHelp}

${rows.map((r) => ``).join("") || ``}
${T.aWhen}${T.aWhat}${T.aDetail}
${esc(r.ts.slice(0, 16).replace("T", " "))} ${esc(r.action)}${esc(r.detail)}
${T.aNothing}
`; } /* ------------------------------------------------------------------ events */ document.addEventListener("click", (e) => { const t = e.target.closest("[data-p],[data-imp],[data-new],[data-edit-item],[data-edit-door],[data-edit-person],[data-edit-cab],[data-edit-kind],[data-close]"); if (!t) return; if (t.dataset.close !== undefined) return closeSheet(); if (t.dataset.imp) { impKind = t.dataset.imp; return pageData(); } if (t.dataset.p) { page = t.dataset.p; q = ""; document.querySelectorAll("#nav button").forEach((b) => b.setAttribute("aria-pressed", b.dataset.p === page)); return render(); } if (t.dataset.new === "item") return sheetItem(null); if (t.dataset.new === "door") return sheetDoor(null); if (t.dataset.new === "person") return sheetPerson(null); if (t.dataset.new === "cabinet") return sheetCabinet(null); if (t.dataset.new === "kind") return sheetKind(null); if (t.dataset.editItem) return sheetItem(t.dataset.editItem); if (t.dataset.editDoor) return sheetDoor(t.dataset.editDoor); if (t.dataset.editPerson) return sheetPerson(t.dataset.editPerson); if (t.dataset.editCab) return sheetCabinet(t.dataset.editCab); if (t.dataset.editKind) return sheetKind(t.dataset.editKind); }); function chrome() { document.getElementById("brand").textContent = T.brand; document.getElementById("apptitle").textContent = T.admin; document.getElementById("backlink").title = T.back; document.querySelectorAll("[data-t]").forEach((el) => (el.textContent = T[el.dataset.t])); document.title = T.brand + " — " + T.admin; } function render() { ({ items: pageItems, doors: pageDoors, people: pagePeople, cabinets: pageCabinets, kinds: pageKinds, data: pageData, audit: pageAudit })[page](); } load();