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

@@ -7,7 +7,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
{% set rp = request.scope.get('root_path', '') %}
<link href="{{ rp }}/static/css/style.css?v=14" rel="stylesheet">
<link href="{{ rp }}/static/css/style.css?v=16" rel="stylesheet">
</head>
<body>
<!-- Top Navbar -->
@@ -27,9 +27,41 @@
{% block content %}{% endblock %}
</main>
<!-- Shared Quick Map Modal -->
<div class="modal fade" id="quickMapModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Mapeaza SKU: <code id="qmSku"></code></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div style="margin-bottom:8px; font-size:0.85rem">
<small class="text-muted">Produs:</small> <strong id="qmProductName"></strong>
</div>
<div class="qm-row" style="font-size:0.7rem; color:#9ca3af; padding:0 0 2px">
<span style="flex:1">CODMAT</span>
<span style="width:70px">Cant.</span>
<span style="width:30px"></span>
</div>
<div id="qmCodmatLines"></div>
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="addQmCodmatLine()" style="font-size:0.8rem; padding:2px 10px">
+ CODMAT
</button>
<div id="qmDirectInfo" class="alert alert-info mt-2" style="display:none; font-size:0.85rem; padding:8px 12px;"></div>
<div id="qmPctWarning" class="text-danger mt-2" style="display:none;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button>
<button type="button" class="btn btn-primary" id="qmSaveBtn" onclick="saveQuickMapping()">Salveaza</button>
</div>
</div>
</div>
</div>
<script>window.ROOT_PATH = "{{ rp }}";</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ rp }}/static/js/shared.js?v=11"></script>
<script src="{{ rp }}/static/js/shared.js?v=12"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -165,41 +165,8 @@
</div>
<!-- Quick Map Modal (used from order detail) -->
<div class="modal fade" id="quickMapModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Mapeaza SKU: <code id="qmSku"></code></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div style="margin-bottom:8px; font-size:0.85rem">
<small class="text-muted">Produs:</small> <strong id="qmProductName"></strong>
</div>
<div class="qm-row" style="font-size:0.7rem; color:#9ca3af; padding:0 0 2px">
<span style="flex:1">CODMAT</span>
<span style="width:70px">Cant.</span>
<span style="width:70px">%</span>
<span style="width:30px"></span>
</div>
<div id="qmCodmatLines">
<!-- Dynamic CODMAT lines -->
</div>
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="addQmCodmatLine()" style="font-size:0.8rem; padding:2px 10px">
+ CODMAT
</button>
<div id="qmDirectInfo" class="alert alert-info mt-2" style="display:none; font-size:0.85rem; padding:8px 12px;"></div>
<div id="qmPctWarning" class="text-danger mt-2" style="display:none;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button>
<button type="button" class="btn btn-primary" id="qmSaveBtn" onclick="saveQuickMapping()">Salveaza</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=22"></script>
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=24"></script>
{% endblock %}

View File

@@ -151,37 +151,10 @@
</div>
<!-- Quick Map Modal (used from order detail) -->
<div class="modal fade" id="quickMapModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Mapeaza SKU: <code id="qmSku"></code></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<small class="text-muted">Produs web:</small> <strong id="qmProductName"></strong>
</div>
<div id="qmCodmatLines">
<!-- Dynamic CODMAT lines -->
</div>
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="addQmCodmatLine()">
<i class="bi bi-plus"></i> Adauga CODMAT
</button>
<div id="qmPctWarning" class="text-danger mt-2" style="display:none;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button>
<button type="button" class="btn btn-primary" onclick="saveQuickMapping()">Salveaza</button>
</div>
</div>
</div>
</div>
<!-- Hidden field for pre-selected run from URL/server -->
<input type="hidden" id="preselectedRun" value="{{ selected_run }}">
{% endblock %}
{% block scripts %}
<script src="{{ request.scope.get('root_path', '') }}/static/js/logs.js?v=10"></script>
<script src="{{ request.scope.get('root_path', '') }}/static/js/logs.js?v=11"></script>
{% endblock %}

View File

@@ -61,27 +61,31 @@
</div>
<!-- Add/Edit Modal with multi-CODMAT support (R11) -->
<div class="modal fade" id="addModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal fade" id="addModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addModalTitle">Adauga Mapare</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">SKU</label>
<input type="text" class="form-control" id="inputSku" placeholder="Ex: 8714858124284">
<div class="mb-2">
<label class="form-label form-label-sm mb-1">SKU</label>
<input type="text" class="form-control form-control-sm" id="inputSku" placeholder="Ex: 8714858124284">
</div>
<div class="mb-2" id="addModalProductName" style="display:none;">
<small class="text-muted">Produs web:</small> <strong id="inputProductName"></strong>
<div id="addModalProductName" style="display:none; margin-bottom:8px; font-size:0.85rem">
<small class="text-muted">Produs:</small> <strong id="inputProductName"></strong>
</div>
<div class="qm-row" style="font-size:0.7rem; color:#9ca3af; padding:0 0 2px">
<span style="flex:1">CODMAT</span>
<span style="width:70px">Cant.</span>
<span style="width:30px"></span>
</div>
<hr>
<div id="codmatLines">
<!-- Dynamic CODMAT lines will be added here -->
</div>
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="addCodmatLine()">
<i class="bi bi-plus"></i> Adauga CODMAT
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="addCodmatLine()" style="font-size:0.8rem; padding:2px 10px">
+ CODMAT
</button>
<div id="pctWarning" class="text-danger mt-2" style="display:none;"></div>
</div>
@@ -146,5 +150,5 @@
{% endblock %}
{% block scripts %}
<script src="{{ request.scope.get('root_path', '') }}/static/js/mappings.js?v=9"></script>
<script src="{{ request.scope.get('root_path', '') }}/static/js/mappings.js?v=10"></script>
{% endblock %}

