- Oracle PL/SQL: kit pricing logic with Mode A (distributed discount) and
Mode B (separate discount line), dual policy support, PRETURI_CU_TVA flag
- Eliminate procent_pret from entire stack (Oracle, Python, JS, HTML)
- New settings: kit_pricing_mode, kit_discount_codmat, price_sync_enabled
- Settings UI: cards for Kit Pricing and Price Sync configuration
- Mappings UI: kit badges with lazy-loaded component prices from price list
- Price sync from orders: auto-update ROA prices when web prices differ
- Catalog price sync: new service to sync all GoMag product prices to ROA
- Kit component price validation: pre-check prices before import
- New endpoint GET /api/mappings/{sku}/prices for component price display
- New endpoints POST /api/price-sync/start, GET status, GET history
- DDL script 07_drop_procent_pret.sql (run after deploy confirmation)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
282 lines
13 KiB
JavaScript
282 lines
13 KiB
JavaScript
let settAcTimeout = null;
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
await loadDropdowns();
|
|
await loadSettings();
|
|
wireAutocomplete('settTransportCodmat', 'settTransportAc');
|
|
wireAutocomplete('settDiscountCodmat', 'settDiscountAc');
|
|
wireAutocomplete('settKitDiscountCodmat', 'settKitDiscountAc');
|
|
|
|
// Kit pricing mode radio toggle
|
|
document.querySelectorAll('input[name="kitPricingMode"]').forEach(r => {
|
|
r.addEventListener('change', () => {
|
|
document.getElementById('kitModeBFields').style.display =
|
|
document.getElementById('kitModeSeparate').checked ? '' : 'none';
|
|
});
|
|
});
|
|
|
|
// Catalog sync toggle
|
|
const catChk = document.getElementById('settCatalogSyncEnabled');
|
|
if (catChk) catChk.addEventListener('change', () => {
|
|
document.getElementById('catalogSyncOptions').style.display = catChk.checked ? '' : 'none';
|
|
});
|
|
});
|
|
|
|
async function loadDropdowns() {
|
|
try {
|
|
const [sectiiRes, politiciRes, gestiuniRes] = await Promise.all([
|
|
fetch('/api/settings/sectii'),
|
|
fetch('/api/settings/politici'),
|
|
fetch('/api/settings/gestiuni')
|
|
]);
|
|
const sectii = await sectiiRes.json();
|
|
const politici = await politiciRes.json();
|
|
const gestiuni = await gestiuniRes.json();
|
|
|
|
const gestContainer = document.getElementById('settGestiuniContainer');
|
|
if (gestContainer) {
|
|
gestContainer.innerHTML = '';
|
|
gestiuni.forEach(g => {
|
|
gestContainer.innerHTML += `<div class="form-check mb-0"><input class="form-check-input" type="checkbox" value="${escHtml(g.id)}" id="gestChk_${escHtml(g.id)}"><label class="form-check-label" for="gestChk_${escHtml(g.id)}">${escHtml(g.label)}</label></div>`;
|
|
});
|
|
if (gestiuni.length === 0) gestContainer.innerHTML = '<span class="text-muted small">Nicio gestiune disponibilă</span>';
|
|
}
|
|
|
|
const sectieEl = document.getElementById('settIdSectie');
|
|
if (sectieEl) {
|
|
sectieEl.innerHTML = '<option value="">— selectează secție —</option>';
|
|
sectii.forEach(s => {
|
|
sectieEl.innerHTML += `<option value="${escHtml(s.id)}">${escHtml(s.label)}</option>`;
|
|
});
|
|
}
|
|
|
|
const polEl = document.getElementById('settIdPol');
|
|
if (polEl) {
|
|
polEl.innerHTML = '<option value="">— selectează politică —</option>';
|
|
politici.forEach(p => {
|
|
polEl.innerHTML += `<option value="${escHtml(p.id)}">${escHtml(p.label)}</option>`;
|
|
});
|
|
}
|
|
|
|
const tPolEl = document.getElementById('settTransportIdPol');
|
|
if (tPolEl) {
|
|
tPolEl.innerHTML = '<option value="">— implicită —</option>';
|
|
politici.forEach(p => {
|
|
tPolEl.innerHTML += `<option value="${escHtml(p.id)}">${escHtml(p.label)}</option>`;
|
|
});
|
|
}
|
|
|
|
const dPolEl = document.getElementById('settDiscountIdPol');
|
|
if (dPolEl) {
|
|
dPolEl.innerHTML = '<option value="">— implicită —</option>';
|
|
politici.forEach(p => {
|
|
dPolEl.innerHTML += `<option value="${escHtml(p.id)}">${escHtml(p.label)}</option>`;
|
|
});
|
|
}
|
|
|
|
const pPolEl = document.getElementById('settIdPolProductie');
|
|
if (pPolEl) {
|
|
pPolEl.innerHTML = '<option value="">— fără politică producție —</option>';
|
|
politici.forEach(p => {
|
|
pPolEl.innerHTML += `<option value="${escHtml(p.id)}">${escHtml(p.label)}</option>`;
|
|
});
|
|
}
|
|
|
|
const kdPolEl = document.getElementById('settKitDiscountIdPol');
|
|
if (kdPolEl) {
|
|
kdPolEl.innerHTML = '<option value="">— implicită —</option>';
|
|
politici.forEach(p => {
|
|
kdPolEl.innerHTML += `<option value="${escHtml(p.id)}">${escHtml(p.label)}</option>`;
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('loadDropdowns error:', err);
|
|
}
|
|
}
|
|
|
|
async function loadSettings() {
|
|
try {
|
|
const res = await fetch('/api/settings');
|
|
const data = await res.json();
|
|
const el = (id) => document.getElementById(id);
|
|
if (el('settTransportCodmat')) el('settTransportCodmat').value = data.transport_codmat || '';
|
|
if (el('settTransportVat')) el('settTransportVat').value = data.transport_vat || '21';
|
|
if (el('settTransportIdPol')) el('settTransportIdPol').value = data.transport_id_pol || '';
|
|
if (el('settDiscountCodmat')) el('settDiscountCodmat').value = data.discount_codmat || '';
|
|
if (el('settDiscountVat')) el('settDiscountVat').value = data.discount_vat || '21';
|
|
if (el('settDiscountIdPol')) el('settDiscountIdPol').value = data.discount_id_pol || '';
|
|
if (el('settSplitDiscountVat')) el('settSplitDiscountVat').checked = data.split_discount_vat === "1";
|
|
if (el('settIdPol')) el('settIdPol').value = data.id_pol || '';
|
|
if (el('settIdPolProductie')) el('settIdPolProductie').value = data.id_pol_productie || '';
|
|
if (el('settIdSectie')) el('settIdSectie').value = data.id_sectie || '';
|
|
// Multi-gestiune checkboxes
|
|
const gestVal = data.id_gestiune || '';
|
|
if (gestVal) {
|
|
const selectedIds = gestVal.split(',').map(s => s.trim());
|
|
selectedIds.forEach(id => {
|
|
const chk = document.getElementById('gestChk_' + id);
|
|
if (chk) chk.checked = true;
|
|
});
|
|
}
|
|
if (el('settGomagApiKey')) el('settGomagApiKey').value = data.gomag_api_key || '';
|
|
if (el('settGomagApiShop')) el('settGomagApiShop').value = data.gomag_api_shop || '';
|
|
if (el('settGomagDaysBack')) el('settGomagDaysBack').value = data.gomag_order_days_back || '7';
|
|
if (el('settGomagLimit')) el('settGomagLimit').value = data.gomag_limit || '100';
|
|
if (el('settDashPollSeconds')) el('settDashPollSeconds').value = data.dashboard_poll_seconds || '5';
|
|
|
|
// Kit pricing
|
|
const kitMode = data.kit_pricing_mode || '';
|
|
document.querySelectorAll('input[name="kitPricingMode"]').forEach(r => {
|
|
r.checked = r.value === kitMode;
|
|
});
|
|
document.getElementById('kitModeBFields').style.display = kitMode === 'separate_line' ? '' : 'none';
|
|
if (el('settKitDiscountCodmat')) el('settKitDiscountCodmat').value = data.kit_discount_codmat || '';
|
|
if (el('settKitDiscountIdPol')) el('settKitDiscountIdPol').value = data.kit_discount_id_pol || '';
|
|
|
|
// Price sync
|
|
if (el('settPriceSyncEnabled')) el('settPriceSyncEnabled').checked = data.price_sync_enabled !== "0";
|
|
if (el('settCatalogSyncEnabled')) {
|
|
el('settCatalogSyncEnabled').checked = data.catalog_sync_enabled === "1";
|
|
document.getElementById('catalogSyncOptions').style.display = data.catalog_sync_enabled === "1" ? '' : 'none';
|
|
}
|
|
if (el('settPriceSyncSchedule')) el('settPriceSyncSchedule').value = data.price_sync_schedule || '';
|
|
|
|
// Load price sync status
|
|
try {
|
|
const psRes = await fetch('/api/price-sync/status');
|
|
const psData = await psRes.json();
|
|
const psEl = document.getElementById('settPriceSyncStatus');
|
|
if (psEl && psData.last_run) {
|
|
psEl.textContent = `Ultima: ${psData.last_run.finished_at || ''} — ${psData.last_run.updated || 0} actualizate din ${psData.last_run.matched || 0}`;
|
|
}
|
|
} catch {}
|
|
} catch (err) {
|
|
console.error('loadSettings error:', err);
|
|
}
|
|
}
|
|
|
|
async function saveSettings() {
|
|
const el = (id) => document.getElementById(id);
|
|
const payload = {
|
|
transport_codmat: el('settTransportCodmat')?.value?.trim() || '',
|
|
transport_vat: el('settTransportVat')?.value || '21',
|
|
transport_id_pol: el('settTransportIdPol')?.value?.trim() || '',
|
|
discount_codmat: el('settDiscountCodmat')?.value?.trim() || '',
|
|
discount_vat: el('settDiscountVat')?.value || '21',
|
|
discount_id_pol: el('settDiscountIdPol')?.value?.trim() || '',
|
|
split_discount_vat: el('settSplitDiscountVat')?.checked ? "1" : "",
|
|
id_pol: el('settIdPol')?.value?.trim() || '',
|
|
id_pol_productie: el('settIdPolProductie')?.value?.trim() || '',
|
|
id_sectie: el('settIdSectie')?.value?.trim() || '',
|
|
id_gestiune: Array.from(document.querySelectorAll('#settGestiuniContainer input:checked')).map(c => c.value).join(','),
|
|
gomag_api_key: el('settGomagApiKey')?.value?.trim() || '',
|
|
gomag_api_shop: el('settGomagApiShop')?.value?.trim() || '',
|
|
gomag_order_days_back: el('settGomagDaysBack')?.value?.trim() || '7',
|
|
gomag_limit: el('settGomagLimit')?.value?.trim() || '100',
|
|
dashboard_poll_seconds: el('settDashPollSeconds')?.value?.trim() || '5',
|
|
kit_pricing_mode: document.querySelector('input[name="kitPricingMode"]:checked')?.value || '',
|
|
kit_discount_codmat: el('settKitDiscountCodmat')?.value?.trim() || '',
|
|
kit_discount_id_pol: el('settKitDiscountIdPol')?.value?.trim() || '',
|
|
price_sync_enabled: el('settPriceSyncEnabled')?.checked ? "1" : "0",
|
|
catalog_sync_enabled: el('settCatalogSyncEnabled')?.checked ? "1" : "0",
|
|
price_sync_schedule: el('settPriceSyncSchedule')?.value || '',
|
|
gomag_products_url: '',
|
|
};
|
|
try {
|
|
const res = await fetch('/api/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const data = await res.json();
|
|
const resultEl = document.getElementById('settSaveResult');
|
|
if (data.success) {
|
|
if (resultEl) { resultEl.textContent = 'Salvat!'; resultEl.style.color = '#16a34a'; }
|
|
setTimeout(() => { if (resultEl) resultEl.textContent = ''; }, 3000);
|
|
} else {
|
|
if (resultEl) { resultEl.textContent = 'Eroare: ' + JSON.stringify(data); resultEl.style.color = '#dc2626'; }
|
|
}
|
|
} catch (err) {
|
|
const resultEl = document.getElementById('settSaveResult');
|
|
if (resultEl) { resultEl.textContent = 'Eroare: ' + err.message; resultEl.style.color = '#dc2626'; }
|
|
}
|
|
}
|
|
|
|
async function startCatalogSync() {
|
|
const btn = document.getElementById('btnCatalogSync');
|
|
const status = document.getElementById('settPriceSyncStatus');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Sincronizare...';
|
|
try {
|
|
const res = await fetch('/api/price-sync/start', { method: 'POST' });
|
|
const data = await res.json();
|
|
if (data.error) {
|
|
status.innerHTML = `<span class="text-danger">${escHtml(data.error)}</span>`;
|
|
btn.disabled = false;
|
|
btn.textContent = 'Sincronizează acum';
|
|
return;
|
|
}
|
|
// Poll status
|
|
const pollInterval = setInterval(async () => {
|
|
const sr = await fetch('/api/price-sync/status');
|
|
const sd = await sr.json();
|
|
if (sd.status === 'running') {
|
|
status.textContent = sd.phase_text || 'Sincronizare în curs...';
|
|
} else {
|
|
clearInterval(pollInterval);
|
|
btn.disabled = false;
|
|
btn.textContent = 'Sincronizează acum';
|
|
if (sd.last_run) status.textContent = `Ultima: ${sd.last_run.finished_at || ''} — ${sd.last_run.updated || 0} actualizate din ${sd.last_run.matched || 0}`;
|
|
}
|
|
}, 2000);
|
|
} catch (err) {
|
|
status.innerHTML = `<span class="text-danger">${escHtml(err.message)}</span>`;
|
|
btn.disabled = false;
|
|
btn.textContent = 'Sincronizează acum';
|
|
}
|
|
}
|
|
|
|
function wireAutocomplete(inputId, dropdownId) {
|
|
const input = document.getElementById(inputId);
|
|
const dropdown = document.getElementById(dropdownId);
|
|
if (!input || !dropdown) return;
|
|
|
|
input.addEventListener('input', () => {
|
|
clearTimeout(settAcTimeout);
|
|
settAcTimeout = setTimeout(async () => {
|
|
const q = input.value.trim();
|
|
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="settSelectArticle('${inputId}', '${dropdownId}', '${escHtml(r.codmat)}')">
|
|
<span class="codmat">${escHtml(r.codmat)}</span> — <span class="denumire">${escHtml(r.denumire)}</span>
|
|
</div>`
|
|
).join('');
|
|
dropdown.classList.remove('d-none');
|
|
} catch { dropdown.classList.add('d-none'); }
|
|
}, 250);
|
|
});
|
|
|
|
input.addEventListener('blur', () => {
|
|
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
|
});
|
|
}
|
|
|
|
function settSelectArticle(inputId, dropdownId, codmat) {
|
|
document.getElementById(inputId).value = codmat;
|
|
document.getElementById(dropdownId).classList.add('d-none');
|
|
}
|
|
|
|
function escHtml(s) {
|
|
if (s == null) return '';
|
|
return String(s)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|