diff --git a/app/navigator_routes.py b/app/navigator_routes.py index c070d57..c6c13dc 100644 --- a/app/navigator_routes.py +++ b/app/navigator_routes.py @@ -199,6 +199,32 @@ async def _resolve_sku(db, ln): return r[0] if r else None +class InsertOneIn(BaseModel): + item: str # release id / sku / barcode → resolved to one inventory sku + crate_id: int + slot: int # insert AT this slot; existing items at >= slot shift down by one + + +@router.post("/insert") +async def insert_one(body: InsertOneIn, ident=Depends(require_token), db=Depends(get_db)): + """Drop a single record into a crate at a chosen slot, shifting the rest down (the Quick Insert).""" + sku = await _resolve_sku(db, body.item.strip()) + if not sku: + raise HTTPException(404, "no record found for that ID / SKU / barcode") + slot = max(1, body.slot) + # detach the record first so it isn't caught by the shift (handles re-filing within the same crate) + await db.execute(text("UPDATE inventory SET crate_id=NULL, slot_number=NULL WHERE sku=:sku"), {"sku": sku}) + shifted = (await db.execute(text( + "UPDATE inventory SET slot_number = slot_number + 1, updated_at = now() " + "WHERE crate_id = :c AND slot_number >= :s"), {"c": body.crate_id, "s": slot})).rowcount or 0 + await db.execute(text( + "UPDATE inventory SET crate_id = :c, slot_number = :s, updated_at = now() WHERE sku = :sku"), + {"c": body.crate_id, "s": slot, "sku": sku}) + await db.execute(text("UPDATE virtual_crate SET updated_at = now() WHERE id = :c"), {"c": body.crate_id}) + await db.commit() + return {"ok": True, "sku": sku, "slot": slot, "shifted": shifted} + + @router.post("/scan") async def scan_assign(body: ScanAssignIn, ident=Depends(require_token), db=Depends(get_db)): resolved, not_found = [], [] diff --git a/site/search.html b/site/search.html index 8ba77e2..902655b 100644 --- a/site/search.html +++ b/site/search.html @@ -50,6 +50,7 @@ .loc{display:inline-block;margin-top:2px;padding:1px 7px;border-radius:20px;background:#fdeef6;color:var(--pink);font-size:11px;font-weight:600} table{width:100%;border-collapse:collapse;font-size:13px} td,th{padding:6px 5px;border-bottom:1px solid #eee;text-align:left}th{color:var(--mut);font-weight:500} + tr.pickrow td{background:#fdeef6;box-shadow:inset 3px 0 0 var(--pink)} .legend{display:flex;gap:14px;margin-top:8px;font-size:11px;color:var(--mut);flex-wrap:wrap} .sw{display:inline-block;width:11px;height:11px;border-radius:3px;vertical-align:-1px;margin-right:4px} .chip{display:inline-flex;gap:5px;align-items:center;background:#eef0f5;border:1px solid #d4d8e0;border-radius:20px;padding:2px 9px;font-size:11px} @@ -162,7 +163,7 @@ const ARROW={forward:'↓',front:'↓',north:'↓',back:'↑',south:'↑',left:' 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}; + pick:[], pickNames:{}, edit:null, crateInfo:null, pickSlot:null, pickPos:'before'}; async function signin(){ TOKEN=$('#tok').value.trim(); @@ -346,6 +347,7 @@ async function rackEdit(){ // ───────────── crate contents (centre column) ───────────── async function loadCrate(id){ + if(id!==ST.selCrate) ST.pickSlot=null; // slot-pick is per-crate ST.selCrate=id; redraw(); const d=await get('/nav/crate/'+id); const c=d.crate; ST.crateInfo=c; @@ -353,7 +355,7 @@ async function loadCrate(id){ 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? ''+ - d.items.map(i=>` + d.items.map(i=>``).join('')+'
SlotTitleArtistGenre/StylePrice
${i.slot??'—'}${esc(i.title||i.sku)}${i.in_stock?'':' sold'} ${esc(i.artist||'')}${esc(i.genre||'')}${i.style?' · '+esc(i.style):''} ${money(i.price)}
' @@ -361,6 +363,12 @@ async function loadCrate(id){ setViewBtns(); renderCrateInfo(); if(ST.tab==='scanner') renderScanner(); } +function ccRowClick(slot, rid){ if(ST.tab==='scanner' && slot!=null) setPickSlot(slot); if(rid) showRelease(rid); } +function setPickSlot(slot){ + ST.pickSlot=slot; + document.querySelectorAll('#ccList tr').forEach(tr=>tr.classList.toggle('pickrow', +tr.dataset.slot===slot)); + updatePickUI(); +} function renderCrateInfo(){ const c=ST.crateInfo, p=$('#crateInfoPanel'); if(!p) return; if(!c){ p.style.display='none'; return; } @@ -431,17 +439,41 @@ function renderScanner(){ 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.
- -
- + f.innerHTML=` +
+
⚡ Quick insert one
+
+ +
+
+
+
Bulk — Release IDs / SKUs / barcodes, one per line.
+ +
${last?'':''}
-
`; +
+
`; + updatePickUI(); +} +function setPos(p){ ST.pickPos=p; updatePickUI(); } +function targetSlot(){ if(ST.pickSlot==null) return null; return ST.pickPos==='after' ? ST.pickSlot+1 : ST.pickSlot; } +function updatePickUI(){ + document.querySelectorAll('#scanForm [data-pos]').forEach(b=>b.classList.toggle('on', b.dataset.pos===ST.pickPos)); + const lbl = ST.pickSlot==null ? 'click a record below to pick the slot' : `${ST.pickPos} slot ${ST.pickSlot}`; + const a=$('#qiAnchor'); if(a) a.innerHTML=lbl; + const h=$('#scanHint'); if(h) h.innerHTML = ($('#scanMode')&&$('#scanMode').value==='insert') ? ('Insert mode — will insert '+lbl) : ''; +} +async function quickInsert(){ + const rec=$('#qiRec').value.trim(); if(!rec){ $('#qiMsg').innerHTML='enter a record'; return; } + const slot=targetSlot(); if(slot==null){ $('#qiMsg').innerHTML='click a record in the crate to pick the slot'; return; } + const d=await post('/nav/insert',{item:rec,crate_id:ST.selCrate,slot}); + if(d.ok){ $('#qiMsg').innerHTML=`✓ inserted at slot ${d.slot}${d.shifted?' · '+d.shifted+' shifted down':''}`; $('#qiRec').value=''; ST.pickSlot=null; loadCrate(ST.selCrate); setTimeout(()=>{const r=$('#qiRec'); if(r)r.focus();},60); } + else $('#qiMsg').innerHTML=''+esc(d.detail||'not found')+''; } 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 }; } + mode:$('#scanMode').value, insert_at:($('#scanMode').value==='insert'?targetSlot():null), dry_run:!!dry }; } async function scanTest(){ const b=scanBody(true); if(!b.lines.length) return; const d=await post('/nav/scan',b); @@ -450,6 +482,7 @@ async function scanTest(){ } async function scanProcess(){ const b=scanBody(false); if(!b.lines.length) return; + if(b.mode==='insert' && b.insert_at==null){ $('#scanOut').innerHTML='pick a slot first — click a record in the crate'; 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 post('/nav/scan',b);