From dc18d128fbaa689c2e9780890d7d76959d349ae3 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:40:54 -0500 Subject: [PATCH] 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 --- app/routes/envios.py | 13 +++++- app/services/scheduler.py | 40 +++++++++++------ app/templates/envios_erp.html | 85 +++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 14 deletions(-) diff --git a/app/routes/envios.py b/app/routes/envios.py index 2ee658c..899c79d 100644 --- a/app/routes/envios.py +++ b/app/routes/envios.py @@ -1,6 +1,8 @@ -from fastapi import APIRouter, Request, Depends +from fastapi import APIRouter, Request, Depends, Query +from fastapi.responses import JSONResponse from app.auth import get_current_user from app.database import get_connection +from app.services.scheduler import sync_recientes router = APIRouter(prefix="/envios", tags=["envios"]) @@ -24,3 +26,12 @@ async def envios_erp(request: Request, user: dict = Depends(get_current_user)): "wa_configurado": bool(wa_url and wa_key), "wa_url": wa_url, }) + + +@router.post("/erp/sync-now") +async def erp_sync_now( + ventana_min: int = Query(default=30, ge=1, le=1440), + user: dict = Depends(get_current_user), +): + resultado = await sync_recientes(ventana_min=ventana_min) + return JSONResponse(resultado) diff --git a/app/services/scheduler.py b/app/services/scheduler.py index 047f73b..7a991be 100644 --- a/app/services/scheduler.py +++ b/app/services/scheduler.py @@ -76,8 +76,8 @@ def _parse_hora(val) -> datetime | None: return None -async def sync_recientes(): - """Job que corre cada minuto: sincroniza pacientes con recepción en los últimos 2 min.""" +async def sync_recientes(ventana_min: int = 2) -> dict: + """Job que corre cada minuto: sincroniza pacientes con recepción en los últimos `ventana_min` min.""" conn = get_connection() configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} conn.close() @@ -85,29 +85,31 @@ async def sync_recientes(): wa_url = configs.get("whatsapp_url", "").rstrip("/") wa_key = configs.get("whatsapp_api_key", "") if not wa_url or not wa_key: - return + return {"ok": False, "error": "whatsapp_url o whatsapp_api_key no configurados"} - fb, ok, _ = get_firebird_from_config(configs) + fb, ok, msg = get_firebird_from_config(configs) if not ok: - return + return {"ok": False, "error": f"Firebird: {msg}"} - ok_pac, _, rows_pac = fb.execute_query(_SQL_HOY, None) - ok_ex, _, rows_ex = fb.execute_query(_SQL_EXAMENES_HOY, None) + ok_pac, err_pac, rows_pac = fb.execute_query(_SQL_HOY, None) + ok_ex, _, rows_ex = fb.execute_query(_SQL_EXAMENES_HOY, None) fb.disconnect() - if not ok_pac or not rows_pac: - return + if not ok_pac: + return {"ok": False, "error": f"Query pacientes: {err_pac}"} - limite = datetime.now() - timedelta(minutes=2) + total_hoy = len(rows_pac) if rows_pac else 0 + limite = datetime.now() - timedelta(minutes=ventana_min) recientes = [ - row for row in rows_pac + row for row in (rows_pac or []) if (h := _parse_hora(row.get("HORAINICIORECEPCION"))) is None or h >= limite ] if not recientes: - return + return {"ok": True, "total": 0, "created": 0, "updated": 0, + "skipped": 0, "errores": 0, "total_hoy": total_hoy, + "ventana_min": ventana_min, "mensaje": "Sin recepciones en la ventana de tiempo"} - # Construir mapa DOCIDENT → lista de exámenes filtrados por la misma ventana de 2 min examenes_map: dict[str, list] = {} if ok_ex and rows_ex: for ex in rows_ex: @@ -138,3 +140,15 @@ async def sync_recientes(): if resultado["total"] > 0: guardar_sync_log(resultado, 0, origen="scheduler", modo="upsert") + + return { + "ok": True, + "total": resultado["total"], + "created": resultado["created"], + "updated": resultado["updated"], + "skipped": resultado["skipped"], + "errores": resultado["errores"], + "total_hoy": total_hoy, + "ventana_min": ventana_min, + "detalle": resultado.get("detalle", []), + } diff --git a/app/templates/envios_erp.html b/app/templates/envios_erp.html index 6f44410..040c72a 100644 --- a/app/templates/envios_erp.html +++ b/app/templates/envios_erp.html @@ -78,6 +78,31 @@ + +
+
+

+ Prueba manual +

+
+
+ + +
+ +
+
@@ -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 = ' Sincronizando…'; + resBox.classList.remove('hidden'); + inner.innerHTML = '

Ejecutando…

'; + + try { + const resp = await fetch(`/envios/erp/sync-now?ventana_min=${ventana}`, { method: 'POST' }); + const d = await resp.json(); + + if (!d.ok) { + inner.innerHTML = ` +
+ + ${d.error || 'Error desconocido'} +
`; + } else if (d.total === 0) { + inner.innerHTML = ` +
+ + ${d.mensaje || 'Sin recepciones en la ventana seleccionada.'} (Hoy: ${d.total_hoy} recepción/es registradas) +
`; + } else { + const detalles = (d.detalle || []).map(p => { + const a = p.action || (p.ok ? 'updated' : 'error'); + const badge = a === 'created' ? 'Nuevo' + : a === 'updated' ? 'Actualizado' + : a === 'skipped' ? 'Omitido' + : `${a}`; + return `
+ ${p.nombre || p.doc || '—'} + ${badge} +
`; + }).join(''); + inner.innerHTML = ` +
+
+ ${d.total} procesados + ${d.created} nuevos + ${d.updated} actualizados + ${d.skipped} omitidos + ${d.errores > 0 ? `${d.errores} errores` : ''} + Hoy: ${d.total_hoy} recepciones +
+
+ ${detalles ? `
${detalles}
` : ''}`; + } + } catch (e) { + inner.innerHTML = `

Error de red: ${e.message}

`; + } finally { + btn.disabled = false; + btn.innerHTML = ' Ejecutar ahora'; + } +} + async function cargarHistorialScheduler() { const el = document.getElementById('scheduler-hist'); el.innerHTML = '

Cargando…

';