// logs.js - Structured order viewer with text log fallback let currentRunId = null; let runsPage = 1; let logPollTimer = null; let currentFilter = 'all'; let ordersPage = 1; let ordersSortColumn = 'order_date'; let ordersSortDirection = 'desc'; function fmtCost(v) { return v > 0 ? Number(v).toFixed(2) : '–'; } function fmtDuration(startedAt, finishedAt) { if (!startedAt || !finishedAt) return '-'; const diffMs = new Date(finishedAt) - new Date(startedAt); if (isNaN(diffMs) || diffMs < 0) return '-'; const secs = Math.round(diffMs / 1000); if (secs < 60) return secs + 's'; return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's'; } function runStatusBadge(status) { switch ((status || '').toLowerCase()) { case 'completed': return 'completed'; case 'running': return 'running'; case 'failed': return 'failed'; 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 logStatusText(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 logsGoPage(p) { loadRunOrders(currentRunId, null, p); } // ── Runs Dropdown ──────────────────────────────── async function loadRuns() { // Load all recent runs for dropdown try { const res = await fetch(`/api/sync/history?page=1&per_page=100`); if (!res.ok) throw new Error('HTTP ' + res.status); const data = await res.json(); const runs = data.runs || []; const dd = document.getElementById('runsDropdown'); if (runs.length === 0) { dd.innerHTML = ''; } else { dd.innerHTML = '' + runs.map(r => { const started = r.started_at ? new Date(r.started_at).toLocaleString('ro-RO', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : '?'; const st = (r.status || '').toUpperCase(); const statusEmoji = st === 'COMPLETED' ? '✓' : st === 'RUNNING' ? '⟳' : '✗'; const newImp = r.new_imported || 0; const already = r.already_imported || 0; const imp = r.imported || 0; const skip = r.skipped || 0; const err = r.errors || 0; const impLabel = already > 0 ? `${newImp} noi, ${already} deja` : `${imp} imp`; const label = `${started} — ${statusEmoji} ${r.status} (${impLabel}, ${skip} skip, ${err} err)`; const selected = r.run_id === currentRunId ? 'selected' : ''; return ``; }).join(''); } const ddMobile = document.getElementById('runsDropdownMobile'); if (ddMobile) ddMobile.innerHTML = dd.innerHTML; } catch (err) { const dd = document.getElementById('runsDropdown'); dd.innerHTML = ``; } } // ── Run Selection ──────────────────────────────── async function selectRun(runId) { if (logPollTimer) { clearInterval(logPollTimer); logPollTimer = null; } currentRunId = runId; currentFilter = 'all'; ordersPage = 1; const url = new URL(window.location); if (runId) { url.searchParams.set('run', runId); } else { url.searchParams.delete('run'); } history.replaceState(null, '', url); // Sync dropdown selection const dd = document.getElementById('runsDropdown'); if (dd && dd.value !== runId) dd.value = runId; const ddMobile = document.getElementById('runsDropdownMobile'); if (ddMobile && ddMobile.value !== runId) ddMobile.value = runId; if (!runId) { document.getElementById('logViewerSection').style.display = 'none'; return; } document.getElementById('logViewerSection').style.display = ''; const logRunIdEl = document.getElementById('logRunId'); if (logRunIdEl) logRunIdEl.textContent = runId; document.getElementById('logStatusBadge').innerHTML = '...'; document.getElementById('textLogSection').style.display = 'none'; await loadRunOrders(runId, 'all', 1); // Also load text log in background fetchTextLog(runId); } // ── Per-Order Filtering (R1) ───────────────────── async function loadRunOrders(runId, statusFilter, page) { if (statusFilter != null) currentFilter = statusFilter; if (page != null) ordersPage = page; // Update filter pill active state document.querySelectorAll('#orderFilterPills .filter-pill').forEach(btn => { btn.classList.toggle('active', btn.dataset.logStatus === currentFilter); }); try { const res = await fetch(`/api/sync/run/${encodeURIComponent(runId)}/orders?status=${currentFilter}&page=${ordersPage}&per_page=50&sort_by=${ordersSortColumn}&sort_dir=${ordersSortDirection}`); if (!res.ok) throw new Error('HTTP ' + res.status); const data = await res.json(); const counts = data.counts || {}; document.getElementById('countAll').textContent = counts.total || 0; document.getElementById('countImported').textContent = counts.imported || 0; document.getElementById('countSkipped').textContent = counts.skipped || 0; document.getElementById('countError').textContent = counts.error || 0; const alreadyEl = document.getElementById('countAlreadyImported'); if (alreadyEl) alreadyEl.textContent = counts.already_imported || 0; const tbody = document.getElementById('runOrdersBody'); const orders = data.orders || []; if (orders.length === 0) { tbody.innerHTML = 'Nicio comanda'; } else { tbody.innerHTML = orders.map((o, i) => { const dateStr = fmtDate(o.order_date); const orderTotal = o.order_total != null ? Number(o.order_total).toFixed(2) : '-'; return ` ${statusDot(o.status)} ${(ordersPage - 1) * 50 + i + 1} ${dateStr} ${esc(o.order_number)} ${esc(o.customer_name)} ${o.items_count || 0} ${fmtCost(o.delivery_cost)} ${fmtCost(o.discount_total)} ${orderTotal} `; }).join(''); } // Mobile flat rows const mobileList = document.getElementById('logsMobileList'); 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 totalStr = o.order_total ? Number(o.order_total).toFixed(2) : ''; return `
${statusDot(o.status)} ${dateFmt} ${esc(o.customer_name || '—')} x${o.items_count || 0}${totalStr ? ' · ' + totalStr + '' : ''}
`; }).join(''); } } // Mobile segmented control renderMobileSegmented('logsMobileSeg', [ { label: 'Toate', count: counts.total || 0, value: 'all', active: currentFilter === 'all', colorClass: 'fc-neutral' }, { label: 'Imp.', count: counts.imported || 0, value: 'IMPORTED', active: currentFilter === 'IMPORTED', colorClass: 'fc-green' }, { label: 'Deja', count: counts.already_imported || 0, value: 'ALREADY_IMPORTED', active: currentFilter === 'ALREADY_IMPORTED', colorClass: 'fc-blue' }, { label: 'Omise', count: counts.skipped || 0, value: 'SKIPPED', active: currentFilter === 'SKIPPED', colorClass: 'fc-yellow' }, { label: 'Erori', count: counts.error || 0, value: 'ERROR', active: currentFilter === 'ERROR', colorClass: 'fc-red' } ], (val) => filterOrders(val)); // Orders pagination const totalPages = data.pages || 1; const infoEl = document.getElementById('ordersPageInfo'); if (infoEl) infoEl.textContent = `${data.total || 0} comenzi | Pagina ${ordersPage} din ${totalPages}`; const pagHtml = `${data.total || 0} comenzi | Pagina ${ordersPage} din ${totalPages}` + renderUnifiedPagination(ordersPage, totalPages, 'logsGoPage'); const pagDiv = document.getElementById('ordersPagination'); if (pagDiv) pagDiv.innerHTML = pagHtml; const pagDivTop = document.getElementById('ordersPaginationTop'); if (pagDivTop) pagDivTop.innerHTML = pagHtml; // Update run status badge const runRes = await fetch(`/api/sync/run/${encodeURIComponent(runId)}`); const runData = await runRes.json(); if (runData.run) { document.getElementById('logStatusBadge').innerHTML = runStatusBadge(runData.run.status); // Update mobile run dot const mDot = document.getElementById('mobileRunDot'); if (mDot) mDot.className = 'sync-status-dot ' + (runData.run.status || 'idle'); } } catch (err) { document.getElementById('runOrdersBody').innerHTML = `${esc(err.message)}`; } } function filterOrders(status) { loadRunOrders(currentRunId, status, 1); } function sortOrdersBy(col) { if (ordersSortColumn === col) { ordersSortDirection = ordersSortDirection === 'asc' ? 'desc' : 'asc'; } else { ordersSortColumn = col; ordersSortDirection = 'asc'; } // Update sort icons document.querySelectorAll('#logViewerSection .sort-icon').forEach(span => { const c = span.dataset.col; span.textContent = c === ordersSortColumn ? (ordersSortDirection === 'asc' ? '\u2191' : '\u2193') : ''; }); loadRunOrders(currentRunId, null, 1); } // ── Text Log (collapsible) ────────────────────── function toggleTextLog() { const section = document.getElementById('textLogSection'); section.style.display = section.style.display === 'none' ? '' : 'none'; if (section.style.display !== 'none' && currentRunId) { fetchTextLog(currentRunId); } } async function fetchTextLog(runId) { // Clear any existing poll timer to prevent accumulation if (logPollTimer) { clearInterval(logPollTimer); logPollTimer = null; } try { const res = await fetch(`/api/sync/run/${encodeURIComponent(runId)}/text-log`); if (!res.ok) throw new Error('HTTP ' + res.status); const data = await res.json(); document.getElementById('logContent').textContent = data.text || '(log gol)'; if (!data.finished) { if (document.getElementById('autoRefreshToggle')?.checked) { logPollTimer = setInterval(async () => { try { const r = await fetch(`/api/sync/run/${encodeURIComponent(runId)}/text-log`); const d = await r.json(); if (currentRunId !== runId) { clearInterval(logPollTimer); return; } document.getElementById('logContent').textContent = d.text || '(log gol)'; const el = document.getElementById('logContent'); el.scrollTop = el.scrollHeight; if (d.finished) { clearInterval(logPollTimer); logPollTimer = null; loadRuns(); loadRunOrders(runId, currentFilter, ordersPage); } } catch (e) { console.error('Poll error:', e); } }, 2500); } } } catch (err) { document.getElementById('logContent').textContent = 'Eroare: ' + err.message; } } // ── Multi-CODMAT helper (D1) ───────────────────── 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)}`; } // Multi-CODMAT: compact list return item.codmat_details.map(d => `
${esc(d.codmat)} \xd7${d.cantitate_roa}
` ).join(''); } // ── Order Detail Modal (R9) ───────────────────── async function openOrderDetail(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 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 || '-'; 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' : '-'; // Mobile article flat list const mobileContainer = document.getElementById('detailItemsMobile'); if (mobileContainer) { mobileContainer.innerHTML = '
' + items.map((item, idx) => { const codmatList = 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)} ${codmatList}
${esc(item.product_name || '–')} x${item.quantity || 0} ${valoare} lei
`; }).join('') + '
'; } document.getElementById('detailItemsBody').innerHTML = items.map(item => { const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2); const codmatCell = `${renderCodmatCell(item)}`; return ` ${esc(item.sku)} ${esc(item.product_name || '-')} ${codmatCell} ${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 (uses shared openQuickMap) ─── function openLogsQuickMap(sku, productName, orderNumber) { openQuickMap({ sku, productName, onSave: () => { if (orderNumber) openOrderDetail(orderNumber); loadRunOrders(currentRunId, currentFilter, ordersPage); } }); } // ── Init ──────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { loadRuns(); document.querySelectorAll('#orderFilterPills .filter-pill').forEach(btn => { btn.addEventListener('click', function() { filterOrders(this.dataset.logStatus || 'all'); }); }); const preselected = document.getElementById('preselectedRun'); const urlParams = new URLSearchParams(window.location.search); const runFromUrl = urlParams.get('run') || (preselected ? preselected.value : ''); if (runFromUrl) { selectRun(runFromUrl); } document.getElementById('autoRefreshToggle')?.addEventListener('change', (e) => { if (e.target.checked) { // Resume polling if we have an active run if (currentRunId) fetchTextLog(currentRunId); } else { // Pause polling if (logPollTimer) { clearInterval(logPollTimer); logPollTimer = null; } } }); document.getElementById('autoRefreshToggleMobile')?.addEventListener('change', (e) => { const desktop = document.getElementById('autoRefreshToggle'); if (desktop) desktop.checked = e.target.checked; desktop?.dispatchEvent(new Event('change')); }); });