feat(sqlite): refactor orders schema + dashboard period filter
Replace import_orders (insert-per-run) with orders table (one row per order, upsert on conflict). Eliminates dedup CTE on every dashboard query and prevents unbounded row growth at 4-500 orders/sync. Key changes: - orders table: PK order_number, upsert via ON CONFLICT DO UPDATE; COALESCE preserves id_comanda once set; times_skipped auto-increments - sync_run_orders: lightweight junction (sync_run_id, order_number) replaces sync_run_id column on orders - order_items: PK changed to (order_number, sku), INSERT OR IGNORE - Auto-migration in init_sqlite(): import_orders → orders on first boot, old table renamed to import_orders_bak - /api/dashboard/orders: period_days param (3/7/30/0=all, default 7) - Dashboard: period selector buttons in orders card header - start.sh: stop existing process on port 5003 before restart; remove --reload (broken on WSL2 /mnt/e/) - Add invoice_service, E2E Playwright tests, Oracle package updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
let currentPage = 1;
|
||||
let currentSearch = '';
|
||||
let searchTimeout = null;
|
||||
let sortColumn = 'sku';
|
||||
let sortDirection = 'asc';
|
||||
let editingMapping = null; // {sku, codmat} when editing
|
||||
|
||||
// Load on page ready
|
||||
document.addEventListener('DOMContentLoaded', loadMappings);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadMappings();
|
||||
initAddModal();
|
||||
initDeleteModal();
|
||||
});
|
||||
|
||||
function debounceSearch() {
|
||||
clearTimeout(searchTimeout);
|
||||
@@ -14,52 +21,132 @@ function debounceSearch() {
|
||||
}, 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: 50
|
||||
per_page: 50,
|
||||
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();
|
||||
renderTable(data.mappings);
|
||||
|
||||
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('mappingsBody').innerHTML =
|
||||
`<tr><td colspan="7" class="text-center text-danger">Eroare: ${err.message}</td></tr>`;
|
||||
`<tr><td colspan="9" class="text-center text-danger">Eroare: ${err.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(mappings) {
|
||||
function renderTable(mappings, showDeleted) {
|
||||
const tbody = document.getElementById('mappingsBody');
|
||||
|
||||
if (!mappings || mappings.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-4">Nu exista mapari</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted py-4">Nu exista mapari</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = mappings.map(m => `
|
||||
<tr>
|
||||
<td><strong>${esc(m.sku)}</strong></td>
|
||||
// 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 ? ` <span class="badge bg-info">Set (${skuGroupCounts[m.sku]})</span>` : '';
|
||||
skuCell = `<td rowspan="${isMulti ? skuGroupCounts[m.sku] : 1}"><strong>${esc(m.sku)}</strong>${badge}</td>`;
|
||||
productCell = `<td rowspan="${isMulti ? skuGroupCounts[m.sku] : 1}">${esc(m.product_name || '-')}</td>`;
|
||||
} else {
|
||||
skuCell = '';
|
||||
productCell = '';
|
||||
}
|
||||
|
||||
html += `<tr class="${groupClass} ${inactiveClass} ${deletedClass}">
|
||||
${skuCell}
|
||||
${productCell}
|
||||
<td><code>${esc(m.codmat)}</code></td>
|
||||
<td>${esc(m.denumire || '-')}</td>
|
||||
<td class="editable" onclick="editCell(this, '${esc(m.sku)}', '${esc(m.codmat)}', 'cantitate_roa', ${m.cantitate_roa})">${m.cantitate_roa}</td>
|
||||
<td class="editable" onclick="editCell(this, '${esc(m.sku)}', '${esc(m.codmat)}', 'procent_pret', ${m.procent_pret})">${m.procent_pret}%</td>
|
||||
<td>${esc(m.um || '-')}</td>
|
||||
<td class="${m.sters ? '' : 'editable'}" ${m.sters ? '' : `onclick="editCell(this, '${esc(m.sku)}', '${esc(m.codmat)}', 'cantitate_roa', ${m.cantitate_roa})"`}>${m.cantitate_roa}</td>
|
||||
<td class="${m.sters ? '' : 'editable'}" ${m.sters ? '' : `onclick="editCell(this, '${esc(m.sku)}', '${esc(m.codmat)}', 'procent_pret', ${m.procent_pret})"`}>${m.procent_pret}%</td>
|
||||
<td>
|
||||
<span class="badge ${m.activ ? 'bg-success' : 'bg-secondary'}" style="cursor:pointer"
|
||||
onclick="toggleActive('${esc(m.sku)}', '${esc(m.codmat)}', ${m.activ})">
|
||||
<span class="badge ${m.activ ? 'bg-success' : 'bg-secondary'}" ${m.sters ? '' : 'style="cursor:pointer"'}
|
||||
${m.sters ? '' : `onclick="toggleActive('${esc(m.sku)}', '${esc(m.codmat)}', ${m.activ})"`}>
|
||||
${m.activ ? 'Activ' : 'Inactiv'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteMappingConfirm('${esc(m.sku)}', '${esc(m.codmat)}')" title="Dezactiveaza">
|
||||
<i class="bi bi-trash"></i>
|
||||
${m.sters ? `<button class="btn btn-sm btn-outline-success" onclick="restoreMapping('${esc(m.sku)}', '${esc(m.codmat)}')" title="Restaureaza"><i class="bi bi-arrow-counterclockwise"></i></button>` : `
|
||||
<button class="btn btn-sm btn-outline-secondary me-1" onclick="openEditModal('${esc(m.sku)}', '${esc(m.codmat)}', ${m.cantitate_roa}, ${m.procent_pret})" title="Editeaza">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteMappingConfirm('${esc(m.sku)}', '${esc(m.codmat)}')" title="Sterge">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>`}
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
</tr>`;
|
||||
|
||||
prevSku = m.sku;
|
||||
});
|
||||
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderPagination(data) {
|
||||
@@ -70,11 +157,9 @@ function renderPagination(data) {
|
||||
if (data.pages <= 1) { ul.innerHTML = ''; return; }
|
||||
|
||||
let html = '';
|
||||
// Previous
|
||||
html += `<li class="page-item ${data.page <= 1 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="goPage(${data.page - 1}); return false;">«</a></li>`;
|
||||
|
||||
// Pages (show max 7)
|
||||
let start = Math.max(1, data.page - 3);
|
||||
let end = Math.min(data.pages, start + 6);
|
||||
start = Math.max(1, end - 6);
|
||||
@@ -84,7 +169,6 @@ function renderPagination(data) {
|
||||
<a class="page-link" href="#" onclick="goPage(${i}); return false;">${i}</a></li>`;
|
||||
}
|
||||
|
||||
// Next
|
||||
html += `<li class="page-item ${data.page >= data.pages ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="goPage(${data.page + 1}); return false;">»</a></li>`;
|
||||
|
||||
@@ -96,73 +180,186 @@ function goPage(p) {
|
||||
loadMappings();
|
||||
}
|
||||
|
||||
// Autocomplete for CODMAT
|
||||
let acTimeout = null;
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const input = document.getElementById('inputCodmat');
|
||||
if (!input) return;
|
||||
// ── 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 = `
|
||||
<div class="mb-2 position-relative">
|
||||
<label class="form-label form-label-sm mb-1">CODMAT (Articol ROA)</label>
|
||||
<input type="text" class="form-control form-control-sm cl-codmat" placeholder="Cauta codmat sau denumire..." autocomplete="off" data-idx="${idx}">
|
||||
<div class="autocomplete-dropdown d-none cl-ac-dropdown"></div>
|
||||
<small class="text-muted cl-selected"></small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<label class="form-label form-label-sm mb-1">Cantitate ROA</label>
|
||||
<input type="number" class="form-control form-control-sm cl-cantitate" value="1" step="0.001" min="0.001">
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<label class="form-label form-label-sm mb-1">Procent Pret (%)</label>
|
||||
<input type="number" class="form-control form-control-sm cl-procent" value="100" step="0.01" min="0" max="100">
|
||||
</div>
|
||||
<div class="col-2 d-flex align-items-end">
|
||||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('.codmat-line').remove()"><i class="bi bi-x-lg"></i></button>` : ''}
|
||||
</div>
|
||||
</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', () => {
|
||||
clearTimeout(acTimeout);
|
||||
acTimeout = setTimeout(() => autocomplete(input.value), 250);
|
||||
const key = 'cl_' + idx;
|
||||
clearTimeout(acTimeouts[key]);
|
||||
acTimeouts[key] = setTimeout(() => clAutocomplete(input, dropdown, selected), 250);
|
||||
});
|
||||
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => document.getElementById('autocompleteDropdown').classList.add('d-none'), 200);
|
||||
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function autocomplete(q) {
|
||||
const dropdown = document.getElementById('autocompleteDropdown');
|
||||
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; }
|
||||
|
||||
if (!data.results || data.results.length === 0) {
|
||||
dropdown.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.innerHTML = data.results.map(r => `
|
||||
<div class="autocomplete-item" onmousedown="selectArticle('${esc(r.codmat)}', '${esc(r.denumire)}')">
|
||||
<span class="codmat">${esc(r.codmat)}</span>
|
||||
<br><span class="denumire">${esc(r.denumire)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
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 (err) {
|
||||
dropdown.classList.add('d-none');
|
||||
}
|
||||
} catch { dropdown.classList.add('d-none'); }
|
||||
}
|
||||
|
||||
function selectArticle(codmat, denumire) {
|
||||
document.getElementById('inputCodmat').value = codmat;
|
||||
document.getElementById('selectedArticle').textContent = denumire;
|
||||
document.getElementById('autocompleteDropdown').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');
|
||||
}
|
||||
|
||||
// Save mapping (create)
|
||||
async function saveMapping() {
|
||||
const sku = document.getElementById('inputSku').value.trim();
|
||||
const codmat = document.getElementById('inputCodmat').value.trim();
|
||||
const cantitate = parseFloat(document.getElementById('inputCantitate').value) || 1;
|
||||
const procent = parseFloat(document.getElementById('inputProcent').value) || 100;
|
||||
if (!sku) { alert('SKU este obligatoriu'); return; }
|
||||
|
||||
if (!sku || !codmat) { alert('SKU si CODMAT sunt obligatorii'); 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 {
|
||||
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();
|
||||
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();
|
||||
clearForm();
|
||||
editingMapping = null;
|
||||
loadMappings();
|
||||
} else {
|
||||
alert('Eroare: ' + (data.error || 'Unknown'));
|
||||
@@ -172,17 +369,117 @@ async function saveMapping() {
|
||||
}
|
||||
}
|
||||
|
||||
function clearForm() {
|
||||
document.getElementById('inputSku').value = '';
|
||||
document.getElementById('inputCodmat').value = '';
|
||||
document.getElementById('inputCantitate').value = '1';
|
||||
document.getElementById('inputProcent').value = '100';
|
||||
document.getElementById('selectedArticle').textContent = '';
|
||||
// ── 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 = `
|
||||
<td colspan="2">
|
||||
<input type="text" class="form-control form-control-sm" id="inlineSku" placeholder="SKU" style="width:160px">
|
||||
</td>
|
||||
<td colspan="2" class="position-relative">
|
||||
<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>
|
||||
</td>
|
||||
<td>-</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" id="inlineCantitate" value="1" step="0.001" min="0.001" style="width:80px">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" id="inlineProcent" value="100" step="0.01" min="0" max="100" style="width:80px">
|
||||
</td>
|
||||
<td>-</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-success me-1" 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>
|
||||
</td>
|
||||
`;
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// Inline edit
|
||||
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;
|
||||
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 {
|
||||
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; // Already editing
|
||||
if (td.querySelector('input')) return;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
@@ -203,25 +500,18 @@ function editCell(td, sku, codmat, field, 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'},
|
||||
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;
|
||||
}
|
||||
if (data.success) { loadMappings(); }
|
||||
else { td.textContent = originalText; alert('Eroare: ' + (data.error || 'Update failed')); }
|
||||
} catch (err) { td.textContent = originalText; }
|
||||
};
|
||||
|
||||
input.addEventListener('blur', save);
|
||||
@@ -231,34 +521,100 @@ function editCell(td, sku, codmat, field, currentValue) {
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle active
|
||||
// ── 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: currentActive ? 0 : 1 })
|
||||
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';
|
||||
newBtn.addEventListener('click', () => {
|
||||
undoCallback();
|
||||
const toastEl = document.getElementById('undoToast');
|
||||
const inst = bootstrap.Toast.getInstance(toastEl);
|
||||
if (inst) inst.hide();
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete (soft)
|
||||
function deleteMappingConfirm(sku, codmat) {
|
||||
if (confirm(`Dezactivezi maparea ${sku} -> ${codmat}?`)) {
|
||||
fetch(`/api/mappings/${encodeURIComponent(sku)}/${encodeURIComponent(codmat)}`, {
|
||||
method: 'DELETE'
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.success) loadMappings();
|
||||
else alert('Eroare: ' + (d.error || 'Delete failed'));
|
||||
});
|
||||
}
|
||||
}
|
||||
// ── CSV ──────────────────────────────────────────
|
||||
|
||||
// CSV import
|
||||
async function importCsv() {
|
||||
const fileInput = document.getElementById('csvFile');
|
||||
if (!fileInput.files.length) { alert('Selecteaza un fisier CSV'); return; }
|
||||
@@ -267,12 +623,8 @@ async function importCsv() {
|
||||
formData.append('file', fileInput.files[0]);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/mappings/import-csv', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const res = await fetch('/api/mappings/import-csv', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
|
||||
let html = `<div class="alert alert-success">Procesate: ${data.processed}</div>`;
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
html += `<div class="alert alert-warning">Erori: <ul>${data.errors.map(e => `<li>${esc(e)}</li>`).join('')}</ul></div>`;
|
||||
@@ -284,15 +636,9 @@ async function importCsv() {
|
||||
}
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
window.location.href = '/api/mappings/export-csv';
|
||||
}
|
||||
function exportCsv() { window.location.href = '/api/mappings/export-csv'; }
|
||||
function downloadTemplate() { window.location.href = '/api/mappings/csv-template'; }
|
||||
|
||||
function downloadTemplate() {
|
||||
window.location.href = '/api/mappings/csv-template';
|
||||
}
|
||||
|
||||
// Escape HTML
|
||||
function esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
|
||||
Reference in New Issue
Block a user