From e9879a7528639165d6743ced0129549296020360 Mon Sep 17 00:00:00 2001 From: mfwadejr <125498560+mfwadejr@users.noreply.github.com> Date: Sun, 30 Aug 2026 15:47:52 -0400 Subject: [PATCH] Add adaptive fulfillment and warranty interface --- public/app.js | 32 ++++++++++++++++++++++++-------- public/extras.css | 1 + public/theme.css | 6 ++++-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/public/app.js b/public/app.js index 63b2d3c..cbecfcd 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,5 @@ const $ = (s) => document.querySelector(s); -const state = { products: [], models: [], tab: "available", query: "", page: 1, user: null }; +const state = { products: [], models: [], warranties: [], 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); @@ -27,6 +27,13 @@ function toast(message) { const el=$("#toast"); el.textContent=message; el.hidde 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 warrantyState(p){if(!p.warrantyName)return{label:"Not recorded",className:"neutral"};if(!p.warrantyEndDate)return{label:p.warrantyName==="No Warranty"?"No Warranty":p.warrantyName,className:"neutral"};const days=Math.round((new Date(`${p.warrantyEndDate}T12:00:00Z`)-new Date(`${today()}T12:00:00Z`))/86400000);return days>=0?{label:`In Warranty · ${days} day${days===1?"":"s"} remaining`,className:"in-warranty"}:{label:`Expired · ${Math.abs(days)} day${days===-1?"":"s"} ago`,className:"expired"};} +function warrantyBadge(p){const w=warrantyState(p);return `${esc(w.label)}`;} +function trackingUrl(p){if(!p.trackingNumber)return"";const n=encodeURIComponent(p.trackingNumber);return p.carrier==="UPS"?`https://www.ups.com/track?tracknum=${n}`:p.carrier==="FedEx"?`https://www.fedex.com/fedextrack/?trknbr=${n}`:p.carrier==="USPS"?`https://tools.usps.com/go/TrackConfirmAction?tLabels=${n}`:"";} +const fulfillmentTitle=method=>({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 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 idCell(p) { return ids(p).length ? ids(p).map(([k,v])=>`${k}: ${esc(v)}`).join("") : "Not entered"; } function customers() { const grouped=new Map(); @@ -54,14 +61,14 @@ function render() { $(".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"?"ProductUID / SN / MACReceivedCostStatus":state.tab==="customers"?"CustomerPhonePurchasesLast purchaseTotal spent":"ProductCustomerUID / SN / MACSoldPaymentSale price"; + $("#thead").innerHTML=state.tab==="available"?"ProductUID / SN / MACReceivedCostStatus":state.tab==="customers"?"CustomerPhonePurchasesLast purchaseTotal spent":"ProductCustomerSoldPaymentSale priceWarranty"; $("#rows").innerHTML=rows.map(p=>state.tab==="available"?inventoryRow(p):state.tab==="customers"?customerRow(p):saleRow(p)).join(""); decorateResponsiveTable(); $("#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
${state.user.role==="admin"?``:""}
`; } -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):"—"}
${state.user.role==="admin"?``:""}
`; } +function saleRow(p) { return `${esc(p.model)}${esc(p.manufacturer)} · ${esc(primaryId(p))}${p.phone?`${esc(p.phone)}`:""}${fmtDate(p.soldAt)}${esc(p.paymentMethod||"—")}${p.paymentReference?`${esc(p.paymentReference)}`:""}${p.salePrice?money.format(p.salePrice):"—"}${warrantyBadge(p)}
${state.user.role==="admin"?``:""}
`; } function customerRow(c) { return `${esc(c.name)}${esc(c.phone||"—")}${c.count}${fmtDate(c.lastPurchase)}${c.total?money.format(c.total):"—"}`; } function receiveForm() { @@ -71,21 +78,23 @@ 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, fulfillment, and warranty details.

`); + 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; } 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"?`
`:`

Transaction notes

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

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

`:""}
${notes}`); $("[data-cancel]").onclick=closeModal; if($("#transactionNotesForm"))$("#transactionNotesForm").onsubmit=e=>submitForm(e,`/api/products/${encodeURIComponent(p.id)}`,"Transaction notes saved.","PATCH"); + 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);}};} 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(" · "); const noteForm=state.user.role==="admin"?`
`:""; - openModal(`

${esc(c.name)}

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

`:""}

Customer notes

${noteForm}${c.notes.length?c.notes.map(n=>`
${esc(n.category)}${fmtDateTime(n.createdAt)}

${esc(n.note)}

${state.user.role==="admin"?``:""}
`).join(""):"

No customer notes yet.

"}

Purchases (${c.purchases.length})

${c.purchases.map(s=>`
${esc(s.model)} · ${esc(primaryId(s))}

${fmtDate(s.soldAt)} · ${s.salePrice?money.format(s.salePrice):"Price not recorded"}

`).join("")}
`); + openModal(`

${esc(c.name)}

Customer record, support notes, and purchase history.

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

${addr?esc(addr):"No permanent address recorded"}

${c.shippingNotes?`

${esc(c.shippingNotes)}

`:""}

Customer notes

${noteForm}${c.notes.length?c.notes.map(n=>`
${esc(n.category)}${fmtDateTime(n.createdAt)}

${esc(n.note)}

${state.user.role==="admin"?``:""}
`).join(""):"

No customer notes yet.

"}

Purchases (${c.purchases.length})

${c.purchases.map(s=>`
${esc(s.model)} · ${esc(primaryId(s))}

${fmtDate(s.soldAt)} · ${s.salePrice?money.format(s.salePrice):"Price not recorded"}

${warrantyBadge(s)}

`).join("")}
`); $("[data-cancel]").onclick=closeModal; document.querySelectorAll("[data-customer-sale]").forEach(b=>b.onclick=()=>viewSale(c.purchases.find(s=>s.id===b.dataset.customerSale))); if($("#customerNoteForm"))$("#customerNoteForm").onsubmit=async e=>{e.preventDefault();try{await api(`/api/customers/${encodeURIComponent(c.id)}/notes`,{method:"POST",body:JSON.stringify(Object.fromEntries(new FormData(e.currentTarget)))});await viewCustomer(p);toast("Customer note added.");}catch(error){toast(error.message);}}; document.querySelectorAll("[data-delete-note]").forEach(b=>b.onclick=async()=>{if(!confirm("Delete this customer note?"))return;try{await api(`/api/customers/${encodeURIComponent(c.id)}/notes/${encodeURIComponent(b.dataset.deleteNote)}`,{method:"DELETE"});await viewCustomer(p);toast("Customer note deleted.");}catch(error){toast(error.message);}}); @@ -93,7 +102,7 @@ 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]=await Promise.all([api("/api/products"),api("/api/models")]); render(); } +async function load(){ [state.products,state.models,state.warranties]=await Promise.all([api("/api/products"),api("/api/models"),api("/api/warranties")]); 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.

"}
`; @@ -104,6 +113,13 @@ async function renderAdmin(){ document.querySelectorAll("[data-rename-model]").forEach(b=>b.onclick=async()=>{const name=prompt(`Rename ${b.dataset.name}:`,b.dataset.name);if(name===null||name.trim()===b.dataset.name)return;try{await api(`/api/admin/models/${encodeURIComponent(b.dataset.renameModel)}`,{method:"PATCH",body:JSON.stringify({name})});await finishModelChange("Model renamed.");}catch(error){toast(error.message);}}); document.querySelectorAll("[data-toggle-model]").forEach(b=>b.onclick=async()=>{const active=b.dataset.active==="true";if(active&&!confirm(`Archive ${b.dataset.name}? It will no longer appear for newly received products. Existing records remain available (${b.dataset.available} available, ${b.dataset.sold} sold).`))return;try{await api(`/api/admin/models/${encodeURIComponent(b.dataset.toggleModel)}`,{method:"PATCH",body:JSON.stringify({active:!active})});await finishModelChange(active?"Model archived.":"Model reactivated.");}catch(error){toast(error.message);}}); document.querySelectorAll("[data-delete-model]").forEach(b=>b.onclick=async()=>{if(!confirm(`Permanently delete unused model ${b.dataset.name}?`))return;try{await api(`/api/admin/models/${encodeURIComponent(b.dataset.deleteModel)}`,{method:"DELETE"});await finishModelChange("Unused model deleted.");}catch(error){toast(error.message);}}); + const warranties=await api("/api/admin/warranties"),warrantySection=document.createElement("section");warrantySection.className="admin-section warranty-section";warrantySection.innerHTML=`

Warranty periods

Choose the default offered on new sales. Used periods are preserved as sale snapshots and can be archived, but not changed or deleted.

${warranties.map(w=>`
${esc(w.name)}${w.isDefault?'Default':`${w.active?"Active":"Archived"}`}
${w.durationValue} ${esc(w.durationUnit)} · ${w.usageCount} sale${w.usageCount===1?"":"s"}
${!w.isDefault&&w.active?``:""}${w.usageCount===0?``:""}${!w.isDefault?``:""}${w.usageCount===0&&!w.isDefault?``:""}
`).join("")}
`;modelSection.insertAdjacentElement("afterend",warrantySection); + const finishWarrantyChange=async message=>{state.warranties=await api("/api/warranties");toast(message);renderAdmin();}; + $("#createWarrantyForm").onsubmit=async e=>{e.preventDefault();try{await api("/api/admin/warranties",{method:"POST",body:JSON.stringify(Object.fromEntries(new FormData(e.currentTarget)))});await finishWarrantyChange("Warranty period added.");}catch(error){toast(error.message);}}; + document.querySelectorAll("[data-default-warranty]").forEach(b=>b.onclick=async()=>{try{await api(`/api/admin/warranties/${encodeURIComponent(b.dataset.defaultWarranty)}`,{method:"PATCH",body:JSON.stringify({isDefault:true})});await finishWarrantyChange(`${b.dataset.name} is now the default warranty.`);}catch(error){toast(error.message);}}); + document.querySelectorAll("[data-edit-warranty]").forEach(b=>b.onclick=async()=>{const name=prompt("Warranty name:",b.dataset.name);if(name===null)return;const value=prompt("Duration value:",b.dataset.value);if(value===null)return;const unit=prompt("Duration unit (days, months, or years):",b.dataset.unit);if(unit===null)return;try{await api(`/api/admin/warranties/${encodeURIComponent(b.dataset.editWarranty)}`,{method:"PATCH",body:JSON.stringify({name,durationValue:Number(value),durationUnit:unit.toLowerCase()})});await finishWarrantyChange("Warranty period updated.");}catch(error){toast(error.message);}}); + document.querySelectorAll("[data-toggle-warranty]").forEach(b=>b.onclick=async()=>{const active=b.dataset.active==="true";if(active&&!confirm(`Archive ${b.dataset.name}? Existing sales will retain it.`))return;try{await api(`/api/admin/warranties/${encodeURIComponent(b.dataset.toggleWarranty)}`,{method:"PATCH",body:JSON.stringify({active:!active})});await finishWarrantyChange(active?"Warranty period archived.":"Warranty period reactivated.");}catch(error){toast(error.message);}}); + document.querySelectorAll("[data-delete-warranty]").forEach(b=>b.onclick=async()=>{if(!confirm(`Permanently delete unused warranty ${b.dataset.name}?`))return;try{await api(`/api/admin/warranties/${encodeURIComponent(b.dataset.deleteWarranty)}`,{method:"DELETE"});await finishWarrantyChange("Unused warranty period deleted.");}catch(error){toast(error.message);}}); $("#createUserForm").onsubmit=async e=>{e.preventDefault();try{await api("/api/admin/users",{method:"POST",body:JSON.stringify(Object.fromEntries(new FormData(e.currentTarget)))});toast("User created.");renderAdmin();}catch(error){toast(error.message);}}; document.querySelectorAll("[data-role-user]").forEach(b=>b.onclick=async()=>{try{await api(`/api/admin/users/${b.dataset.roleUser}`,{method:"PATCH",body:JSON.stringify({role:b.dataset.role==="admin"?"readonly":"admin"})});toast("User role updated.");renderAdmin();}catch(e){toast(e.message);}}); document.querySelectorAll("[data-toggle-user]").forEach(b=>b.onclick=async()=>{try{await api(`/api/admin/users/${b.dataset.toggleUser}`,{method:"PATCH",body:JSON.stringify({enabled:b.dataset.enabled!=="true"})});toast("User status updated.");renderAdmin();}catch(e){toast(e.message);}}); diff --git a/public/extras.css b/public/extras.css index 053500f..403bbe0 100644 --- a/public/extras.css +++ b/public/extras.css @@ -6,3 +6,4 @@ #adminPanel{padding:24px}.admin-page{display:grid;gap:20px}.admin-page h2{margin:0 0 5px}.admin-page p{color:#667085}.admin-actions{display:flex;gap:10px;flex-wrap:wrap}.upload-backup{display:inline-flex;margin:0;background:#fff;border:1px solid #d7deea;border-radius:9px;padding:10px 14px;color:#27344a;cursor:pointer}.upload-backup input{display:none}.backup-warning{padding:13px 15px;border-radius:9px;background:#fff7ed;color:#9a3412;border:1px solid #fed7aa}.backup-list{border:1px solid #e3e7ef;border-radius:10px}.backup-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.backup-list article:last-child{border-bottom:0}.backup-list article small{display:block;color:#7b8799;margin-top:4px}.backup-list article>div:last-child{display:flex;gap:8px}.backup-list a.button{display:inline-flex;align-items:center;background:#fff;border:1px solid #d7deea;border-radius:9px;padding:10px 14px;color:#27344a;font-weight:650;text-decoration:none}.backup-list .delete-backup{background:#fff0f0;color:#c62828}.restart-message{text-align:center;padding:60px 20px}@media(max-width:760px){.backup-list article{align-items:flex-start;flex-direction:column}} .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}} diff --git a/public/theme.css b/public/theme.css index bb15b3f..d095c23 100644 --- a/public/theme.css +++ b/public/theme.css @@ -23,8 +23,9 @@ th{background:var(--surface-table);color:var(--muted-2)}th,td,.records nav,.back .records nav .tab,.customer-link{color:var(--muted)}.records nav .tab.active{color:var(--blue);border-color:var(--blue)}.tab i{background:var(--surface-soft)} .row-actions button,.customer-purchases button{background:var(--blue-soft);color:var(--blue-text)}.row-actions .danger,.customer-notes article>button,.delete-backup,.backup-list .delete-backup{background:var(--danger-soft);color:var(--danger)} .pill{background:var(--green-soft);color:var(--green)}.detail-card{background:var(--surface-soft);border-color:var(--border)} -.customer-notes article,.backup-list,.user-list,.model-list,.audit-list{border-color:var(--border)} +.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)} .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)} @@ -40,6 +41,7 @@ button,.button,input,select,textarea{-webkit-tap-highlight-color:transparent}but .inline-user-form{grid-template-columns:1fr 1fr}.inline-model-form{grid-template-columns:1fr auto}.audit-list article{grid-template-columns:120px 160px 1fr} } @media (max-width:820px){ + html,body{max-width:100%;overflow-x:hidden}input,select,textarea{font-size:16px!important}.auth-screen{min-height:100dvh;width:100%;overflow-x:hidden}.auth-card{max-width:100%} header{position:relative;display:grid;grid-template-columns:auto 1fr auto;height:auto;padding:12px max(14px,env(safe-area-inset-right)) 12px max(14px,env(safe-area-inset-left));gap:10px} header .brand{grid-column:1}.user-menu{grid-column:2/4;grid-row:1;margin-left:auto!important;min-width:0!important;padding-left:0;border-left:0}.user-menu #currentUser{display:grid!important;width:auto!important;min-width:78px;padding:5px 7px}.theme-control{grid-template-columns:1fr;margin-left:0;padding-left:6px}.theme-control>span{font-size:10px;text-align:center} header #search{grid-column:1/-1;grid-row:2;order:initial}.actions{grid-column:1/-1;grid-row:3;width:100%;margin:0}.actions button{font-size:14px!important;flex:1}.actions button::first-letter{font-size:inherit!important} @@ -48,7 +50,7 @@ button,.button,input,select,textarea{-webkit-tap-highlight-color:transparent}but .table-wrap{overflow:visible}table{display:block;min-width:0}thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}tbody{display:grid;gap:10px;padding:10px;background:var(--page)}tr{display:grid;background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden}td{display:grid;grid-template-columns:minmax(92px,34%) 1fr;gap:10px;align-items:start;padding:11px 13px;border-bottom:1px solid var(--border);overflow-wrap:anywhere}td:last-child{border-bottom:0}td::before{content:attr(data-label);font-size:11px;font-weight:750;text-transform:uppercase;letter-spacing:.04em;color:var(--muted-2)}td:empty{display:none}.row-actions{flex-wrap:wrap}.row-actions button{flex:1;min-width:95px} #empty{background:var(--surface);padding:45px 20px}#pagination{background:var(--surface);padding:14px 12px}.address-grid{grid-template-columns:1fr}.form-row,.scan{grid-template-columns:1fr} dialog{width:calc(100vw - 20px);max-height:calc(100dvh - 20px);padding:22px 18px;margin:10px}.form-actions{flex-wrap:wrap}.form-actions button{min-width:110px} - #adminPanel{padding:15px 12px}.admin-page{gap:26px}.admin-section{gap:13px}.inline-user-form,.inline-model-form{grid-template-columns:1fr}.user-list article,.model-list article,.backup-list article{align-items:stretch}.user-list article>div:last-child,.model-actions,.backup-list article>div:last-child{display:grid;grid-template-columns:1fr 1fr}.user-list button,.model-list button,.backup-list button,.backup-list a.button{justify-content:center;text-align:center}.audit-list article{grid-template-columns:1fr;gap:3px}.audit-list{max-height:55vh} + #adminPanel{padding:15px 12px}.admin-page{gap:26px}.admin-section{gap:13px}.inline-user-form,.inline-model-form,.inline-warranty-form{grid-template-columns:1fr}.user-list article,.model-list article,.warranty-list article,.backup-list article{align-items:stretch}.user-list article>div:last-child,.model-actions,.backup-list article>div:last-child{display:grid;grid-template-columns:1fr 1fr}.user-list button,.model-list button,.warranty-list button,.backup-list button,.backup-list a.button{justify-content:center;text-align:center}.audit-list article{grid-template-columns:1fr;gap:3px}.audit-list{max-height:55vh} .auth-screen{padding:16px}.auth-card{padding:25px 20px}.auth-brand span{display:inline} } @media (max-width:430px){