lxc-keyreg/public/admin.js
2026-08-24 16:35:20 +01:00

448 lines
26 KiB
JavaScript

/* 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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[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 = `<div class="empty">${esc(T.noServer)}<br>${esc(e.message)}</div>`; }
}
function openSheet(h) {
$sheet.innerHTML = '<div class="grab"></div>' + 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 = `
<div class="card"><h3>${T.aItems}</h3>
<p style="color:var(--slate);font-size:13px;margin:0 0 10px">
${T.aItemsHelp(S.items.length)}</p>
<input class="field" id="q" placeholder="${esc(T.aFilter)}" value="${esc(q)}" style="margin-bottom:10px">
<button class="mini" data-new="item">${T.aAddItem}</button></div>
<div class="card" style="padding:0;overflow-x:auto">
<table><thead><tr><th>${T.aTag}</th><th>${T.aDoor}</th><th>${T.aCabinet}</th><th>${T.aHook}</th><th>${T.aStatus}</th><th></th></tr></thead>
<tbody>${list.map((i) => `<tr>
<td class="mono"><b>${esc(i.tag)}</b>${i.is_master ? ' <span style="color:var(--brass);font-size:10px">M</span>' : ""}
<div style="font-size:11px;color:var(--slate-2)">${esc((S.kinds.find((k) => k.id === i.kind) || {}).name || i.kind)}</div></td>
<td>${esc(i.door_id ? door(i.door_id).name : i.zone)}<div style="color:var(--slate-2);font-size:11.5px">${esc(i.building)}</div></td>
<td>${esc(cab(i.cabinet_id).name)}</td>
<td class="mono" style="font-size:11.5px">${esc(i.hook)}</td>
<td>${esc((T.statuses && T.statuses[i.status]) || i.status)}</td>
<td style="text-align:right"><button class="mini" data-edit-item="${i.id}">${T.aEdit}</button></td>
</tr>`).join("") || `<tr><td colspan="6" style="text-align:center;color:var(--slate);padding:24px">${T.aNothing}</td></tr>`}
</tbody></table></div>`;
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(`<h2>${id ? T.aEdit + " " + esc(i.tag) : T.aAddItem.replace("+ ", "")}</h2>
<p class="sub">${T.vKey} / ${T.vTag}</p>
<div class="grid2">
<div><span class="lab">${T.aTag}</span><input class="field" id="f_tag" value="${esc(i.tag)}" placeholder="12-B"></div>
<div><span class="lab">${T.fType}</span><select class="field" id="f_kind">
${S.kinds.map((k) => `<option value="${esc(k.id)}"${i.kind === k.id ? " selected" : ""}>${esc(k.name)}</option>`).join("")}</select></div>
</div>
<span class="lab">${T.fZoneKeys}</span>
<select class="field" id="f_door"><option value="">${T.fNoneTag}</option>
${S.doors.map((d) => `<option value="${d.id}"${i.door_id === d.id ? " selected" : ""}>${esc(d.num)} · ${esc(d.name)} (${esc(d.building)})</option>`).join("")}</select>
<span class="lab">${T.fZoneTags}</span>
<input class="field" id="f_zone" value="${esc(i.zone)}" placeholder="Nave &amp; sacristy">
<div class="grid2">
<div><span class="lab">${T.aCabinet}</span><select class="field" id="f_cab"><option value="">${T.fNone}</option>
${S.cabinets.map((c) => `<option value="${esc(c.id)}"${i.cabinet_id === c.id ? " selected" : ""}>${esc(c.full_name)}</option>`).join("")}</select></div>
<div><span class="lab">${T.aHook}</span><input class="field" id="f_hook" value="${esc(i.hook)}" placeholder="SAC·R2·05"></div>
</div>
<div class="grid2">
<div><span class="lab">${T.aStatus}</span><select class="field" id="f_status">
${["in", "out", "reserve", "lost", "retired"].map((s) => `<option value="${s}"${i.status === s ? " selected" : ""}>${esc((T.statuses && T.statuses[s]) || s)}</option>`).join("")}</select></div>
<div><span class="lab">${T.fChipUid}</span><input class="field" id="f_uid" value="${esc(i.uid)}"></div>
</div>
<label style="display:flex;gap:9px;align-items:center;margin-bottom:12px;font-size:14px">
<input type="checkbox" id="f_master"${i.is_master ? " checked" : ""}> ${T.fIsMaster}</label>
<span class="lab">${T.fPermNote}</span>
<textarea class="field" id="f_notes" style="min-height:70px">${esc(i.notes)}</textarea>
<div class="actions">
<button class="btn btn-p" id="save">${T.save}</button>
<button class="btn btn-g" data-close>Cancel</button></div>
${id ? `<button class="btn btn-d" style="width:100%;margin-bottom:16px" id="rm">${T.deleteRetire}</button>
<p class="note" style="text-align:left">${T.retireNote}</p>` : ""}`);
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 = `
<div class="card"><h3>${T.aDoors}</h3>
<p style="color:var(--slate);font-size:13px;margin:0 0 10px">
${T.aDoorsHelp(S.doors.length)}</p>
<button class="mini" data-new="door">${T.aAddDoor}</button></div>
${Object.entries(groups).map(([b, list]) => `<div class="card" style="padding:0">
<div style="padding:11px 14px 0"><h3>${esc(b)}</h3></div>
<table><thead><tr><th>${T.aNo}</th><th>${T.aDoors}</th><th>${T.aLock}</th><th>${T.aCopies}</th><th></th></tr></thead><tbody>
${list.map((d) => `<tr><td class="mono"><b>${esc(d.num)}</b></td><td>${esc(d.name)}</td>
<td class="mono">${esc(d.lock_group)}</td>
<td>${S.items.filter((i) => i.door_id === d.id).length}</td>
<td style="text-align:right"><button class="mini" data-edit-door="${d.id}">${T.aEdit}</button></td></tr>`).join("")}
</tbody></table></div>`).join("") || `<div class="empty">${T.aNothing}</div>`}`;
}
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(`<h2>${id ? T.aEdit + " " + esc(d.num) : T.aAddDoor.replace("+ ", "")}</h2>
<div class="grid2">
<div><span class="lab">${T.fNum}</span><input class="field" id="f_num" value="${esc(d.num)}" placeholder="12"></div>
<div><span class="lab">${T.fLockGroup}</span><input class="field" id="f_group" value="${esc(d.lock_group)}" placeholder="B"></div>
</div>
<span class="lab">${T.fBuilding}</span>
<input class="field" id="f_building" value="${esc(d.building)}" list="blist" placeholder="Church">
<datalist id="blist">${buildings.map((b) => `<option value="${esc(b)}">`).join("")}</datalist>
<span class="lab">${T.fDoorName}</span>
<input class="field" id="f_name" value="${esc(d.name)}" placeholder="Sacristy">
<span class="lab">${T.fNote}</span>
<textarea class="field" id="f_notes" style="min-height:60px">${esc(d.notes)}</textarea>
<div class="actions">
<button class="btn btn-p" id="save">${T.save}</button>
<button class="btn btn-g" data-close>Cancel</button></div>
${id ? `<button class="btn btn-d" style="width:100%;margin-bottom:16px" id="rm">${T.deleteDoor}</button>` : ""}
<p class="note" style="text-align:left">${T.lockGroupNote}</p>`);
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 = `
<div class="card"><h3>${T.aPeople}</h3>
<p style="color:var(--slate);font-size:13px;margin:0 0 10px">
${T.aPeopleHelp}</p>
<button class="mini" data-new="person">${T.aAddPerson}</button></div>
<div class="card" style="padding:0;overflow-x:auto">
<table><thead><tr><th>${T.aName}</th><th>${T.aDept}</th><th>${T.aHolding}</th><th></th></tr></thead><tbody>
${S.people.map((p) => {
const held = S.items.filter((i) => i.loan && i.loan.person_id === p.id && i.status === "out").length;
return `<tr><td><b>${esc(p.name)}</b>${p.active ? "" : ` <span style="color:var(--slate-2);font-size:11px">${T.inactive}</span>`}
<div style="color:var(--slate-2);font-size:11.5px">${esc(p.role)}</div></td>
<td>${esc(p.dept)}</td><td>${held || "—"}</td>
<td style="text-align:right"><button class="mini" data-edit-person="${p.id}">${T.aEdit}</button></td></tr>`;
}).join("") || `<tr><td colspan="4" style="text-align:center;color:var(--slate);padding:24px">${T.aNothing}</td></tr>`}
</tbody></table></div>`;
}
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(`<h2>${id ? T.aEdit : T.aAddPerson.replace("+ ", "")}</h2>
<span class="lab">${T.aName}</span><input class="field" id="f_name" value="${esc(p.name)}">
<div class="grid2">
<div><span class="lab">${T.aDept}</span><input class="field" id="f_dept" value="${esc(p.dept)}" placeholder="Choir"></div>
<div><span class="lab">${T.fFunction}</span><input class="field" id="f_role" value="${esc(p.role)}" placeholder="Director"></div>
</div>
<div class="grid2">
<div><span class="lab">${T.fPhone}</span><input class="field" id="f_phone" value="${esc(p.phone)}"></div>
<div><span class="lab">${T.fEmail}</span><input class="field" id="f_email" value="${esc(p.email)}"></div>
</div>
<label style="display:flex;gap:9px;align-items:center;margin-bottom:12px;font-size:14px">
<input type="checkbox" id="f_active"${p.active ? " checked" : ""}> ${T.fActiveLabel}</label>
${held.length ? `<div class="warnbox">${esc(T.holdsWarn(held.length, held.map((i) => i.tag).join(", ")))}</div>` : ""}
<div class="actions">
<button class="btn btn-p" id="save">${T.save}</button>
<button class="btn btn-g" data-close>Cancel</button></div>
${id ? `<button class="btn btn-d" style="width:100%;margin-bottom:16px" id="rm">${T.deactivate}</button>
<p class="note" style="text-align:left">${T.peopleNote}</p>` : ""}`);
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 = `
<div class="card"><h3>${T.aCabinets}</h3>
<p style="color:var(--slate);font-size:13px;margin:0 0 10px">
${T.aCabinetsHelp}</p>
<button class="mini" data-new="cabinet">${T.aAddCabinet}</button></div>
${S.cabinets.map((c) => `<div class="card">
<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start">
<div><h3 style="margin-bottom:4px">${esc(c.full_name)}</h3>
<div class="mono" style="font-size:11.5px;color:var(--slate-2)">${esc(c.id)}${c.off_site ? " · " + T.aOffSite : ""}</div>
<p style="color:var(--slate);font-size:13px;margin:6px 0 0">${esc(c.note)}</p>
<p style="font-size:13px;margin:6px 0 0">${T.aItemsCount(S.items.filter((i) => i.cabinet_id === c.id).length)}</p></div>
<button class="mini" data-edit-cab="${esc(c.id)}">${T.aEdit}</button></div></div>`).join("")
|| `<div class="empty">${T.aNothing}</div>`}`;
}
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(`<h2>${id ? T.aEdit : T.aAddCabinet.replace("+ ", "")}</h2>
<div class="grid2">
<div><span class="lab">${T.fShortCode}</span>
<input class="field" id="f_id" value="${esc(c.id)}" placeholder="SAC"${id ? " readonly" : ""}></div>
<div><span class="lab">${T.fShortName}</span><input class="field" id="f_name" value="${esc(c.name)}" placeholder="Sacristy"></div>
</div>
<span class="lab">${T.fFullName}</span>
<input class="field" id="f_full" value="${esc(c.full_name)}" placeholder="Sacristy cabinet">
<span class="lab">${T.fWhereIs}</span>
<textarea class="field" id="f_note" style="min-height:70px" placeholder="Sacristy, behind the door. Its own key lives in the office cabinet.">${esc(c.note)}</textarea>
<label style="display:flex;gap:9px;align-items:center;margin-bottom:6px;font-size:14px">
<input type="checkbox" id="f_off"${c.off_site ? " checked" : ""}> ${T.fOffSiteLabel}</label>
<p class="note" style="text-align:left;margin-top:0">${T.offSiteNote}</p>
<span class="lab">${T.fSort}</span>
<input class="field" id="f_sort" type="number" value="${c.sort || 0}" style="margin-bottom:4px">
<p class="note" style="text-align:left;margin:0 0 12px">${T.fSortHelp}</p>
<div class="actions">
<button class="btn btn-p" id="save">${T.save}</button>
<button class="btn btn-g" data-close>Cancel</button></div>
${id ? `<button class="btn btn-d" style="width:100%;margin-bottom:16px" id="rm">${T.deleteCabinet}</button>` : ""}`);
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 = `
<div class="card"><h3>${T.aKinds}</h3>
<p style="color:var(--slate);font-size:13px;margin:0 0 10px">${T.aKindsHelp}</p>
<button class="mini" data-new="kind">${T.aAddKind}</button></div>
${S.kinds.map((k) => {
const n = S.items.filter((i) => i.kind === k.id).length;
return `<div class="card" style="display:flex;justify-content:space-between;gap:12px;align-items:center">
<div style="display:flex;align-items:center;gap:12px;min-width:0">
<span class="kind-dot" style="background:${esc(k.colour)};border-color:${esc(k.colour)};${SHAPE_CSS[k.shape] || ""}"></span>
<div style="min-width:0">
<b>${esc(k.name)}</b>
<div class="mono" style="font-size:11.5px;color:var(--slate-2)">${esc(k.id)} · ${esc(k.colour)}</div>
<div style="font-size:12.5px;color:var(--slate)">${T.kindInUse(n)}</div>
</div>
</div>
<button class="mini" data-edit-kind="${esc(k.id)}">${T.aEdit}</button>
</div>`;
}).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(`<h2>${id ? T.aEdit : T.aAddKind.replace("+ ", "")}</h2>
<div class="grid2">
<div><span class="lab">${T.fKindCode}</span>
<input class="field" id="f_id" value="${esc(k.id)}" placeholder="comando"${id ? " readonly" : ""}></div>
<div><span class="lab">${T.fKindName}</span>
<input class="field" id="f_name" value="${esc(k.name)}" placeholder="Comando de portão"></div>
</div>
<div class="grid2">
<div><span class="lab">${T.fShape}</span><select class="field" id="f_shape">
${[["circle", T.shCircle], ["rounded", T.shRounded], ["square", T.shSquare]]
.map(([v, l]) => `<option value="${v}"${k.shape === v ? " selected" : ""}>${esc(l)}</option>`).join("")}</select></div>
<div><span class="lab">${T.fColour}</span>
<input class="field" id="f_colour" type="color" value="${esc(k.colour)}" style="height:46px;padding:4px"></div>
</div>
<span class="lab">${T.fSort}</span><input class="field" id="f_sort" type="number" value="${k.sort || 0}">
<p class="note" style="text-align:left">${T.kindNote}</p>
${used ? `<div class="warnbox">${T.kindInUse(used)}</div>` : ""}
<div class="actions">
<button class="btn btn-p" id="save">${T.save}</button>
<button class="btn btn-g" data-close>${T.cancel}</button></div>
${id ? `<button class="btn btn-d" style="width:100%;margin-bottom:16px" id="rm">${T.deleteKind}</button>` : ""}`);
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 = `
<div class="card"><h3>${T.impTitle}</h3>
<span class="lab">${T.impWhat}</span>
<p style="color:var(--slate);font-size:12.5px;margin:0 0 8px">${T.impOrder}</p>
<div class="adm-nav" style="padding:0 0 12px">
${[["cabinets", T.impCabinets], ["doors", T.impDoors], ["people", T.impPeople], ["items", T.impItems]]
.map(([k, l], n) => `<button data-imp="${k}" aria-pressed="${impKind === k}">${n + 1}. ${l}</button>`).join("")}
</div>
<p style="color:var(--slate);font-size:13px">
<b>${esc(spec.req)}</b>${spec.opt ? " · " + esc(spec.opt) : ""}</p>
<textarea class="field" id="csv" style="min-height:150px;font-family:'IBM Plex Mono',monospace;font-size:12.5px"
placeholder="${esc(spec.ex)}"></textarea>
<button class="mini" id="imp">${T.impBtn}</button>
<div id="impres" style="margin-top:10px"></div></div>
<div class="card"><h3>${T.expTitle}</h3>
<p style="color:var(--slate);font-size:13px">${T.expHelp}</p>
<a class="mini" style="display:inline-block;text-decoration:none" href="/api/admin/export.csv">${T.expBtn}</a>
<a class="mini" style="display:inline-block;text-decoration:none;margin-left:6px;background:var(--card);color:var(--ink);border:1px solid var(--line)" href="/report">${T.reports}</a></div>
<div class="card"><h3>${T.bkTitle}</h3>
<p style="color:var(--slate);font-size:13px">${T.bkHelp}</p>
<a class="mini" style="display:inline-block;text-decoration:none" href="/api/admin/backup.db">${T.bkBtn}</a></div>
<div class="card"><h3 style="color:var(--late)">${T.wipeTitle}</h3>
<div class="warnbox">${T.wipeWarn}</div>
<input class="field" id="wipe" placeholder="${esc(T.wipePlaceholder)}">
<button class="btn btn-d" id="wipebtn" style="width:100%">${T.wipeBtn}</button></div>`;
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 =
`<div class="status-line in">${body}${
r.skipped && r.skipped.length
? T.impSkipped(r.skipped.length, esc(r.skipped.slice(0, 12).join(", ")) + (r.skipped.length > 12 ? "…" : ""))
: ""}</div>`;
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 = `<div class="card"><h3>${T.auditTitle}</h3>
<p style="color:var(--slate);font-size:13px;margin:0">${T.auditHelp}</p></div>
<div class="card" style="padding:0"><table><thead><tr><th>${T.aWhen}</th><th>${T.aWhat}</th><th>${T.aDetail}</th></tr></thead><tbody>
${rows.map((r) => `<tr><td class="mono" style="font-size:11.5px">${esc(r.ts.slice(0, 16).replace("T", " "))}</td>
<td>${esc(r.action)}</td><td style="color:var(--slate)">${esc(r.detail)}</td></tr>`).join("")
|| `<tr><td colspan="3" style="text-align:center;color:var(--slate);padding:24px">${T.aNothing}</td></tr>`}
</tbody></table></div>`;
}
/* ------------------------------------------------------------------ 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();