// ── State ───────────────────────────────────────── let dashPage = 1; let dashPerPage = 50; let dashSortCol = 'order_date'; let dashSortDir = 'desc'; let dashSearchTimeout = null; // Sync polling state let _pollInterval = null; let _lastSyncStatus = null; let _lastRunId = null; let _currentRunId = null; let _pollIntervalMs = 5000; // default, overridden from settings let _knownLastRunId = null; // track last_run.run_id to detect missed syncs // ── Init ────────────────────────────────────────── document.addEventListener('DOMContentLoaded', async () => { await initPollInterval(); loadSchedulerStatus(); loadDashOrders(); startSyncPolling(); wireFilterBar(); }); async function initPollInterval() { try { const data = await fetchJSON('/api/settings'); const sec = parseInt(data.dashboard_poll_seconds) || 5; _pollIntervalMs = sec * 1000; } catch(e) {} } // ── Smart Sync Polling ──────────────────────────── function startSyncPolling() { if (_pollInterval) clearInterval(_pollInterval); _pollInterval = setInterval(pollSyncStatus, _pollIntervalMs); pollSyncStatus(); // immediate first call } async function pollSyncStatus() { try { const data = await fetchJSON('/api/sync/status'); updateSyncPanel(data); const isRunning = data.status === 'running'; const wasRunning = _lastSyncStatus === 'running'; // Detect missed sync completions via last_run.run_id change const newLastRunId = data.last_run?.run_id || null; const missedSync = !isRunning && !wasRunning && _knownLastRunId && newLastRunId && newLastRunId !== _knownLastRunId; _knownLastRunId = newLastRunId; if (isRunning && !wasRunning) { // Switched to running — speed up polling clearInterval(_pollInterval); _pollInterval = setInterval(pollSyncStatus, 3000); } else if (!isRunning && wasRunning) { // Sync just completed — slow down and refresh orders clearInterval(_pollInterval); _pollInterval = setInterval(pollSyncStatus, _pollIntervalMs); loadDashOrders(); } else if (missedSync) { // Sync completed while we weren't watching (e.g. auto-sync) — refresh orders loadDashOrders(); } _lastSyncStatus = data.status; } catch (e) { console.warn('Sync status poll failed:', e); } } function updateSyncPanel(data) { const dot = document.getElementById('syncStatusDot'); const txt = document.getElementById('syncStatusText'); const progressArea = document.getElementById('syncProgressArea'); const progressText = document.getElementById('syncProgressText'); const startBtn = document.getElementById('syncStartBtn'); if (dot) { dot.className = 'sync-status-dot ' + (data.status || 'idle'); } const statusLabels = { running: 'A ruleaza...', idle: 'Inactiv', completed: 'Finalizat', failed: 'Eroare' }; if (txt) txt.textContent = statusLabels[data.status] || data.status || 'Inactiv'; if (startBtn) startBtn.disabled = data.status === 'running'; // Track current running sync run_id if (data.status === 'running' && data.run_id) { _currentRunId = data.run_id; } else { _currentRunId = null; } // Live progress area if (progressArea) { progressArea.style.display = data.status === 'running' ? 'flex' : 'none'; } if (progressText && data.phase_text) { progressText.textContent = data.phase_text; } // Last run info const lr = data.last_run; if (lr) { _lastRunId = lr.run_id; const d = document.getElementById('lastSyncDate'); const dur = document.getElementById('lastSyncDuration'); const cnt = document.getElementById('lastSyncCounts'); const st = document.getElementById('lastSyncStatus'); if (d) d.textContent = lr.started_at ? lr.started_at.replace('T', ' ').slice(0, 16) : '\u2014'; if (dur) dur.textContent = lr.duration_seconds ? Math.round(lr.duration_seconds) + 's' : '\u2014'; if (cnt) { const newImp = lr.new_imported || 0; const already = lr.already_imported || 0; if (already > 0) { cnt.innerHTML = `${newImp} noi, ${already} deja   ${lr.skipped || 0} omise   ${lr.errors || 0} erori`; } else { cnt.innerHTML = `${lr.imported || 0} imp.   ${lr.skipped || 0} omise   ${lr.errors || 0} erori`; } } if (st) { st.textContent = lr.status === 'completed' ? '\u2713' : '\u2715'; st.style.color = lr.status === 'completed' ? '#10b981' : '#ef4444'; } } } // Wire last-sync-row click → journal (use current running sync if active) document.addEventListener('DOMContentLoaded', () => { document.getElementById('lastSyncRow')?.addEventListener('click', () => { const targetId = _currentRunId || _lastRunId; if (targetId) window.location = (window.ROOT_PATH || '') + '/logs?run=' + targetId; }); document.getElementById('lastSyncRow')?.addEventListener('keydown', (e) => { const targetId = _currentRunId || _lastRunId; if ((e.key === 'Enter' || e.key === ' ') && targetId) { window.location = '/logs?run=' + targetId; } }); }); // ── Sync Controls ───────────────────────────────── async function startSync() { try { const res = await fetch('/api/sync/start', { method: 'POST' }); const data = await res.json(); if (data.error) { alert(data.error); return; } // Polling will detect the running state — just speed it up immediately pollSyncStatus(); } catch (err) { alert('Eroare: ' + err.message); } } async function stopSync() { try { await fetch('/api/sync/stop', { method: 'POST' }); pollSyncStatus(); } catch (err) { alert('Eroare: ' + err.message); } } async function toggleScheduler() { const enabled = document.getElementById('schedulerToggle').checked; const interval = parseInt(document.getElementById('schedulerInterval').value) || 10; try { await fetch('/api/sync/schedule', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, interval_minutes: interval }) }); } catch (err) { alert('Eroare scheduler: ' + err.message); } } async function updateSchedulerInterval() { const enabled = document.getElementById('schedulerToggle').checked; if (enabled) { await toggleScheduler(); } } async function loadSchedulerStatus() { try { const res = await fetch('/api/sync/schedule'); const data = await res.json(); document.getElementById('schedulerToggle').checked = data.enabled || false; if (data.interval_minutes) { document.getElementById('schedulerInterval').value = data.interval_minutes; } } catch (err) { console.error('loadSchedulerStatus error:', err); } } // ── Filter Bar wiring ───────────────────────────── function wireFilterBar() { // Period dropdown document.getElementById('periodSelect')?.addEventListener('change', function () { const cr = document.getElementById('customRangeInputs'); if (this.value === 'custom') { cr?.classList.add('visible'); } else { cr?.classList.remove('visible'); dashPage = 1; loadDashOrders(); } }); // Custom range inputs ['periodStart', 'periodEnd'].forEach(id => { document.getElementById(id)?.addEventListener('change', () => { const s = document.getElementById('periodStart')?.value; const e = document.getElementById('periodEnd')?.value; if (s && e) { dashPage = 1; loadDashOrders(); } }); }); // Status pills document.querySelectorAll('.filter-pill[data-status]').forEach(btn => { btn.addEventListener('click', function () { document.querySelectorAll('.filter-pill[data-status]').forEach(b => b.classList.remove('active')); this.classList.add('active'); dashPage = 1; loadDashOrders(); }); }); // Search — 300ms debounce document.getElementById('orderSearch')?.addEventListener('input', () => { clearTimeout(dashSearchTimeout); dashSearchTimeout = setTimeout(() => { dashPage = 1; loadDashOrders(); }, 300); }); } // ── Dashboard Orders Table ──────────────────────── function dashSortBy(col) { if (dashSortCol === col) { dashSortDir = dashSortDir === 'asc' ? 'desc' : 'asc'; } else { dashSortCol = col; dashSortDir = 'asc'; } document.querySelectorAll('.sort-icon').forEach(span => { const c = span.dataset.col; span.textContent = c === dashSortCol ? (dashSortDir === 'asc' ? '\u2191' : '\u2193') : ''; }); dashPage = 1; loadDashOrders(); } async function loadDashOrders() { const periodVal = document.getElementById('periodSelect')?.value || '7'; const params = new URLSearchParams(); if (periodVal === 'custom') { const s = document.getElementById('periodStart')?.value; const e = document.getElementById('periodEnd')?.value; if (s && e) { params.set('period_start', s); params.set('period_end', e); params.set('period_days', '0'); } } else { params.set('period_days', periodVal); } const activeStatus = document.querySelector('.filter-pill.active')?.dataset.status; if (activeStatus && activeStatus !== 'all') params.set('status', activeStatus); const search = document.getElementById('orderSearch')?.value?.trim(); if (search) params.set('search', search); params.set('page', dashPage); params.set('per_page', dashPerPage); params.set('sort_by', dashSortCol); params.set('sort_dir', dashSortDir); try { const res = await fetch(`/api/dashboard/orders?${params}`); const data = await res.json(); // Update filter-pill badge counts const c = data.counts || {}; const el = (id) => document.getElementById(id); if (el('cntAll')) el('cntAll').textContent = c.total || 0; if (el('cntImp')) el('cntImp').textContent = c.imported_all || c.imported || 0; if (el('cntSkip')) el('cntSkip').textContent = c.skipped || 0; if (el('cntErr')) el('cntErr').textContent = c.error || c.errors || 0; if (el('cntFact')) el('cntFact').textContent = c.facturate || 0; if (el('cntNef')) el('cntNef').textContent = c.nefacturate || c.uninvoiced || 0; if (el('cntCanc')) el('cntCanc').textContent = c.cancelled || 0; const tbody = document.getElementById('dashOrdersBody'); const orders = data.orders || []; if (orders.length === 0) { tbody.innerHTML = 'Nicio comanda'; } else { tbody.innerHTML = orders.map(o => { const dateStr = fmtDate(o.order_date); const orderTotal = o.order_total != null ? Number(o.order_total).toFixed(2) : '-'; return ` ${statusDot(o.status)} ${dateStr} ${renderClientCell(o)} ${esc(o.order_number)} ${o.items_count || 0} ${fmtCost(o.delivery_cost)} ${fmtCost(o.discount_total)} ${orderTotal} ${invoiceDot(o)} `; }).join(''); } // Mobile flat rows const mobileList = document.getElementById('dashMobileList'); if (mobileList) { if (orders.length === 0) { mobileList.innerHTML = '
Nicio comanda
'; } else { mobileList.innerHTML = orders.map(o => { const d = o.order_date || ''; let dateFmt = '-'; if (d.length >= 10) { dateFmt = d.slice(8, 10) + '.' + d.slice(5, 7) + '.' + d.slice(2, 4); if (d.length >= 16) dateFmt += ' ' + d.slice(11, 16); } const name = o.customer_name || o.shipping_name || o.billing_name || '\u2014'; const totalStr = o.order_total ? Number(o.order_total).toFixed(2) : ''; return `
${statusDot(o.status)} ${dateFmt} ${esc(name)} x${o.items_count || 0}${totalStr ? ' · ' + totalStr + '' : ''}
`; }).join(''); } } // Mobile segmented control renderMobileSegmented('dashMobileSeg', [ { label: 'Toate', count: c.total || 0, value: 'all', active: (activeStatus || 'all') === 'all', colorClass: 'fc-neutral' }, { label: 'Imp.', count: c.imported_all || c.imported || 0, value: 'IMPORTED', active: activeStatus === 'IMPORTED', colorClass: 'fc-green' }, { label: 'Omise', count: c.skipped || 0, value: 'SKIPPED', active: activeStatus === 'SKIPPED', colorClass: 'fc-yellow' }, { label: 'Erori', count: c.error || c.errors || 0, value: 'ERROR', active: activeStatus === 'ERROR', colorClass: 'fc-red' }, { label: 'Fact.', count: c.facturate || 0, value: 'INVOICED', active: activeStatus === 'INVOICED', colorClass: 'fc-green' }, { label: 'Nefact.', count: c.nefacturate || c.uninvoiced || 0, value: 'UNINVOICED', active: activeStatus === 'UNINVOICED', colorClass: 'fc-red' }, { label: 'Anulate', count: c.cancelled || 0, value: 'CANCELLED', active: activeStatus === 'CANCELLED', colorClass: 'fc-dark' } ], (val) => { document.querySelectorAll('.filter-pill[data-status]').forEach(b => b.classList.remove('active')); const pill = document.querySelector(`.filter-pill[data-status="${val}"]`); if (pill) pill.classList.add('active'); dashPage = 1; loadDashOrders(); }); // Pagination const pag = data.pagination || {}; const totalPages = pag.total_pages || data.pages || 1; const totalOrders = (data.counts || {}).total || data.total || 0; const pagOpts = { perPage: dashPerPage, perPageFn: 'dashChangePerPage', perPageOptions: [25, 50, 100, 250] }; const pagHtml = `${totalOrders} comenzi | Pagina ${dashPage} din ${totalPages}` + renderUnifiedPagination(dashPage, totalPages, 'dashGoPage', pagOpts); const pagDiv = document.getElementById('dashPagination'); if (pagDiv) pagDiv.innerHTML = pagHtml; const pagDivTop = document.getElementById('dashPaginationTop'); if (pagDivTop) pagDivTop.innerHTML = pagHtml; // Update sort icons document.querySelectorAll('.sort-icon').forEach(span => { const c = span.dataset.col; span.textContent = c === dashSortCol ? (dashSortDir === 'asc' ? '\u2191' : '\u2193') : ''; }); } catch (err) { document.getElementById('dashOrdersBody').innerHTML = `${esc(err.message)}`; } } function dashGoPage(p) { dashPage = p; loadDashOrders(); } function dashChangePerPage(val) { dashPerPage = parseInt(val) || 50; dashPage = 1; loadDashOrders(); } // ── Client cell with Cont tooltip (Task F4) ─────── function renderClientCell(order) { const display = (order.customer_name || order.shipping_name || '').trim(); const billing = (order.billing_name || '').trim(); const shipping = (order.shipping_name || '').trim(); const isDiff = display !== shipping && shipping; if (isDiff) { return `${escHtml(display)} `; } return `${escHtml(display || billing || '\u2014')}`; } // ── Helper functions ────────────────────────────── async function fetchJSON(url) { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } function escHtml(s) { if (s == null) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function statusLabelText(status) { switch ((status || '').toUpperCase()) { case 'IMPORTED': return 'Importat'; case 'ALREADY_IMPORTED': return 'Deja imp.'; case 'SKIPPED': return 'Omis'; case 'ERROR': return 'Eroare'; default: return esc(status); } } function invoiceDot(order) { if (order.status !== 'IMPORTED' && order.status !== 'ALREADY_IMPORTED') return '–'; if (order.invoice && order.invoice.facturat) return ''; return ''; } // ── Refresh Invoices ────────────────────────────── async function refreshInvoices() { const btn = document.getElementById('btnRefreshInvoices'); const btnM = document.getElementById('btnRefreshInvoicesMobile'); if (btn) { btn.disabled = true; btn.textContent = '⟳ Se verifica...'; } if (btnM) { btnM.disabled = true; } try { const res = await fetch('/api/dashboard/refresh-invoices', { method: 'POST' }); const data = await res.json(); if (data.error) { alert('Eroare: ' + data.error); } else { loadDashOrders(); } } catch (err) { alert('Eroare: ' + err.message); } finally { if (btn) { btn.disabled = false; btn.textContent = '↻ Facturi'; } if (btnM) { btnM.disabled = false; } } } // ── Order Detail Modal ──────────────────────────── function openDashOrderDetail(orderNumber) { _sharedModalQuickMapFn = openDashQuickMap; renderOrderDetailModal(orderNumber, { onQuickMap: openDashQuickMap, onAfterRender: function() { /* nothing extra needed */ } }); } // ── Quick Map Modal (uses shared openQuickMap) ─── function openDashQuickMap(sku, productName, orderNumber, itemIdx) { const item = (window._detailItems || [])[itemIdx]; const details = item?.codmat_details; const isDirect = details?.length === 1 && details[0].direct === true; openQuickMap({ sku, productName, isDirect, directInfo: isDirect ? { codmat: details[0].codmat, denumire: details[0].denumire } : null, prefill: (!isDirect && details?.length) ? details.map(d => ({ codmat: d.codmat, cantitate: d.cantitate_roa, denumire: d.denumire })) : null, onSave: () => { if (orderNumber) openDashOrderDetail(orderNumber); loadDashOrders(); } }); }