// ── State ───────────────────────────────────────── let dashPage = 1; let dashPerPage = 50; let dashSortCol = 'order_date'; let dashSortDir = 'desc'; let dashSearchTimeout = null; let currentQmSku = ''; let currentQmOrderNumber = ''; let qmAcTimeout = null; // Sync polling state let _pollInterval = null; let _lastSyncStatus = null; let _lastRunId = null; let _currentRunId = null; // ── Init ────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { loadSchedulerStatus(); loadDashOrders(); startSyncPolling(); wireFilterBar(); }); // ── Smart Sync Polling ──────────────────────────── function startSyncPolling() { if (_pollInterval) clearInterval(_pollInterval); _pollInterval = setInterval(pollSyncStatus, 30000); 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'; 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, 30000); 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; 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' } ], (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, '''); } // Alias kept for backward compat with inline handlers in modal function esc(s) { return escHtml(s); } function fmtCost(v) { return v > 0 ? Number(v).toFixed(2) : '–'; } 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 orderStatusBadge(status) { switch ((status || '').toUpperCase()) { case 'IMPORTED': return 'Importat'; case 'ALREADY_IMPORTED': return 'Deja importat'; case 'SKIPPED': return 'Omis'; case 'ERROR': return 'Eroare'; case 'DELETED_IN_ROA': return 'Sters din ROA'; default: return `${esc(status)}`; } } function invoiceDot(order) { if (order.status !== 'IMPORTED' && order.status !== 'ALREADY_IMPORTED') return '–'; if (order.invoice && order.invoice.facturat) return ''; return ''; } function renderCodmatCell(item) { if (!item.codmat_details || item.codmat_details.length === 0) { return `${esc(item.codmat || '-')}`; } if (item.codmat_details.length === 1) { const d = item.codmat_details[0]; return `${esc(d.codmat)}`; } return item.codmat_details.map(d => `
${esc(d.codmat)} \xd7${d.cantitate_roa} (${d.procent_pret}%)
` ).join(''); } // ── 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 ──────────────────────────── async function openDashOrderDetail(orderNumber) { document.getElementById('detailOrderNumber').textContent = '#' + orderNumber; document.getElementById('detailCustomer').textContent = '...'; document.getElementById('detailDate').textContent = ''; document.getElementById('detailStatus').innerHTML = ''; document.getElementById('detailIdComanda').textContent = '-'; document.getElementById('detailIdPartener').textContent = '-'; document.getElementById('detailIdAdresaFact').textContent = '-'; document.getElementById('detailIdAdresaLivr').textContent = '-'; document.getElementById('detailItemsBody').innerHTML = 'Se incarca...'; document.getElementById('detailError').style.display = 'none'; const invInfo = document.getElementById('detailInvoiceInfo'); if (invInfo) invInfo.style.display = 'none'; const detailItemsTotal = document.getElementById('detailItemsTotal'); if (detailItemsTotal) detailItemsTotal.textContent = '-'; const detailOrderTotal = document.getElementById('detailOrderTotal'); if (detailOrderTotal) detailOrderTotal.textContent = '-'; const mobileContainer = document.getElementById('detailItemsMobile'); if (mobileContainer) mobileContainer.innerHTML = ''; const modalEl = document.getElementById('orderDetailModal'); const existing = bootstrap.Modal.getInstance(modalEl); if (existing) { existing.show(); } else { new bootstrap.Modal(modalEl).show(); } try { const res = await fetch(`/api/sync/order/${encodeURIComponent(orderNumber)}`); const data = await res.json(); if (data.error) { document.getElementById('detailError').textContent = data.error; document.getElementById('detailError').style.display = ''; return; } const order = data.order || {}; document.getElementById('detailCustomer').textContent = order.customer_name || '-'; document.getElementById('detailDate').textContent = fmtDate(order.order_date); document.getElementById('detailStatus').innerHTML = orderStatusBadge(order.status); document.getElementById('detailIdComanda').textContent = order.id_comanda || '-'; document.getElementById('detailIdPartener').textContent = order.id_partener || '-'; document.getElementById('detailIdAdresaFact').textContent = order.id_adresa_facturare || '-'; document.getElementById('detailIdAdresaLivr').textContent = order.id_adresa_livrare || '-'; // Invoice info const invInfo = document.getElementById('detailInvoiceInfo'); const inv = order.invoice; if (inv && inv.facturat) { const serie = inv.serie_act || ''; const numar = inv.numar_act || ''; document.getElementById('detailInvoiceNumber').textContent = serie ? `${serie} ${numar}` : numar; document.getElementById('detailInvoiceDate').textContent = inv.data_act ? fmtDate(inv.data_act) : '-'; if (invInfo) invInfo.style.display = ''; } else { if (invInfo) invInfo.style.display = 'none'; } if (order.error_message) { document.getElementById('detailError').textContent = order.error_message; document.getElementById('detailError').style.display = ''; } const dlvEl = document.getElementById('detailDeliveryCost'); if (dlvEl) dlvEl.textContent = order.delivery_cost > 0 ? Number(order.delivery_cost).toFixed(2) + ' lei' : '–'; const dscEl = document.getElementById('detailDiscount'); if (dscEl) dscEl.textContent = order.discount_total > 0 ? '–' + Number(order.discount_total).toFixed(2) + ' lei' : '–'; const items = data.items || []; if (items.length === 0) { document.getElementById('detailItemsBody').innerHTML = 'Niciun articol'; return; } // Update totals row const itemsTotal = items.reduce((sum, item) => sum + (Number(item.price || 0) * Number(item.quantity || 0)), 0); document.getElementById('detailItemsTotal').textContent = itemsTotal.toFixed(2) + ' lei'; document.getElementById('detailOrderTotal').textContent = order.order_total != null ? Number(order.order_total).toFixed(2) + ' lei' : '-'; // Store items for quick map pre-population window._detailItems = items; // Mobile article flat list const mobileContainer = document.getElementById('detailItemsMobile'); if (mobileContainer) { mobileContainer.innerHTML = '
' + items.map((item, idx) => { const codmatText = item.codmat_details?.length ? item.codmat_details.map(d => `${esc(d.codmat)}`).join(' ') : `${esc(item.codmat || '–')}`; const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2); return `
${esc(item.sku)} ${codmatText}
${esc(item.product_name || '–')} x${item.quantity || 0} ${valoare} lei
`; }).join('') + '
'; } document.getElementById('detailItemsBody').innerHTML = items.map((item, idx) => { const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2); return ` ${esc(item.sku)} ${esc(item.product_name || '-')} ${renderCodmatCell(item)} ${item.quantity || 0} ${item.price != null ? Number(item.price).toFixed(2) : '-'} ${valoare} `; }).join(''); } catch (err) { document.getElementById('detailError').textContent = err.message; document.getElementById('detailError').style.display = ''; } } // ── Quick Map Modal ─────────────────────────────── function openQuickMap(sku, productName, orderNumber, itemIdx) { currentQmSku = sku; currentQmOrderNumber = orderNumber; document.getElementById('qmSku').textContent = sku; document.getElementById('qmProductName').textContent = productName || '-'; document.getElementById('qmPctWarning').style.display = 'none'; const container = document.getElementById('qmCodmatLines'); container.innerHTML = ''; // Pre-populate with existing codmat_details if available const item = (window._detailItems || [])[itemIdx]; const details = item?.codmat_details; if (details && details.length > 0) { details.forEach(d => { addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate_roa, procent: d.procent_pret, denumire: d.denumire }); }); } else { addQmCodmatLine(); } new bootstrap.Modal(document.getElementById('quickMapModal')).show(); } function addQmCodmatLine(prefill) { const container = document.getElementById('qmCodmatLines'); const idx = container.children.length; const codmatVal = prefill?.codmat || ''; const cantVal = prefill?.cantitate || 1; const pctVal = prefill?.procent || 100; const denumireVal = prefill?.denumire || ''; const div = document.createElement('div'); div.className = 'qm-line'; div.innerHTML = `
${idx > 0 ? `` : ''}
${escHtml(denumireVal)}
`; container.appendChild(div); const input = div.querySelector('.qm-codmat'); const dropdown = div.querySelector('.qm-ac-dropdown'); const selected = div.querySelector('.qm-selected'); input.addEventListener('input', () => { clearTimeout(qmAcTimeout); qmAcTimeout = setTimeout(() => qmAutocomplete(input, dropdown, selected), 250); }); input.addEventListener('blur', () => { setTimeout(() => dropdown.classList.add('d-none'), 200); }); } async function qmAutocomplete(input, dropdown, selectedEl) { const q = input.value; 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)}${r.um ? ` (${esc(r.um)})` : ''}
` ).join(''); dropdown.classList.remove('d-none'); } catch { dropdown.classList.add('d-none'); } } function qmSelectArticle(el, codmat, label) { const line = el.closest('.qm-line'); line.querySelector('.qm-codmat').value = codmat; line.querySelector('.qm-selected').textContent = label; line.querySelector('.qm-ac-dropdown').classList.add('d-none'); } async function saveQuickMapping() { const lines = document.querySelectorAll('.qm-line'); const mappings = []; for (const line of lines) { const codmat = line.querySelector('.qm-codmat').value.trim(); const cantitate = parseFloat(line.querySelector('.qm-cantitate').value) || 1; const procent = parseFloat(line.querySelector('.qm-procent').value) || 100; if (!codmat) continue; mappings.push({ codmat, cantitate_roa: cantitate, procent_pret: procent }); } if (mappings.length === 0) { alert('Selecteaza cel putin un CODMAT'); return; } if (mappings.length > 1) { const totalPct = mappings.reduce((s, m) => s + m.procent_pret, 0); if (Math.abs(totalPct - 100) > 0.01) { document.getElementById('qmPctWarning').textContent = `Suma procentelor trebuie sa fie 100% (actual: ${totalPct.toFixed(2)}%)`; document.getElementById('qmPctWarning').style.display = ''; return; } } document.getElementById('qmPctWarning').style.display = 'none'; try { let res; if (mappings.length === 1) { res = await fetch('/api/mappings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sku: currentQmSku, codmat: mappings[0].codmat, cantitate_roa: mappings[0].cantitate_roa, procent_pret: mappings[0].procent_pret }) }); } else { res = await fetch('/api/mappings/batch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sku: currentQmSku, mappings }) }); } const data = await res.json(); if (data.success) { bootstrap.Modal.getInstance(document.getElementById('quickMapModal')).hide(); if (currentQmOrderNumber) openDashOrderDetail(currentQmOrderNumber); loadDashOrders(); } else { alert('Eroare: ' + (data.error || 'Unknown')); } } catch (err) { alert('Eroare: ' + err.message); } }