From a10a00aa4d6c07e1c9089fc300279ca5c872aedb Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Fri, 27 Mar 2026 12:28:18 +0000 Subject: [PATCH] feat(safety): needs attention card on dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "Needs Attention" card above the orders table that surfaces: - Import errors count (click → ERROR filter) - Unmapped SKUs count (click → Missing SKUs page) - Uninvoiced orders >3 days (click → UNINVOICED filter) Shows green "Totul in ordine" when all metrics are zero. Backend: add uninvoiced_old count to get_orders() and unresolved_skus from get_dashboard_stats() to dashboard/orders API response. Cache-bust: style.css?v=21, dashboard.js?v=29 Co-Authored-By: Claude Opus 4.6 (1M context) --- api/app/routers/sync.py | 7 ++++++ api/app/services/sqlite_service.py | 11 +++++++++ api/app/static/css/style.css | 38 ++++++++++++++++++++++++++++++ api/app/static/js/dashboard.js | 18 ++++++++++++++ api/app/templates/base.html | 2 +- api/app/templates/dashboard.html | 3 ++- 6 files changed, 77 insertions(+), 2 deletions(-) diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py index aab6e69..09f0d7e 100644 --- a/api/app/routers/sync.py +++ b/api/app/routers/sync.py @@ -564,6 +564,13 @@ async def dashboard_orders(page: int = 1, per_page: int = 50, counts["facturate"] = max(0, imported_total - counts["nefacturate"]) counts.setdefault("total", counts.get("imported", 0) + counts.get("skipped", 0) + counts.get("error", 0)) + # Attention metrics: add unresolved SKUs count + try: + stats = await sqlite_service.get_dashboard_stats() + counts["unresolved_skus"] = stats.get("unresolved_skus", 0) + except Exception: + counts["unresolved_skus"] = 0 + # For UNINVOICED filter: apply server-side filtering + pagination if is_uninvoiced_filter: filtered = [o for o in all_orders if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and not o.get("invoice")] diff --git a/api/app/services/sqlite_service.py b/api/app/services/sqlite_service.py index 237db92..f9d8a2c 100644 --- a/api/app/services/sqlite_service.py +++ b/api/app/services/sqlite_service.py @@ -739,6 +739,16 @@ async def get_orders(page: int = 1, per_page: int = 50, cursor = await db.execute(f"SELECT COUNT(*) FROM orders {uninv_where}", base_params) uninvoiced_sqlite = (await cursor.fetchone())[0] + # Uninvoiced > 3 days old + uninv_old_clauses = list(base_clauses) + [ + "UPPER(status) IN ('IMPORTED', 'ALREADY_IMPORTED')", + "(factura_numar IS NULL OR factura_numar = '')", + "order_date < datetime('now', '-3 days')", + ] + uninv_old_where = "WHERE " + " AND ".join(uninv_old_clauses) + cursor = await db.execute(f"SELECT COUNT(*) FROM orders {uninv_old_where}", base_params) + uninvoiced_old = (await cursor.fetchone())[0] + return { "orders": [dict(r) for r in rows], "total": total, @@ -754,6 +764,7 @@ async def get_orders(page: int = 1, per_page: int = 50, "cancelled": status_counts.get("CANCELLED", 0), "total": sum(status_counts.values()), "uninvoiced_sqlite": uninvoiced_sqlite, + "uninvoiced_old": uninvoiced_old, } } finally: diff --git a/api/app/static/css/style.css b/api/app/static/css/style.css index 1a8b638..405a271 100644 --- a/api/app/static/css/style.css +++ b/api/app/static/css/style.css @@ -995,3 +995,41 @@ tr.mapping-deleted td { margin: 0; cursor: pointer; } + +/* ── Attention card ──────────────────────────── */ +.attention-card { + display: flex; + align-items: center; + gap: 16px; + padding: 10px 16px; + border-radius: 8px; + font-size: 0.875rem; + margin-bottom: 8px; +} +.attention-ok { + background: var(--success-light); + color: var(--success-text); +} +.attention-alert { + background: var(--surface); + border: 1px solid var(--border); + flex-wrap: wrap; +} +.attention-item { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + transition: opacity 0.15s; +} +.attention-item:hover { opacity: 0.8; } +.attention-error { + background: var(--error-light); + color: var(--error-text); +} +.attention-warning { + background: var(--warning-light); + color: var(--warning-text); +} diff --git a/api/app/static/js/dashboard.js b/api/app/static/js/dashboard.js index ee7f760..950ea46 100644 --- a/api/app/static/js/dashboard.js +++ b/api/app/static/js/dashboard.js @@ -301,6 +301,24 @@ async function loadDashOrders() { if (el('cntNef')) el('cntNef').textContent = c.nefacturate || c.uninvoiced || 0; if (el('cntCanc')) el('cntCanc').textContent = c.cancelled || 0; + // Attention card + const attnEl = document.getElementById('attentionCard'); + if (attnEl) { + const errors = c.error || 0; + const unmapped = c.unresolved_skus || 0; + const uninvOld = c.uninvoiced_old || 0; + + if (errors === 0 && unmapped === 0 && uninvOld === 0) { + attnEl.innerHTML = '
Totul in ordine
'; + } else { + let items = []; + if (errors > 0) items.push(` ${errors} erori import`); + if (unmapped > 0) items.push(` ${unmapped} SKU-uri nemapate`); + if (uninvOld > 0) items.push(` ${uninvOld} nefacturate >3 zile`); + attnEl.innerHTML = '
' + items.join('') + '
'; + } + } + const tbody = document.getElementById('dashOrdersBody'); const orders = data.orders || []; diff --git a/api/app/templates/base.html b/api/app/templates/base.html index f4d1a4c..46dba28 100644 --- a/api/app/templates/base.html +++ b/api/app/templates/base.html @@ -19,7 +19,7 @@ {% set rp = request.scope.get('root_path', '') %} - + diff --git a/api/app/templates/dashboard.html b/api/app/templates/dashboard.html index cad8195..b95c200 100644 --- a/api/app/templates/dashboard.html +++ b/api/app/templates/dashboard.html @@ -49,6 +49,7 @@ Comenzi
+