feat(pricing): kit/pachet pricing with price list lookup, replace procent_pret
- 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>
This commit is contained in:
@@ -5,14 +5,14 @@ let searchTimeout = null;
|
||||
let sortColumn = 'sku';
|
||||
let sortDirection = 'asc';
|
||||
let editingMapping = null; // {sku, codmat} when editing
|
||||
let pctFilter = 'all';
|
||||
|
||||
const kitPriceCache = new Map();
|
||||
|
||||
// Load on page ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadMappings();
|
||||
initAddModal();
|
||||
initDeleteModal();
|
||||
initPctFilterPills();
|
||||
});
|
||||
|
||||
function debounceSearch() {
|
||||
@@ -48,44 +48,6 @@ function updateSortIcons() {
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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() {
|
||||
@@ -99,7 +61,6 @@ async function loadMappings() {
|
||||
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}`);
|
||||
@@ -113,7 +74,6 @@ async function loadMappings() {
|
||||
mappings = mappings.filter(m => m.activ || m.sters);
|
||||
}
|
||||
|
||||
updatePctCounts(data.counts);
|
||||
renderTable(mappings, showDeleted);
|
||||
renderPagination(data);
|
||||
updateSortIcons();
|
||||
@@ -131,41 +91,52 @@ function renderTable(mappings, showDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Count CODMATs per SKU for kit detection
|
||||
const skuCodmatCount = {};
|
||||
mappings.forEach(m => {
|
||||
skuCodmatCount[m.sku] = (skuCodmatCount[m.sku] || 0) + 1;
|
||||
});
|
||||
|
||||
let prevSku = null;
|
||||
let html = '';
|
||||
mappings.forEach(m => {
|
||||
mappings.forEach((m, i) => {
|
||||
const isNewGroup = m.sku !== prevSku;
|
||||
if (isNewGroup) {
|
||||
let pctBadge = '';
|
||||
if (m.pct_total !== undefined) {
|
||||
pctBadge = m.is_complete
|
||||
? ` <span class="badge-pct complete">✓ 100%</span>`
|
||||
: ` <span class="badge-pct incomplete">${typeof m.pct_total === 'number' ? m.pct_total.toFixed(0) : m.pct_total}%</span>`;
|
||||
}
|
||||
const isKit = (skuCodmatCount[m.sku] || 0) > 1;
|
||||
const kitBadge = isKit
|
||||
? ` <span class="text-muted small">Kit · ${skuCodmatCount[m.sku]}</span><span class="kit-price-loading" data-sku="${esc(m.sku)}" style="display:none"><span class="spinner-border spinner-border-sm ms-1" style="width:0.8rem;height:0.8rem"></span></span>`
|
||||
: '';
|
||||
const inactiveStyle = !m.activ && !m.sters ? 'opacity:0.6;' : '';
|
||||
html += `<div class="flat-row" style="background:#f8fafc;font-weight:600;border-top:1px solid #e5e7eb;${inactiveStyle}">
|
||||
<span class="${m.activ ? 'dot dot-green' : 'dot dot-yellow'}" style="cursor:${m.sters ? 'default' : 'pointer'}"
|
||||
${m.sters ? '' : `onclick="event.stopPropagation();toggleActive('${esc(m.sku)}', '${esc(m.codmat)}', ${m.activ})"`}
|
||||
title="${m.activ ? 'Activ' : 'Inactiv'}"></span>
|
||||
<strong class="me-1 text-nowrap">${esc(m.sku)}</strong>${pctBadge}
|
||||
<strong class="me-1 text-nowrap">${esc(m.sku)}</strong>${kitBadge}
|
||||
<span class="grow truncate text-muted" style="font-size:0.875rem">${esc(m.product_name || '')}</span>
|
||||
${m.sters
|
||||
? `<button class="btn btn-sm btn-outline-success" onclick="event.stopPropagation();restoreMapping('${esc(m.sku)}', '${esc(m.codmat)}')" title="Restaureaza" style="padding:0.1rem 0.4rem"><i class="bi bi-arrow-counterclockwise"></i></button>`
|
||||
: `<button class="context-menu-trigger" data-sku="${esc(m.sku)}" data-codmat="${esc(m.codmat)}" data-cantitate="${m.cantitate_roa}" data-procent="${m.procent_pret}">⋮</button>`
|
||||
: `<button class="context-menu-trigger" data-sku="${esc(m.sku)}" data-codmat="${esc(m.codmat)}" data-cantitate="${m.cantitate_roa}">⋮</button>`
|
||||
}
|
||||
</div>`;
|
||||
}
|
||||
const deletedStyle = m.sters ? 'text-decoration:line-through;opacity:0.5;' : '';
|
||||
const isKitRow = (skuCodmatCount[m.sku] || 0) > 1;
|
||||
const priceSlot = isKitRow ? `<span class="kit-price-slot text-muted small ms-2" data-sku="${esc(m.sku)}" data-codmat="${esc(m.codmat)}"></span>` : '';
|
||||
html += `<div class="flat-row" style="padding-left:1.5rem;font-size:0.9rem;${deletedStyle}">
|
||||
<code>${esc(m.codmat)}</code>
|
||||
<span class="grow truncate text-muted" style="font-size:0.85rem">${esc(m.denumire || '')}</span>
|
||||
<span class="text-nowrap" style="font-size:0.875rem">
|
||||
<span class="${m.sters ? '' : 'editable'}" style="cursor:${m.sters ? 'default' : 'pointer'}"
|
||||
${m.sters ? '' : `onclick="editFlatValue(this, '${esc(m.sku)}', '${esc(m.codmat)}', 'cantitate_roa', ${m.cantitate_roa})"`}>x${m.cantitate_roa}</span>
|
||||
· <span class="${m.sters ? '' : 'editable'}" style="cursor:${m.sters ? 'default' : 'pointer'}"
|
||||
${m.sters ? '' : `onclick="editFlatValue(this, '${esc(m.sku)}', '${esc(m.codmat)}', 'procent_pret', ${m.procent_pret})"`}>${m.procent_pret}%</span>
|
||||
${m.sters ? '' : `onclick="editFlatValue(this, '${esc(m.sku)}', '${esc(m.codmat)}', 'cantitate_roa', ${m.cantitate_roa})"`}>x${m.cantitate_roa}</span>${priceSlot}
|
||||
</span>
|
||||
</div>`;
|
||||
|
||||
// After last CODMAT of a kit, add total row
|
||||
const isLastOfKit = isKitRow && (i === mappings.length - 1 || mappings[i + 1].sku !== m.sku);
|
||||
if (isLastOfKit) {
|
||||
html += `<div class="flat-row kit-total-slot text-muted small" data-sku="${esc(m.sku)}" style="padding-left:1.5rem;display:none;border-top:1px dashed #e5e7eb"></div>`;
|
||||
}
|
||||
|
||||
prevSku = m.sku;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
@@ -174,17 +145,76 @@ function renderTable(mappings, showDeleted) {
|
||||
container.querySelectorAll('.context-menu-trigger').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const { sku, codmat, cantitate, procent } = btn.dataset;
|
||||
const { sku, codmat, cantitate } = btn.dataset;
|
||||
const rect = btn.getBoundingClientRect();
|
||||
showContextMenu(rect.left, rect.bottom + 2, [
|
||||
{ label: 'Editeaza', action: () => openEditModal(sku, codmat, parseFloat(cantitate), parseFloat(procent)) },
|
||||
{ label: 'Editeaza', action: () => openEditModal(sku, codmat, parseFloat(cantitate)) },
|
||||
{ label: 'Sterge', action: () => deleteMappingConfirm(sku, codmat), danger: true }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// Load prices for visible kits
|
||||
const loadedKits = new Set();
|
||||
container.querySelectorAll('.kit-price-loading').forEach(el => {
|
||||
const sku = el.dataset.sku;
|
||||
if (!loadedKits.has(sku)) {
|
||||
loadedKits.add(sku);
|
||||
loadKitPrices(sku, container);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Inline edit for flat-row values (cantitate / procent)
|
||||
async function loadKitPrices(sku, container) {
|
||||
if (kitPriceCache.has(sku)) {
|
||||
renderKitPrices(sku, kitPriceCache.get(sku), container);
|
||||
return;
|
||||
}
|
||||
// Show loading spinner
|
||||
const spinner = container.querySelector(`.kit-price-loading[data-sku="${CSS.escape(sku)}"]`);
|
||||
if (spinner) spinner.style.display = '';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/mappings/${encodeURIComponent(sku)}/prices`);
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
if (spinner) spinner.innerHTML = `<small class="text-danger">${esc(data.error)}</small>`;
|
||||
return;
|
||||
}
|
||||
kitPriceCache.set(sku, data.prices || []);
|
||||
renderKitPrices(sku, data.prices || [], container);
|
||||
} catch (err) {
|
||||
if (spinner) spinner.innerHTML = `<small class="text-danger">Eroare la încărcarea prețurilor</small>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderKitPrices(sku, prices, container) {
|
||||
if (!prices || prices.length === 0) return;
|
||||
// Update each codmat row with price info
|
||||
const rows = container.querySelectorAll(`.kit-price-slot[data-sku="${CSS.escape(sku)}"]`);
|
||||
let total = 0;
|
||||
rows.forEach(slot => {
|
||||
const codmat = slot.dataset.codmat;
|
||||
const p = prices.find(pr => pr.codmat === codmat);
|
||||
if (p && p.pret_cu_tva > 0) {
|
||||
slot.innerHTML = `${p.pret_cu_tva.toFixed(2)} lei (${p.ptva}%)`;
|
||||
total += p.pret_cu_tva * (p.cantitate_roa || 1);
|
||||
} else if (p) {
|
||||
slot.innerHTML = `<span class="text-muted">fără preț</span>`;
|
||||
}
|
||||
});
|
||||
// Show total
|
||||
const totalSlot = container.querySelector(`.kit-total-slot[data-sku="${CSS.escape(sku)}"]`);
|
||||
if (totalSlot && total > 0) {
|
||||
totalSlot.innerHTML = `Total componente: ${total.toFixed(2)} lei`;
|
||||
totalSlot.style.display = '';
|
||||
}
|
||||
// Hide loading spinner
|
||||
const spinner = container.querySelector(`.kit-price-loading[data-sku="${CSS.escape(sku)}"]`);
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
}
|
||||
|
||||
// Inline edit for flat-row values (cantitate)
|
||||
function editFlatValue(span, sku, codmat, field, currentValue) {
|
||||
if (span.querySelector('input')) return;
|
||||
|
||||
@@ -276,7 +306,7 @@ function clearAddForm() {
|
||||
addCodmatLine();
|
||||
}
|
||||
|
||||
async function openEditModal(sku, codmat, cantitate, procent) {
|
||||
async function openEditModal(sku, codmat, cantitate) {
|
||||
editingMapping = { sku, codmat };
|
||||
document.getElementById('addModalTitle').textContent = 'Editare Mapare';
|
||||
document.getElementById('inputSku').value = sku;
|
||||
@@ -308,7 +338,6 @@ async function openEditModal(sku, codmat, cantitate, procent) {
|
||||
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) {
|
||||
@@ -320,7 +349,6 @@ async function openEditModal(sku, codmat, cantitate, procent) {
|
||||
line.querySelector('.cl-selected').textContent = m.denumire;
|
||||
}
|
||||
line.querySelector('.cl-cantitate').value = m.cantitate_roa;
|
||||
line.querySelector('.cl-procent').value = m.procent_pret;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -330,7 +358,6 @@ async function openEditModal(sku, codmat, cantitate, procent) {
|
||||
if (line) {
|
||||
line.querySelector('.cl-codmat').value = codmat;
|
||||
line.querySelector('.cl-cantitate').value = cantitate;
|
||||
line.querySelector('.cl-procent').value = procent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,9 +379,6 @@ function addCodmatLine() {
|
||||
<div class="col-auto" style="width:90px">
|
||||
<input type="number" class="form-control form-control-sm cl-cantitate" value="1" step="0.001" min="0.001" placeholder="Cant." title="Cantitate ROA">
|
||||
</div>
|
||||
<div class="col-auto" style="width:90px">
|
||||
<input type="number" class="form-control form-control-sm cl-procent" value="100" step="0.01" min="0" max="100" placeholder="% Pret" title="Procent Pret">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
${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 style="width:31px"></div>'}
|
||||
</div>
|
||||
@@ -412,22 +436,12 @@ async function saveMapping() {
|
||||
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 });
|
||||
mappings.push({ codmat, cantitate_roa: cantitate });
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -442,8 +456,7 @@ async function saveMapping() {
|
||||
body: JSON.stringify({
|
||||
new_sku: sku,
|
||||
new_codmat: mappings[0].codmat,
|
||||
cantitate_roa: mappings[0].cantitate_roa,
|
||||
procent_pret: mappings[0].procent_pret
|
||||
cantitate_roa: mappings[0].cantitate_roa
|
||||
})
|
||||
});
|
||||
} else {
|
||||
@@ -471,7 +484,7 @@ async function saveMapping() {
|
||||
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 })
|
||||
body: JSON.stringify({ sku, codmat: mappings[0].codmat, cantitate_roa: mappings[0].cantitate_roa })
|
||||
});
|
||||
} else {
|
||||
res = await fetch('/api/mappings/batch', {
|
||||
@@ -523,7 +536,6 @@ function showInlineAddRow() {
|
||||
<small class="text-muted" id="inlineSelected"></small>
|
||||
</div>
|
||||
<input type="number" class="form-control form-control-sm" id="inlineCantitate" value="1" step="0.001" min="0.001" style="width:70px" placeholder="Cant.">
|
||||
<input type="number" class="form-control form-control-sm" id="inlineProcent" value="100" step="0.01" min="0" max="100" style="width:70px" placeholder="%">
|
||||
<button class="btn btn-sm btn-success" 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>
|
||||
`;
|
||||
@@ -571,7 +583,6 @@ 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; }
|
||||
@@ -580,7 +591,7 @@ async function saveInlineMapping() {
|
||||
const res = await fetch('/api/mappings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku, codmat, cantitate_roa: cantitate, procent_pret: procent })
|
||||
body: JSON.stringify({ sku, codmat, cantitate_roa: cantitate })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
@@ -755,4 +766,3 @@ function handleMappingConflict(data) {
|
||||
if (warn) { warn.textContent = msg; warn.style.display = ''; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,21 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
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() {
|
||||
@@ -66,6 +81,14 @@ async function loadDropdowns() {
|
||||
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);
|
||||
}
|
||||
@@ -100,6 +123,33 @@ async function loadSettings() {
|
||||
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);
|
||||
}
|
||||
@@ -124,6 +174,13 @@ async function saveSettings() {
|
||||
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', {
|
||||
@@ -145,6 +202,40 @@ async function saveSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user