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:
@@ -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>
|
||||
|
||||
@@ -3,132 +3,165 @@
|
||||
{% block nav_logs %}active{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0">Jurnale Import</h4>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<select class="form-select form-select-sm" id="runSelector" style="min-width: 320px;">
|
||||
<option value="">-- Selecteaza un sync run --</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="loadRuns()" title="Reincarca lista">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="mb-4">Jurnale Import</h4>
|
||||
|
||||
<!-- Sync Runs Table (always visible) -->
|
||||
<!-- Sync Run Selector -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Sync Runs</span>
|
||||
<div id="runsTablePagination" class="d-flex align-items-center gap-2"></div>
|
||||
</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>Fara mapare</th>
|
||||
<th>Erori</th>
|
||||
<th>Durata</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="runsTableBody">
|
||||
<tr><td colspan="7" class="text-center text-muted py-3">Se incarca...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<label class="form-label mb-0 fw-bold text-nowrap">Sync Run:</label>
|
||||
<select class="form-select form-select-sm" id="runsDropdown" onchange="selectRun(this.value)">
|
||||
<option value="">Se incarca...</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-secondary text-nowrap" onclick="loadRuns()" title="Reincarca lista"><i class="bi bi-arrow-clockwise"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Run Detail Section (shown when run selected or live sync) -->
|
||||
<div id="runDetailSection" style="display:none;">
|
||||
|
||||
<!-- Run Summary Bar -->
|
||||
<div class="row g-3 mb-3" id="runSummary">
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-primary" id="sum-total" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Total</div>
|
||||
<!-- Detail Viewer (shown when run selected) -->
|
||||
<div id="logViewerSection" style="display:none;">
|
||||
<!-- Filter bar -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Run: <code id="logRunId"></code> <span class="badge bg-secondary" id="logStatusBadge">-</span></span>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="autoRefreshToggle" checked>
|
||||
<label class="form-check-label small" for="autoRefreshToggle">Auto-refresh</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="btnShowTextLog" onclick="toggleTextLog()">
|
||||
<i class="bi bi-file-text"></i> Log text brut
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-success" id="sum-imported" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Importate</div>
|
||||
<div class="card-body py-2">
|
||||
<div class="btn-group" role="group" id="orderFilterBtns">
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="filterOrders('all')">
|
||||
Toate <span class="badge bg-light text-dark ms-1" id="countAll">0</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-success" onclick="filterOrders('IMPORTED')">
|
||||
Importate <span class="badge bg-light text-dark ms-1" id="countImported">0</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-warning" onclick="filterOrders('SKIPPED')">
|
||||
Omise <span class="badge bg-light text-dark ms-1" id="countSkipped">0</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="filterOrders('ERROR')">
|
||||
Erori <span class="badge bg-light text-dark ms-1" id="countError">0</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-warning" id="sum-skipped" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Omise</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-danger" id="sum-errors" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Erori</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card stat-card px-3 py-2">
|
||||
<div class="stat-value text-secondary" id="sum-duration" style="font-size:1.25rem;">-</div>
|
||||
<div class="stat-label">Durata</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Feed (visible only during active sync) -->
|
||||
<div class="card mb-3" id="liveFeedCard" style="display:none;">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-broadcast"></i> Live Feed
|
||||
<span class="badge bg-danger ms-2 live-pulse">LIVE</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="live-feed" id="liveFeed"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter buttons -->
|
||||
<div class="mb-3" id="filterRow">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary active" data-filter="all">
|
||||
<i class="bi bi-list-ul"></i> Toate
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-success" data-filter="IMPORTED">
|
||||
<i class="bi bi-check-circle"></i> Importate
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning" data-filter="SKIPPED">
|
||||
<i class="bi bi-skip-forward"></i> Fara Mapare
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" data-filter="ERROR">
|
||||
<i class="bi bi-x-circle"></i> Erori
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted ms-3" id="filterCount"></small>
|
||||
</div>
|
||||
|
||||
<!-- Orders table -->
|
||||
<div class="card">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="logsTable">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:140px;">Nr. Comanda</th>
|
||||
<th>Client</th>
|
||||
<th style="width:100px;" class="text-center">Nr. Articole</th>
|
||||
<th style="width:120px;">Status</th>
|
||||
<th>Eroare / Detalii</th>
|
||||
<th>#</th>
|
||||
<th class="sortable" onclick="sortOrdersBy('order_date')">Data comanda <span class="sort-icon" data-col="order_date"></span></th>
|
||||
<th class="sortable" onclick="sortOrdersBy('order_number')">Nr. comanda <span class="sort-icon" data-col="order_number"></span></th>
|
||||
<th class="sortable" onclick="sortOrdersBy('customer_name')">Client <span class="sort-icon" data-col="customer_name"></span></th>
|
||||
<th class="sortable" onclick="sortOrdersBy('items_count')">Articole <span class="sort-icon" data-col="items_count"></span></th>
|
||||
<th class="sortable" onclick="sortOrdersBy('status')">Status <span class="sort-icon" data-col="status"></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logsBody">
|
||||
<tbody id="runOrdersBody">
|
||||
<tr><td colspan="6" class="text-center text-muted py-3">Selecteaza un sync run</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted" id="ordersPageInfo"></small>
|
||||
<div id="ordersPagination" class="d-flex align-items-center gap-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible text log -->
|
||||
<div id="textLogSection" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-header">Log text brut</div>
|
||||
<pre class="log-viewer" id="logContent">Se incarca...</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">Comanda <code id="detailOrderNumber"></code></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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-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="saveQuickMapping()">Salveaza</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="downloadTemplate()"><i class="bi bi-file-earmark-arrow-down"></i> Template CSV</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="exportCsv()"><i class="bi bi-download"></i> Export CSV</button>
|
||||
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#importModal"><i class="bi bi-upload"></i> Import CSV</button>
|
||||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addModal"><i class="bi bi-plus-lg"></i> Adauga Mapare</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="showInlineAddRow()"><i class="bi bi-plus-lg"></i> Adauga Mapare</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#addModal"><i class="bi bi-box-arrow-up-right"></i> Formular complet</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +24,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter controls -->
|
||||
<div class="d-flex align-items-center mb-3 gap-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="showInactive" onchange="loadMappings()">
|
||||
<label class="form-check-label" for="showInactive">Arata inactive</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="showDeleted" onchange="loadMappings()">
|
||||
<label class="form-check-label" for="showDeleted">Arata sterse</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
@@ -30,17 +43,19 @@
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>CODMAT</th>
|
||||
<th>Denumire</th>
|
||||
<th>Cantitate ROA</th>
|
||||
<th>Procent Pret</th>
|
||||
<th>Activ</th>
|
||||
<th class="sortable" onclick="sortBy('sku')">SKU <span class="sort-icon" data-col="sku"></span></th>
|
||||
<th>Produs Web</th>
|
||||
<th class="sortable" onclick="sortBy('codmat')">CODMAT <span class="sort-icon" data-col="codmat"></span></th>
|
||||
<th class="sortable" onclick="sortBy('denumire')">Denumire <span class="sort-icon" data-col="denumire"></span></th>
|
||||
<th>UM</th>
|
||||
<th class="sortable" onclick="sortBy('cantitate_roa')">Cantitate ROA <span class="sort-icon" data-col="cantitate_roa"></span></th>
|
||||
<th class="sortable" onclick="sortBy('procent_pret')">Procent Pret <span class="sort-icon" data-col="procent_pret"></span></th>
|
||||
<th class="sortable" onclick="sortBy('activ')">Activ <span class="sort-icon" data-col="activ"></span></th>
|
||||
<th style="width:100px">Actiuni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="mappingsBody">
|
||||
<tr><td colspan="7" class="text-center text-muted py-4">Se incarca...</td></tr>
|
||||
<tr><td colspan="9" class="text-center text-muted py-4">Se incarca...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -53,9 +68,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Modal -->
|
||||
<!-- Add/Edit Modal with multi-CODMAT support (R11) -->
|
||||
<div class="modal fade" id="addModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addModalTitle">Adauga Mapare</h5>
|
||||
@@ -66,22 +81,17 @@
|
||||
<label class="form-label">SKU</label>
|
||||
<input type="text" class="form-control" id="inputSku" placeholder="Ex: 8714858124284">
|
||||
</div>
|
||||
<div class="mb-3 position-relative">
|
||||
<label class="form-label">CODMAT (Articol ROA)</label>
|
||||
<input type="text" class="form-control" id="inputCodmat" placeholder="Cauta codmat sau denumire..." autocomplete="off">
|
||||
<div class="autocomplete-dropdown d-none" id="autocompleteDropdown"></div>
|
||||
<small class="text-muted" id="selectedArticle"></small>
|
||||
<div class="mb-2" id="addModalProductName" style="display:none;">
|
||||
<small class="text-muted">Produs web:</small> <strong id="inputProductName"></strong>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">Cantitate ROA</label>
|
||||
<input type="number" class="form-control" id="inputCantitate" value="1" step="0.001" min="0.001">
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">Procent Pret (%)</label>
|
||||
<input type="number" class="form-control" id="inputProcent" value="100" step="0.01" min="0" max="100">
|
||||
</div>
|
||||
<hr>
|
||||
<div id="codmatLines">
|
||||
<!-- Dynamic CODMAT lines will be added here -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="addCodmatLine()">
|
||||
<i class="bi bi-plus"></i> Adauga CODMAT
|
||||
</button>
|
||||
<div id="pctWarning" 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>
|
||||
@@ -111,6 +121,36 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteConfirmModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Confirmare stergere</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Sigur vrei sa stergi maparea?<br>
|
||||
SKU: <code id="deleteSkuText"></code><br>
|
||||
CODMAT: <code id="deleteCodmatText"></code>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">Sterge</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast container for undo actions -->
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index:1080">
|
||||
<div id="undoToast" class="toast" role="alert" data-bs-autohide="true" data-bs-delay="5000">
|
||||
<div class="toast-body d-flex align-items-center gap-2">
|
||||
<span id="toastMessage"></span>
|
||||
<button class="btn btn-sm btn-outline-primary ms-auto" id="toastUndoBtn">Anuleaza</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
@@ -15,6 +15,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resolved toggle (R10) -->
|
||||
<div class="btn-group mb-3" role="group">
|
||||
<button type="button" class="btn btn-sm btn-primary" id="btnUnresolved" onclick="setResolvedFilter(0)">
|
||||
Nerezolvate
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-success" id="btnResolved" onclick="setResolvedFilter(1)">
|
||||
Rezolvate
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnAll" onclick="setResolvedFilter(-1)">
|
||||
Toate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
@@ -45,7 +58,7 @@
|
||||
<ul class="pagination justify-content-center" id="paginationControls"></ul>
|
||||
</nav>
|
||||
|
||||
<!-- Map SKU Modal -->
|
||||
<!-- Map SKU Modal with multi-CODMAT support (R11) -->
|
||||
<div class="modal fade" id="mapModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
@@ -54,22 +67,16 @@
|
||||
<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 class="mb-2">
|
||||
<small class="text-muted">Produs web:</small> <strong id="mapProductName"></strong>
|
||||
</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>
|
||||
<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>
|
||||
<div id="mapCodmatLines">
|
||||
<!-- Dynamic CODMAT lines -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="addMapCodmatLine()">
|
||||
<i class="bi bi-plus"></i> Adauga CODMAT
|
||||
</button>
|
||||
<div id="mapPctWarning" 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>
|
||||
@@ -83,27 +90,29 @@
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let currentMapSku = '';
|
||||
let acTimeout = null;
|
||||
let mapAcTimeout = null;
|
||||
let currentPage = 1;
|
||||
let currentResolved = 0;
|
||||
const perPage = 20;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadMissing(1);
|
||||
|
||||
const input = document.getElementById('mapCodmat');
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(acTimeout);
|
||||
acTimeout = setTimeout(() => autocompleteMap(input.value), 250);
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => document.getElementById('mapAutocomplete').classList.add('d-none'), 200);
|
||||
});
|
||||
});
|
||||
|
||||
function setResolvedFilter(val) {
|
||||
currentResolved = val;
|
||||
currentPage = 1;
|
||||
// Update button styles
|
||||
document.getElementById('btnUnresolved').className = 'btn btn-sm ' + (val === 0 ? 'btn-primary' : 'btn-outline-primary');
|
||||
document.getElementById('btnResolved').className = 'btn btn-sm ' + (val === 1 ? 'btn-success' : 'btn-outline-success');
|
||||
document.getElementById('btnAll').className = 'btn btn-sm ' + (val === -1 ? 'btn-secondary' : 'btn-outline-secondary');
|
||||
loadMissing(1);
|
||||
}
|
||||
|
||||
async function loadMissing(page) {
|
||||
currentPage = page || 1;
|
||||
try {
|
||||
const res = await fetch(`/api/validate/missing-skus?page=${currentPage}&per_page=${perPage}`);
|
||||
const res = await fetch(`/api/validate/missing-skus?page=${currentPage}&per_page=${perPage}&resolved=${currentResolved}`);
|
||||
const data = await res.json();
|
||||
const tbody = document.getElementById('missingBody');
|
||||
|
||||
@@ -112,7 +121,9 @@ async function loadMissing(page) {
|
||||
|
||||
const skus = data.missing_skus || [];
|
||||
if (skus.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-4">Toate SKU-urile sunt mapate!</td></tr>';
|
||||
const msg = currentResolved === 0 ? 'Toate SKU-urile sunt mapate!' :
|
||||
currentResolved === 1 ? 'Niciun SKU rezolvat' : 'Niciun SKU gasit';
|
||||
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-muted py-4">${msg}</td></tr>`;
|
||||
renderPagination(data);
|
||||
return;
|
||||
}
|
||||
@@ -126,7 +137,7 @@ async function loadMissing(page) {
|
||||
try {
|
||||
const customers = JSON.parse(s.customers || '[]');
|
||||
if (customers.length > 0) firstCustomer = customers[0];
|
||||
} catch (e) { /* ignore parse errors */ }
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
const orderCount = s.order_count != null ? s.order_count : '-';
|
||||
|
||||
@@ -139,9 +150,9 @@ async function loadMissing(page) {
|
||||
<td>${statusBadge}</td>
|
||||
<td>
|
||||
${!s.resolved
|
||||
? `<button class="btn btn-sm btn-outline-primary" onclick="openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}')">
|
||||
<i class="bi bi-link-45deg"></i> Mapeaza
|
||||
</button>`
|
||||
? `<a href="#" class="btn-map-icon" onclick="openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}'); return false;" title="Mapeaza">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
</a>`
|
||||
: `<small class="text-muted">${s.resolved_at ? new Date(s.resolved_at).toLocaleDateString('ro-RO') : ''}</small>`}
|
||||
</td>
|
||||
</tr>`;
|
||||
@@ -158,103 +169,160 @@ function renderPagination(data) {
|
||||
const ul = document.getElementById('paginationControls');
|
||||
const total = data.pages || 1;
|
||||
const page = data.page || 1;
|
||||
|
||||
if (total <= 1) {
|
||||
ul.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (total <= 1) { ul.innerHTML = ''; return; }
|
||||
|
||||
let html = '';
|
||||
|
||||
html += `<li class="page-item ${page <= 1 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadMissing(${page - 1}); return false;">Anterior</a>
|
||||
</li>`;
|
||||
<a class="page-link" href="#" onclick="loadMissing(${page - 1}); return false;">Anterior</a></li>`;
|
||||
|
||||
const range = 2;
|
||||
for (let i = 1; i <= total; i++) {
|
||||
if (i === 1 || i === total || (i >= page - range && i <= page + range)) {
|
||||
html += `<li class="page-item ${i === page ? 'active' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadMissing(${i}); return false;">${i}</a>
|
||||
</li>`;
|
||||
<a class="page-link" href="#" onclick="loadMissing(${i}); return false;">${i}</a></li>`;
|
||||
} else if (i === page - range - 1 || i === page + range + 1) {
|
||||
html += `<li class="page-item disabled"><span class="page-link">…</span></li>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += `<li class="page-item ${page >= total ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadMissing(${page + 1}); return false;">Urmator</a>
|
||||
</li>`;
|
||||
|
||||
<a class="page-link" href="#" onclick="loadMissing(${page + 1}); return false;">Urmator</a></li>`;
|
||||
ul.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Multi-CODMAT Map Modal ───────────────────────
|
||||
|
||||
function openMapModal(sku, productName) {
|
||||
currentMapSku = sku;
|
||||
document.getElementById('mapSku').textContent = sku;
|
||||
document.getElementById('mapCodmat').value = productName || '';
|
||||
document.getElementById('mapCantitate').value = '1';
|
||||
document.getElementById('mapProcent').value = '100';
|
||||
document.getElementById('mapSelectedArticle').textContent = '';
|
||||
document.getElementById('mapAutocomplete').classList.add('d-none');
|
||||
document.getElementById('mapProductName').textContent = productName || '-';
|
||||
document.getElementById('mapPctWarning').style.display = 'none';
|
||||
|
||||
const container = document.getElementById('mapCodmatLines');
|
||||
container.innerHTML = '';
|
||||
addMapCodmatLine();
|
||||
|
||||
// Pre-search with product name
|
||||
if (productName) {
|
||||
autocompleteMap(productName);
|
||||
setTimeout(() => {
|
||||
const input = container.querySelector('.mc-codmat');
|
||||
if (input) {
|
||||
input.value = productName;
|
||||
mcAutocomplete(input,
|
||||
container.querySelector('.mc-ac-dropdown'),
|
||||
container.querySelector('.mc-selected'));
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('mapModal')).show();
|
||||
}
|
||||
|
||||
async function autocompleteMap(q) {
|
||||
const dropdown = document.getElementById('mapAutocomplete');
|
||||
if (q.length < 2) { dropdown.classList.add('d-none'); return; }
|
||||
function addMapCodmatLine() {
|
||||
const container = document.getElementById('mapCodmatLines');
|
||||
const idx = container.children.length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'border rounded p-2 mb-2 mc-line';
|
||||
div.innerHTML = `
|
||||
<div class="mb-2 position-relative">
|
||||
<label class="form-label form-label-sm mb-1">CODMAT (Articol ROA)</label>
|
||||
<input type="text" class="form-control form-control-sm mc-codmat" placeholder="Cauta codmat sau denumire..." autocomplete="off">
|
||||
<div class="autocomplete-dropdown d-none mc-ac-dropdown"></div>
|
||||
<small class="text-muted mc-selected"></small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<label class="form-label form-label-sm mb-1">Cantitate ROA</label>
|
||||
<input type="number" class="form-control form-control-sm mc-cantitate" value="1" step="0.001" min="0.001">
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<label class="form-label form-label-sm mb-1">Procent Pret (%)</label>
|
||||
<input type="number" class="form-control form-control-sm mc-procent" value="100" step="0.01" min="0" max="100">
|
||||
</div>
|
||||
<div class="col-2 d-flex align-items-end">
|
||||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('.mc-line').remove()"><i class="bi bi-x"></i></button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
|
||||
const input = div.querySelector('.mc-codmat');
|
||||
const dropdown = div.querySelector('.mc-ac-dropdown');
|
||||
const selected = div.querySelector('.mc-selected');
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(mapAcTimeout);
|
||||
mapAcTimeout = setTimeout(() => mcAutocomplete(input, dropdown, selected), 250);
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
||||
});
|
||||
}
|
||||
|
||||
async function mcAutocomplete(input, dropdown, selectedEl) {
|
||||
const q = input.value;
|
||||
if (q.length < 2) { dropdown.classList.add('d-none'); return; }
|
||||
try {
|
||||
const res = await fetch(`/api/articles/search?q=${encodeURIComponent(q)}`);
|
||||
const data = await res.json();
|
||||
if (!data.results || data.results.length === 0) { dropdown.classList.add('d-none'); return; }
|
||||
|
||||
if (!data.results || data.results.length === 0) {
|
||||
dropdown.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.innerHTML = data.results.map(r => `
|
||||
<div class="autocomplete-item" onmousedown="selectMapArticle('${esc(r.codmat)}', '${esc(r.denumire)}')">
|
||||
<span class="codmat">${esc(r.codmat)}</span>
|
||||
<br><span class="denumire">${esc(r.denumire)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
dropdown.innerHTML = data.results.map(r =>
|
||||
`<div class="autocomplete-item" onmousedown="mcSelectArticle(this, '${esc(r.codmat)}', '${esc(r.denumire)}${r.um ? ' (' + esc(r.um) + ')' : ''}')">
|
||||
<span class="codmat">${esc(r.codmat)}</span> — <span class="denumire">${esc(r.denumire)}</span>${r.um ? ` <small class="text-muted">(${esc(r.um)})</small>` : ''}
|
||||
</div>`
|
||||
).join('');
|
||||
dropdown.classList.remove('d-none');
|
||||
} catch (err) {
|
||||
dropdown.classList.add('d-none');
|
||||
}
|
||||
} catch { dropdown.classList.add('d-none'); }
|
||||
}
|
||||
|
||||
function selectMapArticle(codmat, denumire) {
|
||||
document.getElementById('mapCodmat').value = codmat;
|
||||
document.getElementById('mapSelectedArticle').textContent = denumire;
|
||||
document.getElementById('mapAutocomplete').classList.add('d-none');
|
||||
function mcSelectArticle(el, codmat, label) {
|
||||
const line = el.closest('.mc-line');
|
||||
line.querySelector('.mc-codmat').value = codmat;
|
||||
line.querySelector('.mc-selected').textContent = label;
|
||||
line.querySelector('.mc-ac-dropdown').classList.add('d-none');
|
||||
}
|
||||
|
||||
async function saveQuickMap() {
|
||||
const codmat = document.getElementById('mapCodmat').value.trim();
|
||||
const cantitate = parseFloat(document.getElementById('mapCantitate').value) || 1;
|
||||
const procent = parseFloat(document.getElementById('mapProcent').value) || 100;
|
||||
const lines = document.querySelectorAll('.mc-line');
|
||||
const mappings = [];
|
||||
|
||||
if (!codmat) { alert('Selecteaza un CODMAT'); return; }
|
||||
for (const line of lines) {
|
||||
const codmat = line.querySelector('.mc-codmat').value.trim();
|
||||
const cantitate = parseFloat(line.querySelector('.mc-cantitate').value) || 1;
|
||||
const procent = parseFloat(line.querySelector('.mc-procent').value) || 100;
|
||||
if (!codmat) continue;
|
||||
mappings.push({ codmat, cantitate_roa: cantitate, procent_pret: procent });
|
||||
}
|
||||
|
||||
if (mappings.length === 0) { alert('Selecteaza cel putin un CODMAT'); return; }
|
||||
|
||||
if (mappings.length > 1) {
|
||||
const totalPct = mappings.reduce((s, m) => s + m.procent_pret, 0);
|
||||
if (Math.abs(totalPct - 100) > 0.01) {
|
||||
document.getElementById('mapPctWarning').textContent = `Suma procentelor trebuie sa fie 100% (actual: ${totalPct.toFixed(2)}%)`;
|
||||
document.getElementById('mapPctWarning').style.display = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
document.getElementById('mapPctWarning').style.display = 'none';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/mappings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sku: currentMapSku,
|
||||
codmat: codmat,
|
||||
cantitate_roa: cantitate,
|
||||
procent_pret: procent
|
||||
})
|
||||
});
|
||||
let res;
|
||||
if (mappings.length === 1) {
|
||||
res = await fetch('/api/mappings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku: currentMapSku, codmat: mappings[0].codmat, cantitate_roa: mappings[0].cantitate_roa, procent_pret: mappings[0].procent_pret })
|
||||
});
|
||||
} else {
|
||||
res = await fetch('/api/mappings/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku: currentMapSku, mappings })
|
||||
});
|
||||
}
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('mapModal')).hide();
|
||||
loadMissing(currentPage);
|
||||
|
||||
Reference in New Issue
Block a user