diff --git a/app/database.py b/app/database.py index c2b770f..f2e589f 100644 --- a/app/database.py +++ b/app/database.py @@ -76,6 +76,21 @@ def init_db(): created_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (user_id) REFERENCES users(id) ); + + CREATE TABLE IF NOT EXISTS sync_wa_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + origen TEXT NOT NULL DEFAULT 'manual', + modo TEXT NOT NULL DEFAULT 'insertar', + total INTEGER NOT NULL DEFAULT 0, + created INTEGER NOT NULL DEFAULT 0, + skipped INTEGER NOT NULL DEFAULT 0, + updated INTEGER NOT NULL DEFAULT 0, + errores INTEGER NOT NULL DEFAULT 0, + errores_det TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) + ); """) conn.commit() _migrate(conn) diff --git a/app/routes/automation.py b/app/routes/automation.py index 2cde1ca..396b445 100644 --- a/app/routes/automation.py +++ b/app/routes/automation.py @@ -13,7 +13,7 @@ from app.services.json_generator import ( agrupar_por_recepcion, ) from app.services.api_client import get_tns_token, TNS_BASE -from app.services.whatsapp_sync import sync_paciente +from app.services.whatsapp_sync import sync_paciente, sync_todos, guardar_sync_log router = APIRouter(prefix="/automation", tags=["automation"]) @@ -266,9 +266,8 @@ async def run_automation( if wa_url and wa_key and rows_pac: ingest_url = f"{wa_url}/api/lab/ingest_paciente.php" try: - async with httpx.AsyncClient(timeout=timeout) as wa_client: - for row in rows_pac: - await sync_paciente(row, ingest_url, wa_key, wa_client) + wa_resultado = await sync_todos(rows_pac, ingest_url, wa_key, timeout, modo="upsert") + guardar_sync_log(wa_resultado, user["user_id"], origen="automation", modo="upsert") except Exception: pass diff --git a/app/routes/pacientes.py b/app/routes/pacientes.py index 6340f57..86f2949 100644 --- a/app/routes/pacientes.py +++ b/app/routes/pacientes.py @@ -4,7 +4,7 @@ from fastapi.responses import JSONResponse from app.auth import get_current_user from app.database import get_connection from app.services.firebird_service import get_firebird_from_config -from app.services.whatsapp_sync import sync_todos +from app.services.whatsapp_sync import sync_todos, guardar_sync_log router = APIRouter(prefix="/pacientes", tags=["pacientes"]) @@ -118,6 +118,9 @@ async def sync_all( timeout = int(configs.get("api_timeout", 30)) resultado = await sync_todos(rows, ingest_url, wa_key, timeout, modo="insertar") + + guardar_sync_log(resultado, user["user_id"], origen="manual", modo="insertar") + resultado["success"] = True resultado["message"] = ( f"{resultado['total']} procesados — " @@ -126,3 +129,17 @@ async def sync_all( f"{resultado['errores']} errores." ) return JSONResponse(resultado) + + +@router.get("/historial") +async def historial(request: Request, user: dict = Depends(get_current_user)): + conn = get_connection() + rows = conn.execute(""" + SELECT l.*, u.username + FROM sync_wa_log l + LEFT JOIN users u ON u.id = l.user_id + ORDER BY l.created_at DESC + LIMIT 100 + """).fetchall() + conn.close() + return JSONResponse([dict(r) for r in rows]) diff --git a/app/routes/terceros.py b/app/routes/terceros.py index 1e485a7..81f93ff 100644 --- a/app/routes/terceros.py +++ b/app/routes/terceros.py @@ -8,7 +8,7 @@ from app.auth import get_current_user from app.services.firebird_service import get_firebird_from_config from app.services.json_generator import generar_tercero_api from app.services.api_client import get_tns_token, TNS_BASE -from app.services.whatsapp_sync import sync_paciente +from app.services.whatsapp_sync import sync_paciente, guardar_sync_log router = APIRouter(prefix="/terceros", tags=["terceros"]) @@ -191,8 +191,21 @@ async def send_terceros( if wa_url and wa_key and rows: ingest_url = f"{wa_url}/api/lab/ingest_paciente.php" try: - wa_result = await sync_paciente(rows[0], ingest_url, wa_key) + wa_result = await sync_paciente(rows[0], ingest_url, wa_key, modo="upsert") wa_sync = {"ok": wa_result["ok"], "action": wa_result["action"]} + guardar_sync_log( + { + "total": 1, + "created": 1 if wa_result["action"] == "created" else 0, + "skipped": 1 if wa_result["action"] == "skipped" else 0, + "updated": 1 if wa_result["action"] == "updated" else 0, + "errores": 0 if wa_result["ok"] else 1, + "detalle": [] if wa_result["ok"] else [wa_result], + }, + user["user_id"], + origen="tercero", + modo="upsert", + ) except Exception: wa_sync = {"ok": False, "action": "error"} diff --git a/app/services/whatsapp_sync.py b/app/services/whatsapp_sync.py index 7853032..0584d0e 100644 --- a/app/services/whatsapp_sync.py +++ b/app/services/whatsapp_sync.py @@ -4,10 +4,47 @@ Mapea campos Firebird al formato esperado por /api/lab/ingest_paciente.php. """ import re +import json import httpx from datetime import datetime from typing import Optional +from app.database import get_connection + + +def guardar_sync_log( + resultado: dict, + user_id: int, + origen: str = "manual", + modo: str = "insertar", +) -> None: + errores_det = None + errores = [d for d in resultado.get("detalle", []) if not d.get("ok")] + if errores: + errores_det = json.dumps( + [{"doc": e["doc"], "nombre": e["nombre"], "msg": e["message"]} for e in errores], + ensure_ascii=False, + ) + conn = get_connection() + conn.execute( + """INSERT INTO sync_wa_log + (user_id, origen, modo, total, created, skipped, updated, errores, errores_det) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", + ( + user_id, + origen, + modo, + resultado.get("total", 0), + resultado.get("created", 0), + resultado.get("skipped", 0), + resultado.get("updated", 0), + resultado.get("errores", 0), + errores_det, + ), + ) + conn.commit() + conn.close() + _TIPO_DOC_MAP = { "CC": "CC", "TI": "TI", "RC": "RC", "CE": "CE", "PA": "PA", "NIT": "NIT", "MS": "MS", "AS": "CC", diff --git a/app/templates/pacientes.html b/app/templates/pacientes.html index c3e26f6..90ee142 100644 --- a/app/templates/pacientes.html +++ b/app/templates/pacientes.html @@ -73,6 +73,21 @@ + +
+
+

+ Historial de sincronizaciones +

+ +
+
+

Cargando…

+
+
+
@@ -187,5 +202,76 @@ function renderDetalle(detalle) {
`; } + +const _origenLabel = { manual: 'Manual', tercero: 'Tercero', automation: 'Automatización' }; +const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' }; + +async function cargarHistorial() { + const el = document.getElementById('historialContainer'); + el.innerHTML = '

Cargando…

'; + try { + const resp = await fetch('/pacientes/historial'); + const rows = await resp.json(); + if (!rows.length) { + el.innerHTML = '

Sin registros aún.

'; + return; + } + el.innerHTML = ` +
+ + + + + + + + + + + + + + + + ${rows.map(r => { + const errDet = r.errores_det ? JSON.parse(r.errores_det) : []; + const fecha = r.created_at.replace('T', ' ').slice(0, 16); + return ` + + + + + + + + + + + `; + }).join('')} + +
FechaOrigenModoTotalNuevosOmitidosActualizadosErroresUsuario
${fecha} + ${_origenLabel[r.origen] || r.origen} + ${_modoLabel[r.modo] || r.modo}${r.total}${r.created}${r.skipped}${r.updated} + ${r.errores > 0 && errDet.length ? ` +
+ ${r.errores} +
+ ${errDet.map(e => `
${e.doc} — ${e.msg}
`).join('')} +
+
` : r.errores || '—'} +
${r.username || '—'}
+
`; + } catch (e) { + el.innerHTML = `

Error: ${e.message}

`; + } +} + +// Cargar historial al abrir la página +cargarHistorial(); {% endblock %}