Change default period from 7 to 3 days. Add quick-select preset buttons (3 zile / 7 zile / 30 zile) that sync with the dropdown. Reduces noise for daily operators who only need recent orders. Cache-bust: style.css?v=22, dashboard.js?v=30 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
548 lines
23 KiB
JavaScript
548 lines
23 KiB
JavaScript
// ── State ─────────────────────────────────────────
|
||
let dashPage = 1;
|
||
let dashPerPage = 50;
|
||
let dashSortCol = 'order_date';
|
||
let dashSortDir = 'desc';
|
||
let dashSearchTimeout = 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 = `<span class="dot dot-green me-1"></span>${newImp} noi, ${already} deja <span class="dot dot-yellow me-1"></span>${lr.skipped || 0} omise <span class="dot dot-red me-1"></span>${lr.errors || 0} erori`;
|
||
} else {
|
||
cnt.innerHTML = `<span class="dot dot-green me-1"></span>${lr.imported || 0} imp. <span class="dot dot-yellow me-1"></span>${lr.skipped || 0} omise <span class="dot dot-red me-1"></span>${lr.errors || 0} erori`;
|
||
}
|
||
}
|
||
if (st) {
|
||
st.textContent = lr.status === 'completed' ? '\u2713' : '\u2715';
|
||
st.style.color = lr.status === 'completed' ? 'var(--success)' : 'var(--error)';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 () {
|
||
document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
|
||
const matchBtn = document.querySelector(`.preset-btn[data-days="${this.value}"]`);
|
||
if (matchBtn) matchBtn.classList.add('active');
|
||
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);
|
||
});
|
||
|
||
// Period preset buttons
|
||
document.querySelectorAll('.preset-btn[data-days]').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
|
||
this.classList.add('active');
|
||
const days = this.dataset.days;
|
||
const sel = document.getElementById('periodSelect');
|
||
if (sel) { sel.value = days; }
|
||
document.getElementById('customRangeInputs')?.classList.remove('visible');
|
||
dashPage = 1;
|
||
loadDashOrders();
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── 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;
|
||
|
||
// Attention card
|
||
const attnEl = document.getElementById('attentionCard');
|
||
if (attnEl) {
|
||
const errors = c.error || 0;
|
||
const unmapped = c.unresolved_skus || 0;
|
||
const uninvOld = c.uninvoiced_old || 0;
|
||
|
||
if (errors === 0 && unmapped === 0 && uninvOld === 0) {
|
||
attnEl.innerHTML = '<div class="attention-card attention-ok"><i class="bi bi-check-circle"></i> Totul in ordine</div>';
|
||
} else {
|
||
let items = [];
|
||
if (errors > 0) items.push(`<span class="attention-item attention-error" onclick="document.querySelector('.filter-pill[data-status=ERROR]')?.click()"><i class="bi bi-exclamation-triangle"></i> ${errors} erori import</span>`);
|
||
if (unmapped > 0) items.push(`<span class="attention-item attention-warning" onclick="window.location='${window.ROOT_PATH||''}/missing-skus'"><i class="bi bi-puzzle"></i> ${unmapped} SKU-uri nemapate</span>`);
|
||
if (uninvOld > 0) items.push(`<span class="attention-item attention-warning" onclick="document.querySelector('.filter-pill[data-status=UNINVOICED]')?.click()"><i class="bi bi-receipt"></i> ${uninvOld} nefacturate >3 zile</span>`);
|
||
attnEl.innerHTML = '<div class="attention-card attention-alert">' + items.join('') + '</div>';
|
||
}
|
||
}
|
||
|
||
const tbody = document.getElementById('dashOrdersBody');
|
||
const orders = data.orders || [];
|
||
|
||
if (orders.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted py-3">Nicio comanda</td></tr>';
|
||
} 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 `<tr style="cursor:pointer" onclick="openDashOrderDetail('${esc(o.order_number)}')">
|
||
<td>${statusDot(o.status)}</td>
|
||
<td class="text-nowrap">${dateStr}</td>
|
||
${renderClientCell(o)}
|
||
<td><code>${esc(o.order_number)}</code></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>
|
||
<td class="text-center">${invoiceDot(o)}</td>
|
||
<td class="text-center">${priceDot(o)}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
// Mobile flat rows
|
||
const mobileList = document.getElementById('dashMobileList');
|
||
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 name = o.customer_name || o.shipping_name || o.billing_name || '\u2014';
|
||
const totalStr = o.order_total ? Number(o.order_total).toFixed(2) : '';
|
||
const priceMismatch = o.price_match === false ? '<span class="dot dot-red" style="width:6px;height:6px" title="Pret!="></span> ' : '';
|
||
return `<div class="flat-row" onclick="openDashOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem">
|
||
${statusDot(o.status)}
|
||
<span style="color:var(--text-muted)" class="text-nowrap">${dateFmt}</span>
|
||
<span class="grow truncate fw-bold">${esc(name)}</span>
|
||
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · ' + priceMismatch + '<strong>' + totalStr + '</strong>' : ''}</span>
|
||
</div>`;
|
||
}).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 = `<small class="text-muted me-auto">${totalOrders} comenzi | Pagina ${dashPage} din ${totalPages}</small>` + 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 =
|
||
`<tr><td colspan="9" class="text-center text-danger">${esc(err.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
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 `<td class="tooltip-cont fw-bold" data-tooltip="Livrare: ${escHtml(shipping)}">${escHtml(display)} <sup style="color:#6b7280;font-size:0.65rem">▲</sup></td>`;
|
||
}
|
||
return `<td class="fw-bold">${escHtml(display || billing || '\u2014')}</td>`;
|
||
}
|
||
|
||
// ── 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, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
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 priceDot(order) {
|
||
if (order.price_match === true) return '<span class="dot dot-green" title="Preturi OK"></span>';
|
||
if (order.price_match === false) return '<span class="dot dot-red" title="Diferenta de pret"></span>';
|
||
if (order.price_match === null) return '<span class="dot dot-gray" title="Preturi ROA indisponibile"></span>';
|
||
return '–';
|
||
}
|
||
|
||
function invoiceDot(order) {
|
||
if (order.status !== 'IMPORTED' && order.status !== 'ALREADY_IMPORTED') return '–';
|
||
if (order.invoice && order.invoice.facturat) return '<span class="dot dot-green" title="Facturat"></span>';
|
||
return '<span class="dot dot-red" title="Nefacturat"></span>';
|
||
}
|
||
|
||
// ── 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 ────────────────────────────
|
||
|
||
function openDashOrderDetail(orderNumber) {
|
||
_sharedModalQuickMapFn = openDashQuickMap;
|
||
renderOrderDetailModal(orderNumber, {
|
||
onQuickMap: openDashQuickMap,
|
||
onAfterRender: function() { /* nothing extra needed */ }
|
||
});
|
||
}
|
||
|
||
// ── Quick Map Modal (uses shared openQuickMap) ───
|
||
|
||
function openDashQuickMap(sku, productName, orderNumber, itemIdx) {
|
||
const item = (window._detailItems || [])[itemIdx];
|
||
const details = item?.codmat_details;
|
||
const isDirect = details?.length === 1 && details[0].direct === true;
|
||
|
||
openQuickMap({
|
||
sku,
|
||
productName,
|
||
isDirect,
|
||
directInfo: isDirect ? { codmat: details[0].codmat, denumire: details[0].denumire } : null,
|
||
prefill: (!isDirect && details?.length) ? details.map(d => ({ codmat: d.codmat, cantitate: d.cantitate_roa, denumire: d.denumire })) : null,
|
||
onSave: () => {
|
||
if (orderNumber) openDashOrderDetail(orderNumber);
|
||
loadDashOrders();
|
||
}
|
||
});
|
||
}
|
||
|