let currentPage = 1; let mappingsPerPage = 50; 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; // Mobile segmented control renderMobileSegmented('mappingsMobileSeg', [ { label: 'Toate', count: counts.total || 0, value: 'all', active: pctFilter === 'all', colorClass: 'fc-neutral' }, { label: 'Complete', count: counts.complete || 0, value: 'complete', active: pctFilter === 'complete', colorClass: 'fc-green' }, { label: 'Incompl.', count: counts.incomplete || 0, value: 'incomplete', active: pctFilter === 'incomplete', colorClass: 'fc-yellow' } ], (val) => { document.querySelectorAll('.filter-pill[data-pct]').forEach(b => b.classList.remove('active')); const pill = document.querySelector(`.filter-pill[data-pct="${val}"]`); if (pill) pill.classList.add('active'); pctFilter = val; currentPage = 1; loadMappings(); }); } // ── 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'); 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('mappingsFlatList').innerHTML = `
Eroare: ${err.message}
`; } } function renderTable(mappings, showDeleted) { const container = document.getElementById('mappingsFlatList'); if (!mappings || mappings.length === 0) { container.innerHTML = '
Nu exista mapari
'; return; } let prevSku = null; let html = ''; mappings.forEach(m => { const isNewGroup = m.sku !== prevSku; if (isNewGroup) { let pctBadge = ''; if (m.pct_total !== undefined) { pctBadge = m.is_complete ? ` ✓ 100%` : ` ${typeof m.pct_total === 'number' ? m.pct_total.toFixed(0) : m.pct_total}%`; } const inactiveStyle = !m.activ && !m.sters ? 'opacity:0.6;' : ''; html += `
${esc(m.sku)}${pctBadge} ${esc(m.product_name || '')} ${m.sters ? `` : `` }
`; } const deletedStyle = m.sters ? 'text-decoration:line-through;opacity:0.5;' : ''; html += `
${esc(m.codmat)} ${esc(m.denumire || '')} x${m.cantitate_roa} · ${m.procent_pret}%
`; 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, procent } = btn.dataset; const rect = btn.getBoundingClientRect(); showContextMenu(rect.left, rect.bottom + 2, [ { label: 'Editeaza', action: () => openEditModal(sku, codmat, parseFloat(cantitate), parseFloat(procent)) }, { label: 'Sterge', action: () => deleteMappingConfirm(sku, codmat), danger: true } ]); }); }); } // Inline edit for flat-row values (cantitate / procent) 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 = `${data.total} mapari | Pagina ${data.page} din ${data.pages || 1}`; 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, procent) { editingMapping = { sku, codmat }; document.getElementById('addModalTitle').textContent = 'Editare Mapare'; document.getElementById('inputSku').value = sku; document.getElementById('inputSku').readOnly = true; 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); 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; line.querySelector('.cl-procent').value = procent; } } 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; line.querySelector('.cl-cantitate').value = m.cantitate_roa; line.querySelector('.cl-procent').value = m.procent_pret; } } } 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; 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) { 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, procent_pret: mappings[0].procent_pret }) }); } else { // Multi-CODMAT set: delete all existing then create new batch const existRes = await fetch(`/api/mappings?search=${encodeURIComponent(editingMapping.sku)}&per_page=100`); const existData = await existRes.json(); const existing = (existData.mappings || []).filter(m => m.sku === editingMapping.sku && !m.sters); // Delete each existing CODMAT for (const m of existing) { await fetch(`/api/mappings/${encodeURIComponent(m.sku)}/${encodeURIComponent(m.codmat)}`, { method: 'DELETE' }); } // Create new batch res = await fetch('/api/mappings/batch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sku, mappings }) }); } } 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() { // 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 = `
`; 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 => `
${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; } // ── 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 = ''; } } }