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

@@ -4,12 +4,14 @@ let searchTimeout = null;
let sortColumn = 'sku';
let sortDirection = 'asc';
let editingMapping = null; // {sku, codmat} when editing
let pctFilter = 'all';
// Load on page ready
document.addEventListener('DOMContentLoaded', () => {
loadMappings();
initAddModal();
initDeleteModal();
initPctFilterPills();
});
function debounceSearch() {
@@ -45,6 +47,30 @@ function updateSortIcons() {
});
}
// ── Pct Filter Pills ─────────────────────────────
function initPctFilterPills() {
document.querySelectorAll('.filter-pill[data-pct]').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.filter-pill[data-pct]').forEach(b => b.classList.remove('active'));
this.classList.add('active');
pctFilter = this.dataset.pct;
currentPage = 1;
loadMappings();
});
});
}
function updatePctCounts(counts) {
if (!counts) return;
const elAll = document.getElementById('mCntAll');
const elComplete = document.getElementById('mCntComplete');
const elIncomplete = document.getElementById('mCntIncomplete');
if (elAll) elAll.textContent = counts.total || 0;
if (elComplete) elComplete.textContent = counts.complete || 0;
if (elIncomplete) elIncomplete.textContent = counts.incomplete || 0;
}
// ── Load & Render ────────────────────────────────
async function loadMappings() {
@@ -58,6 +84,7 @@ async function loadMappings() {
sort_dir: sortDirection
});
if (showDeleted) params.set('show_deleted', 'true');
if (pctFilter && pctFilter !== 'all') params.set('pct_filter', pctFilter);
try {
const res = await fetch(`/api/mappings?${params}`);
@@ -71,6 +98,7 @@ async function loadMappings() {
mappings = mappings.filter(m => m.activ || m.sters);
}
updatePctCounts(data.counts);
renderTable(mappings, showDeleted);
renderPagination(data);
updateSortIcons();
@@ -111,7 +139,17 @@ function renderTable(mappings, showDeleted) {
let skuCell, productCell;
if (isNewGroup) {
const badge = isMulti ? ` <span class="badge bg-info">Set (${skuGroupCounts[m.sku]})</span>` : '';
skuCell = `<td rowspan="${isMulti ? skuGroupCounts[m.sku] : 1}"><strong>${esc(m.sku)}</strong>${badge}</td>`;
// Percentage total badge
let pctBadge = '';
if (m.pct_total !== undefined) {
if (m.is_complete) {
pctBadge = ` <span class="badge-pct complete" title="100% alocat">&#10003; 100%</span>`;
} else {
const pctVal = typeof m.pct_total === 'number' ? m.pct_total.toFixed(0) : m.pct_total;
pctBadge = ` <span class="badge-pct incomplete" title="${pctVal}% alocat">&#9888; ${pctVal}%</span>`;
}
}
skuCell = `<td rowspan="${isMulti ? skuGroupCounts[m.sku] : 1}"><strong>${esc(m.sku)}</strong>${badge}${pctBadge}</td>`;
productCell = `<td rowspan="${isMulti ? skuGroupCounts[m.sku] : 1}">${esc(m.product_name || '-')}</td>`;
} else {
skuCell = '';
@@ -361,6 +399,8 @@ async function saveMapping() {
bootstrap.Modal.getInstance(document.getElementById('addModal')).hide();
editingMapping = null;
loadMappings();
} else if (res.status === 409) {
handleMappingConflict(data);
} else {
alert('Eroare: ' + (data.error || 'Unknown'));
}
@@ -462,6 +502,8 @@ async function saveInlineMapping() {
if (data.success) {
cancelInlineAdd();
loadMappings();
} else if (res.status === 409) {
handleMappingConflict(data);
} else {
alert('Eroare: ' + (data.error || 'Unknown'));
}
@@ -555,12 +597,17 @@ function showUndoToast(message, undoCallback) {
const newBtn = undoBtn.cloneNode(true);
undoBtn.parentNode.replaceChild(newBtn, undoBtn);
newBtn.id = 'toastUndoBtn';
newBtn.addEventListener('click', () => {
undoCallback();
const toastEl = document.getElementById('undoToast');
const inst = bootstrap.Toast.getInstance(toastEl);
if (inst) inst.hide();
});
if (undoCallback) {
newBtn.style.display = '';
newBtn.addEventListener('click', () => {
undoCallback();
const toastEl = document.getElementById('undoToast');
const inst = bootstrap.Toast.getInstance(toastEl);
if (inst) inst.hide();
});
} else {
newBtn.style.display = 'none';
}
const toast = new bootstrap.Toast(document.getElementById('undoToast'));
toast.show();
}
@@ -639,6 +686,33 @@ async function importCsv() {
function exportCsv() { window.location.href = '/api/mappings/export-csv'; }
function downloadTemplate() { window.location.href = '/api/mappings/csv-template'; }
// ── Duplicate / Conflict handling ────────────────
function handleMappingConflict(data) {
const msg = data.error || 'Conflict la salvare';
if (data.can_restore) {
const restore = confirm(`${msg}\n\nDoriti sa restaurati maparea stearsa?`);
if (restore) {
// Find sku/codmat from the inline row or modal
const sku = (document.getElementById('inlineSku') || document.getElementById('inputSku'))?.value?.trim();
const codmat = (document.getElementById('inlineCodmat') || document.querySelector('.cl-codmat'))?.value?.trim();
if (sku && codmat) {
fetch(`/api/mappings/${encodeURIComponent(sku)}/${encodeURIComponent(codmat)}/restore`, { method: 'POST' })
.then(r => r.json())
.then(d => {
if (d.success) { cancelInlineAdd(); loadMappings(); }
else alert('Eroare la restaurare: ' + (d.error || ''));
});
}
}
} else {
showUndoToast(msg, null);
// Show non-dismissible inline error
const warn = document.getElementById('pctWarning');
if (warn) { warn.textContent = msg; warn.style.display = ''; }
}
}
function esc(s) {
if (s == null) return '';
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');