diff --git a/app/routes/automation.py b/app/routes/automation.py index e24ae5d..2cde1ca 100644 --- a/app/routes/automation.py +++ b/app/routes/automation.py @@ -13,6 +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 router = APIRouter(prefix="/automation", tags=["automation"]) @@ -259,6 +260,18 @@ async def run_automation( _guardar_envio(user["user_id"], "terceros", fecha, tercero_json, msg, ok) + # ── Sync paralelo a WhatsApp Lab (silencioso) ───────────────────────────── + wa_url = configs.get("whatsapp_url", "").rstrip("/") + wa_key = configs.get("whatsapp_api_key", "") + 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) + except Exception: + pass + # ── PASO 2: Enviar RDA Paciente ─────────────────────────────────────────── grupos = agrupar_por_recepcion(rows_rda) endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal or '00'}" diff --git a/app/routes/config.py b/app/routes/config.py index 0e10823..3c72e4d 100644 --- a/app/routes/config.py +++ b/app/routes/config.py @@ -27,6 +27,8 @@ DEFAULT_KEYS = [ ("especialidad_default", ""), ("remisionante_default", "00"), ("prefijo_tns_default", "00"), + ("whatsapp_url", ""), + ("whatsapp_api_key", "rips-lab-sync-2026"), ] diff --git a/app/routes/pacientes.py b/app/routes/pacientes.py new file mode 100644 index 0000000..9cf4a0c --- /dev/null +++ b/app/routes/pacientes.py @@ -0,0 +1,128 @@ +from fastapi import APIRouter, Request, Form, Depends +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 + +router = APIRouter(prefix="/pacientes", tags=["pacientes"]) + +# Trae todos los pacientes distintos en un rango de fechas de recepción. +# Reutiliza la misma query de automation para no duplicar lógica. +_SQL_TODOS = """ +SELECT DISTINCT + p.CODIGO, + p.TIPOIDENT, + p.DOCIDENT, + p.NOMBRES, + p.APELLIDOS, + p.DIRECCION, + p.CIUDAD AS COD_CIUDAD, + c.NOMBRE AS NOM_CIUDAD, + p.TELEFONOS, + p.EMAIL, + p.F_NACIMIENTO, + p.SEXO, + p.TIPORES, + p.CODETNIA +FROM PACIENTE p +LEFT JOIN CIUDAD c ON c.CODIGO = p.CIUDAD +LEFT JOIN RECEPCION r ON r.COD_PACIENTE = p.CODIGO +WHERE (:fecha_ini IS NULL OR r.FECHA_RECEPCION BETWEEN :fecha_ini AND :fecha_fin) + AND r.NUM_FACTURA > 0 +""" + +_SQL_SIN_FILTRO = """ +SELECT DISTINCT + p.CODIGO, + p.TIPOIDENT, + p.DOCIDENT, + p.NOMBRES, + p.APELLIDOS, + p.DIRECCION, + p.CIUDAD AS COD_CIUDAD, + c.NOMBRE AS NOM_CIUDAD, + p.TELEFONOS, + p.EMAIL, + p.F_NACIMIENTO, + p.SEXO, + p.TIPORES, + p.CODETNIA +FROM PACIENTE p +LEFT JOIN CIUDAD c ON c.CODIGO = p.CIUDAD +WHERE p.DOCIDENT IS NOT NULL AND p.NOMBRES IS NOT NULL +""" + + +@router.get("") +async def pacientes_page(request: Request, user: dict = Depends(get_current_user)): + conn = get_connection() + configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + wa_url = configs.get("whatsapp_url", "") + wa_key = configs.get("whatsapp_api_key", "") + return request.app.state.templates.TemplateResponse("pacientes.html", { + "request": request, + "user": user, + "wa_configurado": bool(wa_url and wa_key), + "wa_url": wa_url, + }) + + +@router.post("/sync-all") +async def sync_all( + request: Request, + user: dict = Depends(get_current_user), + fecha_ini: str = Form(""), + fecha_fin: str = Form(""), + todos: str = Form(""), +): + conn = get_connection() + configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + + wa_url = configs.get("whatsapp_url", "").rstrip("/") + wa_key = configs.get("whatsapp_api_key", "") + + if not wa_url or not wa_key: + return JSONResponse({ + "success": False, + "message": "Configura la URL y API Key de WhatsApp Lab antes de sincronizar.", + }) + + ingest_url = f"{wa_url}/api/lab/ingest_paciente.php" + + fb, ok, msg = get_firebird_from_config(configs) + if not ok: + return JSONResponse({"success": False, "message": f"Error Firebird: {msg}"}) + + if todos == "1" or (not fecha_ini and not fecha_fin): + ok_q, err_q, rows = fb.execute_query(_SQL_SIN_FILTRO, None) + else: + if not fecha_ini or not fecha_fin: + fb.disconnect() + return JSONResponse({"success": False, "message": "Indica fecha inicio y fecha fin."}) + params = { + "fecha_ini": f"{fecha_ini} 00:00:00", + "fecha_fin": f"{fecha_fin} 23:59:59", + } + ok_q, err_q, rows = fb.execute_query(_SQL_TODOS, params) + + fb.disconnect() + + if not ok_q: + return JSONResponse({"success": False, "message": f"Error al consultar Firebird: {err_q}"}) + if not rows: + return JSONResponse({"success": False, "message": "No se encontraron pacientes con esos criterios."}) + + timeout = int(configs.get("api_timeout", 30)) + resultado = await sync_todos(rows, ingest_url, wa_key, timeout) + resultado["success"] = True + resultado["message"] = ( + f"{resultado['total']} pacientes procesados — " + f"{resultado['created']} creados, " + f"{resultado['updated']} actualizados, " + f"{resultado['errores']} errores." + ) + return JSONResponse(resultado) diff --git a/app/routes/terceros.py b/app/routes/terceros.py index fb99c02..1e485a7 100644 --- a/app/routes/terceros.py +++ b/app/routes/terceros.py @@ -8,6 +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 router = APIRouter(prefix="/terceros", tags=["terceros"]) @@ -183,9 +184,22 @@ async def send_terceros( conn.commit() conn.close() + # ── Sync silencioso a WhatsApp Lab ─────────────────────────────────────── + wa_url = configs.get("whatsapp_url", "").rstrip("/") + wa_key = configs.get("whatsapp_api_key", "") + wa_sync = None + 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_sync = {"ok": wa_result["ok"], "action": wa_result["action"]} + except Exception: + wa_sync = {"ok": False, "action": "error"} + return JSONResponse({ "success": resp_ok, "status_code": resp_code, "message": "Envío exitoso" if resp_ok else f"Error: {resp_text}", "cuv": resp_text[:200] if resp_ok else None, + "wa_sync": wa_sync, }) diff --git a/app/services/whatsapp_sync.py b/app/services/whatsapp_sync.py new file mode 100644 index 0000000..156a81b --- /dev/null +++ b/app/services/whatsapp_sync.py @@ -0,0 +1,115 @@ +""" +Servicio de sincronización de pacientes RIPS → WhatsApp Lab. +Mapea campos Firebird al formato esperado por /api/lab/ingest_paciente.php. +""" + +import re +import httpx +from datetime import datetime +from typing import Optional + +_TIPO_DOC_MAP = { + "CC": "CC", "TI": "TI", "RC": "RC", "CE": "CE", + "PA": "PA", "NIT": "NIT", "MS": "MS", "AS": "CC", + "SI": "CC", "CN": "CC", "DE": "CE", "CD": "PA", + "PE": "CE", "PT": "PA", +} + +_SEXO_MAP = {"M": "M", "F": "F", "H": "M"} + + +def _fmt_fecha_iso(val) -> Optional[str]: + if not val: + return None + if hasattr(val, "strftime"): + return val.strftime("%Y-%m-%d") + s = str(val)[:10] + if re.match(r"\d{4}-\d{2}-\d{2}", s): + return s + # dd/mm/yyyy + parts = s.split("/") + if len(parts) == 3: + return f"{parts[2]}-{parts[1]}-{parts[0]}" + return None + + +def mapear_paciente(row: dict) -> dict: + """Convierte una fila de Firebird PACIENTE al body de ingest_paciente.php.""" + nombres = (row.get("NOMBRES") or "").strip() + apellidos = (row.get("APELLIDOS") or "").strip() + nombre_completo = f"{nombres} {apellidos}".strip().upper() + + tipo_raw = str(row.get("TIPOIDENT") or "CC").strip() + tipo_doc = _TIPO_DOC_MAP.get(tipo_raw, "CC") + + sexo_raw = str(row.get("SEXO") or "M").strip().upper() + genero = _SEXO_MAP.get(sexo_raw, "M") + + email = (row.get("EMAIL") or "").strip().lower() + if not email or "@sinregistro" in email: + email = "" + + return { + "nombre_completo": nombre_completo, + "numero_documento": str(row.get("DOCIDENT") or "").strip(), + "tipo_documento": tipo_doc, + "telefono": str(row.get("TELEFONOS") or "").strip(), + "email": email, + "fecha_nacimiento": _fmt_fecha_iso(row.get("F_NACIMIENTO")), + "genero": genero, + "direccion": (row.get("DIRECCION") or "").strip(), + "ciudad": (row.get("NOM_CIUDAD") or "").strip(), + "origen": "rips", + } + + +async def sync_paciente(row: dict, url: str, api_key: str, client: Optional[httpx.AsyncClient] = None) -> dict: + """Envía un paciente al endpoint de WhatsApp. Retorna {ok, action, message}.""" + payload = mapear_paciente(row) + headers = { + "Content-Type": "application/json", + "X-Lab-Key": api_key, + } + try: + if client: + resp = await client.post(url, json=payload, headers=headers) + else: + async with httpx.AsyncClient(timeout=15) as c: + resp = await c.post(url, json=payload, headers=headers) + + data = resp.json() + return { + "ok": data.get("ok", False), + "action": data.get("action", ""), + "message": data.get("message") or data.get("error", ""), + "doc": payload["numero_documento"], + "nombre": payload["nombre_completo"], + } + except Exception as e: + return { + "ok": False, + "action": "error", + "message": str(e), + "doc": payload.get("numero_documento", ""), + "nombre": payload.get("nombre_completo", ""), + } + + +async def sync_todos(rows: list, url: str, api_key: str, timeout: int = 30) -> dict: + """Envía una lista de filas de pacientes. Retorna resumen {total, created, updated, errores}.""" + resultado = {"total": len(rows), "created": 0, "updated": 0, "errores": 0, "detalle": []} + headers = {"Content-Type": "application/json", "X-Lab-Key": api_key} + + async with httpx.AsyncClient(timeout=timeout) as client: + for row in rows: + r = await sync_paciente(row, url, api_key, client) + if r["ok"]: + if r["action"] == "created": + resultado["created"] += 1 + else: + resultado["updated"] += 1 + else: + resultado["errores"] += 1 + resultado["detalle"].append(r) + + return resultado diff --git a/app/templates/base.html b/app/templates/base.html index f97bbd4..1eebcf8 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -57,6 +57,9 @@ Automatización + + Pacientes WA + Prueba RDA diff --git a/app/templates/config.html b/app/templates/config.html index 0a36d49..988425f 100644 --- a/app/templates/config.html +++ b/app/templates/config.html @@ -154,6 +154,39 @@ +
+ URL base del sistema WhatsApp Lab. Los pacientes de RIPS se sincronizarán a
+ /api/lab/ingest_paciente.php.
+