diff --git a/site/admin.html b/site/admin.html index 953153f..a252e93 100644 --- a/site/admin.html +++ b/site/admin.html @@ -126,24 +126,30 @@ const kpi = (n,l,pink) => `
${n // ---------- Inventory ---------- let invState = { q:'', kind:'', crate_id:null, page:1 }; +let invItems = {}; // sku -> row (for the edit modal) +let invSel = new Set(); // selected skus (bulk) async function vInv(){ const m = $('#content'); m.innerHTML = '

Inventory

' + '
' + '' - + '
' + + '' + + '
' + + '
' + '
loading…
'; $('#iq').addEventListener('keydown', e => { if(e.key==='Enter') invSearch(); }); $('#ikind').value = invState.kind; - loadInv(); + invSel.clear(); loadInv(); } -function invSearch(){ invState.q = $('#iq').value.trim(); invState.kind = $('#ikind').value; invState.crate_id = null; invState.page = 1; loadInv(); } +function invSearch(){ invState.q = $('#iq').value.trim(); invState.kind = $('#ikind').value; invState.crate_id = null; invState.page = 1; invSel.clear(); loadInv(); } async function loadInv(){ const cr = invState.crate_id ? `&crate_id=${invState.crate_id}` : ''; const d = await api(`/inventory?q=${encodeURIComponent(invState.q)}&kind=${invState.kind}&page=${invState.page}${cr}`); + invItems = {}; d.items.forEach(r => invItems[r.sku] = r); const rows = d.items.map(r => ` - ${r.thumb?``:''} - ${r.title?escapeH(r.title):''+r.sku+''}
${r.artist?escapeH(r.artist):''}
+ + ${r.release_id?``:''} + ${r.title?escapeH(r.title):''+r.sku+''}
${r.artist?escapeH(r.artist):''}
${r.kind} ${r.condition||'—'} ${money(r.price)} @@ -152,11 +158,87 @@ async function loadInv(){ ${r.in_stock?'in':'sold'} `).join(''); const pages = Math.max(1, Math.ceil(d.total/d.per)); - $('#invrows').innerHTML = `
${d.total.toLocaleString()} items
` - + `${rows||''}
Title / artistKindCondPriceTargetCrateStock
nothing found
` + $('#invrows').innerHTML = `
${d.total.toLocaleString()} items · click a row to edit
` + + `${rows||''}
Title / artistKindCondPriceTargetCrateStock
nothing found
` + `
page ${d.page} / ${pages}
`; + invBulkBar(); } function invPage(n){ invState.page += n; loadInv(); } +function invSelTog(sku){ invSel.has(sku)?invSel.delete(sku):invSel.add(sku); invBulkBar(); } +function invSelAll(on){ if(on) Object.keys(invItems).forEach(s=>invSel.add(s)); else invSel.clear(); loadInv(); } +function invBulkBar(){ + const b = $('#invbulk'); if(!b) return; const n = invSel.size; + b.innerHTML = n ? `
+ ${n} selected + + +
` : ''; +} +async function invBulk(){ + const action = $('#bAct').value; let value = $('#bVal').value; + if(action==='delete' && !confirm(`Delete ${invSel.size} items? This can't be undone.`)) return; + if(['set_price','adjust_pct','set_crate'].includes(action)) value = parseFloat(value); + await fetch('/admin/inventory/bulk',{method:'POST',headers:hdr(),body:JSON.stringify({skus:[...invSel],action,value})}); + invSel.clear(); loadInv(); +} +function invModalBox(html){ + let o = $('#invModal'); + if(!o){ o = document.createElement('div'); o.id='invModal'; o.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:flex-start;justify-content:center;z-index:200;padding-top:6vh'; document.body.appendChild(o); o.addEventListener('mousedown',e=>{ if(e.target===o) o.remove(); }); } + o.innerHTML = '
'+html+'
'; +} +function invModalClose(){ const o = $('#invModal'); if(o) o.remove(); } +function numOrNull(id){ const x = $('#'+id).value; return x===''?null:parseFloat(x); } +function fld(label,id,val,attr){ return `
${label}
`; } +function invEdit(sku){ + const r = invItems[sku] || {}; + invModalBox(`

${escapeH(r.title||sku)}

+ ${fld('Title','eTitle',r.title)} + ${fld('Price','ePrice',r.price,'type=number step=0.01')} + ${fld('Target','eTarget',r.target,'type=number step=0.01')} + ${fld('Condition','eCond',r.condition)} + ${fld('Sleeve','eSleeve',r.sleeve_cond)} + ${fld('Crate id','eCrate',r.crate_id,'type=number')} + ${fld('Slot','eSlot',r.slot_number,'type=number')} + ${fld('Kind','eKind',r.kind)} +
In stock
+ ${fld('Notes','eNotes',r.notes)} +
+ +
`); +} +async function invSave(sku){ + const v = id => { const x = $('#'+id).value.trim(); return x===''?null:x; }; + const body = { title:v('eTitle'), price:numOrNull('ePrice'), target_price:numOrNull('eTarget'), + condition:v('eCond'), sleeve_cond:v('eSleeve'), crate_id:numOrNull('eCrate'), slot_number:numOrNull('eSlot'), + kind:v('eKind'), notes:v('eNotes'), in_stock:$('#eStock').checked }; + await fetch('/admin/inventory/'+encodeURIComponent(sku)+'/edit',{method:'POST',headers:hdr(),body:JSON.stringify(body)}); + invModalClose(); loadInv(); +} +async function invDelete(sku){ + if(!confirm('Delete this item?')) return; + await fetch('/admin/inventory/'+encodeURIComponent(sku)+'/delete',{method:'POST',headers:hdr()}); + invModalClose(); loadInv(); +} +function invAdd(){ + invModalBox(`

Add item

+ ${fld('Title','aTitle','')} + ${fld('Release ID','aRid','','type=number')} + ${fld('Barcode/UPC','aIdent','')} + ${fld('Price','aPrice','','type=number step=0.01')} + ${fld('Condition','aCond','')} + ${fld('Kind','aKind','vinyl')} + ${fld('Crate id','aCrate','','type=number')} + ${fld('SKU (blank=auto)','aSku','')} +
Tip: enter a Release ID and the title fills from the catalog.
+
`); +} +async function invDoAdd(){ + const v = id => { const x = $('#'+id).value.trim(); return x===''?null:x; }; + const body = { sku:v('aSku'), title:v('aTitle'), release_id:numOrNull('aRid'), identifier:v('aIdent'), + price:numOrNull('aPrice'), condition:v('aCond'), kind:v('aKind')||'vinyl', crate_id:numOrNull('aCrate') }; + await fetch('/admin/inventory/add',{method:'POST',headers:hdr(),body:JSON.stringify(body)}); + invModalClose(); loadInv(); +} // ---------- Orders ---------- async function vOrders(){