${esc(n.note)}
${state.user.role==="admin"?``:""}const $ = (s) => document.querySelector(s);
const state = { products: [], tab: "available", query: "", page: 1, user: null };
const PAGE_SIZE = 10;
const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
const today = () => new Date().toISOString().slice(0, 10);
const fmtDate = (v) => v ? new Date(`${v}T12:00:00`).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }) : "—";
const fmtDateTime = (v) => v ? new Date(v.includes("T")?v:`${v.replace(" ","T")}Z`).toLocaleString("en-US", { year:"numeric", month:"short", day:"numeric", hour:"numeric", minute:"2-digit" }) : "—";
const esc = (v = "") => String(v).replace(/[&<>'"]/g, c => ({ "&":"&", "<":"<", ">":">", "'":"'", '"':""" })[c]);
const ids = p => [["UID",p.uid],["SN",p.sn],["MAC",p.mac]].filter(([,v]) => v);
const primaryId = p => ids(p)[0]?.[1] || "No identifier";
function storageStatus(state,message) { const el=$("#storageStatus"); if(!el)return; el.className=`storage-status ${state}`; el.querySelector("b").textContent=message; }
const systemTheme=window.matchMedia("(prefers-color-scheme: dark)");
function savedTheme(){try{return localStorage.getItem("stockroom-theme")||"system"}catch{return "system"}}
function applyTheme(choice=savedTheme()){const resolved=choice==="system"?(systemTheme.matches?"dark":"light"):choice;document.documentElement.dataset.theme=resolved;document.querySelector('meta[name="theme-color"]').content=resolved==="dark"?"#0d1320":"#f5f7fb";if($("#themeSelect"))$("#themeSelect").value=choice;}
function decorateResponsiveTable(){const labels=[...document.querySelectorAll("#thead th")].map(th=>th.textContent.trim()||"Actions");document.querySelectorAll("#rows tr").forEach(row=>[...row.children].forEach((cell,index)=>cell.dataset.label=labels[index]||"Details"));}
async function api(path, options = {}) {
const writing=options.method&&options.method!=="GET"; if(writing)storageStatus("saving","Saving to database");
try { const response = await fetch(path, { headers: { "content-type":"application/json" }, ...options });
if (response.status === 204) { storageStatus("connected","Database connected"); return null; }
const data = await response.json();
if (!response.ok) { storageStatus("connected","Database connected"); const error=new Error(data.error||"Something went wrong."); error.databaseHealthy=true; error.status=response.status; error.code=data.code; if(response.status===401&&state.user)showLogin("Your session expired. Please sign in again."); throw error; }
storageStatus("connected","Database connected"); return data;
} catch(error) { if(!error.databaseHealthy)storageStatus("critical","Database error"); throw error; }
}
function toast(message) { const el=$("#toast"); el.textContent=message; el.hidden=false; clearTimeout(toast.timer); toast.timer=setTimeout(()=>el.hidden=true,2600); }
function openModal(html) { $("#modalBody").innerHTML=html; $("#modal").showModal(); }
function closeModal() { $("#modal").close(); }
function address(p) { return [p.shipAddress1,p.shipAddress2,[p.shipCity,p.shipState].filter(Boolean).join(", "),p.shipZip].filter(Boolean).join(" · "); }
function idCell(p) { return ids(p).length ? ids(p).map(([k,v])=>`${k}: ${esc(v)}`).join("") : "Not entered"; }
function customers() {
const grouped=new Map();
state.products.filter(p=>p.status==="sold"&&p.customerName).forEach(p=>{
const key=p.customerId||p.customerName.toLowerCase(), current=grouped.get(key)||{key,name:p.customerName,phone:p.phone,customerId:p.customerId,purchases:[],representative:p};
current.purchases.push(p); if((p.soldAt||"")>(current.representative.soldAt||"")) current.representative=p; grouped.set(key,current);
});
return [...grouped.values()].map(c=>({...c,count:c.purchases.length,total:c.purchases.reduce((n,p)=>n+Number(p.salePrice||0),0),lastPurchase:c.representative.soldAt})).sort((a,b)=>(b.lastPurchase||"").localeCompare(a.lastPurchase||""));
}
function filtered() {
const q=state.query.toLowerCase().trim();
if(state.tab==="customers") return customers().filter(c=>!q||[c.name,c.phone].some(v=>String(v||"").toLowerCase().includes(q))||c.purchases.some(p=>Object.values(p).some(v=>String(v??"").toLowerCase().includes(q))));
const status=state.tab==="available"?"available":"sold";
return state.products.filter(p=>p.status===status && (!q || Object.values(p).some(v=>String(v??"").toLowerCase().includes(q))));
}
function render() {
const available=state.products.filter(p=>p.status==="available"), sold=state.products.filter(p=>p.status==="sold"), month=today().slice(0,7), monthSales=sold.filter(p=>p.soldAt?.startsWith(month));
$("#availableCount").textContent=available.length; $("#soldCount").textContent=monthSales.length;
$("#value").textContent=money.format(available.reduce((n,p)=>n+Number(p.cost||0),0));
$("#revenue").textContent=monthSales.length?`${money.format(monthSales.reduce((n,p)=>n+Number(p.salePrice||0),0))} in sales`:"No sales recorded";
$("#availableBadge").textContent=available.length; $("#soldBadge").textContent=sold.length; $("#customerBadge").textContent=customers().length;
document.querySelectorAll("[data-tab]").forEach(b=>b.classList.toggle("active",b.dataset.tab===state.tab));
if(state.tab==="admin") { $(".table-wrap").hidden=true; $("#adminPanel").hidden=false; $("#pagination").innerHTML=""; renderAdmin(); return; }
$(".table-wrap").hidden=false; $("#adminPanel").hidden=true;
const all=filtered(), pages=Math.max(1,Math.ceil(all.length/PAGE_SIZE)); state.page=Math.min(state.page,pages);
const start=(state.page-1)*PAGE_SIZE, rows=all.slice(start,start+PAGE_SIZE);
$("#thead").innerHTML=state.tab==="available"?"
Add a vSeeBox unit to available inventory.
`); $("#receiveForm").onsubmit=e=>submitForm(e,"/api/products","Product received."); $("[data-cancel]").onclick=closeModal; } function sellForm(id="") { const available=state.products.filter(p=>p.status==="available"); openModal(`Enter the customer, payment, and shipped-to details.
`); $("#sellForm").onsubmit=async e=>{ e.preventDefault(); const data=Object.fromEntries(new FormData(e.currentTarget)), productId=data.productId; delete data.productId; await change(`/api/products/${encodeURIComponent(productId)}/sell`,"POST",data,"Sale recorded."); }; $("[data-cancel]").onclick=closeModal; } async function submitForm(e,path,message,method="POST"){ e.preventDefault(); await change(path,method,Object.fromEntries(new FormData(e.currentTarget)),message); } async function change(path,method,body,message){ try{ await api(path,{method,body:body?JSON.stringify(body):undefined}); closeModal(); await load(); toast(message); }catch(e){ toast(e.message); } } function viewSale(p) { const notes=state.user.role==="admin"?``:`${esc(p.saleNotes||"No transaction notes recorded")}
${esc(p.model)} · ${fmtDate(p.soldAt)}
${esc(p.customerName||"Unknown")}
${esc(p.phone||"No phone recorded")}
${esc(p.paymentMethod||"Not recorded")}
${p.paymentReference?`Reference: ${esc(p.paymentReference)}
`:""}${address(p)?esc(address(p)):"No shipping address recorded"}
${p.shippingNotes?`${esc(p.shippingNotes)}
`:""}${esc(p.manufacturer)} ${esc(p.model)} · ${esc(p.condition)}
Received: ${fmtDate(p.receivedAt)} · Sold: ${fmtDate(p.soldAt)}
Cost: ${p.cost?money.format(p.cost):"—"} · Sale price: ${p.salePrice?money.format(p.salePrice):"—"}
${p.notes?`Inventory notes: ${esc(p.notes)}
`:""}Customer record, support notes, and purchase history.
${esc(c.phone||"No phone recorded")}
${addr?esc(addr):"No shipping address recorded"}
${c.shippingNotes?`${esc(c.shippingNotes)}
`:""}${esc(n.note)}
${state.user.role==="admin"?``:""}No customer notes yet.
"}${fmtDate(s.soldAt)} · ${s.salePrice?money.format(s.salePrice):"Price not recorded"}
Return ${esc(p.model)} to inventory.
`); $("[data-cancel]").onclick=closeModal; $("#restockForm").onsubmit=e=>submitForm(e,`/api/products/${encodeURIComponent(p.id)}/restock`,"Product restocked."); } async function load(){ state.products=await api("/api/products"); render(); } async function renderAdmin(){ const panel=$("#adminPanel"); panel.innerHTML='Create administrators or read-only accounts. New and reset passwords must be changed at next login.
Create snapshots inside /data/backups, download an off-server copy, or restore a previous database.
No local backups yet.
"}The latest 250 security and data-changing events. Audit entries cannot be edited or deleted.
No audit events yet.
"}Use your Stockroom account to continue.
${message?``:""}`; $("#loginForm").onsubmit=async e=>{e.preventDefault();const button=e.currentTarget.querySelector("button");button.disabled=true;try{const user=await api("/api/auth/login",{method:"POST",body:JSON.stringify(Object.fromEntries(new FormData(e.currentTarget)))});user.mustChangePassword?showPasswordChange(user):await enterApp(user);}catch(error){showLogin(error.message);}}; } function showPasswordChange(user){ state.user=user;document.body.className="auth-required";$("#authBody").innerHTML=`Your temporary password must be replaced before you can use Stockroom.
`; $("#passwordForm").onsubmit=async e=>{e.preventDefault();try{await api("/api/auth/change-password",{method:"POST",body:JSON.stringify(Object.fromEntries(new FormData(e.currentTarget)))});await enterApp({...user,mustChangePassword:false});}catch(error){toast(error.message);}};$("#changeLogout").onclick=logout; } async function enterApp(user){state.user=user;document.body.className=user.role==="admin"?"role-admin":"role-readonly";const account=$("#currentUser"),menu=$(".user-menu");account.innerHTML=`Logged in as${esc(user.username)}${user.role==="admin"?"Admin":"Read-Only"}`;account.style.width="104px";account.style.textAlign="right";menu.style.marginLeft="auto";menu.style.minWidth="280px";menu.style.justifyContent="flex-end";const adminTab=document.querySelector('[data-tab="admin"]');adminTab.hidden=user.role!=="admin";adminTab.style.display=user.role==="admin"?"":"none";document.querySelectorAll(".edit-only").forEach(el=>{el.hidden=user.role!=="admin";el.style.display=user.role==="admin"?"":"none";});if(user.role!=="admin"&&state.tab==="admin")state.tab="available";await load();} async function logout(){try{await api("/api/auth/logout",{method:"POST",body:"{}"});}catch{}showLogin();} async function initialize(){try{const response=await fetch("/api/auth/me"),data=await response.json();if(!response.ok)return showLogin();data.mustChangePassword?showPasswordChange(data):await enterApp(data);}catch{showLogin("Unable to connect to Stockroom.");}} document.querySelectorAll("[data-tab]").forEach(b=>b.onclick=()=>{state.tab=b.dataset.tab;state.page=1;render();}); $("#search").oninput=e=>{state.query=e.target.value;state.page=1;render();}; $("[data-open=receive]").onclick=receiveForm; $("[data-open=sell]").onclick=()=>sellForm(); $("#pagination").onclick=e=>{if(!e.target.dataset.page)return;state.page+=e.target.dataset.page==="next"?1:-1;render();}; $("#rows").onclick=async e=>{const b=e.target.closest("button");if(!b)return;const id=b.dataset.sell||b.dataset.view||b.dataset.restock||b.dataset.delete||b.dataset.id,p=state.products.find(x=>x.id===id);if(!p)return;if(b.dataset.sell)sellForm(id);else if(b.dataset.view)viewSale(p);else if(b.dataset.customer!==undefined)viewCustomer(p);else if(b.dataset.restock)restockForm(p);else if(b.dataset.delete&&confirm(`Permanently delete this ${p.status==="sold"?"sale":"product"} record?`))await change(`/api/products/${encodeURIComponent(id)}`,"DELETE",null,"Record deleted.");}; $("#modal .close").onclick=closeModal; $("#modal").onclick=e=>{if(e.target===$("#modal"))closeModal();}; $("#logout").onclick=logout; $("#themeSelect").onchange=e=>{try{localStorage.setItem("stockroom-theme",e.target.value)}catch{}applyTheme(e.target.value);}; systemTheme.addEventListener?.("change",()=>{if(savedTheme()==="system")applyTheme("system")}); $("#date").textContent=new Date().toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}); applyTheme(); initialize();