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 = `
`;
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, ''');
}