let refreshInterval = null; let dashPage = 1; let dashFilter = 'all'; let dashSearch = ''; let dashSortCol = 'order_date'; let dashSortDir = 'desc'; let dashSearchTimeout = null; let dashPeriodDays = 7; let currentQmSku = ''; let currentQmOrderNumber = ''; let qmAcTimeout = null; let syncEventSource = null; document.addEventListener('DOMContentLoaded', () => { loadSchedulerStatus(); loadSyncStatus(); loadLastSync(); loadDashOrders(); refreshInterval = setInterval(() => { loadSyncStatus(); }, 10000); }); // ── Sync Status ────────────────────────────────── async function loadSyncStatus() { try { const res = await fetch('/api/sync/status'); const data = await res.json(); 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'); 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'); const stats = data.stats || {}; 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} nemapate, ${lr.errors || 0} erori`; } else { document.getElementById('syncProgressText').textContent = ''; } } } catch (err) { console.error('loadSyncStatus error:', err); } } // ── Last Sync Summary Card ─────────────────────── async function loadLastSync() { try { const res = await fetch('/api/sync/history?per_page=1'); const data = await res.json(); const runs = data.runs || []; if (runs.length === 0) { document.getElementById('lastSyncDate').textContent = '-'; return; } const r = runs[0]; document.getElementById('lastSyncDate').textContent = r.started_at ? new Date(r.started_at).toLocaleString('ro-RO', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) : '-'; const statusClass = r.status === 'completed' ? 'bg-success' : r.status === 'running' ? 'bg-primary' : 'bg-danger'; document.getElementById('lastSyncStatus').innerHTML = `${esc(r.status)}`; document.getElementById('lastSyncImported').textContent = r.imported || 0; document.getElementById('lastSyncSkipped').textContent = r.skipped || 0; document.getElementById('lastSyncErrors').textContent = r.errors || 0; if (r.started_at && r.finished_at) { const sec = Math.round((new Date(r.finished_at) - new Date(r.started_at)) / 1000); document.getElementById('lastSyncDuration').textContent = sec < 60 ? `${sec}s` : `${Math.floor(sec/60)}m ${sec%60}s`; } else { document.getElementById('lastSyncDuration').textContent = '-'; } } catch (err) { console.error('loadLastSync error:', err); } } // ── Dashboard Orders Table ─────────────────────── function debounceDashSearch() { clearTimeout(dashSearchTimeout); dashSearchTimeout = setTimeout(() => { dashSearch = document.getElementById('dashSearchInput').value; dashPage = 1; loadDashOrders(); }, 300); } function dashFilterOrders(filter) { dashFilter = filter; dashPage = 1; // Update button styles const colorMap = { 'all': 'primary', 'IMPORTED': 'success', 'SKIPPED': 'warning', 'ERROR': 'danger', 'UNINVOICED': 'info' }; document.querySelectorAll('#dashFilterBtns button').forEach(btn => { const text = btn.textContent.trim().split(' ')[0]; let btnFilter = 'all'; if (text === 'Importate') btnFilter = 'IMPORTED'; else if (text === 'Omise') btnFilter = 'SKIPPED'; else if (text === 'Erori') btnFilter = 'ERROR'; else if (text === 'Nefacturate') btnFilter = 'UNINVOICED'; const color = colorMap[btnFilter] || 'primary'; if (btnFilter === filter) { btn.className = `btn btn-sm btn-${color}`; } else { btn.className = `btn btn-sm btn-outline-${color}`; } }); loadDashOrders(); } function dashSortBy(col) { if (dashSortCol === col) { dashSortDir = dashSortDir === 'asc' ? 'desc' : 'asc'; } else { dashSortCol = col; dashSortDir = 'asc'; } // Update sort icons document.querySelectorAll('#dashOrdersBody').forEach(() => {}); // noop document.querySelectorAll('.sort-icon').forEach(span => { const c = span.dataset.col; span.textContent = c === dashSortCol ? (dashSortDir === 'asc' ? '\u2191' : '\u2193') : ''; }); dashPage = 1; loadDashOrders(); } function dashSetPeriod(days) { dashPeriodDays = days; dashPage = 1; document.querySelectorAll('#dashPeriodBtns button').forEach(btn => { const val = parseInt(btn.dataset.days); btn.className = val === days ? 'btn btn-sm btn-secondary' : 'btn btn-sm btn-outline-secondary'; }); loadDashOrders(); } async function loadDashOrders() { const params = new URLSearchParams({ page: dashPage, per_page: 50, search: dashSearch, status: dashFilter, sort_by: dashSortCol, sort_dir: dashSortDir, period_days: dashPeriodDays }); try { const res = await fetch(`/api/dashboard/orders?${params}`); const data = await res.json(); const counts = data.counts || {}; document.getElementById('dashCountAll').textContent = counts.total || 0; document.getElementById('dashCountImported').textContent = counts.imported || 0; document.getElementById('dashCountSkipped').textContent = counts.skipped || 0; document.getElementById('dashCountError').textContent = counts.error || 0; document.getElementById('dashCountUninvoiced').textContent = counts.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 statusBadge = orderStatusBadge(o.status); // Invoice info let invoiceBadge = ''; let invoiceTotal = ''; if (o.status !== 'IMPORTED') { invoiceBadge = '-'; } else if (o.invoice && o.invoice.facturat) { invoiceBadge = `Facturat`; if (o.invoice.serie_act || o.invoice.numar_act) { invoiceBadge += `
${esc(o.invoice.serie_act || '')} ${esc(String(o.invoice.numar_act || ''))}`; } invoiceTotal = o.invoice.total_cu_tva ? Number(o.invoice.total_cu_tva).toFixed(2) : '-'; } else { invoiceBadge = 'Nefacturat'; } return ` ${esc(o.order_number)} ${dateStr} ${esc(o.customer_name)} ${o.items_count || 0} ${statusBadge} ${o.id_comanda || '-'} ${invoiceBadge} ${invoiceTotal} `; }).join(''); } // Pagination const totalPages = data.pages || 1; document.getElementById('dashPageInfo').textContent = `${data.total || 0} comenzi | Pagina ${dashPage} din ${totalPages}`; const pagDiv = document.getElementById('dashPagination'); if (totalPages > 1) { pagDiv.innerHTML = ` ${dashPage} / ${totalPages} `; } else { pagDiv.innerHTML = ''; } // 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(); } // ── Helper functions ───────────────────────────── function fmtDate(dateStr) { if (!dateStr) return '-'; try { const d = new Date(dateStr); const hasTime = dateStr.includes(':'); if (hasTime) { return d.toLocaleString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } return d.toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' }); } catch { return dateStr; } } function orderStatusBadge(status) { switch ((status || '').toUpperCase()) { case 'IMPORTED': return 'Importat'; case 'SKIPPED': return 'Omis'; case 'ERROR': return 'Eroare'; default: return `${esc(status)}`; } } 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(''); } // ── 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 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 || '-'; if (order.error_message) { document.getElementById('detailError').textContent = order.error_message; document.getElementById('detailError').style.display = ''; } const items = data.items || []; if (items.length === 0) { document.getElementById('detailItemsBody').innerHTML = 'Niciun articol'; return; } document.getElementById('detailItemsBody').innerHTML = items.map(item => { let statusBadge; switch (item.mapping_status) { case 'mapped': statusBadge = 'Mapat'; break; case 'direct': statusBadge = 'Direct'; break; case 'missing': statusBadge = 'Lipsa'; break; default: statusBadge = '?'; } const action = item.mapping_status === 'missing' ? `` : ''; return ` ${esc(item.sku)} ${esc(item.product_name || '-')} ${item.quantity || 0} ${item.price != null ? Number(item.price).toFixed(2) : '-'} ${item.vat != null ? Number(item.vat).toFixed(2) : '-'} ${renderCodmatCell(item)} ${statusBadge} ${action} `; }).join(''); } catch (err) { document.getElementById('detailError').textContent = err.message; document.getElementById('detailError').style.display = ''; } } // ── Quick Map Modal ────────────────────────────── function openQuickMap(sku, productName, orderNumber) { 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 = ''; addQmCodmatLine(); new bootstrap.Modal(document.getElementById('quickMapModal')).show(); } function addQmCodmatLine() { const container = document.getElementById('qmCodmatLines'); const idx = container.children.length; const div = document.createElement('div'); div.className = 'border rounded p-2 mb-2 qm-line'; div.innerHTML = `
${idx > 0 ? `` : ''}
`; 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); } } // ── 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; } 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'); } // Subscribe to SSE for live progress + auto-refresh on completion listenToSyncStream(data.run_id); } loadSyncStatus(); } catch (err) { alert('Eroare: ' + err.message); } } function listenToSyncStream(runId) { // Close any previous SSE connection if (syncEventSource) { syncEventSource.close(); syncEventSource = null; } syncEventSource = new EventSource('/api/sync/stream'); syncEventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'phase') { document.getElementById('syncProgressText').textContent = data.message || ''; } if (data.type === 'order_result') { // Update progress text with current order info const status = data.status === 'IMPORTED' ? 'OK' : data.status === 'SKIPPED' ? 'OMIS' : 'ERR'; document.getElementById('syncProgressText').textContent = `[${data.progress || ''}] #${data.order_number} ${data.customer_name || ''} → ${status}`; } if (data.type === 'completed' || data.type === 'failed') { syncEventSource.close(); syncEventSource = null; // Refresh all dashboard sections loadLastSync(); loadDashOrders(); loadSyncStatus(); // Hide banner after 5s setTimeout(() => { document.getElementById('syncStartedBanner')?.classList.add('d-none'); }, 5000); } } catch (e) { console.error('SSE parse error:', e); } }; syncEventSource.onerror = () => { syncEventSource.close(); syncEventSource = null; // Refresh anyway — sync may have finished loadLastSync(); loadDashOrders(); loadSyncStatus(); }; } async function stopSync() { try { await fetch('/api/sync/stop', { method: 'POST' }); loadSyncStatus(); } catch (err) { alert('Eroare: ' + err.message); } } 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(); } } 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); } } function esc(s) { if (s == null) return ''; return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); }