- PRECIO and VALORTOTAL are Decimal from fdb driver; convert via _safe_num() - HORAINICIORECEPCION is a datetime object; use _fmt_hora_str() for HH:MM:SS - _parse_hora() now handles datetime objects directly (no str() roundtrip) - Same fixes applied to pacientes.py exam builder - sync-now endpoint catches exceptions and returns traceback as JSON Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
257 lines
8.9 KiB
Python
257 lines
8.9 KiB
Python
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])
|