- deploy.ps1, iis-web.config: Windows Server deployment scripts - api/app/routers/sync.py, dashboard.py: router updates - api/app/services/import_service.py, sync_service.py: service updates - api/app/static/css/style.css, js/*.js: UI updates - api/database-scripts/08_PACK_FACTURARE.pck: Oracle package - .gitignore: add .gittoken - CLAUDE.md, agent configs: documentation updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
597 lines
28 KiB
JavaScript
597 lines
28 KiB
JavaScript
// 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 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 '<span style="color:#16a34a;font-weight:600">completed</span>';
|
||
case 'running': return '<span style="color:#2563eb;font-weight:600">running</span>';
|
||
case 'failed': return '<span style="color:#dc2626;font-weight:600">failed</span>';
|
||
default: return `<span style="font-weight:600">${esc(status)}</span>`;
|
||
}
|
||
}
|
||
|
||
function orderStatusBadge(status) {
|
||
switch ((status || '').toUpperCase()) {
|
||
case 'IMPORTED': return '<span class="badge bg-success">Importat</span>';
|
||
case 'ALREADY_IMPORTED': return '<span class="badge bg-info">Deja importat</span>';
|
||
case 'SKIPPED': return '<span class="badge bg-warning">Omis</span>';
|
||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||
}
|
||
}
|
||
|
||
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 = '<option value="">Niciun sync run</option>';
|
||
} else {
|
||
dd.innerHTML = '<option value="">-- Selecteaza un run --</option>' +
|
||
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 `<option value="${esc(r.run_id)}" ${selected}>${esc(label)}</option>`;
|
||
}).join('');
|
||
}
|
||
const ddMobile = document.getElementById('runsDropdownMobile');
|
||
if (ddMobile) ddMobile.innerHTML = dd.innerHTML;
|
||
} catch (err) {
|
||
const dd = document.getElementById('runsDropdown');
|
||
dd.innerHTML = `<option value="">Eroare: ${esc(err.message)}</option>`;
|
||
}
|
||
}
|
||
|
||
// ── 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 = '<tr><td colspan="9" class="text-center text-muted py-3">Nicio comanda</td></tr>';
|
||
} 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 `<tr style="cursor:pointer" onclick="openOrderDetail('${esc(o.order_number)}')">
|
||
<td>${statusDot(o.status)}</td>
|
||
<td>${(ordersPage - 1) * 50 + i + 1}</td>
|
||
<td class="text-nowrap">${dateStr}</td>
|
||
<td><code>${esc(o.order_number)}</code></td>
|
||
<td class="fw-bold">${esc(o.customer_name)}</td>
|
||
<td>${o.items_count || 0}</td>
|
||
<td class="text-end text-muted">${fmtCost(o.delivery_cost)}</td>
|
||
<td class="text-end text-muted">${fmtCost(o.discount_total)}</td>
|
||
<td class="text-end fw-bold">${orderTotal}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
// Mobile flat rows
|
||
const mobileList = document.getElementById('logsMobileList');
|
||
if (mobileList) {
|
||
if (orders.length === 0) {
|
||
mobileList.innerHTML = '<div class="flat-row text-muted py-3 justify-content-center">Nicio comanda</div>';
|
||
} 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 `<div class="flat-row" onclick="openOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem">
|
||
${statusDot(o.status)}
|
||
<span style="color:#6b7280" class="text-nowrap">${dateFmt}</span>
|
||
<span class="grow truncate fw-bold">${esc(o.customer_name || '—')}</span>
|
||
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · <strong>' + totalStr + '</strong>' : ''}</span>
|
||
</div>`;
|
||
}).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 = `<small class="text-muted me-auto">${data.total || 0} comenzi | Pagina ${ordersPage} din ${totalPages}</small>` + 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 =
|
||
`<tr><td colspan="9" class="text-center text-danger">${esc(err.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
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 `<code>${esc(item.codmat || '-')}</code>`;
|
||
}
|
||
if (item.codmat_details.length === 1) {
|
||
const d = item.codmat_details[0];
|
||
return `<code>${esc(d.codmat)}</code>`;
|
||
}
|
||
// Multi-CODMAT: compact list
|
||
return item.codmat_details.map(d =>
|
||
`<div class="small"><code>${esc(d.codmat)}</code> <span class="text-muted">\xd7${d.cantitate_roa} (${d.procent_pret}%)</span></div>`
|
||
).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 = '<tr><td colspan="6" class="text-center">Se incarca...</td></tr>';
|
||
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 = '<tr><td colspan="6" class="text-center text-muted">Niciun articol</td></tr>';
|
||
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 = '<div class="detail-item-flat">' + items.map((item, idx) => {
|
||
const codmatList = item.codmat_details?.length
|
||
? item.codmat_details.map(d => `<span class="dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(d.codmat)}</span>`).join(' ')
|
||
: `<span class="dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(item.codmat || '–')}</span>`;
|
||
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
|
||
return `<div class="dif-item">
|
||
<div class="dif-row">
|
||
<span class="dif-sku">${esc(item.sku)}</span>
|
||
${codmatList}
|
||
</div>
|
||
<div class="dif-row">
|
||
<span class="dif-name">${esc(item.product_name || '–')}</span>
|
||
<span class="dif-qty">x${item.quantity || 0}</span>
|
||
<span class="dif-val">${valoare} lei</span>
|
||
</div>
|
||
</div>`;
|
||
}).join('') + '</div>';
|
||
}
|
||
|
||
document.getElementById('detailItemsBody').innerHTML = items.map(item => {
|
||
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
|
||
const codmatCell = `<span class="codmat-link" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}')" title="Click pentru mapare">${renderCodmatCell(item)}</span>`;
|
||
return `<tr>
|
||
<td><code>${esc(item.sku)}</code></td>
|
||
<td>${esc(item.product_name || '-')}</td>
|
||
<td>${codmatCell}</td>
|
||
<td>${item.quantity || 0}</td>
|
||
<td>${item.price != null ? Number(item.price).toFixed(2) : '-'}</td>
|
||
<td class="text-end">${valoare}</td>
|
||
</tr>`;
|
||
}).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 = `
|
||
<div class="mb-2 position-relative">
|
||
<label class="form-label form-label-sm mb-1">CODMAT (Articol ROA)</label>
|
||
<input type="text" class="form-control form-control-sm qm-codmat" placeholder="Cauta codmat sau denumire..." autocomplete="off">
|
||
<div class="autocomplete-dropdown d-none qm-ac-dropdown"></div>
|
||
<small class="text-muted qm-selected"></small>
|
||
</div>
|
||
<div class="row">
|
||
<div class="col-5">
|
||
<label class="form-label form-label-sm mb-1">Cantitate ROA</label>
|
||
<input type="number" class="form-control form-control-sm qm-cantitate" value="1" step="0.001" min="0.001">
|
||
</div>
|
||
<div class="col-5">
|
||
<label class="form-label form-label-sm mb-1">Procent Pret (%)</label>
|
||
<input type="number" class="form-control form-control-sm qm-procent" value="100" step="0.01" min="0" max="100">
|
||
</div>
|
||
<div class="col-2 d-flex align-items-end">
|
||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('.qm-line').remove()"><i class="bi bi-x"></i></button>` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 =>
|
||
`<div class="autocomplete-item" onmousedown="qmSelectArticle(this, '${esc(r.codmat)}', '${esc(r.denumire)}${r.um ? ' (' + esc(r.um) + ')' : ''}')">
|
||
<span class="codmat">${esc(r.codmat)}</span> — <span class="denumire">${esc(r.denumire)}</span>${r.um ? ` <small class="text-muted">(${esc(r.um)})</small>` : ''}
|
||
</div>`
|
||
).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();
|
||
|
||
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'));
|
||
});
|
||
});
|