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:
2026-03-13 17:55:36 +02:00
parent 82196b9dc0
commit 5f8b9b6003
14 changed files with 1235 additions and 648 deletions

View File

@@ -1,6 +1,7 @@
from fastapi import APIRouter, Query, Request, UploadFile, File
from fastapi.responses import StreamingResponse, HTMLResponse
from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi import HTTPException
from pydantic import BaseModel
from pathlib import Path
from typing import Optional
@@ -49,15 +50,19 @@ async def mappings_page(request: Request):
@router.get("/api/mappings")
async def list_mappings(search: str = "", page: int = 1, per_page: int = 50,
sort_by: str = "sku", sort_dir: str = "asc",
show_deleted: bool = False):
show_deleted: bool = False, pct_filter: str = None):
result = mapping_service.get_mappings(search=search, page=page, per_page=per_page,
sort_by=sort_by, sort_dir=sort_dir,
show_deleted=show_deleted)
show_deleted=show_deleted,
pct_filter=pct_filter)
# Merge product names from web_products (R4)
skus = list({m["sku"] for m in result.get("mappings", [])})
product_names = await sqlite_service.get_web_products_batch(skus)
for m in result.get("mappings", []):
m["product_name"] = product_names.get(m["sku"], "")
# Ensure counts key is always present
if "counts" not in result:
result["counts"] = {"total": 0, "complete": 0, "incomplete": 0}
return result
@router.post("/api/mappings")
@@ -67,6 +72,12 @@ async def create_mapping(data: MappingCreate):
# Mark SKU as resolved in missing_skus tracking
await sqlite_service.resolve_missing_sku(data.sku)
return {"success": True, **result}
except HTTPException as e:
can_restore = e.headers.get("X-Can-Restore") == "true" if e.headers else False
resp: dict = {"error": e.detail}
if can_restore:
resp["can_restore"] = True
return JSONResponse(status_code=e.status_code, content=resp)
except Exception as e:
return {"success": False, "error": str(e)}