feat(sqlite): refactor orders schema + dashboard period filter

Replace import_orders (insert-per-run) with orders table (one row per
order, upsert on conflict). Eliminates dedup CTE on every dashboard
query and prevents unbounded row growth at 4-500 orders/sync.

Key changes:
- orders table: PK order_number, upsert via ON CONFLICT DO UPDATE;
  COALESCE preserves id_comanda once set; times_skipped auto-increments
- sync_run_orders: lightweight junction (sync_run_id, order_number)
  replaces sync_run_id column on orders
- order_items: PK changed to (order_number, sku), INSERT OR IGNORE
- Auto-migration in init_sqlite(): import_orders → orders on first boot,
  old table renamed to import_orders_bak
- /api/dashboard/orders: period_days param (3/7/30/0=all, default 7)
- Dashboard: period selector buttons in orders card header
- start.sh: stop existing process on port 5003 before restart;
  remove --reload (broken on WSL2 /mnt/e/)
- Add invoice_service, E2E Playwright tests, Oracle package updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 16:18:57 +02:00
parent 650e98539e
commit 82196b9dc0
32 changed files with 4164 additions and 1192 deletions

View File

@@ -3,76 +3,13 @@
{% block nav_dashboard %}active{% endblock %}
{% block content %}
<h4 class="mb-4">Dashboard</h4>
<!-- Stat cards - Row 1: Comenzi -->
<div class="row g-3 mb-2" id="statsRow">
<div class="col">
<div class="card stat-card">
<div class="stat-value text-info" id="stat-new">-</div>
<div class="stat-label">Comenzi Noi</div>
</div>
</div>
<div class="col">
<div class="card stat-card">
<div class="stat-value text-primary" id="stat-ready">-</div>
<div class="stat-label">Ready</div>
</div>
</div>
<div class="col">
<div class="card stat-card">
<div class="stat-value text-success" id="stat-imported">-</div>
<div class="stat-label">Importate</div>
</div>
</div>
<div class="col">
<div class="card stat-card">
<div class="stat-value text-warning" id="stat-skipped">-</div>
<div class="stat-label">Fără Mapare</div>
</div>
</div>
<div class="col">
<div class="card stat-card">
<div class="stat-value text-danger" id="stat-errors">-</div>
<div class="stat-label">Erori Import</div>
</div>
</div>
</div>
<!-- Stat cards - Row 2: Articole -->
<div class="row g-3 mb-4" id="statsRowArticles">
<div class="col">
<div class="card stat-card">
<div class="stat-value text-secondary" id="stat-total-skus">-</div>
<div class="stat-label">Total SKU Scanate</div>
</div>
</div>
<div class="col">
<div class="card stat-card">
<div class="stat-value text-success" id="stat-mapped-skus">-</div>
<div class="stat-label">Cu Mapare</div>
</div>
</div>
<div class="col">
<div class="card stat-card">
<div class="stat-value text-warning" id="stat-missing-skus">-</div>
<div class="stat-label">Fără Mapare</div>
</div>
</div>
<div class="col d-none d-md-block"></div>
<div class="col d-none d-md-block"></div>
</div>
<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>
<div class="d-flex align-items-center gap-2">
<a href="/logs" class="btn btn-sm btn-outline-info">
<i class="bi bi-journal-text"></i> Jurnale Import
</a>
<span class="badge bg-secondary" id="syncStatusBadge">idle</span>
</div>
<span class="badge bg-secondary" id="syncStatusBadge">idle</span>
</div>
<div class="card-body">
<div class="row align-items-center">
@@ -80,9 +17,6 @@
<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-outline-secondary btn-sm" id="btnScan" onclick="scanOrders()">
<i class="bi bi-search"></i> Scan
</button>
<button class="btn btn-danger btn-sm d-none" id="btnStopSync" onclick="stopSync()">
<i class="bi bi-stop-fill"></i> Stop
</button>
@@ -113,86 +47,161 @@
</div>
</div>
<!-- Recent Sync Runs -->
<!-- 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>
</div>
</div>
<!-- Orders Table -->
<div class="card mb-4">
<div class="card-header">Ultimele Sync Runs</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Data</th>
<th>Status</th>
<th>Total</th>
<th>OK</th>
<th>Fără mapare</th>
<th>Erori</th>
<th>Durata</th>
</tr>
</thead>
<tbody id="syncRunsBody">
<tr><td colspan="7" class="text-center text-muted py-3">Se incarca...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Missing SKUs (quick resolve) -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>SKU-uri Lipsa</span>
<a href="/missing-skus" class="btn btn-sm btn-outline-primary">Vezi toate</a>
<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>
</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>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>SKU</th>
<th>Produs</th>
<th>Nr. Comenzi</th>
<th>Primul Client</th>
<th colspan="2">Acțiune</th>
<th class="sortable" onclick="dashSortBy('order_number')">Nr Comanda <span class="sort-icon" data-col="order_number"></span></th>
<th class="sortable" onclick="dashSortBy('order_date')">Data <span class="sort-icon" data-col="order_date"></span></th>
<th class="sortable" onclick="dashSortBy('customer_name')">Client <span class="sort-icon" data-col="customer_name"></span></th>
<th class="sortable" onclick="dashSortBy('items_count')">Art. <span class="sort-icon" data-col="items_count"></span></th>
<th class="sortable" onclick="dashSortBy('status')">Status Import <span class="sort-icon" data-col="status"></span></th>
<th>ID ROA</th>
<th>Factura</th>
<th>Total</th>
</tr>
</thead>
<tbody id="missingSkusBody">
<tr><td colspan="5" class="text-center text-muted py-3">Se incarca...</td></tr>
<tbody id="dashOrdersBody">
<tr><td colspan="8" class="text-center text-muted py-3">Se incarca...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer d-flex justify-content-between align-items-center">
<small class="text-muted" id="dashPageInfo"></small>
<div id="dashPagination" class="d-flex align-items-center gap-2"></div>
</div>
</div>
<!-- Map SKU Modal (copied from missing_skus.html) -->
<div class="modal fade" id="mapModal" tabindex="-1">
<div class="modal-dialog">
<!-- Order Detail Modal -->
<div class="modal fade" id="orderDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Mapeaza SKU: <code id="mapSku"></code></h5>
<h5 class="modal-title">Comanda <code id="detailOrderNumber"></code></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3 position-relative">
<label class="form-label">CODMAT (Articol ROA)</label>
<input type="text" class="form-control" id="mapCodmat" placeholder="Cauta codmat sau denumire..." autocomplete="off">
<div class="autocomplete-dropdown d-none" id="mapAutocomplete"></div>
<small class="text-muted" id="mapSelectedArticle"></small>
</div>
<div class="row">
<div class="col-6 mb-3">
<label class="form-label">Cantitate ROA</label>
<input type="number" class="form-control" id="mapCantitate" value="1" step="0.001" min="0.001">
<div class="row mb-3">
<div class="col-md-6">
<small class="text-muted">Client:</small> <strong id="detailCustomer"></strong><br>
<small class="text-muted">Data comanda:</small> <span id="detailDate"></span><br>
<small class="text-muted">Status:</small> <span id="detailStatus"></span>
</div>
<div class="col-6 mb-3">
<label class="form-label">Procent Pret (%)</label>
<input type="number" class="form-control" id="mapProcent" value="100" step="0.01" min="0" max="100">
<div class="col-md-6">
<small class="text-muted">ID Comanda ROA:</small> <span id="detailIdComanda">-</span><br>
<small class="text-muted">ID Partener:</small> <span id="detailIdPartener">-</span><br>
<small class="text-muted">ID Adr. Facturare:</small> <span id="detailIdAdresaFact">-</span><br>
<small class="text-muted">ID Adr. Livrare:</small> <span id="detailIdAdresaLivr">-</span>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0">
<thead class="table-light">
<tr>
<th>SKU</th>
<th>Produs</th>
<th>Cant.</th>
<th>Pret</th>
<th>TVA</th>
<th>CODMAT</th>
<th>Status</th>
<th>Actiune</th>
</tr>
</thead>
<tbody id="detailItemsBody">
</tbody>
</table>
</div>
<div id="detailError" class="alert alert-danger mt-3" style="display:none;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Inchide</button>
</div>
</div>
</div>
</div>
<!-- Quick Map Modal (used from order detail) -->
<div class="modal fade" id="quickMapModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Mapeaza SKU: <code id="qmSku"></code></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<small class="text-muted">Produs web:</small> <strong id="qmProductName"></strong>
</div>
<div id="qmCodmatLines">
<!-- Dynamic CODMAT lines -->
</div>
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="addQmCodmatLine()">
<i class="bi bi-plus"></i> Adauga CODMAT
</button>
<div id="qmPctWarning" class="text-danger mt-2" style="display:none;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button>
<button type="button" class="btn btn-primary" onclick="saveQuickMap()">Salveaza</button>
<button type="button" class="btn btn-primary" onclick="saveQuickMapping()">Salveaza</button>
</div>
</div>
</div>