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

@@ -9,8 +9,14 @@ logger = logging.getLogger(__name__)
def get_mappings(search: str = "", page: int = 1, per_page: int = 50,
sort_by: str = "sku", sort_dir: str = "asc",
show_deleted: bool = False):
"""Get paginated mappings with optional search and sorting."""
show_deleted: bool = False, pct_filter: str = None):
"""Get paginated mappings with optional search, sorting, and pct_filter.
pct_filter values:
'complete' only SKU groups where sum(procent_pret for active rows) == 100
'incomplete' only SKU groups where sum < 100
None / 'all' no filter
"""
if database.pool is None:
raise HTTPException(status_code=503, detail="Oracle unavailable")
@@ -49,16 +55,7 @@ def get_mappings(search: str = "", page: int = 1, per_page: int = 50,
params["search"] = search
where = "WHERE " + " AND ".join(where_clauses) if where_clauses else ""
# Count total
count_sql = f"""
SELECT COUNT(*) FROM ARTICOLE_TERTI at
LEFT JOIN nom_articole na ON na.codmat = at.codmat
{where}
"""
cur.execute(count_sql, params)
total = cur.fetchone()[0]
# Get page
# Fetch ALL matching rows (no pagination yet — we need to group by SKU first)
data_sql = f"""
SELECT at.sku, at.codmat, na.denumire, na.um, at.cantitate_roa,
at.procent_pret, at.activ, at.sters,
@@ -67,30 +64,114 @@ def get_mappings(search: str = "", page: int = 1, per_page: int = 50,
LEFT JOIN nom_articole na ON na.codmat = at.codmat
{where}
ORDER BY {order_clause}
OFFSET :offset ROWS FETCH NEXT :per_page ROWS ONLY
"""
params["offset"] = offset
params["per_page"] = per_page
cur.execute(data_sql, params)
columns = [col[0].lower() for col in cur.description]
rows = [dict(zip(columns, row)) for row in cur.fetchall()]
all_rows = [dict(zip(columns, row)) for row in cur.fetchall()]
# Group by SKU and compute pct_total for each group
from collections import OrderedDict
groups = OrderedDict()
for row in all_rows:
sku = row["sku"]
if sku not in groups:
groups[sku] = []
groups[sku].append(row)
# Compute counts across ALL groups (before pct_filter)
total_skus = len(groups)
complete_skus = 0
incomplete_skus = 0
for sku, rows in groups.items():
pct_total = sum(
(r["procent_pret"] or 0)
for r in rows
if r.get("activ") == 1
)
if pct_total >= 99.99:
complete_skus += 1
else:
incomplete_skus += 1
counts = {
"total": total_skus,
"complete": complete_skus,
"incomplete": incomplete_skus,
}
# Apply pct_filter
if pct_filter in ("complete", "incomplete"):
filtered_groups = {}
for sku, rows in groups.items():
pct_total = sum(
(r["procent_pret"] or 0)
for r in rows
if r.get("activ") == 1
)
is_complete = pct_total >= 99.99
if pct_filter == "complete" and is_complete:
filtered_groups[sku] = rows
elif pct_filter == "incomplete" and not is_complete:
filtered_groups[sku] = rows
groups = filtered_groups
# Flatten back to rows for pagination (paginate by raw row count)
filtered_rows = [row for rows in groups.values() for row in rows]
total = len(filtered_rows)
page_rows = filtered_rows[offset: offset + per_page]
# Attach pct_total and is_complete to each row for the renderer
# Re-compute per visible group
sku_pct = {}
for sku, rows in groups.items():
pct_total = sum(
(r["procent_pret"] or 0)
for r in rows
if r.get("activ") == 1
)
sku_pct[sku] = {"pct_total": pct_total, "is_complete": pct_total >= 99.99}
for row in page_rows:
meta = sku_pct.get(row["sku"], {"pct_total": 0, "is_complete": False})
row["pct_total"] = meta["pct_total"]
row["is_complete"] = meta["is_complete"]
return {
"mappings": rows,
"mappings": page_rows,
"total": total,
"page": page,
"per_page": per_page,
"pages": (total + per_page - 1) // per_page
"pages": (total + per_page - 1) // per_page if total > 0 else 0,
"counts": counts,
}
def create_mapping(sku: str, codmat: str, cantitate_roa: float = 1, procent_pret: float = 100):
"""Create a new mapping."""
"""Create a new mapping. Returns dict or raises HTTPException on duplicate."""
if database.pool is None:
raise HTTPException(status_code=503, detail="Oracle unavailable")
with database.pool.acquire() as conn:
with conn.cursor() as cur:
# Check for active duplicate
cur.execute("""
SELECT COUNT(*) FROM ARTICOLE_TERTI
WHERE sku = :sku AND codmat = :codmat AND NVL(sters, 0) = 0
""", {"sku": sku, "codmat": codmat})
if cur.fetchone()[0] > 0:
raise HTTPException(status_code=409, detail="Maparea SKU-CODMAT există deja")
# Check for soft-deleted record that could be restored
cur.execute("""
SELECT COUNT(*) FROM ARTICOLE_TERTI
WHERE sku = :sku AND codmat = :codmat AND sters = 1
""", {"sku": sku, "codmat": codmat})
if cur.fetchone()[0] > 0:
raise HTTPException(
status_code=409,
detail="Maparea a fost ștearsă anterior",
headers={"X-Can-Restore": "true"}
)
cur.execute("""
INSERT INTO ARTICOLE_TERTI (sku, codmat, cantitate_roa, procent_pret, activ, sters, data_creare, id_util_creare)
VALUES (:sku, :codmat, :cantitate_roa, :procent_pret, 1, 0, SYSDATE, -3)