From 327f0e6ea256d0814b1b57fc02a6f872146b799e Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Thu, 19 Mar 2026 23:21:43 +0000 Subject: [PATCH] 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 + 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) --- api/app/static/css/style.css | 15 ++- api/app/static/js/dashboard.js | 157 +++------------------------- api/app/static/js/logs.js | 137 ++---------------------- api/app/static/js/mappings.js | 18 ++-- api/app/static/js/shared.js | 148 ++++++++++++++++++++++++++ api/app/templates/base.html | 36 ++++++- api/app/templates/dashboard.html | 35 +------ api/app/templates/logs.html | 29 +---- api/app/templates/mappings.html | 26 +++-- api/app/templates/missing_skus.html | 147 ++------------------------ 10 files changed, 247 insertions(+), 501 deletions(-) diff --git a/api/app/static/css/style.css b/api/app/static/css/style.css index 31f0b8a..55cd285 100644 --- a/api/app/static/css/style.css +++ b/api/app/static/css/style.css @@ -356,11 +356,16 @@ body { .qm-row { display: flex; gap: 6px; align-items: center; } .qm-codmat-wrap { flex: 1; min-width: 0; } .qm-rm-btn { padding: 2px 6px; line-height: 1; } -#qmCodmatLines .qm-selected:empty { display: none; } -#quickMapModal .modal-body { padding-top: 12px; padding-bottom: 8px; } -#quickMapModal .modal-header { padding: 10px 16px; } -#quickMapModal .modal-header h5 { font-size: 0.95rem; margin: 0; } -#quickMapModal .modal-footer { padding: 8px 16px; } +#qmCodmatLines .qm-selected:empty, +#codmatLines .qm-selected:empty { display: none; } +#quickMapModal .modal-body, +#addModal .modal-body { padding-top: 12px; padding-bottom: 8px; } +#quickMapModal .modal-header, +#addModal .modal-header { padding: 10px 16px; } +#quickMapModal .modal-header h5, +#addModal .modal-header h5 { font-size: 0.95rem; margin: 0; } +#quickMapModal .modal-footer, +#addModal .modal-footer { padding: 8px 16px; } /* ── Deleted mapping rows ────────────────────────── */ tr.mapping-deleted td { diff --git a/api/app/static/js/dashboard.js b/api/app/static/js/dashboard.js index 3f7b2fc..f078a9f 100644 --- a/api/app/static/js/dashboard.js +++ b/api/app/static/js/dashboard.js @@ -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 `
- ${esc(item.sku)} + ${esc(item.sku)} ${codmatText}
@@ -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 ` - ${esc(item.sku)} + ${esc(item.sku)} ${esc(item.product_name || '-')} ${renderCodmatCell(item)} ${item.quantity || 0} @@ -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 = ` SKU = CODMAT direct in nomenclator (${escHtml(details[0].codmat)} — ${escHtml(details[0].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'; - - // 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 = ` -
-
- -
-
- - ${idx > 0 ? `` : ''} -
-
${escHtml(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('.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); - } + }); } diff --git a/api/app/static/js/logs.js b/api/app/static/js/logs.js index 6cf8a97..b70dedb 100644 --- a/api/app/static/js/logs.js +++ b/api/app/static/js/logs.js @@ -5,8 +5,6 @@ let runsPage = 1; let logPollTimer = null; let currentFilter = 'all'; let ordersPage = 1; -let currentQmSku = ''; -let currentQmOrderNumber = ''; let ordersSortColumn = 'order_date'; let ordersSortDirection = 'desc'; @@ -384,8 +382,8 @@ async function openOrderDetail(orderNumber) { if (mobileContainer) { mobileContainer.innerHTML = '
' + items.map((item, idx) => { const codmatList = item.codmat_details?.length - ? item.codmat_details.map(d => `${esc(d.codmat)}`).join(' ') - : `${esc(item.codmat || '–')}`; + ? item.codmat_details.map(d => `${esc(d.codmat)}`).join(' ') + : `${esc(item.codmat || '–')}`; const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2); return `
@@ -403,7 +401,7 @@ async function openOrderDetail(orderNumber) { document.getElementById('detailItemsBody').innerHTML = items.map(item => { const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2); - const codmatCell = `${renderCodmatCell(item)}`; + const codmatCell = `${renderCodmatCell(item)}`; return ` ${esc(item.sku)} ${esc(item.product_name || '-')} @@ -419,130 +417,17 @@ async function openOrderDetail(orderNumber) { } } -// ── Quick Map Modal (from order detail) ────────── +// ── Quick Map Modal (uses shared openQuickMap) ─── -let qmAcTimeout = null; - -function openQuickMap(sku, productName, orderNumber) { - currentQmSku = sku; - currentQmOrderNumber = orderNumber; - document.getElementById('qmSku').textContent = sku; - document.getElementById('qmProductName').textContent = productName || '-'; - document.getElementById('qmPctWarning').style.display = 'none'; - - // Reset CODMAT lines - const container = document.getElementById('qmCodmatLines'); - container.innerHTML = ''; - addQmCodmatLine(); - - // Show quick map on top of order detail (modal stacking) - new bootstrap.Modal(document.getElementById('quickMapModal')).show(); -} - -function addQmCodmatLine() { - const container = document.getElementById('qmCodmatLines'); - const idx = container.children.length; - const div = document.createElement('div'); - div.className = 'border rounded p-2 mb-2 qm-line'; - div.innerHTML = ` -
- - -
- -
-
-
- - -
-
- ${idx > 0 ? `` : ''} -
-
- `; - container.appendChild(div); - - // Setup autocomplete on the new input - 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('.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(); - // Refresh order detail items in the still-open modal - if (currentQmOrderNumber) openOrderDetail(currentQmOrderNumber); - // Refresh orders view +function openLogsQuickMap(sku, productName, orderNumber) { + openQuickMap({ + sku, + productName, + onSave: () => { + if (orderNumber) openOrderDetail(orderNumber); loadRunOrders(currentRunId, currentFilter, ordersPage); - } else { - alert('Eroare: ' + (data.error || 'Unknown')); } - } catch (err) { - alert('Eroare: ' + err.message); - } + }); } // ── Init ──────────────────────────────────────── diff --git a/api/app/static/js/mappings.js b/api/app/static/js/mappings.js index 5a7c5d7..fdd4158 100644 --- a/api/app/static/js/mappings.js +++ b/api/app/static/js/mappings.js @@ -368,21 +368,17 @@ function addCodmatLine() { const container = document.getElementById('codmatLines'); const idx = container.children.length; const div = document.createElement('div'); - div.className = 'border rounded p-2 mb-2 codmat-line'; + div.className = 'qm-line codmat-line'; div.innerHTML = ` -
-
- +
+
+
- -
-
- -
-
- ${idx > 0 ? `` : '
'}
+ + ${idx > 0 ? `` : ''}
+
`; container.appendChild(div); diff --git a/api/app/static/js/shared.js b/api/app/static/js/shared.js index 075cc32..5486eb2 100644 --- a/api/app/static/js/shared.js +++ b/api/app/static/js/shared.js @@ -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 = ` 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 = ` +
+
+ +
+
+ + ${idx > 0 ? `` : ''} +
+
${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); + } else { + alert('Eroare: ' + (data.error || 'Unknown')); + } + } catch (err) { + alert('Eroare: ' + err.message); + } +} + // ── Dot helper ──────────────────────────────────── function statusDot(status) { switch ((status || '').toUpperCase()) { diff --git a/api/app/templates/base.html b/api/app/templates/base.html index b4f5ea4..d6918a0 100644 --- a/api/app/templates/base.html +++ b/api/app/templates/base.html @@ -7,7 +7,7 @@ {% set rp = request.scope.get('root_path', '') %} - + @@ -27,9 +27,41 @@ {% block content %}{% endblock %} + + + - + {% block scripts %}{% endblock %} diff --git a/api/app/templates/dashboard.html b/api/app/templates/dashboard.html index 2c5ab21..8a01f5a 100644 --- a/api/app/templates/dashboard.html +++ b/api/app/templates/dashboard.html @@ -165,41 +165,8 @@
- {% endblock %} {% block scripts %} - + {% endblock %} diff --git a/api/app/templates/logs.html b/api/app/templates/logs.html index 1f8b7ef..cc6eb8f 100644 --- a/api/app/templates/logs.html +++ b/api/app/templates/logs.html @@ -151,37 +151,10 @@
- - {% endblock %} {% block scripts %} - + {% endblock %} diff --git a/api/app/templates/mappings.html b/api/app/templates/mappings.html index bc53c93..fb2bf15 100644 --- a/api/app/templates/mappings.html +++ b/api/app/templates/mappings.html @@ -61,27 +61,31 @@
-