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

@@ -5,102 +5,86 @@
{% block content %}
<h4 class="mb-4">Panou de Comanda</h4>
<!-- Sync Control -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Sync Control</span>
<span class="badge bg-secondary" id="syncStatusBadge">idle</span>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<button class="btn btn-success btn-sm" id="btnStartSync" onclick="startSync()">
<i class="bi bi-play-fill"></i> Start Sync
</button>
<button class="btn btn-danger btn-sm d-none" id="btnStopSync" onclick="stopSync()">
<i class="bi bi-stop-fill"></i> Stop
</button>
</div>
<div class="col-auto">
<div class="form-check form-switch d-inline-block me-2">
<input class="form-check-input" type="checkbox" id="schedulerToggle" onchange="toggleScheduler()">
<label class="form-check-label" for="schedulerToggle">Scheduler</label>
</div>
<select class="form-select form-select-sm d-inline-block" style="width:auto" id="schedulerInterval" onchange="updateSchedulerInterval()">
<option value="1">1 min</option>
<option value="5" selected>5 min</option>
<option value="10">10 min</option>
<option value="15">15 min</option>
<option value="30">30 min</option>
<option value="60">60 min</option>
</select>
</div>
<div class="col">
<small class="text-muted" id="syncProgressText"></small>
</div>
</div>
<div class="mt-2 d-none" id="syncStartedBanner">
<div class="alert alert-info alert-sm py-1 px-2 mb-0 d-inline-block">
<small><i class="bi bi-broadcast"></i> Sync pornit — <a href="#" id="syncRunLink">vezi progresul live</a></small>
</div>
</div>
</div>
</div>
<!-- Last Sync Summary Card -->
<div class="card mb-4" id="lastSyncCard">
<div class="card-header d-flex justify-content-between align-items-center cursor-pointer" data-bs-toggle="collapse" data-bs-target="#lastSyncBody">
<span>Ultimul Sync</span>
<i class="bi bi-chevron-down"></i>
</div>
<div class="collapse show" id="lastSyncBody">
<div class="card-body">
<div class="row text-center" id="lastSyncRow">
<div class="col last-sync-col"><small class="text-muted">Data</small><br><strong id="lastSyncDate">-</strong></div>
<div class="col last-sync-col"><small class="text-muted">Status</small><br><span id="lastSyncStatus">-</span></div>
<div class="col last-sync-col"><small class="text-muted">Importate</small><br><strong class="text-success" id="lastSyncImported">0</strong></div>
<div class="col last-sync-col"><small class="text-muted">Omise</small><br><strong class="text-warning" id="lastSyncSkipped">0</strong></div>
<div class="col last-sync-col"><small class="text-muted">Erori</small><br><strong class="text-danger" id="lastSyncErrors">0</strong></div>
<div class="col"><small class="text-muted">Durata</small><br><strong id="lastSyncDuration">-</strong></div>
</div>
</div>
<!-- Sync Card (unified two-row panel) -->
<div class="sync-card">
<!-- TOP ROW: Status + Controls -->
<div class="sync-card-controls">
<span id="syncStatusDot" class="sync-status-dot idle"></span>
<span id="syncStatusText" style="font-size:0.8125rem;color:#374151;">Inactiv</span>
<div style="display:flex;align-items:center;gap:0.5rem;margin-left:auto;">
<label style="display:flex;align-items:center;gap:0.4rem;font-size:0.8125rem;color:#6b7280;">
Auto:
<input type="checkbox" id="schedulerToggle" style="cursor:pointer;" onchange="toggleScheduler()">
</label>
<select id="schedulerInterval" class="select-compact" onchange="updateSchedulerInterval()">
<option value="5">5 min</option>
<option value="10" selected>10 min</option>
<option value="30">30 min</option>
</select>
<button id="syncStartBtn" class="btn btn-primary btn-compact" onclick="startSync()">&#9654; Start Sync</button>
</div>
</div>
<div class="sync-card-divider"></div>
<!-- BOTTOM ROW: Last sync info (clickable → jurnal) -->
<div class="sync-card-info" id="lastSyncRow" role="button" tabindex="0" title="Ver jurnal sync">
<span id="lastSyncDate" style="font-weight:500;">&#8212;</span>
<span id="lastSyncDuration" style="color:#9ca3af;">&#8212;</span>
<span id="lastSyncCounts">&#8212;</span>
<span id="lastSyncStatus">&#8212;</span>
<span style="margin-left:auto;font-size:0.75rem;color:#9ca3af;">&#8599; jurnal</span>
</div>
<!-- LIVE PROGRESS (shown only when sync is running) -->
<div class="sync-card-progress" id="syncProgressArea" style="display:none;">
<span class="sync-live-dot"></span>
<span id="syncProgressText">Se proceseaza...</span>
</div>
</div>
<!-- Orders Table -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<span>Comenzi</span>
<div class="btn-group btn-group-sm" role="group" id="dashPeriodBtns">
<button type="button" class="btn btn-sm btn-outline-secondary" data-days="3" onclick="dashSetPeriod(3)">3 zile</button>
<button type="button" class="btn btn-sm btn-secondary" data-days="7" onclick="dashSetPeriod(7)">7 zile</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-days="30" onclick="dashSetPeriod(30)">30 zile</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-days="0" onclick="dashSetPeriod(0)">Toate</button>
</div>
</div>
<div class="input-group input-group-sm" style="width:250px">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="dashSearchInput" placeholder="Cauta..." oninput="debounceDashSearch()">
<div class="card-header">
<span>Comenzi</span>
</div>
<div class="card-body py-2 px-3">
<div class="filter-bar" id="ordersFilterBar">
<!-- Period dropdown -->
<select id="periodSelect" class="select-compact">
<option value="3">3 zile</option>
<option value="7" selected>7 zile</option>
<option value="30">30 zile</option>
<option value="90">3 luni</option>
<option value="0">Toate</option>
<option value="custom">Perioada personalizata...</option>
</select>
<!-- Custom date range (hidden until 'custom' selected) -->
<div class="period-custom-range" id="customRangeInputs">
<input type="date" id="periodStart" class="select-compact">
<span>&#8212;</span>
<input type="date" id="periodEnd" class="select-compact">
</div>
<!-- Status pills -->
<button class="filter-pill active" data-status="all">Toate <span class="filter-count" id="cntAll">0</span></button>
<button class="filter-pill" data-status="IMPORTED">Imp. <span class="filter-count" id="cntImp">0</span></button>
<button class="filter-pill" data-status="SKIPPED">Omise <span class="filter-count" id="cntSkip">0</span></button>
<button class="filter-pill" data-status="ERROR">Erori <span class="filter-count" id="cntErr">0</span></button>
<button class="filter-pill" data-status="UNINVOICED">Nefact. <span class="filter-count" id="cntNef">0</span></button>
<!-- Search (integrated, end of row) -->
<input type="search" id="orderSearch" placeholder="Cauta..." class="search-input">
</div>
</div>
<div class="card-body py-2">
<div class="btn-group" role="group" id="dashFilterBtns">
<button type="button" class="btn btn-sm btn-primary" onclick="dashFilterOrders('all')">
Toate <span class="badge bg-light text-dark ms-1" id="dashCountAll">0</span>
</button>
<button type="button" class="btn btn-sm btn-outline-success" onclick="dashFilterOrders('IMPORTED')">
Importate <span class="badge bg-light text-dark ms-1" id="dashCountImported">0</span>
</button>
<button type="button" class="btn btn-sm btn-outline-warning" onclick="dashFilterOrders('SKIPPED')">
Omise <span class="badge bg-light text-dark ms-1" id="dashCountSkipped">0</span>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="dashFilterOrders('ERROR')">
Erori <span class="badge bg-light text-dark ms-1" id="dashCountError">0</span>
</button>
<button type="button" class="btn btn-sm btn-outline-info" onclick="dashFilterOrders('UNINVOICED')">
Nefacturate <span class="badge bg-light text-dark ms-1" id="dashCountUninvoiced">0</span>
</button>
<!-- Pagination top bar -->
<div class="card-body py-1 px-3 border-bottom d-flex justify-content-between align-items-center" style="gap:0.5rem;">
<small class="text-muted" id="dashPageInfoTop"></small>
<div style="display:flex;align-items:center;gap:0.5rem;">
<label style="font-size:0.8125rem;color:#6b7280;white-space:nowrap;">Per pagina:
<select id="perPageSelect" class="select-compact" style="margin-left:0.25rem;" onchange="dashChangePerPage(this.value)">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="250">250</option>
</select>
</label>
<div id="dashPaginationTop" class="d-flex align-items-center gap-2"></div>
</div>
</div>
<div class="card-body p-0">