feat(sync): already_imported tracking, invoice cache, path fixes, remove vfp
- Track already_imported/new_imported counts separately in sync_runs and surface them in status API + dashboard last-run card - Cache invoice data in SQLite orders table (factura_* columns); dashboard falls back to Oracle only for uncached imported orders - Resolve JSON_OUTPUT_DIR and SQLITE_DB_PATH relative to known anchored roots in config.py, independent of CWD (fixes WSL2 start) - Use single Oracle connection for entire validation phase (perf) - Batch upsert web_products instead of one-by-one - Remove stale VFP scripts (replaced by gomag-vending.prg workflow) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ let qmAcTimeout = null;
|
||||
let _pollInterval = null;
|
||||
let _lastSyncStatus = null;
|
||||
let _lastRunId = null;
|
||||
let _currentRunId = null;
|
||||
|
||||
// ── Init ──────────────────────────────────────────
|
||||
|
||||
@@ -66,6 +67,13 @@ function updateSyncPanel(data) {
|
||||
if (txt) txt.textContent = statusLabels[data.status] || data.status || 'Inactiv';
|
||||
if (startBtn) startBtn.disabled = data.status === 'running';
|
||||
|
||||
// Track current running sync run_id
|
||||
if (data.status === 'running' && data.run_id) {
|
||||
_currentRunId = data.run_id;
|
||||
} else {
|
||||
_currentRunId = null;
|
||||
}
|
||||
|
||||
// Live progress area
|
||||
if (progressArea) {
|
||||
progressArea.style.display = data.status === 'running' ? 'flex' : 'none';
|
||||
@@ -84,7 +92,16 @@ function updateSyncPanel(data) {
|
||||
const st = document.getElementById('lastSyncStatus');
|
||||
if (d) d.textContent = lr.started_at ? lr.started_at.replace('T', ' ').slice(0, 16) : '\u2014';
|
||||
if (dur) dur.textContent = lr.duration_seconds ? Math.round(lr.duration_seconds) + 's' : '\u2014';
|
||||
if (cnt) cnt.textContent = '\u2191' + (lr.imported || 0) + ' \u2298' + (lr.skipped || 0) + ' \u2715' + (lr.errors || 0);
|
||||
// Updated counts: ↑new =already ⊘skipped ✕errors
|
||||
if (cnt) {
|
||||
const newImp = lr.new_imported || 0;
|
||||
const already = lr.already_imported || 0;
|
||||
if (already > 0) {
|
||||
cnt.textContent = '\u2191' + newImp + ' =' + already + ' \u2298' + (lr.skipped || 0) + ' \u2715' + (lr.errors || 0);
|
||||
} else {
|
||||
cnt.textContent = '\u2191' + (lr.imported || 0) + ' \u2298' + (lr.skipped || 0) + ' \u2715' + (lr.errors || 0);
|
||||
}
|
||||
}
|
||||
if (st) {
|
||||
st.textContent = lr.status === 'completed' ? '\u2713' : '\u2715';
|
||||
st.style.color = lr.status === 'completed' ? '#10b981' : '#ef4444';
|
||||
@@ -92,14 +109,16 @@ function updateSyncPanel(data) {
|
||||
}
|
||||
}
|
||||
|
||||
// Wire last-sync-row click → journal
|
||||
// Wire last-sync-row click → journal (use current running sync if active)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('lastSyncRow')?.addEventListener('click', () => {
|
||||
if (_lastRunId) window.location = '/logs?run=' + _lastRunId;
|
||||
const targetId = _currentRunId || _lastRunId;
|
||||
if (targetId) window.location = '/logs?run=' + targetId;
|
||||
});
|
||||
document.getElementById('lastSyncRow')?.addEventListener('keydown', (e) => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && _lastRunId) {
|
||||
window.location = '/logs?run=' + _lastRunId;
|
||||
const targetId = _currentRunId || _lastRunId;
|
||||
if ((e.key === 'Enter' || e.key === ' ') && targetId) {
|
||||
window.location = '/logs?run=' + targetId;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -391,10 +410,11 @@ function fmtDate(dateStr) {
|
||||
|
||||
function orderStatusBadge(status) {
|
||||
switch ((status || '').toUpperCase()) {
|
||||
case 'IMPORTED': return '<span class="badge bg-success">Importat</span>';
|
||||
case 'SKIPPED': return '<span class="badge bg-warning text-dark">Omis</span>';
|
||||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
||||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||||
case 'IMPORTED': return '<span class="badge bg-success">Importat</span>';
|
||||
case 'ALREADY_IMPORTED': return '<span class="badge bg-info">Deja importat</span>';
|
||||
case 'SKIPPED': return '<span class="badge bg-warning text-dark">Omis</span>';
|
||||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
||||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ let currentFilter = 'all';
|
||||
let ordersPage = 1;
|
||||
let currentQmSku = '';
|
||||
let currentQmOrderNumber = '';
|
||||
let ordersSortColumn = 'created_at';
|
||||
let ordersSortDirection = 'asc';
|
||||
let ordersSortColumn = 'order_date';
|
||||
let ordersSortDirection = 'desc';
|
||||
|
||||
function esc(s) {
|
||||
if (s == null) return '';
|
||||
@@ -50,10 +50,11 @@ function runStatusBadge(status) {
|
||||
|
||||
function orderStatusBadge(status) {
|
||||
switch ((status || '').toUpperCase()) {
|
||||
case 'IMPORTED': return '<span class="badge bg-success">Importat</span>';
|
||||
case 'SKIPPED': return '<span class="badge bg-warning text-dark">Omis</span>';
|
||||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
||||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||||
case 'IMPORTED': return '<span class="badge bg-success">Importat</span>';
|
||||
case 'ALREADY_IMPORTED': return '<span class="badge bg-info">Deja importat</span>';
|
||||
case 'SKIPPED': return '<span class="badge bg-warning text-dark">Omis</span>';
|
||||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
||||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,10 +77,13 @@ async function loadRuns() {
|
||||
const started = r.started_at ? new Date(r.started_at).toLocaleString('ro-RO', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : '?';
|
||||
const st = (r.status || '').toUpperCase();
|
||||
const statusEmoji = st === 'COMPLETED' ? '✓' : st === 'RUNNING' ? '⟳' : '✗';
|
||||
const newImp = r.new_imported || 0;
|
||||
const already = r.already_imported || 0;
|
||||
const imp = r.imported || 0;
|
||||
const skip = r.skipped || 0;
|
||||
const err = r.errors || 0;
|
||||
const label = `${started} — ${statusEmoji} ${r.status} (${imp} imp, ${skip} skip, ${err} err)`;
|
||||
const impLabel = already > 0 ? `${newImp} noi, ${already} deja` : `${imp} imp`;
|
||||
const label = `${started} — ${statusEmoji} ${r.status} (${impLabel}, ${skip} skip, ${err} err)`;
|
||||
const selected = r.run_id === currentRunId ? 'selected' : '';
|
||||
return `<option value="${esc(r.run_id)}" ${selected}>${esc(label)}</option>`;
|
||||
}).join('');
|
||||
@@ -133,6 +137,7 @@ async function loadRunOrders(runId, statusFilter, page) {
|
||||
document.querySelectorAll('#orderFilterBtns button').forEach(btn => {
|
||||
btn.className = btn.className.replace(' btn-primary', ' btn-outline-primary')
|
||||
.replace(' btn-success', ' btn-outline-success')
|
||||
.replace(' btn-info', ' btn-outline-info')
|
||||
.replace(' btn-warning', ' btn-outline-warning')
|
||||
.replace(' btn-danger', ' btn-outline-danger');
|
||||
});
|
||||
@@ -147,13 +152,15 @@ async function loadRunOrders(runId, statusFilter, page) {
|
||||
document.getElementById('countImported').textContent = counts.imported || 0;
|
||||
document.getElementById('countSkipped').textContent = counts.skipped || 0;
|
||||
document.getElementById('countError').textContent = counts.error || 0;
|
||||
const alreadyEl = document.getElementById('countAlreadyImported');
|
||||
if (alreadyEl) alreadyEl.textContent = counts.already_imported || 0;
|
||||
|
||||
// Highlight active filter
|
||||
const filterMap = { 'all': 0, 'IMPORTED': 1, 'SKIPPED': 2, 'ERROR': 3 };
|
||||
const filterMap = { 'all': 0, 'IMPORTED': 1, 'ALREADY_IMPORTED': 2, 'SKIPPED': 3, 'ERROR': 4 };
|
||||
const btns = document.querySelectorAll('#orderFilterBtns button');
|
||||
const idx = filterMap[currentFilter] || 0;
|
||||
const idx = filterMap[currentFilter] ?? 0;
|
||||
if (btns[idx]) {
|
||||
const colorMap = ['primary', 'success', 'warning', 'danger'];
|
||||
const colorMap = ['primary', 'success', 'info', 'warning', 'danger'];
|
||||
btns[idx].className = btns[idx].className.replace(`btn-outline-${colorMap[idx]}`, `btn-${colorMap[idx]}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user