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:
Claude Agent
2026-03-19 23:21:43 +00:00
parent c806ca2d81
commit 327f0e6ea2
10 changed files with 247 additions and 501 deletions

View File

@@ -204,6 +204,154 @@ function renderMobileSegmented(containerId, pills, onSelect) {
});
}
// ── 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> &mdash; <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);
}
}
// ── Dot helper ────────────────────────────────────
function statusDot(status) {
switch ((status || '').toUpperCase()) {