diff --git a/app/admin_routes.py b/app/admin_routes.py new file mode 100644 index 0000000..41ccb4a --- /dev/null +++ b/app/admin_routes.py @@ -0,0 +1,86 @@ +import httpx +from fastapi import APIRouter, Depends, Query +from sqlalchemy import text + +from . import vault +from .auth import require_token +from .db import get_db + +# The MEGA admin — back-office cockpit, all admin-gated. Inspired by WowPlatter's wp-admin nav +# (Dashboard / Inventory / Orders / Import / Sales / Reports), but on RecordGod's own data. +router = APIRouter(prefix="/admin", tags=["admin"]) + + +@router.get("/stats") +async def stats(ident=Depends(require_token), db=Depends(get_db)): + s = ident["store_id"] + row = (await db.execute(text(""" + SELECT + (SELECT count(*) FROM inventory WHERE store_id=:s) AS items, + (SELECT count(*) FROM inventory WHERE store_id=:s AND in_stock) AS in_stock, + (SELECT count(*) FROM inventory WHERE store_id=:s AND kind='vinyl') AS vinyl, + (SELECT count(*) FROM inventory WHERE store_id=:s AND kind<>'vinyl') AS other_goods, + (SELECT coalesce(sum(price),0)::float FROM inventory WHERE store_id=:s AND in_stock) AS stock_value, + (SELECT count(*) FROM inventory WHERE staged) AS staged, + (SELECT count(*) FROM crate) AS crates, + (SELECT count(*) FROM sales) AS sales, + (SELECT coalesce(sum(total),0)::float FROM sales) AS sales_total + """), {"s": s})).mappings().first() + return {"ok": True, **dict(row)} + + +@router.get("/inventory") +async def inventory(q: str = Query(""), kind: str = Query(""), page: int = Query(1, ge=1), + ident=Depends(require_token), db=Depends(get_db)): + per = 50 + where = ["i.store_id = :s"] + params = {"s": ident["store_id"], "per": per, "off": (page - 1) * per} + if q: + where.append("(i.sku ILIKE :q OR coalesce(i.title,dc.title) ILIKE :q OR dc.artist ILIKE :q)") + params["q"] = f"%{q}%" + if kind: + where.append("i.kind = :k") + params["k"] = kind + w = " AND ".join(where) + items = [dict(r) for r in (await db.execute(text(f""" + SELECT i.sku, i.kind, i.release_id, coalesce(i.title, dc.title) AS title, dc.artist, + i.price::float AS price, i.condition, i.in_stock, i.crate_id, + c.label_text AS crate, dc.thumb, i.target_price::float AS target + FROM inventory i + LEFT JOIN disc_cache dc ON dc.release_id = i.release_id + LEFT JOIN crate c ON c.id = i.crate_id + WHERE {w} + ORDER BY i.updated_at DESC NULLS LAST + LIMIT :per OFFSET :off + """), params)).mappings()] + cparams = {k: v for k, v in params.items() if k not in ("per", "off")} + total = (await db.execute(text( + f"SELECT count(*) FROM inventory i LEFT JOIN disc_cache dc ON dc.release_id=i.release_id WHERE {w}" + ), cparams)).scalar() + return {"ok": True, "items": items, "page": page, "per": per, "total": total} + + +@router.get("/orders") +async def orders(ident=Depends(require_token), db=Depends(get_db)): + base = await vault.get_secret(db, "woo_base_url") + ck = await vault.get_secret(db, "woo_consumer_key") + cs = await vault.get_secret(db, "woo_consumer_secret") + if not (base and ck and cs): + return {"ok": False, "reason": "WooCommerce not connected — add keys in Connections"} + try: + async with httpx.AsyncClient(timeout=20, follow_redirects=True) as c: + r = await c.get(base.rstrip("/") + "/wp-json/wc/v3/orders", + params={"per_page": 20, "orderby": "date", "order": "desc"}, auth=(ck, cs)) + except Exception as e: + return {"ok": False, "reason": f"connection error: {e}"} + if r.status_code != 200: + return {"ok": False, "reason": f"Woo API HTTP {r.status_code}"} + out = [] + for o in r.json(): + b = o.get("billing", {}) + name = (b.get("first_name", "") + " " + b.get("last_name", "")).strip() or b.get("email", "") + out.append({"id": o["id"], "number": o.get("number"), "status": o.get("status"), + "total": o.get("total"), "currency": o.get("currency"), + "date": (o.get("date_created") or "")[:10], "customer": name, + "items": len(o.get("line_items", []))}) + return {"ok": True, "orders": out} diff --git a/app/main.py b/app/main.py index 16e4f48..e0bd140 100644 --- a/app/main.py +++ b/app/main.py @@ -17,11 +17,13 @@ from . import __version__ # noqa: E402 from .wowplatter_v1 import router as wowplatter_v1_router # noqa: E402 from .virtual import router as virtual_router # noqa: E402 from .settings_routes import router as settings_router # noqa: E402 +from .admin_routes import router as admin_router # noqa: E402 app = FastAPI(title="RECORDGOD", version=__version__) app.include_router(wowplatter_v1_router) app.include_router(virtual_router) app.include_router(settings_router) +app.include_router(admin_router) @app.get("/healthz") diff --git a/site/admin.html b/site/admin.html new file mode 100644 index 0000000..c329868 --- /dev/null +++ b/site/admin.html @@ -0,0 +1,205 @@ + + +
+ + +admin sign-in
+ + +