feat(safety): needs attention card on dashboard
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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")]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = '<div class="attention-card attention-ok"><i class="bi bi-check-circle"></i> Totul in ordine</div>';
|
||||
} else {
|
||||
let items = [];
|
||||
if (errors > 0) items.push(`<span class="attention-item attention-error" onclick="document.querySelector('.filter-pill[data-status=ERROR]')?.click()"><i class="bi bi-exclamation-triangle"></i> ${errors} erori import</span>`);
|
||||
if (unmapped > 0) items.push(`<span class="attention-item attention-warning" onclick="window.location='${window.ROOT_PATH||''}/missing-skus'"><i class="bi bi-puzzle"></i> ${unmapped} SKU-uri nemapate</span>`);
|
||||
if (uninvOld > 0) items.push(`<span class="attention-item attention-warning" onclick="document.querySelector('.filter-pill[data-status=UNINVOICED]')?.click()"><i class="bi bi-receipt"></i> ${uninvOld} nefacturate >3 zile</span>`);
|
||||
attnEl.innerHTML = '<div class="attention-card attention-alert">' + items.join('') + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
const tbody = document.getElementById('dashOrdersBody');
|
||||
const orders = data.orders || [];
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
|
||||
{% set rp = request.scope.get('root_path', '') %}
|
||||
<link href="{{ rp }}/static/css/style.css?v=20" rel="stylesheet">
|
||||
<link href="{{ rp }}/static/css/style.css?v=21" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Top Navbar (hidden on mobile via CSS) -->
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
<span>Comenzi</span>
|
||||
</div>
|
||||
<div class="card-body py-2 px-3">
|
||||
<div id="attentionCard"></div>
|
||||
<div class="filter-bar" id="ordersFilterBar">
|
||||
<!-- Period dropdown -->
|
||||
<select id="periodSelect" class="select-compact">
|
||||
@@ -114,5 +115,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=28"></script>
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=29"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user