Move duplicated order detail modal logic from dashboard.js and logs.js into a shared renderOrderDetailModal() function in shared.js. Move modal HTML from dashboard.html and logs.html into base.html. Shared functions: renderCodmatCell, orderStatusBadge, fmtCost, fmtNum, computeDiscountSplit, renderReceipt. Both pages now call the shared modal with page-specific quick map callbacks. Net -152 lines. Logs page gains invoice info, TVA column, and receipt footer that were previously dashboard-only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
690 lines
30 KiB
JavaScript
690 lines
30 KiB
JavaScript
// 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, '"')
|
||
.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 = '<div class="d-flex align-items-center gap-2 flex-wrap">';
|
||
|
||
// Per-page selector
|
||
if (opts && opts.perPage && opts.perPageFn) {
|
||
const options = opts.perPageOptions || [25, 50, 100, 250];
|
||
html += `<label class="per-page-label">Per pagina: <select class="select-compact ms-1" onchange="${opts.perPageFn}(this.value)">`;
|
||
options.forEach(v => {
|
||
html += `<option value="${v}"${v === opts.perPage ? ' selected' : ''}>${v}</option>`;
|
||
});
|
||
html += '</select></label>';
|
||
}
|
||
|
||
if (totalPages <= 1) {
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
html += '<div class="pagination-bar">';
|
||
|
||
// First
|
||
html += `<button class="page-btn" onclick="${goToFnName}(1)" ${currentPage <= 1 ? 'disabled' : ''}>«</button>`;
|
||
// Prev
|
||
html += `<button class="page-btn" onclick="${goToFnName}(${currentPage - 1})" ${currentPage <= 1 ? 'disabled' : ''}>‹</button>`;
|
||
|
||
// Page numbers with ellipsis
|
||
const range = 2;
|
||
let pages = [];
|
||
for (let i = 1; i <= totalPages; i++) {
|
||
if (i === 1 || i === totalPages || (i >= currentPage - range && i <= currentPage + range)) {
|
||
pages.push(i);
|
||
}
|
||
}
|
||
|
||
let lastP = 0;
|
||
pages.forEach(p => {
|
||
if (lastP && p - lastP > 1) {
|
||
html += `<span class="page-btn disabled page-ellipsis">…</span>`;
|
||
}
|
||
html += `<button class="page-btn page-number${p === currentPage ? ' active' : ''}" onclick="${goToFnName}(${p})">${p}</button>`;
|
||
lastP = p;
|
||
});
|
||
|
||
// Next
|
||
html += `<button class="page-btn" onclick="${goToFnName}(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled' : ''}>›</button>`;
|
||
// Last
|
||
html += `<button class="page-btn" onclick="${goToFnName}(${totalPages})" ${currentPage >= totalPages ? 'disabled' : ''}>»</button>`;
|
||
|
||
html += '</div></div>';
|
||
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 = `<div class="btn-group btn-group-sm w-100">${pills.map(p => {
|
||
const cls = p.active ? 'btn btn-primary' : 'btn btn-outline-secondary';
|
||
const countColor = (!p.active && p.colorClass) ? ` class="${p.colorClass}"` : '';
|
||
return `<button type="button" class="${cls}" style="${btnStyle}" data-seg-value="${esc(p.value)}">${esc(p.label)} <b${countColor}>${p.count}</b></button>`;
|
||
}).join('')}</div>`;
|
||
|
||
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 = `<i class="bi bi-info-circle"></i> SKU = CODMAT direct in nomenclator (<code>${esc(opts.directInfo.codmat)}</code> — ${esc(opts.directInfo.denumire || '')}).<br><small class="text-muted">Poti suprascrie cu un alt CODMAT daca e necesar (ex: reambalare).</small>`;
|
||
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 = `
|
||
<div class="qm-row">
|
||
<div class="qm-codmat-wrap position-relative">
|
||
<input type="text" class="form-control form-control-sm qm-codmat" placeholder="CODMAT..." autocomplete="off" value="${esc(codmatVal)}">
|
||
<div class="autocomplete-dropdown d-none qm-ac-dropdown"></div>
|
||
</div>
|
||
<input type="number" class="form-control form-control-sm qm-cantitate" value="${cantVal}" step="0.001" min="0.001" title="Cantitate ROA" style="width:70px">
|
||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger qm-rm-btn" onclick="this.closest('.qm-line').remove()"><i class="bi bi-x"></i></button>` : '<span style="width:30px"></span>'}
|
||
</div>
|
||
<div class="qm-selected text-muted" style="font-size:0.75rem;padding-left:2px">${esc(denumireVal)}</div>
|
||
`;
|
||
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 =>
|
||
`<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('#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);
|
||
} 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 '<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>';
|
||
case 'CANCELLED': return '<span class="badge bg-secondary">Anulat</span>';
|
||
case 'DELETED_IN_ROA': return '<span class="badge bg-dark">Sters din ROA</span>';
|
||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||
}
|
||
}
|
||
|
||
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];
|
||
if (d.direct) {
|
||
return `<code>${esc(d.codmat)}</code> <span class="badge bg-secondary" style="font-size:0.6rem;vertical-align:middle">direct</span>`;
|
||
}
|
||
return `<code>${esc(d.codmat)}</code>`;
|
||
}
|
||
return item.codmat_details.map(d =>
|
||
`<div class="small"><code>${esc(d.codmat)}</code> <span class="text-muted">\xd7${d.cantitate_roa}</span></div>`
|
||
).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 = `<span class="text-muted">Articole: <strong class="text-body">${fmtNum(articole)}</strong></span>`;
|
||
if (discount > 0) dHtml += `<span class="text-muted">Discount: <strong class="text-danger">\u2013${fmtNum(discount)}</strong></span>`;
|
||
if (transport > 0) dHtml += `<span class="text-muted">Transport: <strong class="text-body">${fmtNum(transport)}</strong></span>`;
|
||
dHtml += `<span>Total: <strong>${total} lei</strong></span>`;
|
||
if (desktop) desktop.innerHTML = dHtml;
|
||
|
||
let mHtml = `<span class="text-muted">Art: <strong class="text-body">${fmtNum(articole)}</strong></span>`;
|
||
if (discount > 0) mHtml += `<span class="text-muted">Disc: <strong class="text-danger">\u2013${fmtNum(discount)}</strong></span>`;
|
||
if (transport > 0) mHtml += `<span class="text-muted">Transp: <strong class="text-body">${fmtNum(transport)}</strong></span>`;
|
||
mHtml += `<span>Total: <strong>${total} lei</strong></span>`;
|
||
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 || {};
|
||
|
||
// 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('detailIdAdresaFact').textContent = '-';
|
||
document.getElementById('detailIdAdresaLivr').textContent = '-';
|
||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="7" class="text-center">Se incarca...</td></tr>';
|
||
document.getElementById('detailError').style.display = 'none';
|
||
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 modalEl = document.getElementById('orderDetailModal');
|
||
const existing = bootstrap.Modal.getInstance(modalEl);
|
||
if (existing) { existing.show(); } else { new bootstrap.Modal(modalEl).show(); }
|
||
|
||
try {
|
||
const res = await fetch(`/api/sync/order/${encodeURIComponent(orderNumber)}`);
|
||
const data = await res.json();
|
||
|
||
if (data.error) {
|
||
document.getElementById('detailError').textContent = data.error;
|
||
document.getElementById('detailError').style.display = '';
|
||
return;
|
||
}
|
||
|
||
const order = data.order || {};
|
||
document.getElementById('detailCustomer').textContent = order.customer_name || '-';
|
||
document.getElementById('detailDate').textContent = fmtDate(order.order_date);
|
||
document.getElementById('detailStatus').innerHTML = orderStatusBadge(order.status);
|
||
document.getElementById('detailIdComanda').textContent = order.id_comanda || '-';
|
||
document.getElementById('detailIdPartener').textContent = order.id_partener || '-';
|
||
document.getElementById('detailIdAdresaFact').textContent = order.id_adresa_facturare || '-';
|
||
document.getElementById('detailIdAdresaLivr').textContent = order.id_adresa_livrare || '-';
|
||
|
||
// Invoice info
|
||
const 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 = '';
|
||
}
|
||
|
||
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 = '<tr><td colspan="7" class="text-center text-muted">Niciun articol</td></tr>';
|
||
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 => `<code>${esc(d.codmat)}</code>${d.direct ? ' <span class="badge bg-secondary" style="font-size:0.55rem">direct</span>' : ''}`).join(' ')
|
||
: `<code>${esc(item.codmat || '–')}</code>`;
|
||
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})"` : '';
|
||
return `<div class="dif-item">
|
||
<div class="dif-row">
|
||
<span class="dif-sku${opts.onQuickMap ? ' dif-codmat-link' : ''}" ${clickAttr}>${esc(item.sku)}</span>
|
||
${codmatText}
|
||
</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">${fmtNum(valoare)} lei</span>
|
||
<span class="dif-vat text-muted" style="font-size:0.75rem">TVA ${item.vat != null ? Number(item.vat) : '?'}</span>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
// Transport row (mobile)
|
||
if (order.delivery_cost > 0) {
|
||
const tVat = order.transport_vat || '21';
|
||
mobileHtml += `<div class="dif-item" style="opacity:0.7">
|
||
<div class="dif-row">
|
||
<span class="dif-name text-muted">Transport</span>
|
||
<span class="dif-qty">x1</span>
|
||
<span class="dif-val">${fmtNum(order.delivery_cost)} lei</span>
|
||
<span class="dif-vat text-muted" style="font-size:0.75rem">TVA ${tVat}</span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// 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 += `<div class="dif-item" style="opacity:0.7">
|
||
<div class="dif-row">
|
||
<span class="dif-name text-muted">Discount</span>
|
||
<span class="dif-qty">x\u20131</span>
|
||
<span class="dif-val">${fmtNum(amt)} lei</span>
|
||
<span class="dif-vat text-muted" style="font-size:0.75rem">TVA ${Number(rate)}</span>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
} else {
|
||
mobileHtml += `<div class="dif-item" style="opacity:0.7">
|
||
<div class="dif-row">
|
||
<span class="dif-name text-muted">Discount</span>
|
||
<span class="dif-qty">x\u20131</span>
|
||
<span class="dif-val">${fmtNum(order.discount_total)} lei</span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
}
|
||
|
||
mobileContainer.innerHTML = '<div class="detail-item-flat">' + mobileHtml + '</div>';
|
||
}
|
||
|
||
// 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);
|
||
return `<tr>
|
||
<td><code class="${opts.onQuickMap ? 'codmat-link' : ''}" ${clickAttrFn(item, idx)}>${esc(item.sku)}</code></td>
|
||
<td>${esc(item.product_name || '-')}</td>
|
||
<td>${renderCodmatCell(item)}</td>
|
||
<td class="text-end">${item.quantity || 0}</td>
|
||
<td class="text-end">${item.price != null ? fmtNum(item.price) : '-'}</td>
|
||
<td class="text-end">${item.vat != null ? Number(item.vat) : '-'}</td>
|
||
<td class="text-end">${fmtNum(valoare)}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
|
||
// Transport row
|
||
if (order.delivery_cost > 0) {
|
||
const tVat = order.transport_vat || '21';
|
||
const tCodmat = order.transport_codmat || '';
|
||
tableHtml += `<tr class="table-light">
|
||
<td></td><td class="text-muted">Transport</td>
|
||
<td>${tCodmat ? '<code>' + esc(tCodmat) + '</code>' : ''}</td>
|
||
<td class="text-end">1</td><td class="text-end">${fmtNum(order.delivery_cost)}</td>
|
||
<td class="text-end">${tVat}</td><td class="text-end">${fmtNum(order.delivery_cost)}</td>
|
||
</tr>`;
|
||
}
|
||
|
||
// 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 += `<tr class="table-light">
|
||
<td></td><td class="text-muted">Discount</td>
|
||
<td>${dCodmat ? '<code>' + esc(dCodmat) + '</code>' : ''}</td>
|
||
<td class="text-end">\u20131</td><td class="text-end">${fmtNum(amt)}</td>
|
||
<td class="text-end">${Number(rate)}</td><td class="text-end">\u2013${fmtNum(amt)}</td>
|
||
</tr>`;
|
||
});
|
||
} else {
|
||
tableHtml += `<tr class="table-light">
|
||
<td></td><td class="text-muted">Discount</td>
|
||
<td>${dCodmat ? '<code>' + esc(dCodmat) + '</code>' : ''}</td>
|
||
<td class="text-end">\u20131</td><td class="text-end">${fmtNum(order.discount_total)}</td>
|
||
<td class="text-end">-</td><td class="text-end">\u2013${fmtNum(order.discount_total)}</td>
|
||
</tr>`;
|
||
}
|
||
}
|
||
|
||
document.getElementById('detailItemsBody').innerHTML = tableHtml;
|
||
_renderReceipt(items, order);
|
||
|
||
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 '<span class="dot dot-green"></span>';
|
||
case 'SKIPPED':
|
||
case 'UNRESOLVED':
|
||
case 'INCOMPLETE':
|
||
return '<span class="dot dot-yellow"></span>';
|
||
case 'ERROR':
|
||
case 'FAILED':
|
||
return '<span class="dot dot-red"></span>';
|
||
case 'CANCELLED':
|
||
case 'DELETED_IN_ROA':
|
||
return '<span class="dot dot-gray"></span>';
|
||
default:
|
||
return '<span class="dot dot-gray"></span>';
|
||
}
|
||
}
|