from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import text from .auth import require_token from .db import get_db # Store Navigator — the WowPlatter "Search & Inventory Management" drill-down, RecordGod-side. # Store → Rack → Crate → Item, over the migrated virtual_* geometry. See RECORDGOD_NAVIGATOR_PLAN.md. # Dims are METRES here (the 3D-store convention the data was migrated into), not WowPlatter's old cm. router = APIRouter(prefix="/nav", tags=["navigator"]) @router.get("/spaces") async def spaces(ident=Depends(require_token), db=Depends(get_db)): rows = await db.execute(text(""" SELECT id, name, room_width::float AS room_width, room_depth::float AS room_depth, (is_default = 'y') AS is_default FROM virtual_space WHERE visible = 'y' ORDER BY (is_default = 'y') DESC, id """)) return {"spaces": [dict(r) for r in rows.mappings()]} @router.get("/store-layout") async def store_layout(space_id: int | None = None, ident=Depends(require_token), db=Depends(get_db)): """All racks (position + facing + type dims + counts) for one space — the top-down store view.""" if space_id is None: sp = (await db.execute(text( "SELECT id FROM virtual_space WHERE visible='y' ORDER BY (is_default='y') DESC, id LIMIT 1" ))).first() space_id = sp[0] if sp else None space = (await db.execute(text(""" SELECT id, name, room_width::float AS room_width, room_depth::float AS room_depth FROM virtual_space WHERE id = :s """), {"s": space_id})).mappings().first() racks = [dict(r) for r in (await db.execute(text(""" SELECT r.id, r.name, r.pos_x::float AS x, r.pos_z::float AS z, r.rotation_y::float AS rot, r.direction, rt.type AS rack_type, rt.name AS type_name, rt.width::float AS w, rt.depth::float AS d, coalesce(rt.levels,1)::int AS levels, (SELECT count(*) FROM virtual_crate c WHERE c.rack_id = r.id AND c.visible = 'y') AS crates, (SELECT count(*) FROM inventory i WHERE i.crate_id IN (SELECT id FROM virtual_crate c WHERE c.rack_id = r.id) AND i.in_stock) AS items FROM virtual_rack r LEFT JOIN virtual_rack_type rt ON rt.id = r.rack_type_id WHERE r.visible = 'y' AND r.space_id = :s """), {"s": space_id})).mappings()] return {"ok": True, "space": dict(space) if space else None, "racks": racks} @router.get("/rack/{rack_id}") async def rack_detail(rack_id: int, ident=Depends(require_token), db=Depends(get_db)): """One rack: its type dims/levels + every crate (level, slot, facing, label, item count).""" rack = (await db.execute(text(""" SELECT r.id, r.name, r.direction, r.pos_x::float AS x, r.pos_z::float AS z, r.rotation_y::float AS rot, r.space_id, rt.type AS rack_type, rt.name AS type_name, rt.width::float AS w, rt.depth::float AS d, coalesce(rt.levels,1)::int AS levels, coalesce(rt.slots_per_level,0)::int AS slots_per_level FROM virtual_rack r LEFT JOIN virtual_rack_type rt ON rt.id = r.rack_type_id WHERE r.id = :id """), {"id": rack_id})).mappings().first() if not rack: raise HTTPException(404, "rack not found") crates = [dict(r) for r in (await db.execute(text(""" SELECT c.id, c.name, c.label_text, c.rack_level_index::int AS level, c.slot_number::int AS slot, c.direction, c.pos_x::float AS x, c.pos_z::float AS z, c.crate_purpose, (SELECT count(*) FROM inventory i WHERE i.crate_id = c.id AND i.in_stock) AS items FROM virtual_crate c WHERE c.rack_id = :id AND c.visible = 'y' ORDER BY c.rack_level_index NULLS FIRST, c.slot_number NULLS LAST """), {"id": rack_id})).mappings()] levels = [dict(r) for r in (await db.execute(text(""" SELECT l.level_index::int AS level, coalesce(l.slots,0)::int AS slots FROM virtual_rack_type_level l JOIN virtual_rack r ON r.rack_type_id = l.rack_type_id WHERE r.id = :id ORDER BY l.level_index """), {"id": rack_id})).mappings()] return {"ok": True, "rack": dict(rack), "crates": crates, "levels": levels} @router.get("/crate/{crate_id}") async def crate_contents(crate_id: int, ident=Depends(require_token), db=Depends(get_db)): """A crate's records, in slot order — the Crate Contents list.""" crate = (await db.execute(text(""" SELECT c.id, c.name, c.label_text, c.rack_id, c.rack_level_index::int AS level, c.slot_number::int AS slot, c.direction, c.crate_purpose, r.name AS rack_name FROM virtual_crate c LEFT JOIN virtual_rack r ON r.id = c.rack_id WHERE c.id = :id """), {"id": crate_id})).mappings().first() if not crate: raise HTTPException(404, "crate not found") items = [dict(r) for r in (await db.execute(text(""" SELECT i.sku, i.slot_number::int AS slot, coalesce(i.title, dc.title) AS title, dc.artist, i.price::float AS price, i.condition, i.in_stock FROM inventory i LEFT JOIN disc_cache dc ON dc.release_id = i.release_id WHERE i.crate_id = :id ORDER BY i.slot_number NULLS LAST, i.sku """), {"id": crate_id})).mappings()] return {"ok": True, "crate": dict(crate), "items": items}