SysOps: deploy-all — 2026-06-09 10:41 UTC
This commit is contained in:
@@ -168,7 +168,15 @@ class DelegateBody(BaseModel):
|
||||
@admin_router.get("/clients")
|
||||
def list_clients():
|
||||
return {"items": _serialize_rows(fetch_all(
|
||||
"SELECT * FROM clients ORDER BY updated_at DESC NULLS LAST, created_at DESC LIMIT 500"
|
||||
"""SELECT c.*,
|
||||
(SELECT COUNT(*) FROM client_supermarket_links l WHERE l.client_id = c.id)
|
||||
+ (SELECT COUNT(*) FROM supermarkets s WHERE s.client_id = c.id
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM client_supermarket_links l2
|
||||
WHERE l2.supermarket_id = s.id AND l2.client_id = c.id
|
||||
)) AS store_count
|
||||
FROM clients c
|
||||
ORDER BY c.updated_at DESC NULLS LAST, c.created_at DESC LIMIT 500"""
|
||||
))}
|
||||
|
||||
|
||||
@@ -199,6 +207,79 @@ def delete_client(client_id: int):
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@admin_router.get("/clients/stats")
|
||||
def clients_stats():
|
||||
stats: dict[str, Any] = {}
|
||||
try:
|
||||
rows = fetch_all("SELECT stage, COUNT(*) AS n FROM clients GROUP BY stage")
|
||||
stats["by_stage"] = {r["stage"]: int(r["n"]) for r in rows}
|
||||
stats["total"] = sum(stats["by_stage"].values())
|
||||
stats["active"] = stats["by_stage"].get("active", 0)
|
||||
row = fetch_one("SELECT COALESCE(SUM(mrr_estimate),0) AS s FROM clients WHERE stage NOT IN ('churned')")
|
||||
stats["mrr_total"] = float(row["s"] or 0) if row else 0
|
||||
row = fetch_one(
|
||||
"""SELECT COALESCE(SUM(d.value),0) AS s FROM deals d
|
||||
JOIN clients c ON c.id = d.client_id
|
||||
WHERE d.stage NOT IN ('won','lost')"""
|
||||
)
|
||||
stats["pipeline_value"] = float(row["s"] or 0) if row else 0
|
||||
row = fetch_one("SELECT COUNT(*) AS n FROM client_supermarket_links")
|
||||
stats["store_links"] = int(row["n"] or 0) if row else 0
|
||||
row = fetch_one("SELECT COUNT(*) AS n FROM deals")
|
||||
stats["deals"] = int(row["n"] or 0) if row else 0
|
||||
except Exception:
|
||||
stats = {"total": 0, "active": 0, "mrr_total": 0, "pipeline_value": 0, "store_links": 0, "deals": 0, "by_stage": {}}
|
||||
return {"ok": True, "stats": stats}
|
||||
|
||||
|
||||
@admin_router.get("/clients/{client_id}/detail")
|
||||
def client_detail(client_id: int):
|
||||
client = fetch_one("SELECT * FROM clients WHERE id = %s", (client_id,))
|
||||
if not client:
|
||||
raise HTTPException(404, "Client not found")
|
||||
deals = fetch_all(
|
||||
"""SELECT id, title, value, stage, next_action, deadline, updated_at
|
||||
FROM deals WHERE client_id = %s ORDER BY updated_at DESC NULLS LAST LIMIT 20""",
|
||||
(client_id,),
|
||||
)
|
||||
stores = fetch_all(
|
||||
"""
|
||||
SELECT s.id, s.name, s.chain, s.city, s.province, s.postcode, s.phone, s.email,
|
||||
s.partnership_status, s.halal_certified, s.has_halal_section, s.manager_name,
|
||||
l.relationship_type, l.notes AS link_notes, l.deal_id, l.created_at AS linked_at
|
||||
FROM client_supermarket_links l
|
||||
JOIN supermarkets s ON s.id = l.supermarket_id
|
||||
WHERE l.client_id = %s
|
||||
ORDER BY s.chain, s.city, s.name
|
||||
""",
|
||||
(client_id,),
|
||||
)
|
||||
direct = fetch_all(
|
||||
"""
|
||||
SELECT s.id, s.name, s.chain, s.city, s.province, s.postcode, s.phone, s.email,
|
||||
s.partnership_status, s.halal_certified, s.has_halal_section, s.manager_name,
|
||||
'direct' AS relationship_type, NULL AS link_notes, s.deal_id, s.last_updated AS linked_at
|
||||
FROM supermarkets s
|
||||
WHERE s.client_id = %s
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM client_supermarket_links l
|
||||
WHERE l.supermarket_id = s.id AND l.client_id = s.client_id
|
||||
)
|
||||
ORDER BY s.chain, s.city, s.name
|
||||
""",
|
||||
(client_id,),
|
||||
)
|
||||
merged: dict[int, dict] = {}
|
||||
for row in list(stores) + list(direct):
|
||||
merged[int(row["id"])] = row
|
||||
return {
|
||||
"ok": True,
|
||||
"client": _serialize(client),
|
||||
"deals": _serialize_rows(deals),
|
||||
"stores": _serialize_rows(list(merged.values())),
|
||||
}
|
||||
|
||||
|
||||
# --- Deals CRUD ---
|
||||
|
||||
@admin_router.get("/deals")
|
||||
@@ -239,6 +320,36 @@ def delete_deal(deal_id: int):
|
||||
|
||||
# --- Products CRUD ---
|
||||
|
||||
@admin_router.get("/products/stats")
|
||||
def products_stats():
|
||||
stats: dict[str, Any] = {}
|
||||
try:
|
||||
row = fetch_one("SELECT COUNT(*) AS n FROM products")
|
||||
stats["total"] = int(row["n"] or 0) if row else 0
|
||||
rows = fetch_all("SELECT status, COUNT(*) AS n FROM products GROUP BY status")
|
||||
stats["by_status"] = {r["status"]: int(r["n"]) for r in rows}
|
||||
stats["active"] = stats["by_status"].get("active", 0)
|
||||
row = fetch_one("SELECT AVG(margin_pct) AS a FROM products WHERE margin_pct IS NOT NULL")
|
||||
stats["avg_margin"] = round(float(row["a"] or 0), 1) if row else 0
|
||||
row = fetch_one("SELECT COUNT(*) AS n FROM products WHERE client_id IS NOT NULL")
|
||||
stats["with_client"] = int(row["n"] or 0) if row else 0
|
||||
except Exception:
|
||||
stats = {"total": 0, "active": 0, "avg_margin": 0, "with_client": 0, "by_status": {}}
|
||||
return {"ok": True, "stats": stats}
|
||||
|
||||
|
||||
@admin_router.get("/products/{product_id}/detail")
|
||||
def product_detail(product_id: int):
|
||||
product = fetch_one(
|
||||
"""SELECT p.*, c.name AS client_name, c.email AS client_email, c.stage AS client_stage
|
||||
FROM products p LEFT JOIN clients c ON c.id = p.client_id WHERE p.id = %s""",
|
||||
(product_id,),
|
||||
)
|
||||
if not product:
|
||||
raise HTTPException(404, "Product not found")
|
||||
return {"ok": True, "product": _serialize(product)}
|
||||
|
||||
|
||||
@admin_router.get("/products")
|
||||
def list_products():
|
||||
return {"items": _serialize_rows(fetch_all(
|
||||
@@ -284,6 +395,35 @@ def margin_calc(body: MarginBody):
|
||||
|
||||
# --- Suppliers CRUD ---
|
||||
|
||||
@admin_router.get("/suppliers/stats")
|
||||
def suppliers_stats():
|
||||
stats: dict[str, Any] = {}
|
||||
try:
|
||||
row = fetch_one("SELECT COUNT(*) AS n FROM suppliers")
|
||||
stats["total"] = int(row["n"] or 0) if row else 0
|
||||
row = fetch_one("SELECT AVG(rating) AS a FROM suppliers WHERE rating IS NOT NULL")
|
||||
stats["avg_rating"] = round(float(row["a"] or 0), 1) if row else 0
|
||||
row = fetch_one("SELECT COUNT(DISTINCT country) AS n FROM suppliers WHERE country IS NOT NULL AND country <> ''")
|
||||
stats["countries"] = int(row["n"] or 0) if row else 0
|
||||
row = fetch_one("SELECT AVG(lead_time_days) AS a FROM suppliers WHERE lead_time_days IS NOT NULL")
|
||||
stats["avg_lead"] = round(float(row["a"] or 0), 0) if row else 0
|
||||
rows = fetch_all(
|
||||
"SELECT COALESCE(category, 'Overig') AS category, COUNT(*) AS n FROM suppliers GROUP BY category ORDER BY n DESC LIMIT 8"
|
||||
)
|
||||
stats["by_category"] = {r["category"]: int(r["n"]) for r in rows}
|
||||
except Exception:
|
||||
stats = {"total": 0, "avg_rating": 0, "countries": 0, "avg_lead": 0, "by_category": {}}
|
||||
return {"ok": True, "stats": stats}
|
||||
|
||||
|
||||
@admin_router.get("/suppliers/{supplier_id}/detail")
|
||||
def supplier_detail(supplier_id: int):
|
||||
supplier = fetch_one("SELECT * FROM suppliers WHERE id = %s", (supplier_id,))
|
||||
if not supplier:
|
||||
raise HTTPException(404, "Supplier not found")
|
||||
return {"ok": True, "supplier": _serialize(supplier)}
|
||||
|
||||
|
||||
@admin_router.get("/suppliers")
|
||||
def list_suppliers():
|
||||
return {"items": _serialize_rows(fetch_all("SELECT * FROM suppliers ORDER BY name LIMIT 500"))}
|
||||
|
||||
Reference in New Issue
Block a user