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
`
- + `
| Title / artist | Kind | Cond | Price | Target | Crate | Stock |
${rows||'| nothing found |
'}
`
+ $('#invrows').innerHTML = `
${d.total.toLocaleString()} items · click a row to edit
`
+ + `
`
+ `
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(){