// shared.js - Unified utilities for all pages
// ── Root path patch — prepend ROOT_PATH to all relative fetch calls ───────
(function() {
const _fetch = window.fetch.bind(window);
window.fetch = function(url, ...args) {
if (typeof url === 'string' && url.startsWith('/') && window.ROOT_PATH) {
url = window.ROOT_PATH + url;
}
return _fetch(url, ...args);
};
})();
// ── HTML escaping ─────────────────────────────────
function esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// ── Date formatting ───────────────────────────────
function fmtDate(dateStr, includeSeconds) {
if (!dateStr) return '-';
try {
const d = new Date(dateStr);
const hasTime = dateStr.includes(':');
if (hasTime) {
const opts = { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' };
if (includeSeconds) opts.second = '2-digit';
return d.toLocaleString('ro-RO', opts);
}
return d.toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
} catch { return dateStr; }
}
// ── Unified Pagination ────────────────────────────
/**
* Renders a full pagination bar with First/Prev/numbers/Next/Last.
* @param {number} currentPage
* @param {number} totalPages
* @param {string} goToFnName - name of global function to call with page number
* @param {object} [opts] - optional: { perPage, perPageFn, perPageOptions }
* @returns {string} HTML string
*/
function renderUnifiedPagination(currentPage, totalPages, goToFnName, opts) {
if (totalPages <= 1 && !(opts && opts.perPage)) {
return '';
}
let html = '
';
// Per-page selector
if (opts && opts.perPage && opts.perPageFn) {
const options = opts.perPageOptions || [25, 50, 100, 250];
html += `Per pagina: `;
options.forEach(v => {
html += `${v} `;
});
html += ' ';
}
if (totalPages <= 1) {
html += '
';
return html;
}
html += '';
return html;
}
// ── Context Menu ──────────────────────────────────
let _activeContextMenu = null;
function closeAllContextMenus() {
if (_activeContextMenu) {
_activeContextMenu.remove();
_activeContextMenu = null;
}
}
document.addEventListener('click', closeAllContextMenus);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeAllContextMenus();
});
/**
* Show a context menu at the given position.
* @param {number} x - clientX
* @param {number} y - clientY
* @param {Array} items - [{label, action, danger}]
*/
function showContextMenu(x, y, items) {
closeAllContextMenus();
const menu = document.createElement('div');
menu.className = 'context-menu';
items.forEach(item => {
const btn = document.createElement('button');
btn.className = 'context-menu-item' + (item.danger ? ' text-danger' : '');
btn.textContent = item.label;
btn.addEventListener('click', (e) => {
e.stopPropagation();
closeAllContextMenus();
item.action();
});
menu.appendChild(btn);
});
document.body.appendChild(menu);
_activeContextMenu = menu;
// Position menu, keeping it within viewport
const rect = menu.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
let left = x;
let top = y;
if (left + 160 > vw) left = vw - 165;
if (top + rect.height > vh) top = vh - rect.height - 5;
menu.style.left = left + 'px';
menu.style.top = top + 'px';
}
/**
* Wire right-click on desktop + three-dots button on mobile for a table.
* @param {string} rowSelector - CSS selector for clickable rows
* @param {function} menuItemsFn - called with row element, returns [{label, action, danger}]
*/
function initContextMenus(rowSelector, menuItemsFn) {
document.addEventListener('contextmenu', (e) => {
const row = e.target.closest(rowSelector);
if (!row) return;
e.preventDefault();
showContextMenu(e.clientX, e.clientY, menuItemsFn(row));
});
document.addEventListener('click', (e) => {
const trigger = e.target.closest('.context-menu-trigger');
if (!trigger) return;
const row = trigger.closest(rowSelector);
if (!row) return;
e.stopPropagation();
const rect = trigger.getBoundingClientRect();
showContextMenu(rect.left, rect.bottom + 2, menuItemsFn(row));
});
}
// ── Mobile segmented control ─────────────────────
/**
* Render a Bootstrap btn-group segmented control for mobile.
* @param {string} containerId - ID of the container div
* @param {Array} pills - [{label, count, colorClass, value, active}]
* @param {function} onSelect - callback(value)
*/
function renderMobileSegmented(containerId, pills, onSelect) {
const container = document.getElementById(containerId);
if (!container) return;
const btnStyle = 'font-size:0.75rem;height:32px;white-space:nowrap;display:inline-flex;align-items:center;justify-content:center;gap:0.25rem;flex:1;padding:0 0.25rem';
container.innerHTML = `${pills.map(p => {
const cls = p.active ? 'btn seg-active' : 'btn btn-outline-secondary';
const countColor = (!p.active && p.colorClass) ? ` class="${p.colorClass}"` : '';
return `${esc(p.label)} ${p.count} `;
}).join('')}
`;
container.querySelectorAll('[data-seg-value]').forEach(btn => {
btn.addEventListener('click', () => onSelect(btn.dataset.segValue));
});
}
// ── Shared Quick Map Modal ────────────────────────
let _qmOnSave = null;
let _qmAcTimeout = null;
/**
* Open the shared quick-map modal.
* @param {object} opts
* @param {string} opts.sku
* @param {string} opts.productName
* @param {Array} [opts.prefill] - [{codmat, cantitate, denumire}]
* @param {boolean}[opts.isDirect] - true if SKU=CODMAT direct
* @param {object} [opts.directInfo] - {codmat, denumire} for direct SKU info
* @param {function} opts.onSave - callback(sku, mappings) after successful save
*/
function openQuickMap(opts) {
_qmOnSave = opts.onSave || null;
document.getElementById('qmSku').textContent = opts.sku;
document.getElementById('qmProductName').textContent = opts.productName || '-';
document.getElementById('qmPctWarning').style.display = 'none';
const container = document.getElementById('qmCodmatLines');
container.innerHTML = '';
const directInfo = document.getElementById('qmDirectInfo');
const saveBtn = document.getElementById('qmSaveBtn');
if (opts.isDirect && opts.directInfo) {
if (directInfo) {
directInfo.innerHTML = ` SKU = CODMAT direct in nomenclator (${esc(opts.directInfo.codmat)} — ${esc(opts.directInfo.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';
if (opts.prefill && opts.prefill.length > 0) {
opts.prefill.forEach(d => addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate, 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 = `
${esc(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('#qmCodmatLines .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; }
const sku = document.getElementById('qmSku').textContent;
try {
let res;
if (mappings.length === 1) {
res = await fetch('/api/mappings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sku, 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, mappings })
});
}
const data = await res.json();
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('quickMapModal')).hide();
if (_qmOnSave) _qmOnSave(sku, mappings);
// Check for SKIPPED orders that can now be imported
try {
const pendingRes = await fetch(`/api/orders/by-sku/${encodeURIComponent(sku)}/pending`);
const pendingData = await pendingRes.json();
if (pendingData.count > 0) {
const banner = document.createElement('div');
banner.className = 'alert alert-info d-flex align-items-center gap-2 mt-2';
banner.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:1060;min-width:300px;max-width:500px;box-shadow:var(--card-shadow)';
banner.innerHTML = ` ${pendingData.count} comenzi SKIPPED pot fi importate acum Importa ✕ `;
document.body.appendChild(banner);
document.getElementById('batchRetryBtn').onclick = async function() {
this.disabled = true;
this.innerHTML = ' ';
try {
const retryRes = await fetch('/api/orders/batch-retry', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({order_numbers: pendingData.order_numbers})
});
const retryData = await retryRes.json();
banner.className = retryData.errors > 0 ? 'alert alert-warning d-flex align-items-center gap-2 mt-2' : 'alert alert-success d-flex align-items-center gap-2 mt-2';
banner.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:1060;min-width:300px;max-width:500px;box-shadow:var(--card-shadow)';
banner.innerHTML = ` ${esc(retryData.message)} ✕ `;
setTimeout(() => banner.remove(), 5000);
if (typeof loadDashOrders === 'function') loadDashOrders();
} catch(e) {
banner.innerHTML = `Eroare: ${esc(e.message)} ✕ `;
}
};
setTimeout(() => { if (banner.parentElement) banner.remove(); }, 15000);
}
} catch(e) { /* ignore */ }
} else {
alert('Eroare: ' + (data.error || 'Unknown'));
}
} catch (err) {
alert('Eroare: ' + err.message);
}
}
// ── Shared helpers (moved from dashboard.js/logs.js) ─
function fmtCost(v) {
return v > 0 ? Number(v).toFixed(2) : '–';
}
function fmtNum(v) {
return Number(v).toLocaleString('ro-RO', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
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 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('');
}
function computeDiscountSplit(items, order) {
if (order.discount_split && typeof order.discount_split === 'object')
return order.discount_split;
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 (!desktop && !mobile) return;
if (!items.length) {
if (desktop) desktop.innerHTML = '';
if (mobile) mobile.innerHTML = '';
return;
}
const articole = items.reduce((s, i) => s + Number(i.price || 0) * Number(i.quantity || 0), 0);
const discount = Number(order.discount_total || 0);
const transport = Number(order.delivery_cost || 0);
const total = order.order_total != null ? fmtNum(order.order_total) : '-';
let dHtml = `Articole: ${fmtNum(articole)} `;
if (discount > 0) dHtml += `Discount: \u2013${fmtNum(discount)} `;
if (transport > 0) dHtml += `Transport: ${fmtNum(transport)} `;
dHtml += `Total: ${total} lei `;
if (desktop) desktop.innerHTML = dHtml;
let mHtml = `Art: ${fmtNum(articole)} `;
if (discount > 0) mHtml += `Disc: \u2013${fmtNum(discount)} `;
if (transport > 0) mHtml += `Transp: ${fmtNum(transport)} `;
mHtml += `Total: ${total} lei `;
if (mobile) mobile.innerHTML = mHtml;
}
// ── Order Detail Modal (shared) ──────────────────
/**
* Render and show the order detail modal.
* @param {string} orderNumber
* @param {object} opts
* @param {function} opts.onQuickMap - (sku, productName, orderNumber, itemIdx) => void
* @param {function} [opts.onAfterRender] - (order, items) => void
*/
async function renderOrderDetailModal(orderNumber, opts) {
opts = opts || {};
window._detailOrderNumber = orderNumber;
// Reset modal state
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('detailItemsBody').innerHTML = 'Se incarca... ';
document.getElementById('detailError').style.display = 'none';
const retryBtn = document.getElementById('detailRetryBtn');
if (retryBtn) { retryBtn.style.display = 'none'; retryBtn.disabled = false; retryBtn.innerHTML = ' Reimporta'; retryBtn.className = 'btn btn-sm btn-outline-primary'; }
const receiptEl = document.getElementById('detailReceipt');
if (receiptEl) receiptEl.innerHTML = '';
const receiptMEl = document.getElementById('detailReceiptMobile');
if (receiptMEl) receiptMEl.innerHTML = '';
const invInfo = document.getElementById('detailInvoiceInfo');
if (invInfo) invInfo.style.display = 'none';
const mobileContainer = document.getElementById('detailItemsMobile');
if (mobileContainer) mobileContainer.innerHTML = '';
const reconEl = document.getElementById('detailInvoiceRecon');
if (reconEl) { reconEl.innerHTML = ''; reconEl.style.display = 'none'; }
// Remove diff badge from previous render
const prevDiffBadge = document.querySelector('.diff-badge');
if (prevDiffBadge) prevDiffBadge.remove();
// Reset compact header elements
const partenerRoa = document.getElementById('detailPartenerRoa');
if (partenerRoa) { partenerRoa.style.display = 'none'; partenerRoa.textContent = ''; }
const cuiGomag = document.getElementById('detailCuiGomag');
if (cuiGomag) cuiGomag.style.display = 'none';
const cuiRoa = document.getElementById('detailCuiRoa');
if (cuiRoa) {
cuiRoa.style.display = 'none';
// Restore original structure (may have been replaced by PF indicator)
cuiRoa.innerHTML = 'CUI: ';
}
const denomMismatch = document.getElementById('detailDenomMismatch');
if (denomMismatch) { denomMismatch.style.display = 'none'; denomMismatch.innerHTML = ''; }
const addressBlock = document.getElementById('detailAddressBlock');
if (addressBlock) addressBlock.style.display = 'none';
const addressLines = document.getElementById('detailAddressLines');
if (addressLines) addressLines.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 || '-';
// Invoice info
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 = '';
}
// Invoice reconciliation
const reconEl = document.getElementById('detailInvoiceRecon');
if (reconEl && inv && inv.reconciliation) {
const r = inv.reconciliation;
if (r.match) {
reconEl.innerHTML = `✓ Total factura OK (${fmtNum(r.invoice_total)} lei) `;
} else {
const sign = r.difference > 0 ? '+' : '';
reconEl.innerHTML = `Diferenta: ${sign}${fmtNum(r.difference)} lei
Factura: ${fmtNum(r.invoice_total)} | Comanda: ${fmtNum(r.order_total)} `;
}
reconEl.style.display = '';
} else if (reconEl) {
reconEl.style.display = 'none';
}
// Render compact header info (partner + addresses)
_renderHeaderInfo(order);
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;
const qmFn = opts.onQuickMap ? opts.onQuickMap.name || '_sharedQuickMap' : null;
// Mobile article flat list
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));
const clickAttr = opts.onQuickMap ? `onclick="_sharedModalQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}',${idx})"` : '';
const priceInfo = { pret_roa: item.pret_roa, match: item.price_match };
const priceMismatchHtml = priceInfo.match === false
? `ROA: ${fmtNum(priceInfo.pret_roa)} lei
`
: '';
return `
${esc(item.sku)}
${codmatText}
${esc(item.product_name || '–')}
x${item.quantity || 0}
${fmtNum(valoare)} lei
TVA ${item.vat != null ? Number(item.vat) : '?'}
${priceMismatchHtml}
`;
}).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 + '
';
}
// Desktop items table
const clickAttrFn = (item, idx) => opts.onQuickMap
? `onclick="_sharedModalQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}', ${idx})" title="Click pentru mapare"`
: '';
let tableHtml = items.map((item, idx) => {
const valoare = Number(item.price || 0) * Number(item.quantity || 0);
const priceInfo = { pret_roa: item.pret_roa, match: item.price_match };
const pretRoaHtml = priceInfo.pret_roa != null ? fmtNum(priceInfo.pret_roa) : '–';
let matchDot, rowStyle;
if (item.kit) {
matchDot = 'Kit ';
rowStyle = '';
} else if (priceInfo.pret_roa == null && priceInfo.match == null) {
matchDot = ' ';
rowStyle = '';
} else if (priceInfo.match === false) {
matchDot = ' ';
rowStyle = ' style="background:var(--error-light)"';
} else {
matchDot = ' ';
rowStyle = '';
}
return `
${esc(item.sku)}
${esc(item.product_name || '-')}
${renderCodmatCell(item)}
${item.quantity || 0}
${item.price != null ? fmtNum(item.price) : '-'}
${pretRoaHtml}
${item.vat != null ? Number(item.vat) : '-'}
${fmtNum(valoare)}
${matchDot}
`;
}).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;
_renderReceipt(items, order);
// Retry button (only for ERROR/SKIPPED orders)
const retryBtn = document.getElementById('detailRetryBtn');
if (retryBtn) {
const canRetry = ['ERROR', 'SKIPPED'].includes((order.status || '').toUpperCase());
retryBtn.style.display = canRetry ? '' : 'none';
if (canRetry) {
retryBtn.onclick = async () => {
retryBtn.disabled = true;
retryBtn.innerHTML = ' Reimportare...';
try {
const res = await fetch(`/api/orders/${encodeURIComponent(orderNumber)}/retry`, { method: 'POST' });
const data = await res.json();
if (data.success) {
retryBtn.innerHTML = ' ' + (data.message || 'Reimportat');
retryBtn.className = 'btn btn-sm btn-success';
// Refresh modal after short delay
setTimeout(() => renderOrderDetailModal(orderNumber, opts), 1500);
} else {
retryBtn.innerHTML = ' ' + (data.message || 'Eroare');
retryBtn.className = 'btn btn-sm btn-danger';
setTimeout(() => {
retryBtn.innerHTML = ' Reimporta';
retryBtn.className = 'btn btn-sm btn-outline-primary';
retryBtn.disabled = false;
}, 3000);
}
} catch (err) {
retryBtn.innerHTML = 'Eroare: ' + err.message;
retryBtn.disabled = false;
}
};
}
}
if (opts.onAfterRender) opts.onAfterRender(order, items);
} catch (err) {
document.getElementById('detailError').textContent = err.message;
document.getElementById('detailError').style.display = '';
}
}
// Global quick map dispatcher — set by each page
let _sharedModalQuickMapFn = null;
function _sharedModalQuickMap(sku, productName, orderNumber, itemIdx) {
if (_sharedModalQuickMapFn) _sharedModalQuickMapFn(sku, productName, orderNumber, itemIdx);
}
// ── Dot helper ────────────────────────────────────
function statusDot(status) {
switch ((status || '').toUpperCase()) {
case 'IMPORTED':
case 'ALREADY_IMPORTED':
case 'COMPLETED':
case 'RESOLVED':
return ' ';
case 'SKIPPED':
case 'UNRESOLVED':
case 'INCOMPLETE':
return ' ';
case 'ERROR':
case 'FAILED':
return ' ';
case 'CANCELLED':
case 'DELETED_IN_ROA':
return ' ';
default:
return ' ';
}
}
// ── Address helpers (module scope) ───────────────
function fmtAddr(a) {
if (!a) return '\u2014';
if (typeof a === 'string') return a;
const parts = [a.address || a.strada || '', a.numar || ''].filter(Boolean);
const extras = [
a.bloc ? 'Bl.' + a.bloc : '',
a.scara ? 'Sc.' + a.scara : '',
a.apart ? 'Ap.' + a.apart : '',
a.etaj ? 'Et.' + a.etaj : '',
].filter(Boolean).join(' ');
if (extras) parts.push(extras);
const line1 = parts.join(' ').trim();
const line2 = [a.city || a.localitate || '', a.region || a.judet || ''].filter(Boolean).join(', ');
return [line1, line2].filter(Boolean).join(', ');
}
function addrMatch(gomag, roa) {
if (!gomag || !roa) return true; // can't compare
function norm(s) {
return (s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.toUpperCase()
.replace(/\b(STR|STRADA|NR|NUMAR|NUMARUL|BL|BLOC|SC|SCARA|AP|APART|APARTAMENT|ET|ETAJ|COM|COMUNA|SAT|MUN|MUNICIPIUL|JUD|JUDETUL|CARTIER|PARTER|SECTOR|SECTORUL|ORAS)(?:\b|(?=\d))/g, '')
.replace(/[^A-Z0-9]/g, '');
}
const gStreet = norm(gomag.address || gomag.strada || '');
const rStreet = norm((roa.strada||'') + (roa.numar||'') + (roa.bloc||'') + (roa.scara||'') + (roa.apart||'') + (roa.etaj||''));
const gCity = norm(gomag.city || gomag.localitate || '');
const rCity = norm(roa.localitate || '');
const gRegion = norm(gomag.region || gomag.judet || '');
const rRegion = norm(roa.judet || '');
return gStreet === rStreet && gCity === rCity && gRegion === rRegion;
}
function hasEfacturaRisk(roa) {
if (!roa || typeof roa === 'string') return false;
return !roa.judet || !roa.localitate;
}
// ── Compact Header Info Rendering ────────────────
function _renderHeaderInfo(order) {
const pi = order.partner_info;
const isPJ = pi && pi.cod_fiscal_gomag;
// GoMag CUI (PJ only)
if (isPJ) {
const cuiGomagEl = document.getElementById('detailCuiGomag');
if (cuiGomagEl) {
document.getElementById('detailCuiGomagVal').textContent = pi.cod_fiscal_gomag;
cuiGomagEl.style.display = '';
}
}
// ROA column — show partner name for both PJ and PF
if (pi && pi.denumire_roa) {
const partenerRoa = document.getElementById('detailPartenerRoa');
if (partenerRoa) {
partenerRoa.textContent = pi.denumire_roa;
partenerRoa.style.display = '';
}
}
if (isPJ) {
const cuiRoaEl = document.getElementById('detailCuiRoa');
if (cuiRoaEl) {
document.getElementById('detailCuiRoaVal').textContent = pi.cod_fiscal_roa || '\u2014';
cuiRoaEl.style.display = '';
// CUI correction badge: +RO (added prefix) or −RO (removed prefix)
let cuiCorrHtml = '';
if (pi.anaf_cod_fiscal_adjusted && pi.cod_fiscal_gomag && pi.cod_fiscal_roa) {
const gomagHasRO = /^RO\s*/i.test(pi.cod_fiscal_gomag);
const roaHasRO = /^RO\s*/i.test(pi.cod_fiscal_roa);
if (!gomagHasRO && roaHasRO) {
cuiCorrHtml = ' +RO ';
} else if (gomagHasRO && !roaHasRO) {
cuiCorrHtml = ' −RO ';
}
}
// ANAF badge
const anafArea = document.getElementById('detailPartnerAnafArea');
if (anafArea) {
let anafBadge;
if (pi.anaf_platitor_tva === 1) {
anafBadge = 'Platitor TVA ';
} else if (pi.anaf_platitor_tva === 0) {
anafBadge = 'Neplatitor TVA ';
} else {
anafBadge = '? ';
}
anafArea.innerHTML = cuiCorrHtml + ' ' + anafBadge;
}
}
} else {
// PF indicator — show muted text in CUI area
const cuiRoaEl = document.getElementById('detailCuiRoa');
if (cuiRoaEl) {
document.getElementById('detailCuiRoaVal').textContent = '';
document.getElementById('detailPartnerAnafArea').innerHTML = '';
cuiRoaEl.innerHTML = 'Persoana fizica ';
cuiRoaEl.style.display = '';
}
}
// ERROR orders: muted dashes for ROA fields
if (order.status === 'ERROR' && !order.id_comanda) {
document.getElementById('detailIdComanda').innerHTML = '\u2014 ';
document.getElementById('detailIdPartener').innerHTML = '\u2014 ';
}
// Denomination mismatch alert
if (isPJ && pi.anaf_denumire_mismatch && pi.denumire_anaf) {
const denomEl = document.getElementById('detailDenomMismatch');
if (denomEl) {
denomEl.innerHTML = `
Denumire diferita
GoMag: ${esc(order.customer_name || '')}
ANAF: ${esc(pi.denumire_anaf)}
`;
denomEl.style.display = '';
}
}
// Compact address lines
const addr = order.addresses;
if (!addr || (!addr.livrare_gomag && !addr.facturare_gomag)) return;
const addressBlock = document.getElementById('detailAddressBlock');
const addressLines = document.getElementById('detailAddressLines');
if (!addressBlock || !addressLines) return;
addressBlock.style.display = '';
let html = '';
function addrLine(label, addrObj, matchIcon) {
const text = fmtAddr(addrObj);
const escaped = esc(text);
let icon = '';
if (matchIcon === 'match') {
icon = ' ';
} else if (matchIcon === 'mismatch') {
icon = ' ';
} else if (matchIcon === 'risk') {
icon = ' ';
}
return `
${label}
${escaped} ${icon}
`;
}
// Livrare
if (addr.livrare_gomag || addr.livrare_roa) {
html += addrLine('Livrare GoMag:', addr.livrare_gomag, null);
const livrRisk = hasEfacturaRisk(addr.livrare_roa);
const livrMatch = addrMatch(addr.livrare_gomag, addr.livrare_roa);
let matchType = null;
if (addr.livrare_roa) {
matchType = livrRisk ? 'risk' : (livrMatch ? 'match' : 'mismatch');
}
html += addrLine('Livrare ROA:', addr.livrare_roa, matchType);
}
// Facturare
if (addr.facturare_gomag || addr.facturare_roa) {
html += addrLine('Facturare GoMag:', addr.facturare_gomag, null);
const factRisk = hasEfacturaRisk(addr.facturare_roa);
const factMatch = addrMatch(addr.facturare_gomag, addr.facturare_roa);
let matchType = null;
if (addr.facturare_roa) {
matchType = factRisk ? 'risk' : (factMatch ? 'match' : 'mismatch');
}
html += addrLine('Facturare ROA:', addr.facturare_roa, matchType);
}
addressLines.innerHTML = html;
// Typed diff badges in modal header
const orderNumEl = document.getElementById('detailOrderNumber');
if (orderNumEl) {
orderNumEl.parentNode.querySelectorAll('.diff-badge').forEach(b => b.remove());
const badges = [];
if (isPJ && pi.anaf_cod_fiscal_adjusted) badges.push({label:'CUI', cls:'diff-badge-anaf', aria:'CUI ajustat conform ANAF'});
if (isPJ && pi.anaf_denumire_mismatch) badges.push({label:'Denumire', cls:'diff-badge-denumire', aria:'Denumire diferita fata de ANAF'});
if (isPJ && !pi.anaf_cod_fiscal_adjusted && pi.anaf_platitor_tva !== null && pi.anaf_platitor_tva !== undefined) {
const gomagImpliesPlatitor = pi.cod_fiscal_gomag && /^RO/i.test(pi.cod_fiscal_gomag);
const anafPlatitor = pi.anaf_platitor_tva === 1;
if (gomagImpliesPlatitor !== anafPlatitor) badges.push({label:'TVA', cls:'diff-badge-anaf', aria: anafPlatitor ? 'Platitor TVA conform ANAF (GoMag fara RO)' : 'Neplatitor TVA conform ANAF (GoMag cu RO)'});
}
if (addr && addr.livrare_roa && !addrMatch(addr.livrare_gomag, addr.livrare_roa)) badges.push({label:'Adr. livr.', cls:'diff-badge-addr', aria:'Adresa livrare diferita'});
if (addr && addr.facturare_roa && !addrMatch(addr.facturare_gomag, addr.facturare_roa)) badges.push({label:'Adr. fact.', cls:'diff-badge-addr', aria:'Adresa facturare diferita'});
if (order.price_check && order.price_check.mismatches > 0) badges.push({label:'Preturi (' + order.price_check.mismatches + ')', cls:'diff-badge-price', aria:'Preturi diferite: ' + order.price_check.mismatches});
let insertAfter = orderNumEl;
badges.forEach(b => {
const el = document.createElement('span');
el.className = 'diff-badge ' + b.cls;
el.setAttribute('aria-label', b.aria);
el.textContent = b.label;
insertAfter.parentNode.insertBefore(el, insertAfter.nextSibling);
insertAfter = el;
});
}
}