from datetime import datetime, timedelta 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, guardar_sync_log 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_EXAMENES_CEDULA = """ SELECT r.IDRECEPCION, r.HORAINICIORECEPCION, TRIM(rel.COD_EXAMEN) AS COD_EXAMEN, TRIM(ex.NOMBRE) AS NOM_EXAMEN, COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS, rel.PRECIO, TRIM(r.DIAG_PPAL) AS DIAG_PPAL, TRIM(d.CONCEPTO) AS DIAG_CONCEPTO, TRIM(m.DOCIDMEDICO) AS DOCIDMEDICO, TRIM(r.NIT_EMPRESA) AS NIT_EMPRESA, r.VALORTOTAL FROM RECEPCION r JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION LEFT JOIN EXAMEN ex ON TRIM(ex.CODIGO) = TRIM(rel.COD_EXAMEN) LEFT JOIN DIAGNOSTICO d ON TRIM(d.COD_DIAG) = TRIM(r.DIAG_PPAL) LEFT JOIN MEDICO m ON m.CODIGO = r.COD_MEDICO JOIN PACIENTE p ON p.CODIGO = r.COD_PACIENTE WHERE TRIM(p.DOCIDENT) = :cedula AND CAST(r.FECHA_RECEPCION AS DATE) = CURRENT_DATE ORDER BY r.IDRECEPCION DESC """ _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("/examenes") async def examenes_paciente(request: Request, cedula: str = ""): """Server-to-server: exámenes registrados hoy (últimos 5 min) para una cédula.""" conn = get_connection() configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} conn.close() api_key = request.headers.get("X-Lab-Key", "") if not api_key or api_key != configs.get("whatsapp_api_key", ""): return JSONResponse({"ok": False, "error": "No autorizado"}, status_code=401) cedula = cedula.strip() if not cedula: return JSONResponse({"ok": False, "error": "cedula requerida"}, status_code=400) fb, ok, msg = get_firebird_from_config(configs) if not ok: return JSONResponse({"ok": False, "error": f"Error Firebird: {msg}"}, status_code=503) ok_q, err_q, rows = fb.execute_query(_SQL_EXAMENES_CEDULA, {"cedula": cedula}) fb.disconnect() if not ok_q: return JSONResponse({"ok": False, "error": f"Error consulta: {err_q}"}, status_code=500) # Filtrar últimos 5 minutos por HORAINICIORECEPCION ahora = datetime.now() limite = ahora - timedelta(minutes=5) def parse_hora(raw) -> datetime | None: if not raw: return None if isinstance(raw, datetime): return ahora.replace(hour=raw.hour, minute=raw.minute, second=raw.second, microsecond=0) s = str(raw) try: if len(s) > 10 and (s[10] == "T" or s[10] == " "): dt = datetime.fromisoformat(s.replace(" ", "T")) return ahora.replace(hour=dt.hour, minute=dt.minute, second=dt.second, microsecond=0) partes = s.split(":") return ahora.replace(hour=int(partes[0]), minute=int(partes[1]), second=int(partes[2].split(".")[0]), microsecond=0) except Exception: return None def safe_num(val): if val is None: return None try: return float(val) except Exception: return None def fmt_hora_str(val) -> str: if not val: return "" if isinstance(val, datetime): return val.strftime("%H:%M:%S") s = str(val) if len(s) > 10 and (s[10] == "T" or s[10] == " "): return s[11:19] return s[:8] examenes = [] for row in rows: hora_dt = parse_hora(row.get("HORAINICIORECEPCION")) if hora_dt and hora_dt < limite: continue examenes.append({ "recepcion_id": row.get("IDRECEPCION"), "hora": fmt_hora_str(row.get("HORAINICIORECEPCION")), "cod_examen": (row.get("COD_EXAMEN") or "").strip(), "nombre": (row.get("NOM_EXAMEN") or "").strip(), "cups": (row.get("CUPS") or "").strip(), "precio": safe_num(row.get("PRECIO")), "diagnostico_cod": (row.get("DIAG_PPAL") or "").strip(), "diagnostico_nombre": (row.get("DIAG_CONCEPTO") or "").strip(), "medico_docidmedico": (row.get("DOCIDMEDICO") or "").strip(), "nit_empresa": (row.get("NIT_EMPRESA") or "").strip(), "valor_total": safe_num(row.get("VALORTOTAL")), }) return JSONResponse({"ok": True, "examenes": examenes}) @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, modo="insertar") guardar_sync_log(resultado, user["user_id"], origen="manual", modo="insertar") resultado["success"] = True resultado["message"] = ( f"{resultado['total']} procesados — " f"{resultado['created']} nuevos, " f"{resultado['skipped']} ya existían, " 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])