let currentPage = 1; let currentSearch = ''; let searchTimeout = null; let sortColumn = 'sku'; let sortDirection = 'asc'; let editingMapping = null; // {sku, codmat} when editing let pctFilter = 'all'; // Load on page ready document.addEventListener('DOMContentLoaded', () => { loadMappings(); initAddModal(); initDeleteModal(); initPctFilterPills(); }); 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 = ''; } }); } // ── Pct Filter Pills ───────────────────────────── function initPctFilterPills() { document.querySelectorAll('.filter-pill[data-pct]').forEach(btn => { btn.addEventListener('click', function() { document.querySelectorAll('.filter-pill[data-pct]').forEach(b => b.classList.remove('active')); this.classList.add('active'); pctFilter = this.dataset.pct; currentPage = 1; loadMappings(); }); }); } function updatePctCounts(counts) { if (!counts) return; const elAll = document.getElementById('mCntAll'); const elComplete = document.getElementById('mCntComplete'); const elIncomplete = document.getElementById('mCntIncomplete'); if (elAll) elAll.textContent = counts.total || 0; if (elComplete) elComplete.textContent = counts.complete || 0; if (elIncomplete) elIncomplete.textContent = counts.incomplete || 0; } // ── 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: 50, sort_by: sortColumn, sort_dir: sortDirection }); if (showDeleted) params.set('show_deleted', 'true'); if (pctFilter && pctFilter !== 'all') params.set('pct_filter', pctFilter); 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); } updatePctCounts(data.counts); renderTable(mappings, showDeleted); renderPagination(data); updateSortIcons(); } catch (err) { document.getElementById('mappingsBody').innerHTML = `Eroare: ${err.message}`; } } function renderTable(mappings, showDeleted) { const tbody = document.getElementById('mappingsBody'); if (!mappings || mappings.length === 0) { tbody.innerHTML = 'Nu exista mapari'; return; } // Group by SKU for visual grouping (R6) let html = ''; let prevSku = null; let groupIdx = 0; let skuGroupCounts = {}; // Count items per SKU mappings.forEach(m => { skuGroupCounts[m.sku] = (skuGroupCounts[m.sku] || 0) + 1; }); mappings.forEach((m, i) => { const isNewGroup = m.sku !== prevSku; if (isNewGroup) groupIdx++; const groupClass = groupIdx % 2 === 0 ? 'sku-group-even' : 'sku-group-odd'; const isMulti = skuGroupCounts[m.sku] > 1; const inactiveClass = !m.activ && !m.sters ? 'table-secondary opacity-75' : ''; const deletedClass = m.sters ? 'mapping-deleted' : ''; // SKU cell: show only on first row of group let skuCell, productCell; if (isNewGroup) { const badge = isMulti ? ` Set (${skuGroupCounts[m.sku]})` : ''; // Percentage total badge let pctBadge = ''; if (m.pct_total !== undefined) { if (m.is_complete) { pctBadge = ` ✓ 100%`; } else { const pctVal = typeof m.pct_total === 'number' ? m.pct_total.toFixed(2) : m.pct_total; pctBadge = ` ⚠ ${pctVal}%`; } } skuCell = `${esc(m.sku)}${badge}${pctBadge}`; productCell = `${esc(m.product_name || '-')}`; } else { skuCell = ''; productCell = ''; } html += ` ${skuCell} ${productCell} ${esc(m.codmat)} ${esc(m.denumire || '-')} ${esc(m.um || '-')} ${m.cantitate_roa} ${m.procent_pret}% ${m.activ ? 'Activ' : 'Inactiv'} ${m.sters ? `` : ` `} `; prevSku = m.sku; }); tbody.innerHTML = html; } function renderPagination(data) { const info = document.getElementById('pageInfo'); info.textContent = `${data.total} mapari | Pagina ${data.page} din ${data.pages || 1}`; const ul = document.getElementById('pagination'); if (data.pages <= 1) { ul.innerHTML = ''; return; } let html = ''; html += `
  • «
  • `; let start = Math.max(1, data.page - 3); let end = Math.min(data.pages, start + 6); start = Math.max(1, end - 6); for (let i = start; i <= end; i++) { html += `
  • ${i}
  • `; } html += `
  • »
  • `; ul.innerHTML = html; } 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(); } function openEditModal(sku, codmat, cantitate, procent) { 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 = ''; addCodmatLine(); // Pre-fill the CODMAT line const line = container.querySelector('.codmat-line'); if (line) { line.querySelector('.cl-codmat').value = codmat; line.querySelector('.cl-cantitate').value = cantitate; line.querySelector('.cl-procent').value = procent; } 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 = 'border rounded p-2 mb-2 codmat-line'; div.innerHTML = `
    ${idx > 0 ? `` : '
    '}
    `; 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 => `
    ${esc(r.codmat)}${esc(r.denumire)}${r.um ? ` (${esc(r.um)})` : ''}
    ` ).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; const procent = parseFloat(line.querySelector('.cl-procent').value) || 100; if (!codmat) continue; mappings.push({ codmat, cantitate_roa: cantitate, procent_pret: procent }); } if (mappings.length === 0) { alert('Adauga cel putin un CODMAT'); return; } // Validate percentage for multi-line if (mappings.length > 1) { const totalPct = mappings.reduce((s, m) => s + m.procent_pret, 0); if (Math.abs(totalPct - 100) > 0.01) { document.getElementById('pctWarning').textContent = `Suma procentelor trebuie sa fie 100% (actual: ${totalPct.toFixed(2)}%)`; document.getElementById('pctWarning').style.display = ''; return; } } document.getElementById('pctWarning').style.display = 'none'; try { let res; if (editingMapping) { // Edit mode: use PUT /api/mappings/{old_sku}/{old_codmat}/edit 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, procent_pret: mappings[0].procent_pret }) }); } 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, procent_pret: mappings[0].procent_pret }) }); } 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() { if (inlineAddVisible) return; inlineAddVisible = true; const tbody = document.getElementById('mappingsBody'); const row = document.createElement('tr'); row.id = 'inlineAddRow'; row.className = 'table-info'; row.innerHTML = `
    - - `; tbody.insertBefore(row, tbody.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 => `
    ${esc(r.codmat)}${esc(r.denumire)}${r.um ? ` (${esc(r.um)})` : ''}
    ` ).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; const procent = parseFloat(document.getElementById('inlineProcent').value) || 100; 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, procent_pret: procent }) }); 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; } // ── Inline Edit ────────────────────────────────── function editCell(td, sku, codmat, field, currentValue) { if (td.querySelector('input')) return; const input = document.createElement('input'); input.type = 'number'; input.className = 'form-control form-control-sm'; input.value = currentValue; input.step = field === 'cantitate_roa' ? '0.001' : '0.01'; input.style.width = '80px'; const originalText = td.textContent; td.textContent = ''; td.appendChild(input); input.focus(); input.select(); const save = async () => { const newValue = parseFloat(input.value); if (isNaN(newValue) || newValue === currentValue) { td.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 { td.textContent = originalText; alert('Eroare: ' + (data.error || 'Update failed')); } } catch (err) { td.textContent = originalText; } }; input.addEventListener('blur', save); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') save(); if (e.key === 'Escape') { td.textContent = originalText; } }); } // ── 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 = `
    ${msg}
    `; if (data.errors && data.errors.length > 0) { html += `
    Erori (${data.errors.length}):
    `; } document.getElementById('importResult').innerHTML = html; loadMappings(); } catch (err) { document.getElementById('importResult').innerHTML = `
    ${err.message}
    `; } } function exportCsv() { window.location.href = '/api/mappings/export-csv'; } function downloadTemplate() { window.location.href = '/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 = ''; } } } function esc(s) { if (s == null) return ''; return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); }