// ── 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' ? 'var(--success)' : 'var(--error)'; } } } // 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 = '
${esc(o.order_number)}