diff --git a/public/app.js b/public/app.js index cbecfcd..3d18384 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,5 @@ const $ = (s) => document.querySelector(s); -const state = { products: [], models: [], warranties: [], tab: "available", query: "", page: 1, user: null }; +const state = { products: [], models: [], warranties: [], customerDirectory: [], tab: "available", query: "", page: 1, user: null, filters:{model:"",payment:"",fulfillment:"",warranty:"",from:"",to:""} }; const PAGE_SIZE = 10; const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }); const today = () => new Date().toISOString().slice(0, 10); @@ -32,8 +32,10 @@ function warrantyBadge(p){const w=warrantyState(p);return `({Shipped:"Shipped to","Dropped Off":"Dropped off","Installed At":"Installed at",Meet:"Meet"}[method]||"Fulfillment"); function warrantyOptions(selected=""){return state.warranties.map(w=>``).join("");} -function fulfillmentFields(method="",p={}){if(!method)return'

Select a delivery method to enter its details.

';const addressRequired=new Set(["Shipped","Installed At"]).has(method),casual=new Set(["Dropped Off","Meet"]).has(method),nameLabel=method==="Meet"?"Meeting place / venue":method==="Dropped Off"?"Drop-off location / venue":"Location name (optional)",noteLabel=method==="Meet"?"Meet details":method==="Dropped Off"?"Drop-off details":"Fulfillment notes";return `

${fulfillmentTitle(method)}

${method!=="Shipped"?``:""}
${method==="Shipped"?`
`:""}${casual?'Enter at least a venue, address, or detail.':""}
`;} +function fulfillmentFields(method="",p={}){if(!method)return'

Select a delivery method to enter its details.

';const addressRequired=new Set(["Shipped","Installed At"]).has(method),casual=new Set(["Dropped Off","Meet"]).has(method),nameLabel=method==="Meet"?"Meeting place / venue":method==="Dropped Off"?"Drop-off location / venue":"Location name (optional)",noteLabel=method==="Meet"?"Meet details":method==="Dropped Off"?"Drop-off details":"Fulfillment notes";return `

${fulfillmentTitle(method)}

${method!=="Shipped"?``:""}
${method==="Shipped"?`
`:""}${casual?'Enter at least a venue, address, or detail.':""}
`;} function bindFulfillment(form,p={}){const select=form.querySelector('[name="fulfillmentMethod"]'),target=form.querySelector(".fulfillment-fields");const draw=()=>target.innerHTML=fulfillmentFields(select.value,p);select.onchange=()=>{p={};draw();};draw();} +function customerFields(p={}){return `

`} +function bindCustomer(form){const name=form.elements.customerName,phone=form.elements.phone,id=form.elements.customerId,status=form.querySelector(".customer-match");const match=()=>{const phoneDigits=phone.value.replace(/\D/g,"").slice(-10),found=state.customerDirectory.find(c=>phoneDigits&&c.phone.replace(/\D/g,"").slice(-10)===phoneDigits)||state.customerDirectory.find(c=>c.name.toLowerCase()===name.value.trim().toLowerCase());if(found){id.value=found.id;name.value=found.name;if(!phone.value)phone.value=found.phone;status.textContent=`Existing customer selected${found.phone?` · ${found.phone}`:""}.`;}else{id.value="";status.textContent=name.value?"A new customer will be created.":"";}};name.onchange=match;name.oninput=()=>{id.value="";status.textContent=""};phone.onchange=match;match();} function idCell(p) { return ids(p).length ? ids(p).map(([k,v])=>`${k}: ${esc(v)}`).join("") : "Not entered"; } function customers() { const grouped=new Map(); @@ -48,8 +50,9 @@ 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)))); + return state.products.filter(p=>p.status===status&&(!q||Object.values(p).some(v=>String(v??"").toLowerCase().includes(q)))&&(!state.filters.model||p.model===state.filters.model)&&(!state.filters.payment||p.paymentMethod===state.filters.payment)&&(!state.filters.fulfillment||p.fulfillmentMethod===state.filters.fulfillment)&&(!state.filters.from||(p.soldAt||p.receivedAt)>=state.filters.from)&&(!state.filters.to||(p.soldAt||p.receivedAt)<=state.filters.to)&&(!state.filters.warranty||(state.filters.warranty==="active"&&warrantyState(p).className==="in-warranty")||(state.filters.warranty==="expired"&&warrantyState(p).className==="expired")||(state.filters.warranty==="none"&&warrantyState(p).className==="neutral"))); } +function renderFilters(){const bar=$("#filterBar");if(state.tab==="admin"){bar.innerHTML="";bar.hidden=true;return}if(state.tab==="customers"){bar.hidden=false;bar.innerHTML='Export customers CSV';return}bar.hidden=false;const models=[...new Set(state.products.map(p=>p.model))].sort();bar.innerHTML=`${state.tab==="sold"?``:""}Export CSV`;bar.querySelectorAll("[data-filter]").forEach(el=>el.onchange=()=>{state.filters[el.dataset.filter]=el.value;state.page=1;render()});$("#clearFilters").onclick=()=>{state.filters={model:"",payment:"",fulfillment:"",warranty:"",from:"",to:""};state.page=1;render()}} 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; @@ -57,6 +60,7 @@ function render() { $("#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)); + renderFilters(); 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); @@ -78,9 +82,9 @@ function receiveForm() { } function sellForm(id="") { const available=state.products.filter(p=>p.status==="available"); - openModal(`

Record a sale

Enter the customer, payment, fulfillment, and warranty details.

`); + openModal(`

Record a sale

Enter the customer, payment, fulfillment, and warranty details.

${customerFields()}
`); bindFulfillment($("#sellForm")); - $("#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; + bindCustomer($("#sellForm"));$("#sellForm").onsubmit=async e=>{ e.preventDefault();const button=e.submitter;button.disabled=true;const data=Object.fromEntries(new FormData(e.currentTarget)), productId=data.productId;delete data.productId;try{await change(`/api/products/${encodeURIComponent(productId)}/sell`,"POST",data,"Sale recorded.");}finally{button.disabled=false} }; $("[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); } } @@ -89,7 +93,7 @@ function viewSale(p) { const link=trackingUrl(p),fulfillment=p.fulfillmentMethod?`

${fulfillmentTitle(p.fulfillmentMethod)}

${p.fulfillmentName?`

${esc(p.fulfillmentName)}

`:""}

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

${p.fulfillmentNotes?`

${esc(p.fulfillmentNotes)}

`:""}${p.fulfillmentMethod==="Shipped"?`

Carrier: ${esc(p.carrier||"Not recorded")}

Tracking: ${p.trackingNumber?(link?`${esc(p.trackingNumber)}`:esc(p.trackingNumber)):"Not entered"}

Delivery status: ${esc(p.deliveryStatus||"Not recorded")}

`:""}
`:'

Fulfillment

Not recorded

'; openModal(`

Sale record

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

Customer

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

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

Payment and sale

${esc(p.paymentMethod||"Not recorded")}${p.paymentReference?` · Reference: ${esc(p.paymentReference)}`:""}

Sale price: ${p.salePrice?money.format(p.salePrice):"—"} · Sold: ${fmtDate(p.soldAt)}

${fulfillment}

Warranty

${warrantyBadge(p)}

${p.warrantyName?`${esc(p.warrantyName)}${p.warrantyEndDate?` · Through ${fmtDate(p.warrantyEndDate)}`:""}`:"No warranty was recorded for this sale."}

Product

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

${idCell(p)}

Received: ${fmtDate(p.receivedAt)} · Cost: ${p.cost?money.format(p.cost):"—"}

${p.notes?`

Inventory notes: ${esc(p.notes)}

`:""}

Transaction notes

${esc(p.saleNotes||"No transaction notes recorded")}

${state.user.role==="admin"?'':""}
`); $("[data-cancel]").onclick=closeModal;if($("#editSaleDetails"))$("#editSaleDetails").onclick=()=>editSaleForm(p); } -function editSaleForm(p){const historicalWarranty=p.warrantyPresetId&&!state.warranties.some(w=>w.id===p.warrantyPresetId)?``:"";openModal(`

Edit sale details

Update fulfillment, tracking, warranty, or transaction notes.

`);bindFulfillment($("#editSaleForm"),p);$("[data-cancel]").onclick=()=>viewSale(p);$("#editSaleForm").onsubmit=async e=>{e.preventDefault();try{const updated=await api(`/api/products/${encodeURIComponent(p.id)}`,{method:"PATCH",body:JSON.stringify(Object.fromEntries(new FormData(e.currentTarget)))});await load();toast("Sale details updated.");viewSale(updated);}catch(error){toast(error.message);}};} +function editSaleForm(p){const historicalWarranty=p.warrantyPresetId&&!state.warranties.some(w=>w.id===p.warrantyPresetId)?``:"";openModal(`

Edit sale details

Correct customer, payment, fulfillment, tracking, warranty, or notes.

${customerFields(p)}
`);bindFulfillment($("#editSaleForm"),p);bindCustomer($("#editSaleForm"));$("[data-cancel]").onclick=()=>viewSale(p);$("#editSaleForm").onsubmit=async e=>{e.preventDefault();const button=e.submitter;button.disabled=true;try{const updated=await api(`/api/products/${encodeURIComponent(p.id)}`,{method:"PATCH",body:JSON.stringify(Object.fromEntries(new FormData(e.currentTarget)))});await load();toast("Sale details updated.");viewSale(updated);}catch(error){toast(error.message);}finally{button.disabled=false}};} async function viewCustomer(p) { if(!p.customerId) return viewSale(p); try { const c=await api(`/api/customers/${encodeURIComponent(p.customerId)}`), addr=[c.address1,c.address2,[c.city,c.state].filter(Boolean).join(", "),c.zip].filter(Boolean).join(" · "); @@ -102,11 +106,12 @@ async function viewCustomer(p) { } function restockForm(p){ openModal(`

Void sale & restock

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,state.models,state.warranties]=await Promise.all([api("/api/products"),api("/api/models"),api("/api/warranties")]); render(); } +async function load(){ [state.products,state.models,state.warranties,state.customerDirectory]=await Promise.all([api("/api/products"),api("/api/models"),api("/api/warranties"),api("/api/customers")]); render(); } async function renderAdmin(){ const panel=$("#adminPanel"); panel.innerHTML='
Loading administration…
'; - try{const [backups,users,audit]=await Promise.all([api("/api/admin/backups"),api("/api/admin/users"),api("/api/admin/audit")]);panel.innerHTML=`

User accounts

Create administrators or read-only accounts. New and reset passwords must be changed at next login.

${users.map(u=>`
${esc(u.username)}${u.role==="admin"?"Admin":"Read-Only"} · ${u.enabled?"Enabled":"Disabled"}${u.mustChangePassword?" · Password change required":""}${u.lastLoginAt?` · Last login ${fmtDateTime(u.lastLoginAt)}`:""}
`).join("")}

Database backups

Create snapshots inside /data/backups, download an off-server copy, or restore a previous database.

Restore replaces the active database and all user accounts. Your current password is required. Everyone will be signed out afterward.
${backups.length?backups.map(b=>`
${esc(b.name)}${fmtDateTime(b.createdAt)} · ${(b.size/1024).toFixed(1)} KB
Download
`).join(""):"

No local backups yet.

"}

Audit log

The latest 250 security and data-changing events. Audit entries cannot be edited or deleted.

${audit.map(a=>`
${esc(a.username)}${esc(a.action.replaceAll("_"," "))}${fmtDateTime(a.createdAt)}${a.target?` · ${esc(a.target)}`:""}${a.details?` · ${esc(a.details)}`:""}${a.ipAddress?` · ${esc(a.ipAddress)}`:""}
`).join("")||"

No audit events yet.

"}
`; + try{const [backups,users,audit,diagnostics]=await Promise.all([api("/api/admin/backups"),api("/api/admin/users"),api("/api/admin/audit"),api("/api/admin/diagnostics")]);panel.innerHTML=`

User accounts

Create administrators or read-only accounts. New and reset passwords must be changed at next login.

${users.map(u=>`
${esc(u.username)}${u.role==="admin"?"Admin":"Read-Only"} · ${u.enabled?"Enabled":"Disabled"}${u.mustChangePassword?" · Password change required":""}${u.lastLoginAt?` · Last login ${fmtDateTime(u.lastLoginAt)}`:""}
`).join("")}

Database backups

Create snapshots inside /data/backups, download an off-server copy, or restore a previous database.

Restore replaces the active database and all user accounts. Your current password is required. Everyone will be signed out afterward.
${backups.length?backups.map(b=>`
${esc(b.name)}${fmtDateTime(b.createdAt)} · ${(b.size/1024).toFixed(1)} KB
Download
`).join(""):"

No local backups yet.

"}

Audit log

The latest 250 security and data-changing events. Audit entries cannot be edited or deleted.

${audit.map(a=>`
${esc(a.username)}${esc(a.action.replaceAll("_"," "))}${fmtDateTime(a.createdAt)}${a.target?` · ${esc(a.target)}`:""}${a.details?` · ${esc(a.details)}`:""}${a.ipAddress?` · ${esc(a.ipAddress)}`:""}
`).join("")||"

No audit events yet.

"}
`; panel.querySelector(".admin-page").insertAdjacentHTML("afterbegin",'
vBoxStock

Administration

Manage product models, access, data protection, and account activity.

'); + const bytes=n=>n>1073741824?`${(n/1073741824).toFixed(1)} GB`:n>1048576?`${(n/1048576).toFixed(1)} MB`:`${(n/1024).toFixed(1)} KB`,systemSection=document.createElement("section");systemSection.className="admin-section system-section";systemSection.innerHTML=`

System and data tools

vBoxStock ${esc(diagnostics.appVersion)} · Database schema ${diagnostics.schemaVersion} · ${esc(diagnostics.nodeVersion)}

Database${bytes(diagnostics.databaseSize)}${diagnostics.dataWritable?"/data is writable":"/data is not writable"}
Storage available${bytes(diagnostics.diskFree)}of ${bytes(diagnostics.diskTotal)}
Backups${diagnostics.backupCount}${diagnostics.lastBackupAt?`Latest ${fmtDateTime(diagnostics.lastBackupAt)}`:"None created"}
Time zone${esc(diagnostics.timeZone)}${esc(diagnostics.dataDirectory)}
`;panel.querySelector(".admin-intro").insertAdjacentElement("afterend",systemSection);$("#backupScheduleForm").onsubmit=async e=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.currentTarget));try{await api("/api/admin/backup-settings",{method:"PATCH",body:JSON.stringify({enabled:Boolean(data.enabled),hour:Number(data.hour),retention:Number(data.retention)})});toast("Backup schedule saved.");renderAdmin()}catch(error){toast(error.message)}}; const models=await api("/api/admin/models"),modelSection=document.createElement("section");modelSection.className="admin-section model-section";modelSection.innerHTML=`

Product models

Active models are available when receiving products. Archiving removes a model from new receiving while preserving inventory and sales history.

${models.map(m=>`
${esc(m.name)}${m.active?"Active":"Archived"}
${m.availableCount} available · ${m.soldCount} sold
${m.totalCount===0?``:""}${m.totalCount===0?``:""}
`).join("")||"

No models configured.

"}
`;panel.querySelector(".admin-intro").insertAdjacentElement("afterend",modelSection); const finishModelChange=async message=>{state.models=await api("/api/models");toast(message);renderAdmin();}; $("#createModelForm").onsubmit=async e=>{e.preventDefault();try{await api("/api/admin/models",{method:"POST",body:JSON.stringify(Object.fromEntries(new FormData(e.currentTarget)))});await finishModelChange("Model added.");}catch(error){toast(error.message);}}; diff --git a/public/extras.css b/public/extras.css index 403bbe0..b67fa60 100644 --- a/public/extras.css +++ b/public/extras.css @@ -7,3 +7,4 @@ .auth-pending #appShell,.auth-required #appShell{display:none}.role-admin #authScreen,.role-readonly #authScreen{display:none}.auth-screen{min-height:100vh;display:grid;place-items:center;padding:24px;background:linear-gradient(145deg,#eef4ff,#f8fafc)}.auth-card{width:min(430px,100%);background:#fff;border:1px solid #dfe5ef;border-radius:18px;box-shadow:0 22px 60px #17203320;padding:34px}.auth-brand{margin-bottom:28px}.auth-card h1{margin:0 0 7px}.auth-card>div>p{color:#68758b}.auth-submit{width:100%;margin-top:14px}.auth-message{background:#fff0f0;color:#a51d1d;border:1px solid #fecaca;border-radius:8px;padding:10px 12px;margin:14px 0}.auth-logout{width:100%;margin-top:10px}.user-menu{display:flex;align-items:center;gap:10px;border-left:1px solid #e3e7ef;padding-left:18px}.user-menu span{display:grid;font-size:13px}.user-menu small{color:#758197}.admin-section{display:grid;gap:16px;border-bottom:1px solid #e3e7ef;padding-bottom:28px}.admin-section:last-child{border-bottom:0}.inline-user-form{display:grid;grid-template-columns:1fr 1fr 180px auto;gap:10px;align-items:end}.inline-user-form label{margin:0}.user-list,.audit-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.user-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.user-list article:last-child{border-bottom:0}.user-list small{display:block;color:#7b8799;margin-top:4px}.user-list article>div:last-child{display:flex;gap:7px;flex-wrap:wrap}.delete-backup{background:#fff0f0;color:#c62828}.audit-list{max-height:420px;overflow:auto}.audit-list article{display:grid;grid-template-columns:140px 180px 1fr;gap:12px;padding:10px 13px;border-bottom:1px solid #edf0f5;font-size:13px}.audit-list span{text-transform:capitalize}.audit-list small{color:#7b8799}@media(max-width:900px){.inline-user-form{grid-template-columns:1fr 1fr}.user-list article{align-items:flex-start;flex-direction:column}.audit-list article{grid-template-columns:1fr}.user-menu span{display:none}}@media(max-width:600px){.inline-user-form{grid-template-columns:1fr}.user-menu{padding-left:5px}.user-menu button{font-size:12px;padding:8px}.auth-card{padding:25px}} .inline-model-form{display:grid;grid-template-columns:minmax(220px,1fr) auto;gap:10px;align-items:end}.inline-model-form label{margin:0}.model-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.model-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.model-list article:last-child{border-bottom:0}.model-summary>div{display:flex;align-items:center;gap:9px}.model-summary small{display:block;color:#7b8799;margin-top:5px}.model-status{display:inline-flex;border-radius:99px;padding:3px 8px;font-size:11px;font-weight:750}.model-status.active{background:#dcfce7;color:#166534}.model-status.archived{background:#eef1f5;color:#526071}.model-actions{display:flex;gap:7px;flex-wrap:wrap}@media(max-width:760px){.inline-model-form{grid-template-columns:1fr}.model-list article{align-items:stretch;flex-direction:column}.model-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.model-actions button{text-align:center}} .conditional-fields{margin:2px 0 14px;padding:16px;border:1px solid #e3e7ef;border-radius:11px;background:#f8fafc}.conditional-fields h3{margin:0 0 12px}.conditional-hint{margin:4px 0 16px;padding:13px;border:1px dashed #d7deea;border-radius:9px;color:#68758b}.field-help{display:block;margin-top:-8px;color:#68758b}.warranty-pill{display:inline-flex;align-items:center;border-radius:99px;padding:5px 9px;font-size:12px;font-weight:750;white-space:nowrap}.warranty-pill.in-warranty{background:#dcfce7;color:#166534}.warranty-pill.expired{background:#fee2e2;color:#b91c1c}.warranty-pill.neutral{background:#eef1f5;color:#526071}.inline-warranty-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px 150px auto;gap:10px;align-items:end}.inline-warranty-form label{margin:0}.warranty-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.warranty-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.warranty-list article:last-child{border-bottom:0}@media(max-width:760px){.inline-warranty-form{grid-template-columns:1fr}.warranty-list article{align-items:stretch;flex-direction:column}} +.filter-bar{display:flex;align-items:end;gap:9px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid #e3e7ef}.filter-bar label{margin:0;min-width:125px;font-size:11px}.filter-bar select,.filter-bar input{padding:8px 9px;font-size:13px}.filter-bar .button,.filter-bar button{padding:9px 12px}.export-button{text-decoration:none}.customer-match{min-height:18px;margin:-7px 0 10px!important;color:#1d4ed8!important;font-size:12px}.diagnostic-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.diagnostic-grid article{display:grid;gap:4px;padding:14px;border:1px solid #e3e7ef;border-radius:10px}.diagnostic-grid span,.diagnostic-grid small{color:#68758b}.diagnostic-grid strong{font-size:18px}.backup-schedule{display:grid;grid-template-columns:minmax(220px,1fr) 180px 200px auto;gap:10px;align-items:end}.backup-schedule label{margin:0}.check-label{display:flex!important;align-items:center;gap:8px;min-height:42px}.check-label input{width:auto}.export-links{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.export-links .button{padding:8px 11px;text-decoration:none}@media(max-width:900px){.diagnostic-grid{grid-template-columns:1fr 1fr}.backup-schedule{grid-template-columns:1fr 1fr}}@media(max-width:600px){.filter-bar{align-items:stretch}.filter-bar label,.filter-bar .button,.filter-bar button{width:100%}.diagnostic-grid,.backup-schedule{grid-template-columns:1fr}} diff --git a/public/index.html b/public/index.html index 4e199e4..ac16416 100644 --- a/public/index.html +++ b/public/index.html @@ -1,5 +1,5 @@ vBoxStock
vBoxStock
vBoxStock

Inventory Overview

Available products0Ready to sell
Sold this month0No sales recorded
Inventory value$0Based on purchase cost
-
+
diff --git a/public/theme.css b/public/theme.css index d095c23..f390991 100644 --- a/public/theme.css +++ b/public/theme.css @@ -26,6 +26,7 @@ th{background:var(--surface-table);color:var(--muted-2)}th,td,.records nav,.back .customer-notes article,.backup-list,.user-list,.model-list,.warranty-list,.audit-list{border-color:var(--border)} .model-list article{border-color:var(--border)}.model-summary small{color:var(--muted)}.model-status.active{background:var(--green-soft);color:var(--green)}.model-status.archived{background:var(--surface-soft);color:var(--muted)} .warranty-list article,.conditional-fields{border-color:var(--border)}.conditional-fields{background:var(--surface-soft)}.conditional-hint{border-color:var(--border-strong);color:var(--muted)}.field-help{color:var(--muted)}.warranty-pill.in-warranty{background:var(--green-soft);color:var(--green)}.warranty-pill.expired{background:var(--danger-soft);color:var(--danger)}.warranty-pill.neutral{background:var(--surface-soft);color:var(--muted)} +.filter-bar{border-color:var(--border);background:var(--surface-soft)}.filter-bar .button{background:var(--surface);border:1px solid var(--border-strong);color:var(--text)}.customer-match{color:var(--blue-text)!important}.diagnostic-grid article{background:var(--surface-soft);border-color:var(--border)}.diagnostic-grid span,.diagnostic-grid small{color:var(--muted)}.export-links .button{background:var(--surface-soft);border:1px solid var(--border-strong);color:var(--text)} .note-category{background:var(--blue-soft);color:var(--blue-text)}.backup-warning{background:var(--warning-soft);border-color:color-mix(in srgb,var(--warning) 40%,transparent);color:var(--warning)} .auth-screen{background:linear-gradient(145deg,color-mix(in srgb,var(--blue) 10%,var(--page)),var(--page))}.auth-card{box-shadow:0 22px 60px var(--shadow)} dialog{max-height:min(90dvh,900px);overflow:auto}dialog::backdrop{background:#050914aa}.close{color:var(--muted)}