diff --git a/app/database.py b/app/database.py index 94b3674..c2b770f 100644 --- a/app/database.py +++ b/app/database.py @@ -13,6 +13,17 @@ def get_connection(): return conn +def _migrate(conn): + cols = {r[1] for r in conn.execute("PRAGMA table_info(envios)")} + if "idrecepcion" not in cols: + conn.execute("ALTER TABLE envios ADD COLUMN idrecepcion INTEGER") + if "contrato" not in cols: + conn.execute("ALTER TABLE envios ADD COLUMN contrato TEXT") + if "mensaje_tns" not in cols: + conn.execute("ALTER TABLE envios ADD COLUMN mensaje_tns TEXT") + conn.commit() + + def init_db(): conn = get_connection() conn.executescript(""" @@ -67,4 +78,5 @@ def init_db(): ); """) conn.commit() + _migrate(conn) conn.close() diff --git a/app/routes/logs.py b/app/routes/logs.py index d35580c..0a4eae4 100644 --- a/app/routes/logs.py +++ b/app/routes/logs.py @@ -1,9 +1,12 @@ from fastapi import APIRouter, Request, Depends +from fastapi.responses import JSONResponse from app.database import get_connection from app.auth import get_current_user router = APIRouter(prefix="/logs", tags=["logs"]) +PAGE_SIZE = 50 + @router.get("") async def logs_page( @@ -12,9 +15,12 @@ async def logs_page( tipo: str = "", status: str = "", factura: str = "", + paciente: str = "", + fecha_desde: str = "", + fecha_hasta: str = "", + offset: int = 0, ): conn = get_connection() - where = ["1=1"] params = [] @@ -27,29 +33,71 @@ async def logs_page( if factura: where.append("e.factura LIKE ?") params.append(f"%{factura}%") + if paciente: + where.append("(CAST(e.idrecepcion AS TEXT) LIKE ? OR e.contrato LIKE ?)") + params += [f"%{paciente}%", f"%{paciente}%"] + if fecha_desde: + where.append("DATE(e.created_at) >= ?") + params.append(fecha_desde) + if fecha_hasta: + where.append("DATE(e.created_at) <= ?") + params.append(fecha_hasta) + + w = " AND ".join(where) + + total = conn.execute(f"SELECT COUNT(*) FROM envios e WHERE {w}", params).fetchone()[0] envios = conn.execute(f""" SELECT e.*, u.username FROM envios e LEFT JOIN users u ON e.user_id = u.id - WHERE {' AND '.join(where)} - ORDER BY e.created_at DESC LIMIT 100 - """, params).fetchall() + WHERE {w} + ORDER BY e.created_at DESC + LIMIT ? OFFSET ? + """, params + [PAGE_SIZE, offset]).fetchall() stats = conn.execute(""" - SELECT - tipo, - status, - COUNT(*) as total, - SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as exitosos, - SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as fallidos + SELECT tipo, status, COUNT(*) as cnt FROM envios GROUP BY tipo, status + ORDER BY tipo, status """).fetchall() conn.close() + has_more = (offset + PAGE_SIZE) < total + return request.app.state.templates.TemplateResponse("logs.html", { "request": request, "user": user, - "envios": envios, "stats": stats, - "filtro_tipo": tipo, "filtro_status": status, "filtro_factura": factura, + "envios": envios, "stats": stats, "total": total, + "filtro_tipo": tipo, "filtro_status": status, + "filtro_factura": factura, "filtro_paciente": paciente, + "filtro_fecha_desde": fecha_desde, "filtro_fecha_hasta": fecha_hasta, + "offset": offset, "page_size": PAGE_SIZE, "has_more": has_more, + }) + + +@router.get("/detalle/{envio_id}") +async def detalle_envio(envio_id: int, request: Request, user: dict = Depends(get_current_user)): + conn = get_connection() + row = conn.execute( + "SELECT e.*, u.username FROM envios e LEFT JOIN users u ON e.user_id = u.id WHERE e.id = ?", + (envio_id,) + ).fetchone() + conn.close() + if not row: + return JSONResponse({"error": "No encontrado"}, status_code=404) + return JSONResponse({ + "id": row["id"], + "tipo": row["tipo"], + "factura": row["factura"], + "idrecepcion": row["idrecepcion"], + "contrato": row["contrato"], + "status": row["status"], + "mensaje_tns": row["mensaje_tns"], + "respuesta_api": row["respuesta_api"], + "json_enviado": row["json_enviado"], + "fecha_inicio": row["fecha_inicio"], + "fecha_fin": row["fecha_fin"], + "created_at": row["created_at"], + "username": row["username"], }) diff --git a/app/routes/transaccion.py b/app/routes/transaccion.py index 8e420ca..d92cc72 100644 --- a/app/routes/transaccion.py +++ b/app/routes/transaccion.py @@ -12,194 +12,273 @@ from app.services.api_client import get_tns_token, TNS_BASE router = APIRouter(prefix="/transaccion", tags=["transaccion"]) +def _cfg_and_fb(): + conn = get_connection() + cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + return cfg + + +def _query_rows(cfg, query_text, factura, fecha_inicio, fecha_fin): + import re as _re + fb, ok, msg = get_firebird_from_config(cfg) + if not ok: + return None, msg, None + nums = _re.findall(r'\d+', factura or "") + num_val = int(nums[-1]) if nums else (factura or "") + params = {"fecha_ini": f"{fecha_inicio} 00:00:00", "fecha_fin": f"{fecha_fin} 23:59:59"} + if ":num_factura" in query_text: + params["num_factura"] = num_val + ok2, err, rows = fb.execute_query(query_text, params) + fb.disconnect() + if not ok2: + return None, err, None + return rows, None, fb + + +def _is_sent(idrecepcion: int) -> dict: + """Returns the latest envio record for this idrecepcion, or None.""" + conn = get_connection() + row = conn.execute( + "SELECT status, mensaje_tns, created_at FROM envios WHERE idrecepcion=? AND tipo='transaccion' ORDER BY id DESC LIMIT 1", + (idrecepcion,) + ).fetchone() + conn.close() + if row: + return {"status": row["status"], "mensaje": row["mensaje_tns"], "at": row["created_at"]} + return None + + @router.get("") async def transaccion_page(request: Request, user: dict = Depends(get_current_user)): conn = get_connection() - envios = conn.execute(""" - SELECT * FROM envios WHERE tipo = 'transaccion' - ORDER BY created_at DESC LIMIT 20 - """).fetchall() queries = conn.execute( "SELECT * FROM queries WHERE query_type = 'transaccion' ORDER BY name" ).fetchall() configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} conn.close() - return request.app.state.templates.TemplateResponse("transaccion.html", { - "request": request, "user": user, - "envios": envios, "queries": queries, - "configs": configs, + "request": request, "user": user, "queries": queries, "configs": configs, }) @router.post("/preview") async def preview_transaccion( - request: Request, - user: dict = Depends(get_current_user), - query_id: int = Form(...), - factura: str = Form(""), - fecha_inicio: str = Form(...), - fecha_fin: str = Form(...), + request: Request, user: dict = Depends(get_current_user), + query_id: int = Form(...), factura: str = Form(""), + fecha_inicio: str = Form(...), fecha_fin: str = Form(...), ): conn = get_connection() q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() - configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()} conn.close() - if not q: return JSONResponse({"success": False, "message": "Consulta no encontrada"}) - fb, ok, msg = get_firebird_from_config(configs) - if not ok: - return JSONResponse({"success": False, "message": f"Error Firebird: {msg}"}) - - if ":num_factura" in q["query_text"] and not factura: - fb.disconnect() - return JSONResponse({"success": False, "message": "Ingresa el número de factura"}) - - # Extraer solo la parte numérica: "LHXC 3918" o "LHXC-3918" → 3918 - import re as _re - nums = _re.findall(r'\d+', factura) - num_factura_val = int(nums[-1]) if nums else factura - - params = { - "fecha_ini": f"{fecha_inicio} 00:00:00", - "fecha_fin": f"{fecha_fin} 23:59:59", - } - if ":num_factura" in q["query_text"]: - params["num_factura"] = num_factura_val - - success, error, rows = fb.execute_query(q["query_text"], params) - fb.disconnect() - - if not success: - return JSONResponse({"success": False, "message": error}) + rows, err, _ = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin) + if rows is None: + return JSONResponse({"success": False, "message": err}) + if not rows: + return JSONResponse({"success": False, "message": "Sin datos"}) grupos = agrupar_por_recepcion(rows) - prof_def = configs.get("profesional_default", "") - esp_def = configs.get("especialidad_default", "") - json_result = [generar_rda_paciente(grupo, prof_def, esp_def) for grupo in grupos.values()] + prof_def = cfg.get("profesional_default", "") + esp_def = cfg.get("especialidad_default", "") + remis_def = cfg.get("remisionante_default", "00") + prefijo_def = cfg.get("prefijo_tns_default", "00") + + items = [] + for id_rec, grupo_rows in grupos.items(): + enviado = _is_sent(id_rec) + rda = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def) + items.append({ + "idrecepcion": id_rec, + "factura": grupo_rows[0].get("NUM_FACTURA", ""), + "paciente": grupo_rows[0].get("COD_PACIENTE", ""), + "contrato": grupo_rows[0].get("CODCONTRATO", ""), + "examenes": [r.get("COD_EXAMEN", "") for r in grupo_rows], + "valor": float(grupo_rows[0].get("VALORTOTAL") or 0), + "enviado": enviado, + "json": rda, + }) + + pendientes = sum(1 for i in items if not i["enviado"]) + enviados_ok = sum(1 for i in items if i["enviado"] and i["enviado"]["status"] == "success") + enviados_err = sum(1 for i in items if i["enviado"] and i["enviado"]["status"] == "error") return JSONResponse({ "success": True, - "rows_count": len(rows), - "grupos_count": len(grupos), - "columns": list(rows[0].keys()) if rows else [], - "preview": rows[:5], - "generated_json": json_result[0] if json_result else None, - "total_json": len(json_result), + "total": len(items), + "pendientes": pendientes, + "enviados_ok": enviados_ok, + "enviados_err": enviados_err, + "items": items, }) -@router.post("/send") -async def send_transaccion( - request: Request, - user: dict = Depends(get_current_user), - query_id: int = Form(...), - factura: str = Form(""), - fecha_inicio: str = Form(...), - fecha_fin: str = Form(...), +@router.post("/send-one") +async def send_one( + request: Request, user: dict = Depends(get_current_user), + idrecepcion: int = Form(...), query_id: int = Form(...), + factura: str = Form(""), fecha_inicio: str = Form(...), fecha_fin: str = Form(...), ): conn = get_connection() q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() - configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} + cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()} conn.close() - if not q: - return JSONResponse({"success": False, "message": "Consulta no encontrada"}) - - fb, ok, msg = get_firebird_from_config(configs) - if not ok: - return JSONResponse({"success": False, "message": f"Error Firebird: {msg}"}) - - if ":num_factura" in q["query_text"] and not factura: - fb.disconnect() - return JSONResponse({"success": False, "message": "Ingresa el número de factura"}) - - # Extraer solo la parte numérica: "LHXC 3918" o "LHXC-3918" → 3918 - import re as _re - nums = _re.findall(r'\d+', factura) - num_factura_val = int(nums[-1]) if nums else factura - - params = { - "fecha_ini": f"{fecha_inicio} 00:00:00", - "fecha_fin": f"{fecha_fin} 23:59:59", - } - if ":num_factura" in q["query_text"]: - params["num_factura"] = num_factura_val - - success, error, rows = fb.execute_query(q["query_text"], params) - fb.disconnect() - - if not success: - return JSONResponse({"success": False, "message": error}) - if not rows: - return JSONResponse({"success": False, "message": "No se encontraron datos"}) + rows, err, _ = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin) + if rows is None: + return JSONResponse({"success": False, "message": err}) grupos = agrupar_por_recepcion(rows) + grupo_rows = grupos.get(idrecepcion) + if not grupo_rows: + return JSONResponse({"success": False, "message": f"IDRECEPCION {idrecepcion} no encontrado"}) token, token_err = await get_tns_token( - configs.get("tns_empresa", ""), - configs.get("tns_usuario", ""), - configs.get("tns_password", ""), + cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "") ) if not token: return JSONResponse({"success": False, "message": f"Error login TNS: {token_err}"}) headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"} - api_sucursal = configs.get("api_sucursal", "") or "00" + api_sucursal = cfg.get("api_sucursal", "") or "00" + endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}" + + rda_json = generar_rda_paciente( + grupo_rows, + cfg.get("profesional_default", ""), + cfg.get("especialidad_default", ""), + cfg.get("remisionante_default", "00"), + cfg.get("prefijo_tns_default", "00"), + ) + + raw_resp = "" + ok_rda = False + msg_tns = "" + try: + async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client: + r = await client.post(endpoint, json=rda_json, headers=headers) + raw_resp = r.text + try: + data = r.json() + ok_rda = bool(data.get("status") or (data.get("data") or {}).get("success", False)) + msg_tns = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300]) + except Exception: + ok_rda = r.status_code < 300 + msg_tns = raw_resp[:300] + except Exception as ex: + msg_tns = str(ex) + + conn = get_connection() + conn.execute(""" + INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato, + fecha_inicio, fecha_fin, pacientes_count, servicios_count, + status, json_enviado, respuesta_api, mensaje_tns, created_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + """, ( + user["user_id"], "transaccion", + str(grupo_rows[0].get("NUM_FACTURA", idrecepcion)), + idrecepcion, + str(grupo_rows[0].get("CODCONTRATO", "")), + fecha_inicio, fecha_fin, 1, len(grupo_rows), + "success" if ok_rda else "error", + json_lib.dumps(rda_json, ensure_ascii=False)[:5000], + raw_resp[:2000], + msg_tns, + datetime.now().isoformat(), + )) + conn.commit() + conn.close() + + return JSONResponse({"success": ok_rda, "message": msg_tns, "raw_tns": raw_resp, "idrecepcion": idrecepcion}) + + +@router.post("/send") +async def send_transaccion( + request: Request, user: dict = Depends(get_current_user), + query_id: int = Form(...), factura: str = Form(""), + fecha_inicio: str = Form(...), fecha_fin: str = Form(...), + solo_pendientes: str = Form("0"), +): + conn = get_connection() + q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() + cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()} + conn.close() + + rows, err, _ = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin) + if rows is None: + return JSONResponse({"success": False, "message": err}) + if not rows: + return JSONResponse({"success": False, "message": "Sin datos"}) + + grupos = agrupar_por_recepcion(rows) + if solo_pendientes == "1": + grupos = {k: v for k, v in grupos.items() if not _is_sent(k)} + + token, token_err = await get_tns_token( + cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "") + ) + if not token: + return JSONResponse({"success": False, "message": f"Error login TNS: {token_err}"}) + + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"} + api_sucursal = cfg.get("api_sucursal", "") or "00" endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}" - total_enviados = 0 - total_errores = 0 resultados = [] - prof_def = configs.get("profesional_default", "") - esp_def = configs.get("especialidad_default", "") - remis_def = configs.get("remisionante_default", "00") - prefijo_def = configs.get("prefijo_tns_default", "00") - - async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client: - for id_recepcion, grupo_rows in grupos.items(): - trans_json = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def) - - status_ok = False - response_text = "" + async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client: + for id_rec, grupo_rows in grupos.items(): + rda_json = generar_rda_paciente( + grupo_rows, + cfg.get("profesional_default", ""), + cfg.get("especialidad_default", ""), + cfg.get("remisionante_default", "00"), + cfg.get("prefijo_tns_default", "00"), + ) + raw_resp = "" + ok_rda = False + msg_tns = "" try: - resp = await client.post(endpoint, json=trans_json, headers=headers) - status_ok = resp.is_success - response_text = resp.text[:1000] - except Exception as e: - response_text = str(e) - - if status_ok: - total_enviados += 1 - else: - total_errores += 1 - - fact = str(grupo_rows[0].get("NUM_FACTURA", id_recepcion)) - resultados.append({"factura": fact, "success": status_ok, "msg": response_text}) + r = await client.post(endpoint, json=rda_json, headers=headers) + raw_resp = r.text + try: + data = r.json() + ok_rda = bool(data.get("status") or (data.get("data") or {}).get("success", False)) + msg_tns = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300]) + except Exception: + ok_rda = r.status_code < 300 + msg_tns = raw_resp[:300] + except Exception as ex: + msg_tns = str(ex) conn = get_connection() conn.execute(""" - INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin, - pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato, + fecha_inicio, fecha_fin, pacientes_count, servicios_count, + status, json_enviado, respuesta_api, mensaje_tns, created_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, ( - user["user_id"], "transaccion", fact, - fecha_inicio, fecha_fin, - 1, len(grupo_rows), - "success" if status_ok else "error", - json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000], - response_text, + user["user_id"], "transaccion", + str(grupo_rows[0].get("NUM_FACTURA", id_rec)), + id_rec, + str(grupo_rows[0].get("CODCONTRATO", "")), + fecha_inicio, fecha_fin, 1, len(grupo_rows), + "success" if ok_rda else "error", + json_lib.dumps(rda_json, ensure_ascii=False)[:5000], + raw_resp[:2000], msg_tns, datetime.now().isoformat(), )) conn.commit() conn.close() + resultados.append({"idrecepcion": id_rec, "success": ok_rda, "msg": msg_tns}) + ok_count = sum(1 for r in resultados if r["success"]) + err_count = len(resultados) - ok_count return JSONResponse({ - "success": total_errores == 0, - "total_enviados": total_enviados, - "total_errores": total_errores, + "success": err_count == 0, + "total_enviados": ok_count, + "total_errores": err_count, "resultados": resultados, - "message": f"Enviados: {total_enviados}, Errores: {total_errores}", }) diff --git a/app/templates/logs.html b/app/templates/logs.html index 619235a..6096dc2 100644 --- a/app/templates/logs.html +++ b/app/templates/logs.html @@ -2,131 +2,272 @@ {% block title %}Historial{% endblock %} {% block header %}Historial de Envíos{% endblock %} {% block content %} -
-
-
+ + +{% if stats %} +
+ {% for s in stats %} +
+ {{ s.tipo }} + + {{ s.cnt }} + + {{ 'exitosos' if s.status == 'success' else 'errores' }} +
+ {% endfor %} +
+ {{ total }} total +
+
+{% endif %} + + +
+ + +
- - +
- -
- - + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
- - -
-
- {% if envios %} -
- - - - - - - - - - - - - - - - - {% for e in envios %} - - - - - - - - - - - - - {% endfor %} - -
IDTipoFacturaEstadoPacientesServiciosRespuesta APIFechaUsuario
{{ e.id }} - - {{ e.tipo }} - - {{ e.factura or '-' }} - - {{ 'Exitoso' if e.status == 'success' else 'Error' }} - - {{ e.pacientes_count or 0 }}{{ e.servicios_count or 0 }}{{ e.respuesta_api[:80] if e.respuesta_api else '-' }}{{ e.created_at[:19] }}{{ e.username }} - -
- {% else %} -
- -

No hay registros con los filtros seleccionados

-
- {% endif %} -
+
- -