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? '
| Slot | Title | Artist | Genre/Style | Price |
|---|---|---|---|---|
| ${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)} |