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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user