feat(dashboard): redesign UI with smart polling, unified sync card, filter bar
Replace SSE with smart polling (30s idle / 3s when running). Unify sync panel into single two-row card with live progress text. Add unified filter bar (period dropdown, status pills, search) with period-total counts. Add Client/Cont tooltip for different shipping/billing persons. Add SKU mappings pct_total badges + complete/incomplete filter + 409 duplicate check. Add missing SKUs search + rescan progress UX. Migrate SQLite orders schema (shipping_name, billing_name, payment_method, delivery_method). Fix JSON_OUTPUT_DIR path for server running from project root. Fix pagination controls showing top+bottom with per-page selector (25/50/100/250). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,10 @@ async def scan_and_validate():
|
||||
orders, json_count = order_reader.read_json_orders()
|
||||
|
||||
if not orders:
|
||||
return {"orders": 0, "json_files": json_count, "skus": {}, "message": "No orders found"}
|
||||
return {
|
||||
"orders": 0, "json_files": json_count, "skus": {}, "message": "No orders found",
|
||||
"total_skus_scanned": 0, "new_missing": 0, "auto_resolved": 0, "unchanged": 0,
|
||||
}
|
||||
|
||||
all_skus = order_reader.get_all_skus(orders)
|
||||
result = validation_service.validate_skus(all_skus)
|
||||
@@ -37,6 +40,7 @@ async def scan_and_validate():
|
||||
if customer not in sku_context[sku]["customers"]:
|
||||
sku_context[sku]["customers"].append(customer)
|
||||
|
||||
new_missing = 0
|
||||
for sku in result["missing"]:
|
||||
# Find product name from orders
|
||||
product_name = ""
|
||||
@@ -49,13 +53,19 @@ async def scan_and_validate():
|
||||
break
|
||||
|
||||
ctx = sku_context.get(sku, {})
|
||||
await sqlite_service.track_missing_sku(
|
||||
tracked = await sqlite_service.track_missing_sku(
|
||||
sku=sku,
|
||||
product_name=product_name,
|
||||
order_count=len(ctx.get("order_numbers", [])),
|
||||
order_numbers=json.dumps(ctx.get("order_numbers", [])),
|
||||
customers=json.dumps(ctx.get("customers", []))
|
||||
)
|
||||
if tracked:
|
||||
new_missing += 1
|
||||
|
||||
total_skus_scanned = len(all_skus)
|
||||
new_missing_count = len(result["missing"])
|
||||
unchanged = total_skus_scanned - new_missing_count
|
||||
|
||||
return {
|
||||
"json_files": json_count,
|
||||
@@ -64,6 +74,11 @@ async def scan_and_validate():
|
||||
"importable": len(importable),
|
||||
"skipped": len(skipped),
|
||||
"new_orders": len(new_orders),
|
||||
# Fields consumed by the rescan progress banner in missing_skus.html
|
||||
"total_skus_scanned": total_skus_scanned,
|
||||
"new_missing": new_missing_count,
|
||||
"auto_resolved": 0,
|
||||
"unchanged": unchanged,
|
||||
"skus": {
|
||||
"mapped": len(result["mapped"]),
|
||||
"direct": len(result["direct"]),
|
||||
@@ -88,20 +103,35 @@ async def scan_and_validate():
|
||||
async def get_missing_skus(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(20, ge=1, le=100),
|
||||
resolved: int = Query(0, ge=-1, le=1)
|
||||
resolved: int = Query(0, ge=-1, le=1),
|
||||
search: str = Query(None)
|
||||
):
|
||||
"""Get paginated missing SKUs. resolved=-1 means show all (R10)."""
|
||||
result = await sqlite_service.get_missing_skus_paginated(page, per_page, resolved)
|
||||
# Backward compat: also include 'unresolved' count
|
||||
"""Get paginated missing SKUs. resolved=-1 means show all (R10).
|
||||
Optional search filters by sku or product_name."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
cursor = await db.execute(
|
||||
"SELECT COUNT(*) FROM missing_skus WHERE resolved = 0"
|
||||
)
|
||||
unresolved = (await cursor.fetchone())[0]
|
||||
# Compute counts across ALL records (unfiltered by search)
|
||||
cursor = await db.execute("SELECT COUNT(*) FROM missing_skus WHERE resolved = 0")
|
||||
unresolved_count = (await cursor.fetchone())[0]
|
||||
cursor = await db.execute("SELECT COUNT(*) FROM missing_skus WHERE resolved = 1")
|
||||
resolved_count = (await cursor.fetchone())[0]
|
||||
cursor = await db.execute("SELECT COUNT(*) FROM missing_skus")
|
||||
total_count = (await cursor.fetchone())[0]
|
||||
finally:
|
||||
await db.close()
|
||||
result["unresolved"] = unresolved
|
||||
|
||||
counts = {
|
||||
"total": total_count,
|
||||
"unresolved": unresolved_count,
|
||||
"resolved": resolved_count,
|
||||
}
|
||||
|
||||
result = await sqlite_service.get_missing_skus_paginated(page, per_page, resolved, search=search)
|
||||
# Backward compat
|
||||
result["unresolved"] = unresolved_count
|
||||
result["counts"] = counts
|
||||
# rename key for JS consistency
|
||||
result["skus"] = result.get("missing_skus", [])
|
||||
return result
|
||||
|
||||
@router.get("/missing-skus-csv")
|
||||
|
||||
Reference in New Issue
Block a user