let refreshInterval = null; let currentMapSku = ''; let acTimeout = null; document.addEventListener('DOMContentLoaded', () => { loadDashboard(); // Auto-refresh every 10 seconds refreshInterval = setInterval(loadDashboard, 10000); const input = document.getElementById('mapCodmat'); if (input) { input.addEventListener('input', () => { clearTimeout(acTimeout); acTimeout = setTimeout(() => autocompleteMap(input.value), 250); }); input.addEventListener('blur', () => { setTimeout(() => document.getElementById('mapAutocomplete').classList.add('d-none'), 200); }); } }); async function loadDashboard() { await Promise.all([ loadSyncStatus(), loadSyncHistory(), loadMissingSkus(), loadSchedulerStatus() ]); } async function loadSyncStatus() { try { const res = await fetch('/api/sync/status'); const data = await res.json(); const stats = data.stats || {}; // Order-level stat cards from sync status document.getElementById('stat-imported').textContent = stats.imported != null ? stats.imported : 0; document.getElementById('stat-skipped').textContent = stats.skipped != null ? stats.skipped : 0; document.getElementById('stat-errors').textContent = stats.errors != null ? stats.errors : 0; // Article-level stats from sync status if (stats.total_tracked_skus != null) { document.getElementById('stat-total-skus').textContent = stats.total_tracked_skus; } if (stats.unresolved_skus != null) { document.getElementById('stat-missing-skus').textContent = stats.unresolved_skus; const total = stats.total_tracked_skus || 0; const unresolved = stats.unresolved_skus || 0; document.getElementById('stat-mapped-skus').textContent = total - unresolved; } // Restore scan-derived stats from sessionStorage (preserved across auto-refresh) const scanData = getScanData(); if (scanData) { document.getElementById('stat-new').textContent = scanData.new_orders != null ? scanData.new_orders : (scanData.total_orders || '-'); document.getElementById('stat-ready').textContent = scanData.importable != null ? scanData.importable : '-'; if (scanData.skus) { document.getElementById('stat-total-skus').textContent = scanData.skus.total_skus || stats.total_tracked_skus || '-'; document.getElementById('stat-missing-skus').textContent = scanData.skus.missing || stats.unresolved_skus || 0; const mapped = (scanData.skus.total_skus || 0) - (scanData.skus.missing || 0); document.getElementById('stat-mapped-skus').textContent = mapped >= 0 ? mapped : '-'; } } // Update sync status badge const badge = document.getElementById('syncStatusBadge'); const status = data.status || 'idle'; badge.textContent = status; badge.className = 'badge ' + (status === 'running' ? 'bg-primary' : status === 'failed' ? 'bg-danger' : 'bg-secondary'); // Show/hide start/stop buttons if (status === 'running') { document.getElementById('btnStartSync').classList.add('d-none'); document.getElementById('btnStopSync').classList.remove('d-none'); document.getElementById('syncProgressText').textContent = data.progress || 'Running...'; } else { document.getElementById('btnStartSync').classList.remove('d-none'); document.getElementById('btnStopSync').classList.add('d-none'); // Show last run info if (stats.last_run) { const lr = stats.last_run; const started = lr.started_at ? new Date(lr.started_at).toLocaleString('ro-RO') : ''; document.getElementById('syncProgressText').textContent = `Ultimul: ${started} | ${lr.imported || 0} ok, ${lr.skipped || 0} fara mapare, ${lr.errors || 0} erori`; } else { document.getElementById('syncProgressText').textContent = ''; } } } catch (err) { console.error('loadSyncStatus error:', err); } } async function loadSyncHistory() { try { const res = await fetch('/api/sync/history?per_page=10'); const data = await res.json(); const tbody = document.getElementById('syncRunsBody'); if (!data.runs || data.runs.length === 0) { tbody.innerHTML = 'Niciun sync run'; return; } tbody.innerHTML = data.runs.map(r => { const started = r.started_at ? new Date(r.started_at).toLocaleString('ro-RO', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) : '-'; let duration = '-'; if (r.started_at && r.finished_at) { const sec = Math.round((new Date(r.finished_at) - new Date(r.started_at)) / 1000); duration = sec < 60 ? `${sec}s` : `${Math.floor(sec/60)}m ${sec%60}s`; } const statusClass = r.status === 'completed' ? 'bg-success' : r.status === 'running' ? 'bg-primary' : 'bg-danger'; return ` ${started} ${esc(r.status)} ${r.total_orders || 0} ${r.imported || 0} ${r.skipped || 0} ${r.errors || 0} ${duration} `; }).join(''); } catch (err) { console.error('loadSyncHistory error:', err); } } async function loadMissingSkus() { try { const res = await fetch('/api/validate/missing-skus?page=1&per_page=10'); const data = await res.json(); const tbody = document.getElementById('missingSkusBody'); // Update article-level stat card (unresolved count) if (data.total != null) { document.getElementById('stat-missing-skus').textContent = data.total; } const unresolved = (data.missing_skus || []).filter(s => !s.resolved); if (unresolved.length === 0) { tbody.innerHTML = 'Toate SKU-urile sunt mapate'; return; } tbody.innerHTML = unresolved.slice(0, 10).map(s => { let firstCustomer = '-'; try { const customers = JSON.parse(s.customers || '[]'); if (customers.length > 0) firstCustomer = customers[0]; } catch (e) { /* ignore */ } return ` ${esc(s.sku)} ${esc(s.product_name || '-')} ${s.order_count != null ? s.order_count : '-'} ${esc(firstCustomer)} `; }).join(''); } catch (err) { console.error('loadMissingSkus error:', err); } } 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); } } 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; } // Show banner with link to live logs if (data.run_id) { const banner = document.getElementById('syncStartedBanner'); const link = document.getElementById('syncRunLink'); if (banner && link) { link.href = '/logs?run=' + encodeURIComponent(data.run_id); banner.classList.remove('d-none'); } } loadDashboard(); } catch (err) { alert('Eroare: ' + err.message); } } async function stopSync() { try { await fetch('/api/sync/stop', { method: 'POST' }); loadDashboard(); } catch (err) { alert('Eroare: ' + err.message); } } async function scanOrders() { const btn = document.getElementById('btnScan'); btn.disabled = true; btn.innerHTML = ' Scanning...'; try { const res = await fetch('/api/validate/scan', { method: 'POST' }); const data = await res.json(); // Persist scan results so auto-refresh doesn't overwrite them saveScanData(data); // Update stat cards immediately from scan response document.getElementById('stat-new').textContent = data.new_orders != null ? data.new_orders : (data.total_orders || 0); document.getElementById('stat-ready').textContent = data.importable != null ? data.importable : 0; if (data.skus) { document.getElementById('stat-total-skus').textContent = data.skus.total_skus || 0; document.getElementById('stat-missing-skus').textContent = data.skus.missing || 0; const mapped = (data.skus.total_skus || 0) - (data.skus.missing || 0); document.getElementById('stat-mapped-skus').textContent = mapped >= 0 ? mapped : 0; } let msg = `Scan complet: ${data.total_orders || 0} comenzi`; if (data.new_orders != null) msg += `, ${data.new_orders} noi`; msg += `, ${data.importable || 0} ready`; if (data.skus && data.skus.missing > 0) { msg += `, ${data.skus.missing} SKU-uri lipsa`; } alert(msg); loadDashboard(); } catch (err) { alert('Eroare scan: ' + err.message); } finally { btn.disabled = false; btn.innerHTML = ' Scan'; } } async function toggleScheduler() { const enabled = document.getElementById('schedulerToggle').checked; const interval = parseInt(document.getElementById('schedulerInterval').value) || 5; 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(); } } // --- 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'); if (productName) { autocompleteMap(productName); } new bootstrap.Modal(document.getElementById('mapModal')).show(); } async function autocompleteMap(q) { const dropdown = document.getElementById('mapAutocomplete'); if (!dropdown) return; 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; } dropdown.innerHTML = data.results.map(r => `
${esc(r.codmat)}
${esc(r.denumire)}
`).join(''); dropdown.classList.remove('d-none'); } catch (err) { 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'); } 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; if (!codmat) { alert('Selecteaza un CODMAT'); return; } 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 }) }); const data = await res.json(); if (data.success) { bootstrap.Modal.getInstance(document.getElementById('mapModal')).hide(); loadMissingSkus(); } else { alert('Eroare: ' + (data.error || 'Unknown')); } } catch (err) { alert('Eroare: ' + err.message); } } // --- sessionStorage helpers for scan data --- function saveScanData(data) { try { sessionStorage.setItem('lastScanData', JSON.stringify(data)); sessionStorage.setItem('lastScanTime', Date.now().toString()); } catch (e) { /* ignore */ } } function getScanData() { try { const t = parseInt(sessionStorage.getItem('lastScanTime') || '0'); // Expire scan data after 5 minutes if (Date.now() - t > 5 * 60 * 1000) return null; const raw = sessionStorage.getItem('lastScanData'); return raw ? JSON.parse(raw) : null; } catch (e) { return null; } } function esc(s) { if (s == null) return ''; return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); }