feat(envios/erp): add manual sync trigger endpoint and test UI

- POST /envios/erp/sync-now?ventana_min=N calls sync_recientes with configurable window
- sync_recientes() now returns a result dict (ok, total, created, updated, skipped, errores, detalle)
- ERP tab shows a "Prueba manual" card with ventana selector (2m / 30m / 2h / hoy) and result breakdown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-16 21:40:54 -05:00
co-authored by Claude Sonnet 4.6
parent 8e27225326
commit dc18d128fb
3 changed files with 124 additions and 14 deletions
+85
View File
@@ -78,6 +78,31 @@
</div>
</div>
<!-- Prueba manual -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="font-semibold text-gray-800">
<i class="fas fa-play-circle mr-2 text-blue-500"></i>Prueba manual
</h3>
</div>
<div class="p-5 flex flex-wrap items-center gap-4">
<select id="sel-ventana"
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300">
<option value="2">Últimos 2 minutos (igual al scheduler)</option>
<option value="30" selected>Últimos 30 minutos</option>
<option value="120">Últimas 2 horas</option>
<option value="1440">Hoy completo</option>
</select>
<button id="btn-sync-now" onclick="ejecutarSyncNow()"
class="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-bolt text-xs"></i> Ejecutar ahora
</button>
</div>
<div id="sync-now-result" class="hidden px-5 pb-5">
<div id="sync-now-inner"></div>
</div>
</div>
<!-- Historial de sync automático -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
@@ -133,6 +158,66 @@ function switchTab(name) {
const _origenLabel = { manual: 'Manual', tercero: 'Tercero', scheduler: 'Scheduler', automation: 'Automatización' };
const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' };
async function ejecutarSyncNow() {
const btn = document.getElementById('btn-sync-now');
const ventana = document.getElementById('sel-ventana').value;
const resBox = document.getElementById('sync-now-result');
const inner = document.getElementById('sync-now-inner');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin text-xs"></i> Sincronizando…';
resBox.classList.remove('hidden');
inner.innerHTML = '<p class="text-sm text-gray-400">Ejecutando…</p>';
try {
const resp = await fetch(`/envios/erp/sync-now?ventana_min=${ventana}`, { method: 'POST' });
const d = await resp.json();
if (!d.ok) {
inner.innerHTML = `
<div class="flex items-center gap-2 text-red-600 bg-red-50 border border-red-200 rounded-lg px-4 py-3 text-sm">
<i class="fas fa-exclamation-circle"></i>
<span>${d.error || 'Error desconocido'}</span>
</div>`;
} else if (d.total === 0) {
inner.innerHTML = `
<div class="flex items-center gap-2 text-gray-500 bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-sm">
<i class="fas fa-info-circle text-blue-400"></i>
<span>${d.mensaje || 'Sin recepciones en la ventana seleccionada.'} (Hoy: <strong>${d.total_hoy}</strong> recepción/es registradas)</span>
</div>`;
} else {
const detalles = (d.detalle || []).map(p => {
const a = p.action || (p.ok ? 'updated' : 'error');
const badge = a === 'created' ? '<span class="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">Nuevo</span>'
: a === 'updated' ? '<span class="text-xs bg-green-100 text-green-700 px-1.5 py-0.5 rounded">Actualizado</span>'
: a === 'skipped' ? '<span class="text-xs bg-gray-100 text-gray-500 px-1.5 py-0.5 rounded">Omitido</span>'
: `<span class="text-xs bg-red-100 text-red-600 px-1.5 py-0.5 rounded" title="${p.message || ''}">${a}</span>`;
return `<div class="flex items-center justify-between py-1 border-b border-gray-50 text-xs">
<span class="text-gray-700">${p.nombre || p.doc || '—'}</span>
${badge}
</div>`;
}).join('');
inner.innerHTML = `
<div class="bg-green-50 border border-green-200 rounded-lg px-4 py-3 mb-3">
<div class="flex flex-wrap gap-4 text-sm">
<span class="text-green-700 font-semibold"><i class="fas fa-check-circle mr-1"></i>${d.total} procesados</span>
<span class="text-blue-600">${d.created} nuevos</span>
<span class="text-green-600">${d.updated} actualizados</span>
<span class="text-gray-400">${d.skipped} omitidos</span>
${d.errores > 0 ? `<span class="text-red-500 font-medium">${d.errores} errores</span>` : ''}
<span class="text-gray-400 ml-auto text-xs">Hoy: ${d.total_hoy} recepciones</span>
</div>
</div>
${detalles ? `<div class="max-h-48 overflow-y-auto pr-1">${detalles}</div>` : ''}`;
}
} catch (e) {
inner.innerHTML = `<p class="text-sm text-red-500">Error de red: ${e.message}</p>`;
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-bolt text-xs"></i> Ejecutar ahora';
}
}
async function cargarHistorialScheduler() {
const el = document.getElementById('scheduler-hist');
el.innerHTML = '<p class="text-sm text-gray-400 text-center py-4">Cargando…</p>';