// ── 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; let _pollIntervalMs = 5000; // default, overridden from settings let _knownLastRunId = null; // track last_run.run_id to detect missed syncs // ── Init ────────────────────────────────────────── document.addEventListener('DOMContentLoaded', async () => { await initPollInterval(); loadSchedulerStatus(); loadDashOrders(); startSyncPolling(); wireFilterBar(); }); async function initPollInterval() { try { const data = await fetchJSON('/api/settings'); const sec = parseInt(data.dashboard_poll_seconds) || 5; _pollIntervalMs = sec * 1000; } catch(e) {} } // ── Smart Sync Polling ──────────────────────────── function startSyncPolling() { if (_pollInterval) clearInterval(_pollInterval); _pollInterval = setInterval(pollSyncStatus, _pollIntervalMs); 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'; // Detect missed sync completions via last_run.run_id change const newLastRunId = data.last_run?.run_id || null; const missedSync = !isRunning && !wasRunning && _knownLastRunId && newLastRunId && newLastRunId !== _knownLastRunId; _knownLastRunId = newLastRunId; 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, _pollIntervalMs); loadDashOrders(); } else if (missedSync) { // Sync completed while we weren't watching (e.g. auto-sync) — refresh orders 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; if (el('cntCanc')) el('cntCanc').textContent = c.cancelled || 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' }, { label: 'Anulate', count: c.cancelled || 0, value: 'CANCELLED', active: activeStatus === 'CANCELLED', colorClass: 'fc-dark' } ], (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 'CANCELLED': return 'Anulat'; 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]; if (d.direct) { return `${esc(d.codmat)} direct`; } return `${esc(d.codmat)}`; } return item.codmat_details.map(d => `
${esc(d.codmat)} \xd7${d.cantitate_roa}
` ).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'; document.getElementById('detailReceipt').innerHTML = ''; document.getElementById('detailReceiptMobile').innerHTML = ''; const invInfo = document.getElementById('detailInvoiceInfo'); if (invInfo) invInfo.style.display = 'none'; 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 items = data.items || []; if (items.length === 0) { document.getElementById('detailItemsBody').innerHTML = 'Niciun articol'; return; } // Store items for quick map pre-population window._detailItems = items; // Mobile article flat list const mobileContainer = document.getElementById('detailItemsMobile'); if (mobileContainer) { let mobileHtml = items.map((item, idx) => { const codmatText = item.codmat_details?.length ? item.codmat_details.map(d => `${esc(d.codmat)}${d.direct ? ' direct' : ''}`).join(' ') : `${esc(item.codmat || '–')}`; const valoare = (Number(item.price || 0) * Number(item.quantity || 0)); return `
${esc(item.sku)} ${codmatText}
${esc(item.product_name || '–')} x${item.quantity || 0} ${fmtNum(valoare)} lei TVA ${item.vat != null ? Number(item.vat) : '?'}
`; }).join(''); // Transport row (mobile) if (order.delivery_cost > 0) { const tVat = order.transport_vat || '21'; mobileHtml += `
Transport x1 ${fmtNum(order.delivery_cost)} lei TVA ${tVat}
`; } // Discount rows (mobile) if (order.discount_total > 0) { const discSplit = computeDiscountSplit(items, order); if (discSplit) { Object.entries(discSplit) .sort(([a], [b]) => Number(a) - Number(b)) .forEach(([rate, amt]) => { if (amt > 0) mobileHtml += `
Discount x\u20131 ${fmtNum(amt)} lei TVA ${Number(rate)}
`; }); } else { mobileHtml += `
Discount x\u20131 ${fmtNum(order.discount_total)} lei
`; } } mobileContainer.innerHTML = '
' + mobileHtml + '
'; } let tableHtml = items.map((item, idx) => { const valoare = Number(item.price || 0) * Number(item.quantity || 0); return ` ${esc(item.sku)} ${esc(item.product_name || '-')} ${renderCodmatCell(item)} ${item.quantity || 0} ${item.price != null ? fmtNum(item.price) : '-'} ${item.vat != null ? Number(item.vat) : '-'} ${fmtNum(valoare)} `; }).join(''); // Transport row if (order.delivery_cost > 0) { const tVat = order.transport_vat || '21'; const tCodmat = order.transport_codmat || ''; tableHtml += ` Transport ${tCodmat ? '' + esc(tCodmat) + '' : ''} 1${fmtNum(order.delivery_cost)} ${tVat}${fmtNum(order.delivery_cost)} `; } // Discount rows (split by VAT rate) if (order.discount_total > 0) { const dCodmat = order.discount_codmat || ''; const discSplit = computeDiscountSplit(items, order); if (discSplit) { Object.entries(discSplit) .sort(([a], [b]) => Number(a) - Number(b)) .forEach(([rate, amt]) => { if (amt > 0) tableHtml += ` Discount ${dCodmat ? '' + esc(dCodmat) + '' : ''} \u20131${fmtNum(amt)} ${Number(rate)}\u2013${fmtNum(amt)} `; }); } else { tableHtml += ` Discount ${dCodmat ? '' + esc(dCodmat) + '' : ''} \u20131${fmtNum(order.discount_total)} -\u2013${fmtNum(order.discount_total)} `; } } document.getElementById('detailItemsBody').innerHTML = tableHtml; // Receipt footer (just total) renderReceipt(items, order); } catch (err) { document.getElementById('detailError').textContent = err.message; document.getElementById('detailError').style.display = ''; } } function fmtNum(v) { return Number(v).toLocaleString('ro-RO', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function computeDiscountSplit(items, order) { if (order.discount_split && typeof order.discount_split === 'object') return order.discount_split; // Compute proportionally from items by VAT rate const byRate = {}; items.forEach(item => { const rate = item.vat != null ? Number(item.vat) : null; if (rate === null) return; if (!byRate[rate]) byRate[rate] = 0; byRate[rate] += Number(item.price || 0) * Number(item.quantity || 0); }); const rates = Object.keys(byRate).sort((a, b) => Number(a) - Number(b)); if (rates.length === 0) return null; const grandTotal = rates.reduce((s, r) => s + byRate[r], 0); if (grandTotal <= 0) return null; const split = {}; let remaining = order.discount_total; rates.forEach((rate, i) => { if (i === rates.length - 1) { split[rate] = Math.round(remaining * 100) / 100; } else { const amt = Math.round(order.discount_total * byRate[rate] / grandTotal * 100) / 100; split[rate] = amt; remaining -= amt; } }); return split; } function renderReceipt(items, order) { const desktop = document.getElementById('detailReceipt'); const mobile = document.getElementById('detailReceiptMobile'); if (!items.length) { desktop.innerHTML = ''; mobile.innerHTML = ''; return; } const total = order.order_total != null ? fmtNum(order.order_total) : '-'; const html = `Total: ${total} lei`; desktop.innerHTML = html; mobile.innerHTML = html; } // ── 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 = ''; // Check if this is a direct SKU (SKU=CODMAT in NOM_ARTICOLE) const item = (window._detailItems || [])[itemIdx]; const details = item?.codmat_details; const isDirect = details?.length === 1 && details[0].direct === true; const directInfo = document.getElementById('qmDirectInfo'); const saveBtn = document.getElementById('qmSaveBtn'); if (isDirect) { if (directInfo) { directInfo.innerHTML = ` SKU = CODMAT direct in nomenclator (${escHtml(details[0].codmat)} — ${escHtml(details[0].denumire || '')}).
Poti suprascrie cu un alt CODMAT daca e necesar (ex: reambalare).`; directInfo.style.display = ''; } if (saveBtn) { saveBtn.textContent = 'Suprascrie mapare'; } addQmCodmatLine(); } else { if (directInfo) directInfo.style.display = 'none'; if (saveBtn) saveBtn.textContent = 'Salveaza'; // Pre-populate with existing codmat_details if available if (details && details.length > 0) { details.forEach(d => { addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate_roa, 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 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; if (!codmat) continue; mappings.push({ codmat, cantitate_roa: cantitate }); } if (mappings.length === 0) { alert('Selecteaza cel putin un CODMAT'); return; } 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 }) }); } 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 { const msg = data.detail || data.error || 'Unknown'; document.getElementById('qmPctWarning').textContent = msg; document.getElementById('qmPctWarning').style.display = ''; } } catch (err) { alert('Eroare: ' + err.message); } }