From 1152d7b73d2166d81af416680102acf0e2f17ac6 Mon Sep 17 00:00:00 2001 From: mfwadejr <125498560+mfwadejr@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:58:30 -0400 Subject: [PATCH] Add Customers section and sale notes editor --- public/app.js | 27 +++++++++++++++++++-------- public/index.html | 2 +- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/public/app.js b/public/app.js index 38e67b2..28e7557 100644 --- a/public/app.js +++ b/public/app.js @@ -20,9 +20,19 @@ 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 status=state.tab==="available"?"available":"sold", q=state.query.toLowerCase().trim(); + 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() { @@ -30,17 +40,18 @@ function render() { $("#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; + $("#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)); 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"?"ProductUID / SN / MACReceivedCostStatus":"ProductCustomerUID / SN / MACSoldPaymentSale price"; - $("#rows").innerHTML=rows.map(p=>state.tab==="available"?inventoryRow(p):saleRow(p)).join(""); - $("#empty").hidden=Boolean(rows.length); $("#empty").textContent=state.query?"No records match your search.":state.tab==="available"?"No products are available.":"No sales have been recorded."; + $("#thead").innerHTML=state.tab==="available"?"ProductUID / SN / MACReceivedCostStatus":state.tab==="customers"?"CustomerPhonePurchasesLast purchaseTotal spent":"ProductCustomerUID / SN / MACSoldPaymentSale price"; + $("#rows").innerHTML=rows.map(p=>state.tab==="available"?inventoryRow(p):state.tab==="customers"?customerRow(p):saleRow(p)).join(""); + $("#empty").hidden=Boolean(rows.length); $("#empty").textContent=state.query?"No records match your search.":state.tab==="available"?"No products are available.":state.tab==="customers"?"Customer records will appear after the first sale.":"No sales have been recorded."; $("#pagination").innerHTML=all.length?`Showing ${start+1}–${Math.min(start+PAGE_SIZE,all.length)} of ${all.length}
Page ${state.page} of ${pages}
`:""; } function inventoryRow(p) { return `${esc(p.model)}${esc(p.manufacturer)} · ${esc(p.condition)}
${idCell(p)}
${fmtDate(p.receivedAt)}${p.cost?money.format(p.cost):"—"}Available
`; } function saleRow(p) { return `${esc(p.model)}${esc(p.manufacturer)} · ${esc(p.condition)}${p.phone?`${esc(p.phone)}`:""}
${idCell(p)}
${fmtDate(p.soldAt)}${esc(p.paymentMethod||"—")}${p.paymentReference?`${esc(p.paymentReference)}`:""}${p.salePrice?money.format(p.salePrice):"—"}
`; } +function customerRow(c) { return `${esc(c.name)}${esc(c.phone||"—")}${c.count}${fmtDate(c.lastPurchase)}${c.total?money.format(c.total):"—"}`; } function receiveForm() { openModal(`

Receive product

Add a vSeeBox unit to available inventory.

`); @@ -48,14 +59,14 @@ function receiveForm() { } function sellForm(id="") { const available=state.products.filter(p=>p.status==="available"); - openModal(`

Record a sale

Enter the customer, payment, and shipped-to details.

Shipped to

`); + openModal(`

Record a sale

Enter the customer, payment, and shipped-to details.

Shipped to

`); $("#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){ e.preventDefault(); await change(path,"POST",Object.fromEntries(new FormData(e.currentTarget)),message); } +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) { - openModal(`

Sale record

${esc(p.model)} · ${fmtDate(p.soldAt)}

Customer

${esc(p.customerName||"Unknown")}

${esc(p.phone||"No phone recorded")}

Payment

${esc(p.paymentMethod||"Not recorded")}

${p.paymentReference?`

Reference: ${esc(p.paymentReference)}

`:""}

Shipped to

${address(p)?esc(address(p)):"No shipping address recorded"}

${p.shippingNotes?`

${esc(p.shippingNotes)}

`:""}

Product and sale

${esc(p.manufacturer)} ${esc(p.model)} · ${esc(p.condition)}

${idCell(p)}

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?`

Notes: ${esc(p.notes)}

`:""}
`); $("[data-cancel]").onclick=closeModal; + openModal(`

Sale record

${esc(p.model)} · ${fmtDate(p.soldAt)}

Customer

${esc(p.customerName||"Unknown")}

${esc(p.phone||"No phone recorded")}

Payment

${esc(p.paymentMethod||"Not recorded")}

${p.paymentReference?`

Reference: ${esc(p.paymentReference)}

`:""}

Shipped to

${address(p)?esc(address(p)):"No shipping address recorded"}

${p.shippingNotes?`

${esc(p.shippingNotes)}

`:""}

Product and sale

${esc(p.manufacturer)} ${esc(p.model)} · ${esc(p.condition)}

${idCell(p)}

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)}

`:""}
`); $("[data-cancel]").onclick=closeModal; $("#saleNotesForm").onsubmit=e=>submitForm(e,`/api/products/${encodeURIComponent(p.id)}`,"Sale notes saved.","PATCH"); } async function viewCustomer(p) { if(!p.customerId) return viewSale(p); diff --git a/public/index.html b/public/index.html index 81d8f67..b42e5ba 100644 --- a/public/index.html +++ b/public/index.html @@ -1,5 +1,5 @@ vSeeBox Stockroom
SStockroom

Inventory Overview

Available products0Ready to sell
Sold this month0No sales recorded
Inventory value$0Based on purchase cost
-
+