Replace Flask admin with FastAPI app (api/app/) featuring: - Dashboard with stat cards, sync control, and history - Mappings CRUD for ARTICOLE_TERTI with CSV import/export - Article autocomplete from NOM_ARTICOLE - SKU pre-validation before import - Sync orchestration: read JSONs -> validate -> import -> log to SQLite - APScheduler for periodic sync from UI - File logging to logs/sync_comenzi_YYYYMMDD_HHMMSS.log - Oracle pool None guard (503 vs 500 on unavailable) Test suite: - test_app_basic.py: 30 tests (imports + routes) without Oracle - test_integration.py: 9 integration tests with Oracle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
300 lines
10 KiB
JavaScript
300 lines
10 KiB
JavaScript
let currentPage = 1;
|
|
let currentSearch = '';
|
|
let searchTimeout = null;
|
|
|
|
// Load on page ready
|
|
document.addEventListener('DOMContentLoaded', loadMappings);
|
|
|
|
function debounceSearch() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
currentSearch = document.getElementById('searchInput').value;
|
|
currentPage = 1;
|
|
loadMappings();
|
|
}, 300);
|
|
}
|
|
|
|
async function loadMappings() {
|
|
const params = new URLSearchParams({
|
|
search: currentSearch,
|
|
page: currentPage,
|
|
per_page: 50
|
|
});
|
|
|
|
try {
|
|
const res = await fetch(`/api/mappings?${params}`);
|
|
const data = await res.json();
|
|
renderTable(data.mappings);
|
|
renderPagination(data);
|
|
} catch (err) {
|
|
document.getElementById('mappingsBody').innerHTML =
|
|
`<tr><td colspan="7" class="text-center text-danger">Eroare: ${err.message}</td></tr>`;
|
|
}
|
|
}
|
|
|
|
function renderTable(mappings) {
|
|
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>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = mappings.map(m => `
|
|
<tr>
|
|
<td><strong>${esc(m.sku)}</strong></td>
|
|
<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>
|
|
<span class="badge ${m.activ ? 'bg-success' : 'bg-secondary'}" style="cursor:pointer"
|
|
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>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
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 = '';
|
|
// 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);
|
|
|
|
for (let i = start; i <= end; i++) {
|
|
html += `<li class="page-item ${i === data.page ? 'active' : ''}">
|
|
<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>`;
|
|
|
|
ul.innerHTML = html;
|
|
}
|
|
|
|
function goPage(p) {
|
|
currentPage = p;
|
|
loadMappings();
|
|
}
|
|
|
|
// Autocomplete for CODMAT
|
|
let acTimeout = null;
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const input = document.getElementById('inputCodmat');
|
|
if (!input) return;
|
|
|
|
input.addEventListener('input', () => {
|
|
clearTimeout(acTimeout);
|
|
acTimeout = setTimeout(() => autocomplete(input.value), 250);
|
|
});
|
|
|
|
input.addEventListener('blur', () => {
|
|
setTimeout(() => document.getElementById('autocompleteDropdown').classList.add('d-none'), 200);
|
|
});
|
|
});
|
|
|
|
async function autocomplete(q) {
|
|
const dropdown = document.getElementById('autocompleteDropdown');
|
|
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="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.classList.remove('d-none');
|
|
} catch (err) {
|
|
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');
|
|
}
|
|
|
|
// 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 || !codmat) { alert('SKU si CODMAT sunt obligatorii'); 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) {
|
|
bootstrap.Modal.getInstance(document.getElementById('addModal')).hide();
|
|
clearForm();
|
|
loadMappings();
|
|
} else {
|
|
alert('Eroare: ' + (data.error || 'Unknown'));
|
|
}
|
|
} catch (err) {
|
|
alert('Eroare: ' + err.message);
|
|
}
|
|
}
|
|
|
|
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 edit
|
|
function editCell(td, sku, codmat, field, currentValue) {
|
|
if (td.querySelector('input')) return; // Already editing
|
|
|
|
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
|
|
async function toggleActive(sku, codmat, currentActive) {
|
|
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 })
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) loadMappings();
|
|
} 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 import
|
|
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 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>`;
|
|
}
|
|
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 = '/api/mappings/export-csv';
|
|
}
|
|
|
|
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, ''');
|
|
}
|