View File

@@ -65,39 +65,10 @@
</div>
<div id="skusPagBottom" class="pag-strip pag-strip-bottom"></div>
<!-- Map SKU Modal with multi-CODMAT support (R11) -->
<div class="modal fade" id="mapModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Mapeaza SKU: <code id="mapSku"></code></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<small class="text-muted">Produs web:</small> <strong id="mapProductName"></strong>
</div>
<div id="mapCodmatLines">
<!-- Dynamic CODMAT lines -->
</div>
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="addMapCodmatLine()">
<i class="bi bi-plus"></i> Adauga CODMAT
</button>
<div id="mapPctWarning" class="text-danger mt-2" style="display:none;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button>
<button type="button" class="btn btn-primary" onclick="saveQuickMap()">Salveaza</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentMapSku = '';
let mapAcTimeout = null;
let currentPage = 1;
let skuStatusFilter = 'unresolved';
let missingPerPage = 20;
@@ -223,7 +194,7 @@ function renderMissingSkusTable(skus, data) {
<td class="truncate" style="max-width:300px">${esc(s.product_name || '-')}</td>
<td>
${!s.resolved
? `<a href="#" class="btn-map-icon" onclick="openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}'); return false;" title="Mapeaza">
? `<a href="#" class="btn-map-icon" onclick="event.stopPropagation(); openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}'); return false;" title="Mapeaza">
<i class="bi bi-link-45deg"></i>
</a>`
: `<small class="text-muted">${s.resolved_at ? new Date(s.resolved_at).toLocaleDateString('ro-RO') : ''}</small>`}
@@ -234,7 +205,7 @@ function renderMissingSkusTable(skus, data) {
if (mobileList) {
mobileList.innerHTML = skus.map(s => {
const actionHtml = !s.resolved
? `<a href="#" class="btn-map-icon" onclick="openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}'); return false;"><i class="bi bi-link-45deg"></i></a>`
? `<a href="#" class="btn-map-icon" onclick="event.stopPropagation(); openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}'); return false;"><i class="bi bi-link-45deg"></i></a>`
: `<small class="text-muted">${s.resolved_at ? new Date(s.resolved_at).toLocaleDateString('ro-RO') : ''}</small>`;
const flatRowAttrs = !s.resolved
? ` onclick="openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}')" style="cursor:pointer"`
@@ -259,118 +230,14 @@ function renderPagination(data) {
if (bot) bot.innerHTML = pagHtml;
}
// ── Multi-CODMAT Map Modal ───────────────────────
// ── Map Modal (uses shared openQuickMap) ─────────
function openMapModal(sku, productName) {
currentMapSku = sku;
document.getElementById('mapSku').textContent = sku;
document.getElementById('mapProductName').textContent = productName || '-';
document.getElementById('mapPctWarning').style.display = 'none';
const container = document.getElementById('mapCodmatLines');
container.innerHTML = '';
addMapCodmatLine();
new bootstrap.Modal(document.getElementById('mapModal')).show();
}
function addMapCodmatLine() {
const container = document.getElementById('mapCodmatLines');
const idx = container.children.length;
const div = document.createElement('div');
div.className = 'border rounded p-2 mb-2 mc-line';
div.innerHTML = `
<div class="row g-2 align-items-center">
<div class="col position-relative">
<input type="text" class="form-control form-control-sm mc-codmat" placeholder="Cauta CODMAT..." autocomplete="off">
<div class="autocomplete-dropdown d-none mc-ac-dropdown"></div>
<small class="text-muted mc-selected"></small>
</div>
<div class="col-auto" style="width:90px">
<input type="number" class="form-control form-control-sm mc-cantitate" value="1" step="0.001" min="0.001" placeholder="Cant." title="Cantitate ROA">
</div>
<div class="col-auto">
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('.mc-line').remove()"><i class="bi bi-x"></i></button>` : '<div style="width:31px"></div>'}
</div>
</div>
`;
container.appendChild(div);
const input = div.querySelector('.mc-codmat');
const dropdown = div.querySelector('.mc-ac-dropdown');
const selected = div.querySelector('.mc-selected');
input.addEventListener('input', () => {
clearTimeout(mapAcTimeout);
mapAcTimeout = setTimeout(() => mcAutocomplete(input, dropdown, selected), 250);
openQuickMap({
sku,
productName,
onSave: () => { loadMissingSkus(currentPage); }
});
input.addEventListener('blur', () => {
setTimeout(() => dropdown.classList.add('d-none'), 200);
});
}
async function mcAutocomplete(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="mcSelectArticle(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 mcSelectArticle(el, codmat, label) {
const line = el.closest('.mc-line');
line.querySelector('.mc-codmat').value = codmat;
line.querySelector('.mc-selected').textContent = label;
line.querySelector('.mc-ac-dropdown').classList.add('d-none');
}
async function saveQuickMap() {
const lines = document.querySelectorAll('.mc-line');
const mappings = [];
for (const line of lines) {
const codmat = line.querySelector('.mc-codmat').value.trim();
const cantitate = parseFloat(line.querySelector('.mc-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: currentMapSku, 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: currentMapSku, mappings })
});
}
const data = await res.json();
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('mapModal')).hide();
loadMissingSkus(currentPage);
} else {
alert('Eroare: ' + (data.error || 'Unknown'));
}
} catch (err) {
alert('Eroare: ' + err.message);
}
}
function exportMissingCsv() {