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:
@@ -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">✓ 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">⚠ ${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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
|
||||
Reference in New Issue
Block a user