// 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 currentQmSku = ''; let currentQmOrderNumber = ''; let ordersSortColumn = 'order_date'; let ordersSortDirection = 'desc'; function esc(s) { if (s == null) return ''; return String(s) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') .replace(/'/g, '''); } 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 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', second: '2-digit' }); } return d.toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' }); } catch { return dateStr; } } 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'; default: return `${esc(status)}`; } } // ── 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(''); } } 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; if (!runId) { document.getElementById('logViewerSection').style.display = 'none'; return; } document.getElementById('logViewerSection').style.display = ''; document.getElementById('logRunId').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 button styles document.querySelectorAll('#orderFilterBtns button').forEach(btn => { btn.className = btn.className.replace(' btn-primary', ' btn-outline-primary') .replace(' btn-success', ' btn-outline-success') .replace(' btn-info', ' btn-outline-info') .replace(' btn-warning', ' btn-outline-warning') .replace(' btn-danger', ' btn-outline-danger'); }); 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; // Highlight active filter const filterMap = { 'all': 0, 'IMPORTED': 1, 'ALREADY_IMPORTED': 2, 'SKIPPED': 3, 'ERROR': 4 }; const btns = document.querySelectorAll('#orderFilterBtns button'); const idx = filterMap[currentFilter] ?? 0; if (btns[idx]) { const colorMap = ['primary', 'success', 'info', 'warning', 'danger']; btns[idx].className = btns[idx].className.replace(`btn-outline-${colorMap[idx]}`, `btn-${colorMap[idx]}`); } 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); return ` ${(ordersPage - 1) * 50 + i + 1} ${dateStr} ${esc(o.order_number)} ${esc(o.customer_name)} ${o.items_count || 0} ${orderStatusBadge(o.status)} `; }).join(''); } // Orders pagination const totalPages = data.pages || 1; const infoEl = document.getElementById('ordersPageInfo'); infoEl.textContent = `${data.total || 0} comenzi | Pagina ${ordersPage} din ${totalPages}`; const pagDiv = document.getElementById('ordersPagination'); if (totalPages > 1) { pagDiv.innerHTML = ` ${ordersPage} / ${totalPages} `; } else { pagDiv.innerHTML = ''; } // 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); } } 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} (${d.procent_pret}%)
` ).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 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 (from order detail) ────────── let qmAcTimeout = null; 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'; // Reset CODMAT lines const container = document.getElementById('qmCodmatLines'); container.innerHTML = ''; addQmCodmatLine(); // Show quick map on top of order detail (modal stacking) 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); // Setup autocomplete on the new input 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; } // Validate percentage sum for multi-line 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(); // Refresh order detail items in the still-open modal if (currentQmOrderNumber) openOrderDetail(currentQmOrderNumber); // Refresh orders view loadRunOrders(currentRunId, currentFilter, ordersPage); } else { alert('Eroare: ' + (data.error || 'Unknown')); } } catch (err) { alert('Eroare: ' + err.message); } } // ── Init ──────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { loadRuns(); 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; } } }); });