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>
765 lines
32 KiB
JavaScript
765 lines
32 KiB
JavaScript
let currentPage = 1;
|
|
let mappingsPerPage = 50;
|
|
let currentSearch = '';
|
|
let searchTimeout = null;
|
|
let sortColumn = 'sku';
|
|
let sortDirection = 'asc';
|
|
let editingMapping = null; // {sku, codmat} when editing
|
|
|
|
const kitPriceCache = new Map();
|
|
|
|
// Load on page ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadMappings();
|
|
initAddModal();
|
|
initDeleteModal();
|
|
});
|
|
|
|
function debounceSearch() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
currentSearch = document.getElementById('searchInput').value;
|
|
currentPage = 1;
|
|
loadMappings();
|
|
}, 300);
|
|
}
|
|
|
|
// ── Sorting (R7) ─────────────────────────────────
|
|
|
|
function sortBy(col) {
|
|
if (sortColumn === col) {
|
|
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
sortColumn = col;
|
|
sortDirection = 'asc';
|
|
}
|
|
currentPage = 1;
|
|
loadMappings();
|
|
}
|
|
|
|
function updateSortIcons() {
|
|
document.querySelectorAll('.sort-icon').forEach(span => {
|
|
const col = span.dataset.col;
|
|
if (col === sortColumn) {
|
|
span.textContent = sortDirection === 'asc' ? '\u2191' : '\u2193';
|
|
} else {
|
|
span.textContent = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Load & Render ────────────────────────────────
|
|
|
|
async function loadMappings() {
|
|
const showInactive = document.getElementById('showInactive')?.checked;
|
|
const showDeleted = document.getElementById('showDeleted')?.checked;
|
|
const params = new URLSearchParams({
|
|
search: currentSearch,
|
|
page: currentPage,
|
|
per_page: mappingsPerPage,
|
|
sort_by: sortColumn,
|
|
sort_dir: sortDirection
|
|
});
|
|
if (showDeleted) params.set('show_deleted', 'true');
|
|
|
|
try {
|
|
const res = await fetch(`/api/mappings?${params}`);
|
|
const data = await res.json();
|
|
|
|
let mappings = data.mappings || [];
|
|
|
|
// Client-side filter for inactive unless toggle is on
|
|
// (keep deleted rows visible when showDeleted is on, even if inactive)
|
|
if (!showInactive) {
|
|
mappings = mappings.filter(m => m.activ || m.sters);
|
|
}
|
|
|
|
renderTable(mappings, showDeleted);
|
|
renderPagination(data);
|
|
updateSortIcons();
|
|
} catch (err) {
|
|
document.getElementById('mappingsFlatList').innerHTML =
|
|
`<div class="flat-row text-danger py-3 justify-content-center">Eroare: ${err.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function renderTable(mappings, showDeleted) {
|
|
const container = document.getElementById('mappingsFlatList');
|
|
|
|
if (!mappings || mappings.length === 0) {
|
|
container.innerHTML = '<div class="flat-row text-muted py-4 justify-content-center">Nu exista mapari</div>';
|
|
return;
|
|
}
|
|
|
|
// Count CODMATs per SKU for kit detection
|
|
const skuCodmatCount = {};
|
|
mappings.forEach(m => {
|
|
skuCodmatCount[m.sku] = (skuCodmatCount[m.sku] || 0) + 1;
|
|
});
|
|
|
|
let prevSku = null;
|
|
let html = '';
|
|
mappings.forEach((m, i) => {
|
|
const isNewGroup = m.sku !== prevSku;
|
|
if (isNewGroup) {
|
|
const isKit = (skuCodmatCount[m.sku] || 0) > 1;
|
|
const kitBadge = isKit
|
|
? ` <span class="text-muted small">Kit · ${skuCodmatCount[m.sku]}</span><span class="kit-price-loading" data-sku="${esc(m.sku)}" style="display:none"><span class="spinner-border spinner-border-sm ms-1" style="width:0.8rem;height:0.8rem"></span></span>`
|
|
: '';
|
|
const inactiveStyle = !m.activ && !m.sters ? 'opacity:0.6;' : '';
|
|
html += `<div class="flat-row" style="background:#f8fafc;font-weight:600;border-top:1px solid #e5e7eb;${inactiveStyle}">
|
|
<span class="${m.activ ? 'dot dot-green' : 'dot dot-yellow'}" style="cursor:${m.sters ? 'default' : 'pointer'}"
|
|
${m.sters ? '' : `onclick="event.stopPropagation();toggleActive('${esc(m.sku)}', '${esc(m.codmat)}', ${m.activ})"`}
|
|
title="${m.activ ? 'Activ' : 'Inactiv'}"></span>
|
|
<strong class="me-1 text-nowrap">${esc(m.sku)}</strong>${kitBadge}
|
|
<span class="grow truncate text-muted" style="font-size:0.875rem">${esc(m.product_name || '')}</span>
|
|
${m.sters
|
|
? `<button class="btn btn-sm btn-outline-success" onclick="event.stopPropagation();restoreMapping('${esc(m.sku)}', '${esc(m.codmat)}')" title="Restaureaza" style="padding:0.1rem 0.4rem"><i class="bi bi-arrow-counterclockwise"></i></button>`
|
|
: `<button class="context-menu-trigger" data-sku="${esc(m.sku)}" data-codmat="${esc(m.codmat)}" data-cantitate="${m.cantitate_roa}">⋮</button>`
|
|
}
|
|
</div>`;
|
|
}
|
|
const deletedStyle = m.sters ? 'text-decoration:line-through;opacity:0.5;' : '';
|
|
const isKitRow = (skuCodmatCount[m.sku] || 0) > 1;
|
|
const priceSlot = isKitRow ? `<span class="kit-price-slot text-muted small ms-2" data-sku="${esc(m.sku)}" data-codmat="${esc(m.codmat)}"></span>` : '';
|
|
html += `<div class="flat-row" style="padding-left:1.5rem;font-size:0.9rem;${deletedStyle}">
|
|
<code>${esc(m.codmat)}</code>
|
|
<span class="grow truncate text-muted" style="font-size:0.85rem">${esc(m.denumire || '')}</span>
|
|
<span class="text-nowrap" style="font-size:0.875rem">
|
|
<span class="${m.sters ? '' : 'editable'}" style="cursor:${m.sters ? 'default' : 'pointer'}"
|
|
${m.sters ? '' : `onclick="editFlatValue(this, '${esc(m.sku)}', '${esc(m.codmat)}', 'cantitate_roa', ${m.cantitate_roa})"`}>x${m.cantitate_roa}</span>${priceSlot}
|
|
</span>
|
|
</div>`;
|
|
|
|
// After last CODMAT of a kit, add total row
|
|
const isLastOfKit = isKitRow && (i === mappings.length - 1 || mappings[i + 1].sku !== m.sku);
|
|
if (isLastOfKit) {
|
|
html += `<div class="flat-row kit-total-slot text-muted small" data-sku="${esc(m.sku)}" style="padding-left:1.5rem;display:none;border-top:1px dashed #e5e7eb"></div>`;
|
|
}
|
|
|
|
prevSku = m.sku;
|
|
});
|
|
container.innerHTML = html;
|
|
|
|
// Wire context menu triggers
|
|
container.querySelectorAll('.context-menu-trigger').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const { sku, codmat, cantitate } = btn.dataset;
|
|
const rect = btn.getBoundingClientRect();
|
|
showContextMenu(rect.left, rect.bottom + 2, [
|
|
{ label: 'Editeaza', action: () => openEditModal(sku, codmat, parseFloat(cantitate)) },
|
|
{ label: 'Sterge', action: () => deleteMappingConfirm(sku, codmat), danger: true }
|
|
]);
|
|
});
|
|
});
|
|
|
|
// Load prices for visible kits
|
|
const loadedKits = new Set();
|
|
container.querySelectorAll('.kit-price-loading').forEach(el => {
|
|
const sku = el.dataset.sku;
|
|
if (!loadedKits.has(sku)) {
|
|
loadedKits.add(sku);
|
|
loadKitPrices(sku, container);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function loadKitPrices(sku, container) {
|
|
if (kitPriceCache.has(sku)) {
|
|
renderKitPrices(sku, kitPriceCache.get(sku), container);
|
|
return;
|
|
}
|
|
// Show loading spinner
|
|
const spinner = container.querySelector(`.kit-price-loading[data-sku="${CSS.escape(sku)}"]`);
|
|
if (spinner) spinner.style.display = '';
|
|
|
|
try {
|
|
const res = await fetch(`/api/mappings/${encodeURIComponent(sku)}/prices`);
|
|
const data = await res.json();
|
|
if (data.error) {
|
|
if (spinner) spinner.innerHTML = `<small class="text-danger">${esc(data.error)}</small>`;
|
|
return;
|
|
}
|
|
kitPriceCache.set(sku, data.prices || []);
|
|
renderKitPrices(sku, data.prices || [], container);
|
|
} catch (err) {
|
|
if (spinner) spinner.innerHTML = `<small class="text-danger">Eroare la încărcarea prețurilor</small>`;
|
|
}
|
|
}
|
|
|
|
function renderKitPrices(sku, prices, container) {
|
|
if (!prices || prices.length === 0) return;
|
|
// Update each codmat row with price info
|
|
const rows = container.querySelectorAll(`.kit-price-slot[data-sku="${CSS.escape(sku)}"]`);
|
|
let total = 0;
|
|
rows.forEach(slot => {
|
|
const codmat = slot.dataset.codmat;
|
|
const p = prices.find(pr => pr.codmat === codmat);
|
|
if (p && p.pret_cu_tva > 0) {
|
|
slot.innerHTML = `${p.pret_cu_tva.toFixed(2)} lei (${p.ptva}%)`;
|
|
total += p.pret_cu_tva * (p.cantitate_roa || 1);
|
|
} else if (p) {
|
|
slot.innerHTML = `<span class="text-muted">fără preț</span>`;
|
|
}
|
|
});
|
|
// Show total
|
|
const totalSlot = container.querySelector(`.kit-total-slot[data-sku="${CSS.escape(sku)}"]`);
|
|
if (totalSlot && total > 0) {
|
|
totalSlot.innerHTML = `Total componente: ${total.toFixed(2)} lei`;
|
|
totalSlot.style.display = '';
|
|
}
|
|
// Hide loading spinner
|
|
const spinner = container.querySelector(`.kit-price-loading[data-sku="${CSS.escape(sku)}"]`);
|
|
if (spinner) spinner.style.display = 'none';
|
|
}
|
|
|
|
// Inline edit for flat-row values (cantitate)
|
|
function editFlatValue(span, sku, codmat, field, currentValue) {
|
|
if (span.querySelector('input')) return;
|
|
|
|
const input = document.createElement('input');
|
|
input.type = 'number';
|
|
input.className = 'form-control form-control-sm d-inline';
|
|
input.value = currentValue;
|
|
input.step = field === 'cantitate_roa' ? '0.001' : '0.01';
|
|
input.style.width = '70px';
|
|
input.style.display = 'inline';
|
|
|
|
const originalText = span.textContent;
|
|
span.textContent = '';
|
|
span.appendChild(input);
|
|
input.focus();
|
|
input.select();
|
|
|
|
const save = async () => {
|
|
const newValue = parseFloat(input.value);
|
|
if (isNaN(newValue) || newValue === currentValue) {
|
|
span.textContent = originalText;
|
|
return;
|
|
}
|
|
try {
|
|
const body = {};
|
|
body[field] = newValue;
|
|
const res = await fetch(`/api/mappings/${encodeURIComponent(sku)}/${encodeURIComponent(codmat)}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) { loadMappings(); }
|
|
else { span.textContent = originalText; alert('Eroare: ' + (data.error || 'Update failed')); }
|
|
} catch (err) { span.textContent = originalText; }
|
|
};
|
|
|
|
input.addEventListener('blur', save);
|
|
input.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') { e.preventDefault(); save(); }
|
|
if (e.key === 'Escape') { span.textContent = originalText; }
|
|
});
|
|
}
|
|
|
|
function renderPagination(data) {
|
|
const pagOpts = { perPage: mappingsPerPage, perPageFn: 'mappingsChangePerPage', perPageOptions: [25, 50, 100, 250] };
|
|
const infoHtml = `<small class="text-muted me-auto">${data.total} mapari | Pagina ${data.page} din ${data.pages || 1}</small>`;
|
|
const pagHtml = infoHtml + renderUnifiedPagination(data.page, data.pages || 1, 'goPage', pagOpts);
|
|
const top = document.getElementById('mappingsPagTop');
|
|
const bot = document.getElementById('mappingsPagBottom');
|
|
if (top) top.innerHTML = pagHtml;
|
|
if (bot) bot.innerHTML = pagHtml;
|
|
}
|
|
|
|
function mappingsChangePerPage(val) { mappingsPerPage = parseInt(val) || 50; currentPage = 1; loadMappings(); }
|
|
|
|
function goPage(p) {
|
|
currentPage = p;
|
|
loadMappings();
|
|
}
|
|
|
|
// ── Multi-CODMAT Add Modal (R11) ─────────────────
|
|
|
|
let acTimeouts = {};
|
|
|
|
function initAddModal() {
|
|
const modal = document.getElementById('addModal');
|
|
if (!modal) return;
|
|
|
|
modal.addEventListener('show.bs.modal', () => {
|
|
if (!editingMapping) {
|
|
clearAddForm();
|
|
}
|
|
});
|
|
modal.addEventListener('hidden.bs.modal', () => {
|
|
editingMapping = null;
|
|
document.getElementById('addModalTitle').textContent = 'Adauga Mapare';
|
|
});
|
|
}
|
|
|
|
function clearAddForm() {
|
|
document.getElementById('inputSku').value = '';
|
|
document.getElementById('inputSku').readOnly = false;
|
|
document.getElementById('addModalProductName').style.display = 'none';
|
|
document.getElementById('pctWarning').style.display = 'none';
|
|
document.getElementById('addModalTitle').textContent = 'Adauga Mapare';
|
|
const container = document.getElementById('codmatLines');
|
|
container.innerHTML = '';
|
|
addCodmatLine();
|
|
}
|
|
|
|
async function openEditModal(sku, codmat, cantitate) {
|
|
editingMapping = { sku, codmat };
|
|
document.getElementById('addModalTitle').textContent = 'Editare Mapare';
|
|
document.getElementById('inputSku').value = sku;
|
|
document.getElementById('inputSku').readOnly = false;
|
|
document.getElementById('pctWarning').style.display = 'none';
|
|
|
|
const container = document.getElementById('codmatLines');
|
|
container.innerHTML = '';
|
|
|
|
try {
|
|
// Fetch all CODMATs for this SKU
|
|
const res = await fetch(`/api/mappings?search=${encodeURIComponent(sku)}&per_page=100`);
|
|
const data = await res.json();
|
|
const allMappings = (data.mappings || []).filter(m => m.sku === sku && !m.sters);
|
|
|
|
// Show product name if available
|
|
const productName = allMappings[0]?.product_name || '';
|
|
const productNameEl = document.getElementById('addModalProductName');
|
|
const productNameText = document.getElementById('inputProductName');
|
|
if (productName && productNameEl && productNameText) {
|
|
productNameText.textContent = productName;
|
|
productNameEl.style.display = '';
|
|
}
|
|
|
|
if (allMappings.length === 0) {
|
|
// Fallback to single line with passed values
|
|
addCodmatLine();
|
|
const line = container.querySelector('.codmat-line');
|
|
if (line) {
|
|
line.querySelector('.cl-codmat').value = codmat;
|
|
line.querySelector('.cl-cantitate').value = cantitate;
|
|
}
|
|
} else {
|
|
for (const m of allMappings) {
|
|
addCodmatLine();
|
|
const lines = container.querySelectorAll('.codmat-line');
|
|
const line = lines[lines.length - 1];
|
|
line.querySelector('.cl-codmat').value = m.codmat;
|
|
if (m.denumire) {
|
|
line.querySelector('.cl-selected').textContent = m.denumire;
|
|
}
|
|
line.querySelector('.cl-cantitate').value = m.cantitate_roa;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Fallback on error
|
|
addCodmatLine();
|
|
const line = container.querySelector('.codmat-line');
|
|
if (line) {
|
|
line.querySelector('.cl-codmat').value = codmat;
|
|
line.querySelector('.cl-cantitate').value = cantitate;
|
|
}
|
|
}
|
|
|
|
new bootstrap.Modal(document.getElementById('addModal')).show();
|
|
}
|
|
|
|
function addCodmatLine() {
|
|
const container = document.getElementById('codmatLines');
|
|
const idx = container.children.length;
|
|
const div = document.createElement('div');
|
|
div.className = 'qm-line codmat-line';
|
|
div.innerHTML = `
|
|
<div class="qm-row">
|
|
<div class="qm-codmat-wrap position-relative">
|
|
<input type="text" class="form-control form-control-sm cl-codmat" placeholder="CODMAT..." autocomplete="off" data-idx="${idx}">
|
|
<div class="autocomplete-dropdown d-none cl-ac-dropdown"></div>
|
|
</div>
|
|
<input type="number" class="form-control form-control-sm cl-cantitate" value="1" 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('.codmat-line').remove()"><i class="bi bi-x"></i></button>` : '<span style="width:30px"></span>'}
|
|
</div>
|
|
<div class="qm-selected text-muted cl-selected" style="font-size:0.75rem;padding-left:2px"></div>
|
|
`;
|
|
container.appendChild(div);
|
|
|
|
// Setup autocomplete
|
|
const input = div.querySelector('.cl-codmat');
|
|
const dropdown = div.querySelector('.cl-ac-dropdown');
|
|
const selected = div.querySelector('.cl-selected');
|
|
|
|
input.addEventListener('input', () => {
|
|
const key = 'cl_' + idx;
|
|
clearTimeout(acTimeouts[key]);
|
|
acTimeouts[key] = setTimeout(() => clAutocomplete(input, dropdown, selected), 250);
|
|
});
|
|
input.addEventListener('blur', () => {
|
|
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
|
});
|
|
}
|
|
|
|
async function clAutocomplete(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="clSelectArticle(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 clSelectArticle(el, codmat, label) {
|
|
const line = el.closest('.codmat-line');
|
|
line.querySelector('.cl-codmat').value = codmat;
|
|
line.querySelector('.cl-selected').textContent = label;
|
|
line.querySelector('.cl-ac-dropdown').classList.add('d-none');
|
|
}
|
|
|
|
async function saveMapping() {
|
|
const sku = document.getElementById('inputSku').value.trim();
|
|
if (!sku) { alert('SKU este obligatoriu'); return; }
|
|
|
|
const lines = document.querySelectorAll('.codmat-line');
|
|
const mappings = [];
|
|
|
|
for (const line of lines) {
|
|
const codmat = line.querySelector('.cl-codmat').value.trim();
|
|
const cantitate = parseFloat(line.querySelector('.cl-cantitate').value) || 1;
|
|
if (!codmat) continue;
|
|
mappings.push({ codmat, cantitate_roa: cantitate });
|
|
}
|
|
|
|
if (mappings.length === 0) { alert('Adauga cel putin un CODMAT'); return; }
|
|
|
|
document.getElementById('pctWarning').style.display = 'none';
|
|
|
|
try {
|
|
let res;
|
|
|
|
if (editingMapping) {
|
|
if (mappings.length === 1) {
|
|
// Single CODMAT edit: use existing PUT endpoint
|
|
res = await fetch(`/api/mappings/${encodeURIComponent(editingMapping.sku)}/${encodeURIComponent(editingMapping.codmat)}/edit`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
new_sku: sku,
|
|
new_codmat: mappings[0].codmat,
|
|
cantitate_roa: mappings[0].cantitate_roa
|
|
})
|
|
});
|
|
} else {
|
|
// Multi-CODMAT set: delete all existing then create new batch
|
|
const oldSku = editingMapping.sku;
|
|
const existRes = await fetch(`/api/mappings?search=${encodeURIComponent(oldSku)}&per_page=100`);
|
|
const existData = await existRes.json();
|
|
const existing = (existData.mappings || []).filter(m => m.sku === oldSku && !m.sters);
|
|
|
|
// Delete each existing CODMAT for old SKU
|
|
for (const m of existing) {
|
|
await fetch(`/api/mappings/${encodeURIComponent(m.sku)}/${encodeURIComponent(m.codmat)}`, {
|
|
method: 'DELETE'
|
|
});
|
|
}
|
|
|
|
// Create new batch with auto_restore (handles just-soft-deleted records)
|
|
res = await fetch('/api/mappings/batch', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ sku, mappings, auto_restore: true })
|
|
});
|
|
}
|
|
} else 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('addModal')).hide();
|
|
editingMapping = null;
|
|
loadMappings();
|
|
} else if (res.status === 409) {
|
|
handleMappingConflict(data);
|
|
} else {
|
|
alert('Eroare: ' + (data.error || 'Unknown'));
|
|
}
|
|
} catch (err) {
|
|
alert('Eroare: ' + err.message);
|
|
}
|
|
}
|
|
|
|
// ── Inline Add Row ──────────────────────────────
|
|
|
|
let inlineAddVisible = false;
|
|
|
|
function showInlineAddRow() {
|
|
// On mobile, open the full modal instead
|
|
if (window.innerWidth < 768) {
|
|
new bootstrap.Modal(document.getElementById('addModal')).show();
|
|
return;
|
|
}
|
|
|
|
if (inlineAddVisible) return;
|
|
inlineAddVisible = true;
|
|
|
|
const container = document.getElementById('mappingsFlatList');
|
|
const row = document.createElement('div');
|
|
row.id = 'inlineAddRow';
|
|
row.className = 'flat-row';
|
|
row.style.background = '#eff6ff';
|
|
row.style.gap = '0.5rem';
|
|
row.innerHTML = `
|
|
<input type="text" class="form-control form-control-sm" id="inlineSku" placeholder="SKU" style="width:140px">
|
|
<div class="position-relative" style="flex:1;min-width:0">
|
|
<input type="text" class="form-control form-control-sm" id="inlineCodmat" placeholder="Cauta CODMAT..." autocomplete="off">
|
|
<div class="autocomplete-dropdown d-none" id="inlineAcDropdown"></div>
|
|
<small class="text-muted" id="inlineSelected"></small>
|
|
</div>
|
|
<input type="number" class="form-control form-control-sm" id="inlineCantitate" value="1" step="0.001" min="0.001" style="width:70px" placeholder="Cant.">
|
|
<button class="btn btn-sm btn-success" onclick="saveInlineMapping()" title="Salveaza"><i class="bi bi-check-lg"></i></button>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="cancelInlineAdd()" title="Anuleaza"><i class="bi bi-x-lg"></i></button>
|
|
`;
|
|
container.insertBefore(row, container.firstChild);
|
|
document.getElementById('inlineSku').focus();
|
|
|
|
// Setup autocomplete for inline CODMAT
|
|
const input = document.getElementById('inlineCodmat');
|
|
const dropdown = document.getElementById('inlineAcDropdown');
|
|
const selected = document.getElementById('inlineSelected');
|
|
let inlineAcTimeout = null;
|
|
|
|
input.addEventListener('input', () => {
|
|
clearTimeout(inlineAcTimeout);
|
|
inlineAcTimeout = setTimeout(() => inlineAutocomplete(input, dropdown, selected), 250);
|
|
});
|
|
input.addEventListener('blur', () => {
|
|
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
|
});
|
|
}
|
|
|
|
async function inlineAutocomplete(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="inlineSelectArticle('${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 inlineSelectArticle(codmat, label) {
|
|
document.getElementById('inlineCodmat').value = codmat;
|
|
document.getElementById('inlineSelected').textContent = label;
|
|
document.getElementById('inlineAcDropdown').classList.add('d-none');
|
|
}
|
|
|
|
async function saveInlineMapping() {
|
|
const sku = document.getElementById('inlineSku').value.trim();
|
|
const codmat = document.getElementById('inlineCodmat').value.trim();
|
|
const cantitate = parseFloat(document.getElementById('inlineCantitate').value) || 1;
|
|
|
|
if (!sku) { alert('SKU este obligatoriu'); return; }
|
|
if (!codmat) { alert('CODMAT este obligatoriu'); return; }
|
|
|
|
try {
|
|
const res = await fetch('/api/mappings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ sku, codmat, cantitate_roa: cantitate })
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
cancelInlineAdd();
|
|
loadMappings();
|
|
} else if (res.status === 409) {
|
|
handleMappingConflict(data);
|
|
} else {
|
|
alert('Eroare: ' + (data.error || 'Unknown'));
|
|
}
|
|
} catch (err) {
|
|
alert('Eroare: ' + err.message);
|
|
}
|
|
}
|
|
|
|
function cancelInlineAdd() {
|
|
const row = document.getElementById('inlineAddRow');
|
|
if (row) row.remove();
|
|
inlineAddVisible = false;
|
|
}
|
|
|
|
// ── Toggle Active with Toast Undo ────────────────
|
|
|
|
async function toggleActive(sku, codmat, currentActive) {
|
|
const newActive = currentActive ? 0 : 1;
|
|
try {
|
|
const res = await fetch(`/api/mappings/${encodeURIComponent(sku)}/${encodeURIComponent(codmat)}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ activ: newActive })
|
|
});
|
|
const data = await res.json();
|
|
if (!data.success) return;
|
|
|
|
loadMappings();
|
|
|
|
// Show toast with undo
|
|
const action = newActive ? 'activata' : 'dezactivata';
|
|
showUndoToast(`Mapare ${sku} \u2192 ${codmat} ${action}.`, () => {
|
|
fetch(`/api/mappings/${encodeURIComponent(sku)}/${encodeURIComponent(codmat)}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ activ: currentActive })
|
|
}).then(() => loadMappings());
|
|
});
|
|
} catch (err) { alert('Eroare: ' + err.message); }
|
|
}
|
|
|
|
function showUndoToast(message, undoCallback) {
|
|
document.getElementById('toastMessage').textContent = message;
|
|
const undoBtn = document.getElementById('toastUndoBtn');
|
|
// Clone to remove old listeners
|
|
const newBtn = undoBtn.cloneNode(true);
|
|
undoBtn.parentNode.replaceChild(newBtn, undoBtn);
|
|
newBtn.id = 'toastUndoBtn';
|
|
if (undoCallback) {
|
|
newBtn.style.display = '';
|
|
newBtn.addEventListener('click', () => {
|
|
undoCallback();
|
|
const toastEl = document.getElementById('undoToast');
|
|
const inst = bootstrap.Toast.getInstance(toastEl);
|
|
if (inst) inst.hide();
|
|
});
|
|
} else {
|
|
newBtn.style.display = 'none';
|
|
}
|
|
const toast = new bootstrap.Toast(document.getElementById('undoToast'));
|
|
toast.show();
|
|
}
|
|
|
|
// ── Delete with Modal Confirmation ──────────────
|
|
|
|
let pendingDelete = null;
|
|
|
|
function initDeleteModal() {
|
|
const btn = document.getElementById('confirmDeleteBtn');
|
|
if (!btn) return;
|
|
btn.addEventListener('click', async () => {
|
|
if (!pendingDelete) return;
|
|
const { sku, codmat } = pendingDelete;
|
|
try {
|
|
const res = await fetch(`/api/mappings/${encodeURIComponent(sku)}/${encodeURIComponent(codmat)}`, {
|
|
method: 'DELETE'
|
|
});
|
|
const data = await res.json();
|
|
bootstrap.Modal.getInstance(document.getElementById('deleteConfirmModal')).hide();
|
|
if (data.success) loadMappings();
|
|
else alert('Eroare: ' + (data.error || 'Delete failed'));
|
|
} catch (err) {
|
|
bootstrap.Modal.getInstance(document.getElementById('deleteConfirmModal')).hide();
|
|
alert('Eroare: ' + err.message);
|
|
}
|
|
pendingDelete = null;
|
|
});
|
|
}
|
|
|
|
function deleteMappingConfirm(sku, codmat) {
|
|
pendingDelete = { sku, codmat };
|
|
document.getElementById('deleteSkuText').textContent = sku;
|
|
document.getElementById('deleteCodmatText').textContent = codmat;
|
|
new bootstrap.Modal(document.getElementById('deleteConfirmModal')).show();
|
|
}
|
|
|
|
// ── Restore Deleted ──────────────────────────────
|
|
|
|
async function restoreMapping(sku, codmat) {
|
|
try {
|
|
const res = await fetch(`/api/mappings/${encodeURIComponent(sku)}/${encodeURIComponent(codmat)}/restore`, {
|
|
method: 'POST'
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) loadMappings();
|
|
else alert('Eroare: ' + (data.error || 'Restore failed'));
|
|
} catch (err) {
|
|
alert('Eroare: ' + err.message);
|
|
}
|
|
}
|
|
|
|
// ── CSV ──────────────────────────────────────────
|
|
|
|
async function importCsv() {
|
|
const fileInput = document.getElementById('csvFile');
|
|
if (!fileInput.files.length) { alert('Selecteaza un fisier CSV'); return; }
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', fileInput.files[0]);
|
|
|
|
try {
|
|
const res = await fetch('/api/mappings/import-csv', { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
let msg = `${data.processed} mapări importate`;
|
|
if (data.skipped_no_codmat > 0) {
|
|
msg += `, ${data.skipped_no_codmat} rânduri fără CODMAT omise`;
|
|
}
|
|
let html = `<div class="alert alert-success">${msg}</div>`;
|
|
if (data.errors && data.errors.length > 0) {
|
|
html += `<div class="alert alert-warning">Erori (${data.errors.length}): <ul>${data.errors.map(e => `<li>${esc(e)}</li>`).join('')}</ul></div>`;
|
|
}
|
|
document.getElementById('importResult').innerHTML = html;
|
|
loadMappings();
|
|
} catch (err) {
|
|
document.getElementById('importResult').innerHTML = `<div class="alert alert-danger">${err.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function exportCsv() { window.location.href = (window.ROOT_PATH || '') + '/api/mappings/export-csv'; }
|
|
function downloadTemplate() { window.location.href = (window.ROOT_PATH || '') + '/api/mappings/csv-template'; }
|
|
|
|
// ── Duplicate / Conflict handling ────────────────
|
|
|
|
function handleMappingConflict(data) {
|
|
const msg = data.error || 'Conflict la salvare';
|
|
if (data.can_restore) {
|
|
const restore = confirm(`${msg}\n\nDoriti sa restaurati maparea stearsa?`);
|
|
if (restore) {
|
|
// Find sku/codmat from the inline row or modal
|
|
const sku = (document.getElementById('inlineSku') || document.getElementById('inputSku'))?.value?.trim();
|
|
const codmat = (document.getElementById('inlineCodmat') || document.querySelector('.cl-codmat'))?.value?.trim();
|
|
if (sku && codmat) {
|
|
fetch(`/api/mappings/${encodeURIComponent(sku)}/${encodeURIComponent(codmat)}/restore`, { method: 'POST' })
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
if (d.success) { cancelInlineAdd(); loadMappings(); }
|
|
else alert('Eroare la restaurare: ' + (d.error || ''));
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
showUndoToast(msg, null);
|
|
// Show non-dismissible inline error
|
|
const warn = document.getElementById('pctWarning');
|
|
if (warn) { warn.textContent = msg; warn.style.display = ''; }
|
|
}
|
|
}
|