-
-
Store map
-
-
β Store
-
βοΈ rack
-
π Reorganize
-
- β
- Level
- βΆ
-
+
+ π« Scanner
+ β© Returns
+ π¦ Reorganize
+ π Collections
+ π Stock Finder
+
+
+
+ Space
+
+
+ π¬ Store
+ ποΈ Rack
+ π¦ Crate
+ πΏ Item
+
+
+ β
+ Level
+ βΆ
+
+ βοΈ rack
+ β Select Mode β click crates in order
+
+
+
+
+
-
-
+
+
+
+
+
pick a crate on the map β
+
+
+
+
click a record, or look one up β
+
+
+
+
@@ -118,57 +128,91 @@ const $=s=>document.querySelector(s);
let TOKEN=localStorage.getItem('rg_token')||'';
const hdr=()=>({'Authorization':'Bearer '+TOKEN,'Content-Type':'application/json'});
const get=u=>fetch(u,{headers:hdr()}).then(r=>r.json());
+const post=(u,b)=>fetch(u,{method:'POST',headers:hdr(),body:JSON.stringify(b)}).then(r=>r.json());
const money=n=>n==null?'β':'$'+Number(n).toFixed(2);
const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
-// WebP cover via RecordGod's own image cache; fall back to the raw discogs thumb if the cover can't convert
const imgTag=it=> it.release_id ? `
` : (it.thumb?`
`:'
');
+function chip(label,ondel,num){ return `
${esc(label)} β `; }
-// ββ facing convention (copied from WowPlatter production, see RECORDGOD_NAVIGATOR_PLAN Β§3) ββ
+// facing convention (RECORDGOD_NAVIGATOR_PLAN Β§3)
const CELL=0.34;
function directionYaw(d){ d=(d||'').toLowerCase();
if(d==='back'||d==='south') return Math.PI;
if(d==='left'||d==='west') return Math.PI/2;
if(d==='right'||d==='east') return -Math.PI/2;
- return 0; } // forward/front/north
+ return 0; }
const ARROW={forward:'β',front:'β',north:'β',back:'β',south:'β',left:'β',west:'β',right:'β',east:'β'};
-let ST={view:'store', space:null, racks:[], rack:null, level:1, crates:[], levels:[], selCrate:null, hit:[]};
+let ST={tab:'scanner', view:'store', space:null, racks:[], rack:null, level:1, crates:[], levels:[],
+ selCrate:null, hit:[], storeTf:null, itemShown:false,
+ pick:[], pickNames:{}, edit:null, crateInfo:null};
async function signin(){
TOKEN=$('#tok').value.trim();
if(!(await fetch('/admin/stats',{headers:hdr()})).ok){ $('#gerr').textContent='invalid token'; return; }
localStorage.setItem('rg_token',TOKEN);
- $('#gate').style.display='none'; $('#app').style.display='grid';
- $('#q').focus(); await loadSpaces(); toStore(); loadCollections();
+ $('#gate').style.display='none'; $('#app').style.display='block';
+ await loadSpaces(); toStore(); setTab('scanner'); $('#omni').focus();
}
-
async function loadSpaces(){
const d=await get('/nav/spaces');
$('#spaceSel').innerHTML=(d.spaces||[]).map(s=>`
${esc(s.name||('Space '+s.id))}${s.is_default?' (default)':''} `).join('');
$('#spaceSel').onchange=()=>toStore();
}
-// ββ STORE VIEW ββ
+// βββββββββββββ tabs βββββββββββββ
+function setTab(t){
+ ST.tab=t;
+ document.querySelectorAll('.tab').forEach(b=>b.classList.toggle('on',b.dataset.t===t));
+ $('#selModeHint').style.display = t==='reorganize' ? 'inline' : 'none';
+ renderTool(t);
+ redraw(); // pick / collection-edit highlights + click behaviour depend on the tab
+}
+function renderTool(t){
+ const p=$('#toolPanel');
+ if(t==='scanner'){ p.innerHTML=toolScanner(); renderScanner(); }
+ else if(t==='returns'){ p.innerHTML=toolReturns(); }
+ else if(t==='reorganize'){ p.innerHTML=toolReorg(); renderPick(); }
+ else if(t==='collections'){ p.innerHTML=toolCollections(); loadCollections(); }
+ else if(t==='finder'){ p.innerHTML=toolFinder(); $('#q').focus(); }
+}
+
+// βββββββββββββ view modes βββββββββββββ
+function setViewBtns(){
+ document.querySelectorAll('#viewSeg button').forEach(b=>b.classList.toggle('on',b.dataset.v===ST.view));
+ $('#viewSeg [data-v=rack]').disabled=!ST.rack;
+ $('#viewSeg [data-v=crate]').disabled=!ST.selCrate;
+ $('#viewSeg [data-v=item]').disabled=!ST.itemShown;
+}
+function setView(m){
+ if(m==='store') return toStore();
+ if(m==='rack'){ if(ST.rack) openRack(ST.rack.id, ST.selCrate); return; }
+ if(m==='crate'){ if(ST.selCrate){ ST.view='crate'; setViewBtns(); $('#midcol').scrollIntoView({behavior:'smooth',block:'nearest'}); } return; }
+ if(m==='item'){ if(ST.itemShown){ ST.view='item'; setViewBtns(); $('#relBody').scrollIntoView({behavior:'smooth',block:'nearest'}); } return; }
+}
+function redraw(){ if(ST.view==='store') drawStore(); else if(ST.rack) drawRack(); }
+
+// βββββββββββββ STORE VIEW βββββββββββββ
async function toStore(){
ST.view='store'; ST.rack=null; ST.selCrate=null;
- $('#backBtn').style.display='none'; $('#lvlCtl').style.display='none'; $('#rackEditBtn').style.display='none';
+ $('#lvlCtl').style.display='none'; $('#rackEditBtn').style.display='none';
$('#crumb').textContent='Store map';
const sid=$('#spaceSel').value;
const d=await get('/nav/store-layout'+(sid?('?space_id='+sid):''));
ST.space=d.space; ST.racks=d.racks||[];
- $('#legend').innerHTML='
rack β click to open';
- drawStore();
+ $('#legend').innerHTML='
rack β click to open'+(ST.tab==='reorganize'?' Β· drag to move ':'')+'';
+ drawStore(); setViewBtns();
}
function drawStore(){
const cv=$('#cv'), g=cv.getContext('2d'), W=cv.width, H=cv.height;
g.clearRect(0,0,W,H); g.fillStyle='#fbfbfd'; g.fillRect(0,0,W,H);
ST.hit=[];
if(!ST.racks.length){ g.fillStyle='#888'; g.font='14px system-ui'; g.textAlign='center'; g.fillText('No racks in this space',W/2,H/2); return; }
- // bounds over racks (pos Β± half-dims) β fit
let minX=1e9,maxX=-1e9,minZ=1e9,maxZ=-1e9;
ST.racks.forEach(r=>{ const hw=(r.w||0.5)/2, hd=(r.d||0.5)/2;
minX=Math.min(minX,r.x-hw); maxX=Math.max(maxX,r.x+hw); minZ=Math.min(minZ,r.z-hd); maxZ=Math.max(maxZ,r.z+hd); });
const pad=34, sc=Math.min((W-2*pad)/((maxX-minX)||1),(H-2*pad)/((maxZ-minZ)||1));
+ ST.storeTf={minX,minZ,sc,pad};
const X=x=>pad+(x-minX)*sc, Y=z=>pad+(z-minZ)*sc;
ST.racks.forEach(r=>{
const yaw=directionYaw(r.direction)+(r.rot||0)*Math.PI/180;
@@ -184,29 +228,29 @@ function drawStore(){
});
}
-// ββ RACK VIEW ββ
+// βββββββββββββ RACK VIEW βββββββββββββ
async function openRack(id, focusCrate){
const d=await get('/nav/rack/'+id);
ST.view='rack'; ST.rack=d.rack; ST.crates=d.crates||[]; ST.levels=d.levels||[];
- // default to the level holding the most crates (or the focus crate's level)
const byLvl={}; ST.crates.forEach(c=>{ const l=c.level==null?1:c.level; byLvl[l]=(byLvl[l]||0)+1; });
ST.level = focusCrate!=null ? (ST.crates.find(c=>c.id===focusCrate)?.level ?? 1)
: (+Object.keys(byLvl).sort((a,b)=>byLvl[b]-byLvl[a])[0] || 1);
ST.selCrate=focusCrate||null;
- $('#backBtn').style.display=''; $('#rackEditBtn').style.display='';
+ $('#rackEditBtn').style.display='';
$('#crumb').innerHTML='Store /
'+esc(ST.rack.name||('Rack '+ST.rack.id))+' ';
- drawRack();
+ drawRack(); setViewBtns();
+ if(ST.tab==='reorganize') renderPick();
}
function levelMeta(){
- const lv=ST.rack.levels||1;
- const cur=ST.level, label=lv>1?`Level ${cur} of ${lv}${cur===1?' (BOTTOM)':(cur===lv?' (TOP)':'')}`:'Single level';
- return {lv, label};
+ const lv=ST.rack.levels||1, cur=ST.level;
+ return {lv, label: lv>1?`Level ${cur} of ${lv}${cur===1?' (BOTTOM)':(cur===lv?' (TOP)':'')}`:'Single level'};
}
+function crateLabel(id){ const c=(ST.crates||[]).find(x=>x.id===id); return c&&(c.label_text||c.name)||ST.pickNames[id]||('Crate '+id); }
function drawRack(){
const {lv,label}=levelMeta();
$('#lvlCtl').style.display = lv>1?'inline-flex':'none';
$('#lvlLabel').textContent=label; $('#lvlPrev').disabled=ST.level<=1; $('#lvlNext').disabled=ST.level>=lv;
- $('#legend').innerHTML='
crateββββ = crate facing Β· BACK/FRONT = rack frame Β· green=items ';
+ $('#legend').innerHTML='
crateββββ = facing Β· green=items'+(ST.tab==='reorganize'?' Β· click in order to pick ':ST.tab==='collections'&&ST.edit?' Β· click to add to collection ':'')+' ';
const cv=$('#cv'), g=cv.getContext('2d'), W=cv.width, H=cv.height;
g.clearRect(0,0,W,H); g.fillStyle='#fbfbfd'; g.fillRect(0,0,W,H);
ST.hit=[];
@@ -214,52 +258,57 @@ function drawRack(){
const cols=Math.max(1,Math.floor(rw/CELL)), rows=Math.max(1,Math.round(rd/CELL));
const pad=42, sc=0.92*Math.min((W-2*pad)/rw,(H-2*pad)/rd);
const cW=rw*sc, cH=rd*sc, oX=(W-cW)/2, oY=(H-cH)/2, cellW=cW/cols, cellH=cH/rows;
- // boundary
g.fillStyle='#fff'; g.strokeStyle='#222'; g.lineWidth=3; g.beginPath(); g.rect(oX,oY,cW,cH); g.fill(); g.stroke();
- // edge labels (viewRot 0: BACK top, FRONT bottom, L left, R right)
g.fillStyle='#888'; g.font='11px system-ui'; g.textAlign='center';
g.fillText('BACK',oX+cW/2,oY-8); g.fillText('FRONT',oX+cW/2,oY+cH+16);
g.save(); g.translate(oX-10,oY+cH/2); g.rotate(-Math.PI/2); g.fillText('L',0,0); g.restore();
g.save(); g.translate(oX+cW+12,oY+cH/2); g.rotate(Math.PI/2); g.fillText('R',0,0); g.restore();
- // grid
g.strokeStyle='#eee'; g.lineWidth=1;
for(let i=1;i
(c.level==null?1:c.level)===ST.level);
let autoSlot=0;
here.forEach(c=>{
- let slot=c.slot; if(!slot||slot<1){ autoSlot++; slot=autoSlot; } // ponytail: place slotless crates sequentially
+ let slot=c.slot; if(!slot||slot<1){ autoSlot++; slot=autoSlot; }
const idx=slot-1, col=idx%cols, row=Math.floor(idx/cols);
if(row>=rows) return;
const x=oX+col*cellW+3, y=oY+row*cellH+3, w=cellW-6, h=cellH-6;
const sel=ST.selCrate===c.id;
- g.fillStyle=sel?'rgba(255,46,147,.30)':'rgba(30,144,255,.16)';
- g.strokeStyle=sel?'#d10f7a':'#1e90ff'; g.lineWidth=sel?3:1.5;
+ const picked=ST.tab==='reorganize' && ST.pick.indexOf(c.id);
+ const inColl=ST.tab==='collections' && ST.edit && ST.edit.crate_ids.includes(c.id);
+ g.fillStyle = inColl?'rgba(70,180,90,.28)' : sel?'rgba(255,46,147,.30)' : 'rgba(30,144,255,.16)';
+ g.strokeStyle = inColl?'#2e8b57' : sel?'#d10f7a' : '#1e90ff'; g.lineWidth=sel?3:1.5;
g.beginPath(); g.rect(x,y,w,h); g.fill(); g.stroke();
g.fillStyle='#1b1b22'; g.font='bold 10px system-ui'; g.textAlign='center'; g.textBaseline='top';
g.fillText('#'+c.id, x+w/2, y+4);
const gl=(c.label_text||c.crate_purpose||'').toString().split(',')[0].slice(0,12);
g.fillStyle='#555'; g.font='9px system-ui'; g.fillText(gl||'β', x+w/2, y+16);
- // facing arrow (big, centre-bottom)
g.fillStyle=sel?'#d10f7a':'#1e90ff'; g.font='bold 20px system-ui'; g.textBaseline='middle';
g.fillText(ARROW[(c.direction||'').toLowerCase()]||'β’', x+w/2, y+h-13);
- // slot number top-right, item count bottom-left
g.fillStyle='#1a8f54'; g.font='9px system-ui'; g.textAlign='right'; g.textBaseline='top';
g.fillText(slot, x+w-3, y+3);
g.fillStyle='#888'; g.textAlign='left'; g.fillText((c.items||0), x+3, y+h-12);
+ // ordered-pick badge (Reorganize)
+ if(ST.tab==='reorganize'){ const pi=ST.pick.indexOf(c.id);
+ if(pi>=0){ g.fillStyle='#d10f7a'; g.beginPath(); g.arc(x+12,y+h-12,10,0,2*Math.PI); g.fill();
+ g.fillStyle='#fff'; g.font='bold 11px system-ui'; g.textAlign='center'; g.textBaseline='middle'; g.fillText(pi+1, x+12, y+h-12); } }
ST.hit.push({type:'crate', id:c.id, x, y, w, h});
});
}
function stepLevel(d){ ST.level=Math.max(1,Math.min(ST.rack.levels||1, ST.level+d)); drawRack(); }
-// ββ crate contents ββ
+async function rackEdit(){
+ if(!ST.rack) return;
+ const n=prompt('Rack name:', ST.rack.name||''); if(n==null) return;
+ await post('/nav/rack/'+ST.rack.id,{name:n}); openRack(ST.rack.id, ST.selCrate);
+}
+
+// βββββββββββββ crate contents (centre column) βββββββββββββ
async function loadCrate(id){
- ST.selCrate=id; if(ST.view==='rack') drawRack();
+ ST.selCrate=id; redraw();
const d=await get('/nav/crate/'+id); const c=d.crate;
- ST.crateInfo=c; $('#ccBtns').style.display='flex'; $('#scanPanel').style.display='none'; $('#crateEdit').style.display='none';
- $('#ccName').innerHTML='π¦ '+esc(c.label_text||c.name||('Crate '+c.id));
- $('#ccName').className='pink';
+ ST.crateInfo=c;
+ $('#ccName').innerHTML='π¦ '+esc(c.label_text||c.name||('Crate '+c.id)); $('#ccName').className='pink';
const ls = c.last_scanned ? ` Β· scanned ${String(c.last_scanned).slice(0,10)}${c.updated_by?' by '+esc(c.updated_by):''}` : '';
$('#ccMeta').innerHTML=`Β· ${esc(c.rack_name||'')} L${c.level??'?'} Β· ${c.items_count??d.items.length} items${ls} Β· compress `;
$('#ccList').innerHTML=d.items.length? 'Slot Title Artist Genre/Style Price '+
@@ -268,6 +317,8 @@ async function loadCrate(id){
${esc(i.artist||'')} ${esc(i.genre||'')}${i.style?' Β· '+esc(i.style):''}
${money(i.price)} `).join('')+'
'
: 'empty crate
';
+ setViewBtns();
+ if(ST.tab==='scanner') renderScanner();
}
async function compressCrate(id){
if(!confirm('Renumber this crate\'s slots 1..N (close gaps)?')) return;
@@ -276,7 +327,7 @@ async function compressCrate(id){
async function relLookup(){
const q=$('#relQ').value.trim(); if(!q) return;
if(/^\d+$/.test(q)) return showRelease(parseInt(q));
- const d=await get('/nav/locate?sku='+encodeURIComponent(q)); // sku β release_id
+ const d=await get('/nav/locate?sku='+encodeURIComponent(q));
if(d.release_id) showRelease(d.release_id);
else $('#relBody').innerHTML='no release found for that SKU
';
}
@@ -288,7 +339,7 @@ function relItems(items){
}
async function showRelease(rid){
if(!rid){ $('#relBody').innerHTML='that item has no release id
'; return; }
- $('#relQ').value=rid;
+ $('#relQ').value=rid; ST.itemShown=true; setViewBtns();
const d=await get('/nav/release/'+rid); const r=d.release;
if(!r){ $('#relBody').innerHTML=`release ${rid} isn't in the local mirror
`+relItems(d.items); return; }
$('#relBody').innerHTML=`
@@ -302,42 +353,31 @@ async function showRelease(rid){
Inventory copies (${d.items.length})
${relItems(d.items)}`;
}
-// ββ editable crate info ββ
-function crateEditToggle(){
- const p=$('#crateEdit'); if(p.style.display!=='none'){ p.style.display='none'; return; }
- $('#scanPanel').style.display='none'; const c=ST.crateInfo||{};
- p.style.display='block';
- p.innerHTML=`
-
- Save
`;
-}
-async function crateEditSave(){
- await fetch('/nav/crate/'+ST.selCrate,{method:'POST',headers:hdr(),body:JSON.stringify({name:$('#ceName').value,label_text:$('#ceLabel').value})});
- $('#crateEdit').style.display='none'; loadCrate(ST.selCrate);
-}
-
-// ββ scanner (assign scanned items to the active crate's slots) ββ
-function scanToggle(){
- const p=$('#scanPanel'); if(p.style.display!=='none'){ p.style.display='none'; return; }
- $('#crateEdit').style.display='none';
- const last=localStorage.getItem('rg_lastscan')||'', c=ST.crateInfo||{};
- p.style.display='block';
- p.innerHTML=`π« Scan into ${esc(c.label_text||('Crate '+ST.selCrate))} β Release IDs / SKUs / barcodes, one per line
-
-
+// βββββββββββββ TOOL: Scanner βββββββββββββ
+function toolScanner(){ return `
+
π« Scanner
+
pick a crate on the map to scan into β
+
`; }
+function renderScanner(){
+ const t=$('#scanTarget'), f=$('#scanForm'); if(!t) return;
+ const c=ST.crateInfo;
+ if(!c){ t.innerHTML='pick a crate on the map to scan into β'; f.innerHTML=''; return; }
+ t.innerHTML='Active crate: '+esc(c.label_text||('Crate '+c.id))+' ';
+ const last=localStorage.getItem('rg_lastscan')||'';
+ f.innerHTML=`Release IDs / SKUs / barcodes β one per line. Assigned to slots sequentially.
+
+
Replace all Add at end Add at start Insert at slotβ¦
- Test Process
- Clear ${last?'βΊ recover last ':''}
-
`;
-}
-function scanBody(dry){
- return { crate_id:ST.selCrate, lines:$('#scanLines').value.split('\n').map(s=>s.trim()).filter(Boolean),
- mode:$('#scanMode').value, insert_at:($('#scanAt').value?parseInt($('#scanAt').value):null), dry_run:!!dry };
+ Test Process
+ Clear ${last?'βΊ recover ':''}
+
`;
}
+function scanBody(dry){ return { crate_id:ST.selCrate, lines:$('#scanLines').value.split('\n').map(s=>s.trim()).filter(Boolean),
+ mode:$('#scanMode').value, insert_at:($('#scanAt').value?parseInt($('#scanAt').value):null), dry_run:!!dry }; }
async function scanTest(){
const b=scanBody(true); if(!b.lines.length) return;
- const d=await fetch('/nav/scan',{method:'POST',headers:hdr(),body:JSON.stringify(b)}).then(x=>x.json());
+ const d=await post('/nav/scan',b);
$('#scanOut').innerHTML=`Preview β ${d.count} slots: `+(d.plan||[]).map(p=>`${p.slot}. ${esc(p.title||p.sku)} ${esc(p.artist||'')}
`).join('')
+(d.not_found.length?`not found: ${d.not_found.map(esc).join(', ')}
`:'');
}
@@ -345,49 +385,54 @@ async function scanProcess(){
const b=scanBody(false); if(!b.lines.length) return;
if(b.mode==='replace' && !confirm('Replace all β items not scanned will be unfiled from this crate. Continue?')) return;
localStorage.setItem('rg_lastscan', $('#scanLines').value);
- const d=await fetch('/nav/scan',{method:'POST',headers:hdr(),body:JSON.stringify(b)}).then(x=>x.json());
+ const d=await post('/nav/scan',b);
$('#scanOut').innerHTML=`β ${d.assigned} assigned to slots `+(d.not_found.length?`not found: ${d.not_found.map(esc).join(', ')}
`:'');
loadCrate(ST.selCrate);
}
function scanRecover(){ $('#scanLines').value=localStorage.getItem('rg_lastscan')||''; }
-// ββ rack edit ββ
-async function rackEdit(){
- if(!ST.rack) return;
- const n=prompt('Rack name:', ST.rack.name||''); if(n==null) return;
- await fetch('/nav/rack/'+ST.rack.id,{method:'POST',headers:hdr(),body:JSON.stringify({name:n})});
- openRack(ST.rack.id);
-}
+// βββββββββββββ TOOL: Returns (stub) βββββββββββββ
+function toolReturns(){ return `
+
β© Returns
+
Restock a sold copy: look it up in Release Info , then put it back in its crate via the Scanner.
+
A one-scan "return β restock" flow lands here next.
`; }
-// ββ Reorganize β AβZ re-file planner (uses /nav/reorg/scan + /apply) ββ
+// βββββββββββββ TOOL: Reorganize βββββββββββββ
+function toolReorg(){ return `
+
π¦ Source Crate Management
+
Click crates on the map in order , or type IDs.
+
+
Set Clear
+
Scan sources β
+
`; }
+function renderPick(){ const el=$('#pickList'); if(!el) return;
+ el.innerHTML=ST.pick.map((id,i)=>chip(`#${i+1} `+crateLabel(id), `unpick(${id})`, true)).join('')||'none picked '; }
+function togglePick(id){ const i=ST.pick.indexOf(id);
+ if(i<0){ ST.pick.push(id); const c=(ST.crates||[]).find(x=>x.id===id); if(c) ST.pickNames[id]=c.label_text||c.name||('Crate '+id); }
+ else ST.pick.splice(i,1);
+ renderPick(); drawRack(); }
+function unpick(id){ ST.pick=ST.pick.filter(x=>x!==id); renderPick(); redraw(); }
+function pickFromInput(){ ST.pick=($('#roSrc').value.match(/\d+/g)||[]).map(Number); renderPick(); redraw(); }
+function clearPick(){ ST.pick=[]; renderPick(); redraw(); }
let roItems=[], roMoves=[], roLeftovers=[];
-function reorgOpen(){
- let o=$('#reorgModal');
- if(!o){ o=document.createElement('div'); o.id='reorgModal'; o.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:flex-start;justify-content:center;z-index:300;padding-top:4vh'; document.body.appendChild(o); o.addEventListener('mousedown',e=>{if(e.target===o)o.remove();}); }
- o.innerHTML=`
-
π Reorganize β AβZ re-file β
-
Source crate IDs (comma-separated)
-
Scan
-
-
`;
-}
async function reorgScan(){
- const ids=$('#roSrc').value.split(',').map(s=>parseInt(s.trim())).filter(Boolean);
- if(!ids.length) return;
- const d=await fetch('/nav/reorg/scan',{method:'POST',headers:hdr(),body:JSON.stringify({crate_ids:ids})}).then(x=>x.json());
- roItems=d.items||[];
- $('#roScanOut').innerHTML=`${d.count} items in ${ids.length} crate(s)`;
- $('#roStart').value=ids[0]; $('#roCount').value=ids.length;
- $('#roPlanArea').style.display='block';
+ const ids = ST.pick.length ? ST.pick : ($('#roSrc').value.match(/\d+/g)||[]).map(Number);
+ if(!ids.length){ $('#roScanOut').textContent='pick crates first'; return; }
+ ST.pick=ids; renderPick();
+ const d=await post('/nav/reorg/scan',{crate_ids:ids}); roItems=d.items||[];
+ $('#roScanOut').innerHTML=`${d.count} items Β· ${ids.length} crate(s)`;
+ $('#roStart').value=ids[0]; $('#roCount').value=ids.length; $('#roPlanArea').style.display='block';
}
function roCmp(m){ return (a,b)=>{
if(m==='title') return (a.title||'').localeCompare(b.title||'');
@@ -401,14 +446,14 @@ function reorgPlan(){
let items=roItems.filter(i=> (pmax==null||(i.price!=null&&i.price<=pmax)) && (ymin==null||(i.year&&i.year>=ymin)) && (ymax==null||(i.year&&i.year<=ymax)) );
items.sort(roCmp($('#roSort').value));
const start=parseInt($('#roStart').value), count=parseInt($('#roCount').value)||1, slots=parseInt($('#roSlots').value)||50;
- const targets=[]; for(let i=0;i=targets.length){ roLeftovers.push(it); continue; }
roMoves.push({sku:it.sku, new_crate_id:targets[ti], new_slot:slot, title:it.title, artist:it.artist, from:`${it.crate_id||'β'}/${it.slot||'β'}`});
slot++; if(slot>slots){ slot=1; ti++; }
}
- $('#roPlanOut').innerHTML=`${roMoves.length} items β crates ${targets.join(', ')}${roLeftovers.length?' Β· '+roLeftovers.length+' leftover':''} Apply changes
`
+ $('#roPlanOut').innerHTML=`${roMoves.length} β crates ${targets.join(', ')}${roLeftovers.length?' Β· '+roLeftovers.length+' leftover':''} Apply
`
+'β Crate/Slot Title Artist From '
+roMoves.slice(0,300).map(m=>`${m.new_crate_id} / ${m.new_slot} ${esc(m.title||m.sku)} ${esc(m.artist||'')} ${esc(m.from)} `).join('')
+(roMoves.length>300?`β¦+${roMoves.length-300} more `:'')+'
';
@@ -416,16 +461,37 @@ function reorgPlan(){
async function reorgApply(){
if(!roMoves.length) return;
if(!confirm(`Apply ${roMoves.length} moves? Items get re-filed into their new crate/slot.`)) return;
- const d=await fetch('/nav/reorg/apply',{method:'POST',headers:hdr(),body:JSON.stringify({
- matched:roMoves.map(m=>({sku:m.sku,new_crate_id:m.new_crate_id,new_slot:m.new_slot})),
- leftovers:roLeftovers.map(l=>({sku:l.sku}))})}).then(x=>x.json());
+ const d=await post('/nav/reorg/apply',{matched:roMoves.map(m=>({sku:m.sku,new_crate_id:m.new_crate_id,new_slot:m.new_slot})), leftovers:roLeftovers.map(l=>({sku:l.sku}))});
$('#roPlanOut').innerHTML=`β ${d.matched_updated} re-filed${d.leftovers_archived?', '+d.leftovers_archived+' unfiled':''} `;
if(ST.view!=='store') toStore();
}
-// ββ collection editor ββ
+// βββββββββββββ TOOL: Collections βββββββββββββ
+function toolCollections(){ return `
+
π Collections
+ οΌ New
+
+
+
New collection β close
+
`; }
function showEditor(){ $('#colPanel').style.display='none'; $('#editPanel').style.display=''; }
-function edCancel(){ ST.edit=null; $('#editPanel').style.display='none'; $('#colPanel').style.display=''; }
+function edCancel(){ ST.edit=null; $('#editPanel').style.display='none'; $('#colPanel').style.display=''; redraw(); }
let GENRES=[];
async function loadGenres(){ if(!GENRES.length) GENRES=(await get('/nav/genres')).genres||[]; renderGenres(); }
function renderGenres(){ $('#edGenres').innerHTML=GENRES.map(g=>{ const on=ST.edit.genre_ids.includes(g.id);
@@ -434,7 +500,7 @@ function edTogGenre(id){ const a=ST.edit.genre_ids, i=a.indexOf(id); if(i<0)a.pu
function edNew(){ ST.edit={id:null,crate_ids:[],crateNames:{},genre_ids:[],style_ids:[],styleNames:{}};
$('#edTitle').textContent='New collection'; $('#edDel').style.display='none';
$('#edName').value=''; $('#edColor').value='#3498db'; $('#edPrio').value=0; $('#edSort').value='alpha_artist';
- ['edPmin','edPmax','edYmin','edYmax'].forEach(k=>$('#'+k).value=''); loadGenres(); edRender(); showEditor(); }
+ ['edPmin','edPmax','edYmin','edYmax'].forEach(k=>$('#'+k).value=''); loadGenres(); edRender(); showEditor(); redraw(); }
async function edLoad(id){ const d=await get('/nav/collections/'+id); const c=d.collection;
ST.edit={id:c.id,crate_ids:(c.crate_ids||[]).slice(),crateNames:{},genre_ids:(c.genre_ids||[]).slice(),style_ids:(c.style_ids||[]).slice(),styleNames:{}};
(d.crates||[]).forEach(x=>ST.edit.crateNames[x.id]=x.label_text||x.name||('Crate '+x.id));
@@ -442,18 +508,17 @@ async function edLoad(id){ const d=await get('/nav/collections/'+id); const c=d.
$('#edTitle').textContent='Edit: '+esc(c.name); $('#edDel').style.display='';
$('#edName').value=c.name||''; $('#edColor').value=c.color||'#3498db'; $('#edPrio').value=c.priority||0; $('#edSort').value=c.sort_method||'alpha_artist';
$('#edPmin').value=c.price_min??''; $('#edPmax').value=c.price_max??''; $('#edYmin').value=c.year_min??''; $('#edYmax').value=c.year_max??'';
- await loadGenres(); edRender(); showEditor(); }
-function chip(label,ondel){ return `${esc(label)} β `; }
+ await loadGenres(); edRender(); showEditor(); redraw(); }
function edRender(){
$('#edStyles').innerHTML=ST.edit.style_ids.map(s=>chip(ST.edit.styleNames[s]||('style '+s),`edDelStyle(${s})`)).join('');
$('#edCrates').innerHTML=ST.edit.crate_ids.map(c=>chip(ST.edit.crateNames[c]||('crate '+c),`edDelCrate(${c})`)).join('')||'none β click crates on the map ';
edStats();
}
-function edAddCrate(id,label){ if(!ST.edit.crate_ids.includes(id)){ ST.edit.crate_ids.push(id); ST.edit.crateNames[id]=label||('crate '+id); edRender(); } }
-function edDelCrate(id){ ST.edit.crate_ids=ST.edit.crate_ids.filter(x=>x!==id); edRender(); }
+function edAddCrate(id,label){ if(!ST.edit.crate_ids.includes(id)){ ST.edit.crate_ids.push(id); ST.edit.crateNames[id]=label||('crate '+id); edRender(); redraw(); } }
+function edDelCrate(id){ ST.edit.crate_ids=ST.edit.crate_ids.filter(x=>x!==id); edRender(); redraw(); }
function edDelStyle(id){ ST.edit.style_ids=ST.edit.style_ids.filter(x=>x!==id); edRender(); }
let stq;
-$('#edStyleQ').addEventListener('input',e=>{ clearTimeout(stq); stq=setTimeout(()=>edStyleSearch(e.target.value),200); });
+document.addEventListener('input',e=>{ if(e.target&&e.target.id==='edStyleQ'){ clearTimeout(stq); stq=setTimeout(()=>edStyleSearch(e.target.value),200); } });
async function edStyleSearch(q){ if(!q.trim()){ $('#edStyleRes').style.display='none'; return; }
const d=await get('/nav/styles?q='+encodeURIComponent(q)); $('#edStyleRes').style.display='block';
$('#edStyleRes').innerHTML=(d.styles||[]).map(s=>`${esc(s.name)}
`).join('')||'no match
'; }
@@ -464,66 +529,14 @@ function edDraft(){ const v=k=>{const x=$('#'+k).value; return x===''?null:+x;};
price_min:v('edPmin'), price_max:v('edPmax'), year_min:v('edYmin'), year_max:v('edYmax')}; }
let stt;
async function edStats(){ if(!ST.edit) return; clearTimeout(stt); stt=setTimeout(async()=>{
- const r=await fetch('/nav/collections/stats',{method:'POST',headers:hdr(),body:JSON.stringify(edDraft())}).then(x=>x.json());
+ const r=await post('/nav/collections/stats',edDraft());
$('#edStats').innerHTML=`${r.matching} matching Β· ${r.located} located Β· ${r.not_located} not located`; },250); }
async function edSave(){ const d=edDraft(); if(!d.name){ alert('name required'); return; }
- const r=await fetch('/nav/collections',{method:'POST',headers:hdr(),body:JSON.stringify(d)}).then(x=>x.json());
- if(r.ok){ edCancel(); loadCollections(); } }
+ const r=await post('/nav/collections',d); if(r.ok){ edCancel(); loadCollections(); } }
async function edDelete(){ if(!ST.edit.id||!confirm('Delete this collection?')) return;
await fetch('/nav/collections/'+ST.edit.id,{method:'DELETE',headers:hdr()}); edCancel(); loadCollections(); }
-
-// ββ canvas clicks ββ
-$('#cv').addEventListener('click',e=>{
- const r=$('#cv').getBoundingClientRect(), sx=$('#cv').width/r.width, sy=$('#cv').height/r.height;
- const mx=(e.clientX-r.left)*sx, my=(e.clientY-r.top)*sy;
- if(ST.view==='store'){
- let best=null,bd=1e9; for(const h of ST.hit){ const dd=Math.hypot(mx-h.cx,my-h.cy); if(dd=h.x&&mx<=h.x+h.w&&my>=h.y&&my<=h.y+h.h){
- if(ST.edit){ const cr=ST.crates.find(c=>c.id===h.id); edAddCrate(h.id, cr&&(cr.label_text||cr.crate_purpose)); }
- else loadCrate(h.id); return; } }
- }
-});
-
-// ββ search β locate ββ
-let st;
-$('#q').addEventListener('input',e=>{ clearTimeout(st); st=setTimeout(run,200); });
-$('#q').addEventListener('keydown',e=>{ if(e.key==='Enter'){clearTimeout(st);run();} });
-async function run(){
- const q=$('#q').value.trim();
- if(!q){ $('#res').innerHTML=''; $('#count').textContent='type to search β click a hit to locate it'; return; }
- const d=await get('/admin/inventory?q='+encodeURIComponent(q));
- $('#count').textContent=`${d.total} match${d.total===1?'':'es'} β click to locate`;
- $('#res').innerHTML=(d.items||[]).map(it=>`
- ${imgTag(it)}
-
${esc(it.title||it.sku)}
-
${esc(it.artist||'')} ${it.condition?'Β· '+esc(it.condition):''} Β· ${money(it.price)}
- ${it.crate?`
π ${esc(it.crate)} `:'
Β· no bin '}
`).join('')
- || 'no match anywhere in the store
';
-}
-async function locate(crateId, sku){
- if(crateId){ goCrate(crateId); return; }
- // no exact bin β fallback: where SHOULD it be (collection zone)
- const d=await get('/nav/locate?sku='+encodeURIComponent(sku||''));
- if(d.suggested) showSuggestion(d.suggested);
- else { $('#ccName').className='muted'; $('#ccName').textContent='No bin & no matching collection zone'; $('#ccMeta').textContent=''; $('#ccList').innerHTML='this record isn\'t filed and no collection rule matches it yet
'; }
-}
-async function goCrate(id){
- const d=await get('/nav/crate/'+id);
- if(d.crate && d.crate.rack_id) await openRack(d.crate.rack_id, id);
- loadCrate(id);
-}
-function showSuggestion(sug){
- $('#ccName').className='pink'; $('#ccName').innerHTML='π― Should be in: '+esc(sug.name);
- $('#ccMeta').textContent='Β· fallback zone ('+sug.crates.length+' crates)';
- $('#ccList').innerHTML=sug.crates.length? sug.crates.map(c=>`${esc(c.label||('Crate '+c.id))}
go β `).join('')
- : 'collection has no crates assigned
';
-}
-
-// ββ collections ββ
async function loadCollections(){
- const d=await get('/nav/collections');
+ const d=await get('/nav/collections'); if(!$('#colList')) return;
$('#colCount').textContent=(d.collections||[]).length+' saved';
$('#colList').innerHTML=(d.collections||[]).map(c=>{
const rule=[c.price_min!=null?('$'+c.price_min+'+'):'', c.year_min?(c.year_min+(c.year_max?'-'+c.year_max:'+')):'', c.n_styles?(c.n_styles+'st'):'', c.n_genres?(c.n_genres+'g'):''].filter(Boolean).join(' Β· ');
@@ -540,6 +553,86 @@ async function openCollection(id){
: 'no crates in this collection
';
}
+// βββββββββββββ TOOL: Stock Finder βββββββββββββ
+function toolFinder(){ return `
+
π Stock Finder
+
Search
+
type to search β click a hit to locate it
+
`; }
+let st;
+document.addEventListener('input',e=>{ if(e.target&&e.target.id==='q'){ clearTimeout(st); st=setTimeout(runSearch,200); } });
+document.addEventListener('keydown',e=>{ if(e.target&&e.target.id==='q'&&e.key==='Enter'){ clearTimeout(st); runSearch(); } });
+async function runSearch(){
+ const q=$('#q')?$('#q').value.trim():''; if(!$('#finderRes')) return;
+ if(!q){ $('#finderRes').innerHTML=''; $('#count').textContent='type to search β click a hit to locate it'; return; }
+ const d=await get('/admin/inventory?q='+encodeURIComponent(q));
+ $('#count').textContent=`${d.total} match${d.total===1?'':'es'} β click to locate`;
+ $('#finderRes').innerHTML=(d.items||[]).map(it=>`
+ ${imgTag(it)}
+
${esc(it.title||it.sku)}
+
${esc(it.artist||'')} ${it.condition?'Β· '+esc(it.condition):''} Β· ${money(it.price)}
+ ${it.crate?`
π ${esc(it.crate)} `:'
Β· no bin '}
`).join('')
+ || 'no match anywhere in the store
';
+}
+async function locate(crateId, sku){
+ if(crateId){ goCrate(crateId); return; }
+ const d=await get('/nav/locate?sku='+encodeURIComponent(sku||''));
+ if(d.suggested) showSuggestion(d.suggested);
+ else { $('#ccName').className='muted'; $('#ccName').textContent='No bin & no matching collection zone'; $('#ccMeta').textContent=''; $('#ccList').innerHTML='this record isn\'t filed and no collection rule matches it yet
'; }
+}
+async function goCrate(id){
+ const d=await get('/nav/crate/'+id);
+ if(d.crate && d.crate.rack_id) await openRack(d.crate.rack_id, id);
+ loadCrate(id);
+}
+function showSuggestion(sug){
+ $('#ccName').className='pink'; $('#ccName').innerHTML='π― Should be in: '+esc(sug.name);
+ $('#ccMeta').textContent='Β· fallback zone ('+sug.crates.length+' crates)';
+ $('#ccList').innerHTML=sug.crates.length? sug.crates.map(c=>`${esc(c.label||('Crate '+c.id))}
go β `).join('')
+ : 'collection has no crates assigned
';
+}
+
+// βββββββββββββ omni bar βββββββββββββ
+async function omni(){
+ const v=$('#omni').value.trim(); if(!v) return;
+ let m;
+ if(m=v.match(/^c\s*(\d+)$/i)) return goCrate(+m[1]);
+ if(m=v.match(/^r\s*(\d+)$/i)) return openRack(+m[1]);
+ if(/^\d+$/.test(v)) return showRelease(+v);
+ setTab('finder'); $('#q').value=v; runSearch();
+}
+$('#omni').addEventListener('keydown',e=>{ if(e.key==='Enter') omni(); });
+
+// βββββββββββββ canvas: unified click + rack drag βββββββββββββ
+function cvXY(e){ const cv=$('#cv'),r=cv.getBoundingClientRect(); return {mx:(e.clientX-r.left)*cv.width/r.width, my:(e.clientY-r.top)*cv.height/r.height}; }
+function hitRack(mx,my){ let best=null,bd=1e9; for(const h of ST.hit){ if(h.type!=='rack')continue; const dd=Math.hypot(mx-h.cx,my-h.cy); if(dd=h.x&&mx<=h.x+h.w&&my>=h.y&&my<=h.y+h.h) return h; } return null; }
+let down=null;
+$('#cv').addEventListener('mousedown',e=>{
+ const {mx,my}=cvXY(e); down={mx,my,moved:false,dragRack:null};
+ if(ST.tab==='reorganize' && ST.view==='store'){ const h=hitRack(mx,my); if(h) down.dragRack=h.id; }
+});
+$('#cv').addEventListener('mousemove',e=>{
+ if(!down||!down.dragRack) return;
+ const {mx,my}=cvXY(e);
+ if(Math.hypot(mx-down.mx,my-down.my)>5) down.moved=true;
+ if(down.moved){ const tf=ST.storeTf, r=ST.racks.find(x=>x.id===down.dragRack);
+ r.x=tf.minX+(mx-tf.pad)/tf.sc; r.z=tf.minZ+(my-tf.pad)/tf.sc; drawStore(); }
+});
+window.addEventListener('mouseup',async e=>{
+ if(!down) return; const d=down; down=null;
+ if(d.dragRack && d.moved){ const r=ST.racks.find(x=>x.id===d.dragRack);
+ await post('/nav/rack/'+d.dragRack,{pos_x:+r.x.toFixed(3),pos_z:+r.z.toFixed(3)}); return; }
+ if(d.moved) return; // a non-rack drag β ignore
+ // a real click β dispatch by view + tab
+ const {mx,my}=cvXY(e);
+ if(ST.view==='store'){ const h=hitRack(mx,my); if(h) openRack(h.id); return; }
+ const h=hitCrate(mx,my); if(!h) return;
+ if(ST.tab==='reorganize'){ togglePick(h.id); loadCrate(h.id); }
+ else if(ST.tab==='collections' && ST.edit){ const cr=ST.crates.find(c=>c.id===h.id); edAddCrate(h.id, cr&&(cr.label_text||cr.name)); loadCrate(h.id); }
+ else loadCrate(h.id);
+});
+
if(TOKEN){ $('#tok').value=TOKEN; signin(); }