import json as json_lib from datetime import datetime, date as date_cls import httpx from fastapi import APIRouter, Request, Depends from fastapi.responses import JSONResponse from app.database import get_connection from app.auth import get_current_user from app.services.json_generator import _clean_times from app.services.api_client import get_tns_token, TNS_BASE from app.utils.activity import log_activity, get_ip from app.routes.automation import _parse_tns_resp router = APIRouter(prefix="/logs", tags=["logs"]) def _limpiar_json_enviado(raw: str) -> str: if not raw: return raw try: return json_lib.dumps(_clean_times(json_lib.loads(raw)), ensure_ascii=False, indent=2) except Exception: return raw PAGE_SIZE = 50 @router.get("") async def logs_page( request: Request, user: dict = Depends(get_current_user), tipo: str = "", status: str = "", factura: str = "", paciente: str = "", fecha_desde: str = "", fecha_hasta: str = "", offset: int = 0, ): conn = get_connection() where = ["1=1"] params = [] if tipo: where.append("e.tipo = ?") params.append(tipo) if status: where.append("e.status = ?") params.append(status) if factura: where.append("e.factura LIKE ?") params.append(f"%{factura}%") if paciente: where.append("(e.cedula LIKE ? OR e.factura 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 {w} ORDER BY e.created_at DESC LIMIT ? OFFSET ? """, params + [PAGE_SIZE, offset]).fetchall() stats = conn.execute(""" 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, "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("/actividad") async def actividad_page( request: Request, user: dict = Depends(get_current_user), username: str = "", accion: str = "", fecha_desde: str = "", fecha_hasta: str = "", offset: int = 0, ): conn = get_connection() where = ["1=1"] params = [] if username: where.append("a.username = ?") params.append(username) if accion: where.append("a.accion = ?") params.append(accion) if fecha_desde: where.append("DATE(a.created_at) >= ?") params.append(fecha_desde) if fecha_hasta: where.append("DATE(a.created_at) <= ?") params.append(fecha_hasta) w = " AND ".join(where) total = conn.execute(f"SELECT COUNT(*) FROM activity_log a WHERE {w}", params).fetchone()[0] rows = conn.execute(f""" SELECT a.* FROM activity_log a WHERE {w} ORDER BY a.created_at DESC LIMIT ? OFFSET ? """, params + [PAGE_SIZE, offset]).fetchall() usuarios = conn.execute("SELECT DISTINCT username FROM activity_log ORDER BY username").fetchall() acciones = conn.execute("SELECT DISTINCT accion FROM activity_log ORDER BY accion").fetchall() conn.close() return request.app.state.templates.TemplateResponse("actividad.html", { "request": request, "user": user, "rows": rows, "total": total, "usuarios": [u["username"] for u in usuarios], "acciones": [a["accion"] for a in acciones], "filtro_username": username, "filtro_accion": accion, "filtro_fecha_desde": fecha_desde, "filtro_fecha_hasta": fecha_hasta, "offset": offset, "page_size": PAGE_SIZE, "has_more": (offset + PAGE_SIZE) < total, }) @router.post("/reenviar/{envio_id}") async def reenviar_envio(envio_id: int, request: Request, user: dict = Depends(get_current_user)): conn = get_connection() row = conn.execute("SELECT * FROM envios WHERE id = ?", (envio_id,)).fetchone() conn.close() if not row: return JSONResponse({"success": False, "message": "Registro no encontrado"}) if row["status"] in ("success", "warning"): return JSONResponse({"success": False, "message": "No aplica reenvío para este registro"}) if not row["json_enviado"]: return JSONResponse({"success": False, "message": "Sin JSON guardado para reenviar"}) try: rda_json = json_lib.loads(row["json_enviado"]) except Exception: return JSONResponse({"success": False, "message": "JSON inválido en registro"}) conn_cfg = get_connection() cfg = {r["key"]: r["value"] for r in conn_cfg.execute("SELECT * FROM config").fetchall()} conn_cfg.close() 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: {err}"}) headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} api_sucursal = cfg.get("api_sucursal", "00") or "00" tipo = row["tipo"] endpoint = ( f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}" if tipo == "transaccion" else f"{TNS_BASE}/v2/facturacion/Ventas/Crear?codigosucursal={api_sucursal}" if tipo == "ventas" else f"{TNS_BASE}/v2/tablas/Tercero/Crear" ) new_status = "error" msg = "" try: async with httpx.AsyncClient(timeout=30) as client: r = await client.post(endpoint, json=rda_json, headers=headers) new_status, msg = _parse_tns_resp(r) except Exception as ex: msg = str(ex) if new_status != "error": conn = get_connection() conn.execute( "UPDATE envios SET status=?, mensaje_tns=?, respuesta_api=?, created_at=? WHERE id=?", (new_status, msg[:500], msg[:5000], datetime.now().isoformat(), envio_id) ) conn.commit() conn.close() log_activity(user["user_id"], user["username"], "reenvio_ok", f"Envío #{envio_id} factura {row['factura']} reenvío {new_status}", get_ip(request)) return JSONResponse({"success": new_status != "error", "message": msg}) @router.get("/resumen") async def resumen_page( request: Request, user: dict = Depends(get_current_user), fecha: str = "", tipo: str = "", ): if not fecha: fecha = date_cls.today().isoformat() conn = get_connection() where_extra = "" params_q: list = [fecha] if tipo: where_extra = "AND tipo = ?" params_q.append(tipo) rows = conn.execute(f""" SELECT factura, tipo, MAX(contrato) AS contrato, MIN(cedula) AS cedula, MAX(idrecepcion) AS idrecepcion, COUNT(*) AS intentos, SUM(CASE WHEN status != 'error' THEN 1 ELSE 0 END) AS exitos, MIN(created_at) AS primer_envio, MAX(created_at) AS ultimo_envio, MAX(CASE WHEN status = 'error' THEN id END) AS ultimo_error_id, MAX(CASE WHEN status != 'error' THEN id END) AS exitoso_id, CASE WHEN SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) > 0 THEN 'success' WHEN SUM(CASE WHEN status = 'warning' THEN 1 ELSE 0 END) > 0 THEN 'warning' ELSE 'error' END AS estado_final FROM envios WHERE DATE(created_at) = ? {where_extra} GROUP BY CASE WHEN tipo = 'terceros' THEN cedula ELSE factura END, tipo ORDER BY estado_final, tipo, CASE WHEN tipo = 'terceros' THEN cedula ELSE factura END """, params_q).fetchall() conn.close() total = len(rows) exitosos = sum(1 for r in rows if r["estado_final"] != "error") errores = sum(1 for r in rows if r["estado_final"] == "error") return request.app.state.templates.TemplateResponse("resumen.html", { "request": request, "user": user, "fecha": fecha, "filtro_tipo": tipo, "rows": rows, "total": total, "exitosos": exitosos, "errores": errores, }) @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": _limpiar_json_enviado(row["json_enviado"]), "fecha_inicio": row["fecha_inicio"], "fecha_fin": row["fecha_fin"], "created_at": row["created_at"], "username": row["username"], })