refactor(ui): unify mapping form into single shared component
Extract the SKU mapping modal (HTML + JS) from dashboard, logs, and missing_skus into a shared component in base.html + shared.js. All pages now use the same compact layout with CODMAT/Cant. column headers. - Fix missing_skus backdrop bug: event.stopPropagation() on icon click prevents double modal open from <a> + <tr> event bubbling - Shrink mappings addModal from modal-lg to regular size with compact layout - Remove ~500 lines of duplicated modal HTML and JS across 4 pages - Each page keeps a thin wrapper (openDashQuickMap, openLogsQuickMap, openMapModal) that calls shared openQuickMap() with an onSave callback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,6 @@ let dashPerPage = 50;
|
||||
let dashSortCol = 'order_date';
|
||||
let dashSortDir = 'desc';
|
||||
let dashSearchTimeout = null;
|
||||
let currentQmSku = '';
|
||||
let currentQmOrderNumber = '';
|
||||
let qmAcTimeout = null;
|
||||
|
||||
// Sync polling state
|
||||
let _pollInterval = null;
|
||||
let _lastSyncStatus = null;
|
||||
@@ -591,7 +587,7 @@ async function openDashOrderDetail(orderNumber) {
|
||||
const valoare = (Number(item.price || 0) * Number(item.quantity || 0));
|
||||
return `<div class="dif-item">
|
||||
<div class="dif-row">
|
||||
<span class="dif-sku dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}', ${idx})">${esc(item.sku)}</span>
|
||||
<span class="dif-sku dif-codmat-link" onclick="openDashQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}', ${idx})">${esc(item.sku)}</span>
|
||||
${codmatText}
|
||||
</div>
|
||||
<div class="dif-row">
|
||||
@@ -649,7 +645,7 @@ async function openDashOrderDetail(orderNumber) {
|
||||
let tableHtml = items.map((item, idx) => {
|
||||
const valoare = Number(item.price || 0) * Number(item.quantity || 0);
|
||||
return `<tr>
|
||||
<td><code class="codmat-link" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}', ${idx})" title="Click pentru mapare">${esc(item.sku)}</code></td>
|
||||
<td><code class="codmat-link" onclick="openDashQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}', ${idx})" title="Click pentru mapare">${esc(item.sku)}</code></td>
|
||||
<td>${esc(item.product_name || '-')}</td>
|
||||
<td>${renderCodmatCell(item)}</td>
|
||||
<td class="text-end">${item.quantity || 0}</td>
|
||||
@@ -757,150 +753,23 @@ function renderReceipt(items, order) {
|
||||
mobile.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Quick Map Modal ───────────────────────────────
|
||||
// ── Quick Map Modal (uses shared openQuickMap) ───
|
||||
|
||||
function openQuickMap(sku, productName, orderNumber, itemIdx) {
|
||||
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 = '';
|
||||
|
||||
// Check if this is a direct SKU (SKU=CODMAT in NOM_ARTICOLE)
|
||||
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;
|
||||
const directInfo = document.getElementById('qmDirectInfo');
|
||||
const saveBtn = document.getElementById('qmSaveBtn');
|
||||
|
||||
if (isDirect) {
|
||||
if (directInfo) {
|
||||
directInfo.innerHTML = `<i class="bi bi-info-circle"></i> SKU = CODMAT direct in nomenclator (<code>${escHtml(details[0].codmat)}</code> — ${escHtml(details[0].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';
|
||||
|
||||
// Pre-populate with existing codmat_details if available
|
||||
if (details && details.length > 0) {
|
||||
details.forEach(d => {
|
||||
addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate_roa, 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="${escHtml(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">${escHtml(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('.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; }
|
||||
|
||||
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 })
|
||||
});
|
||||
} 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);
|
||||
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();
|
||||
} else {
|
||||
const msg = data.detail || data.error || 'Unknown';
|
||||
document.getElementById('qmPctWarning').textContent = msg;
|
||||
document.getElementById('qmPctWarning').style.display = '';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Eroare: ' + err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user