# backend/main.py """ API REST del Dashboard — FastAPI. Reutiliza la lógica de negocio existente en modules/ y core/. Cada endpoint devuelve JSON; el frontend (React) lo consume. """ from fastapi import FastAPI, Query, HTTPException from fastapi.middleware.cors import CORSMiddleware from datetime import datetime import uvicorn import services from cache_manager import start_background_refresh, cache_invalidate, cache_stats app = FastAPI(title="Dashboard API", version="1.0") # CORS — permite que el frontend React (otro puerto) consuma la API app.add_middleware( CORSMiddleware, allow_origins=["*"], # en red local está bien; en producción restringir allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ── Arranque: precarga + refresco en segundo plano ───────────────────────── @app.on_event("startup") def _startup(): print("[startup] Precargando datos iniciales...") try: services.precargar_todo() print("[startup] Precarga completa.") except Exception as e: print(f"[startup] Precarga falló: {e}") # Refresco cada 4 min en segundo plano (TTL del caché es 5 min) start_background_refresh(services.refrescar_todo, interval=900) # ── Salud / utilidades ───────────────────────────────────────────────────── @app.get("/api/health") def health(): return {"status": "ok", "hora": datetime.now().isoformat()} @app.get("/api/cache/stats") def cache_estadisticas(): return cache_stats() @app.post("/api/cache/refresh") def cache_refresh(): services.refrescar_todo() return {"status": "refrescado"} @app.post("/api/cache/refresh-manual") def cache_refresh_manual(clave: str = None): """Botón 'Actualizar': re-lee las bases para la vista actual (clave) al instante, y recachea el resto en segundo plano.""" return services.refrescar_manual(clave) @app.get("/api/ultima-actualizacion") def api_ultima_actualizacion(): return services.ultima_actualizacion() @app.post("/api/precarga/modulo") def api_precarga_modulo(modulo: str, ano: int): """Calienta en 2º plano la tabla principal del módulo para el año indicado.""" return services.precargar_modulo(modulo, ano) @app.get("/api/precarga/estado") def api_precarga_estado(modulo: str): """Progreso de la precarga.""" return services.precarga_estado(modulo) @app.get("/api/periodo-actual") def periodo_actual(): dm = services.get_dm() return {"ano": dm.get_current_year(), "mes": dm.get_current_month()} # ── OCUPABILIDAD ─────────────────────────────────────────────────────────── @app.get("/api/ocupabilidad") def get_ocupabilidad( ano: int = Query(...), mes: int = Query(...), sede: str = Query("TODOS"), programa: str = Query("TODOS"), ): try: return services.ocupabilidad(ano, mes, sede, programa) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/ocupabilidad/alumnos") def get_ocupabilidad_alumnos(num_indice: str = Query(...)): try: return services.ocupabilidad_alumnos(num_indice) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ── VENTAS ───────────────────────────────────────────────────────────────── @app.get("/api/ventas") def get_ventas(ano: int = Query(...), mes: int = Query(...), sede: str = Query("TODOS"), programa: str = Query("TODOS")): try: return services.ventas(ano, mes, sede, programa) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/ventas/detalle") def get_ventas_detalle( vendedor: str = Query(...), ano: int = Query(...), mes: int = Query(...), tipo: str = Query("Venta Inscritos"), sede: str = Query("TODOS"), programa: str = Query("TODOS"), ): try: return services.ventas_detalle(vendedor, ano, mes, tipo, sede, programa) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ── COBRANZA ─────────────────────────────────────────────────────────────── @app.get("/api/cobranza") def get_cobranza( ano: int = Query(...), mes: int = Query(...), sectorista: str = Query("TODOS"), agrupacion: str = Query("SEDE"), ): try: return services.cobranza(ano, mes, sectorista, agrupacion) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/cobranza/detalle") def get_cobranza_detalle( grupo: str = Query(...), ano: int = Query(...), mes: int = Query(...), sectorista: str = Query("TODOS"), agrupacion: str = Query("PROGRAMA"), ): try: return services.cobranza_detalle(grupo, ano, mes, sectorista, agrupacion) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ── RENTABILIDAD ─────────────────────────────────────────────────────────── @app.get("/api/rentabilidad") def get_rentabilidad( ano: int = Query(...), mes: int = Query(...), sede: str = Query("TODOS"), programa: str = Query("TODOS"), ): try: return services.rentabilidad(ano, mes, sede, programa) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/rentabilidad/detalle") def get_rentabilidad_detalle( programa: str = Query(...), ano: int = Query(...), mes: int = Query(...), ): try: return services.rentabilidad_detalle(programa, ano, mes) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/rentabilidad/costos") def get_rentabilidad_costos( programa: str = Query(...), ano: int = Query(...), mes: int = Query(...), ): try: return services.rentabilidad_costos(programa, ano, mes) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ── SALDO PENDIENTE ──────────────────────────────────────────────────────── @app.get("/api/saldo-pendiente") def get_saldo_pendiente(tipo_cuota: str = Query("1° Cuota")): try: return services.saldo_pendiente(tipo_cuota) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ── GUARDAR COSTOS ───────────────────────────────────────────────────────── from fastapi import Body @app.post("/api/rentabilidad/costos/guardar") def post_guardar_costos(payload: dict = Body(...)): try: num_indice = payload.get("num_indice", "") costos_inicial = payload.get("inicial", {}) costos_actual = payload.get("actual", {}) ok = services.guardar_costos(num_indice, costos_inicial, costos_actual) if ok: return {"status": "ok"} raise HTTPException(status_code=500, detail="No se pudo guardar (verifica Supabase)") except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/cobranza/clasificar-sede") def post_clasificar_sede(payload: dict = Body(...)): try: nombres = payload.get("programas", []) return {"mapa": services.clasificar_programas(nombres)} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/debug/supabase-inicios") def debug_supabase_inicios(ano: int, mes: int): """Diagnóstico: muestra qué hay en Supabase y por qué (no) se inyecta el rojo.""" try: dm = services.get_dm() # 1) ¿Supabase conectado? conectado = dm.supabase_client is not None error_conexion = getattr(dm, "supabase_error", None) # 2) Fechas guardadas en Supabase fechas = dm.obtener_fechas_originales() # 3) Índices que SQL trae este mes crudos = dm.ejecutar_consulta_cursos(str(ano), str(mes)) indices_sql = sorted({str(f.get("num_indice","")).strip() for f in (crudos or [])}) # 4) Para cada fecha en Supabase, calcular su mes y si coincide con el filtrado analisis = [] for idx, info in fechas.items(): fecha_orig = info.get("fecha","") if isinstance(info, dict) else str(info) mes_orig = "" if "/" in fecha_orig: p = fecha_orig.split("/") if len(p) >= 2: mes_orig = str(int(p[1])) elif "-" in fecha_orig: p = fecha_orig.split("-") if len(p) >= 2: mes_orig = str(int(p[1])) analisis.append({ "num_indice": idx, "fecha_supabase": fecha_orig, "mes_detectado": mes_orig, "mes_filtrado": str(int(mes)), "coincide_mes": mes_orig == str(int(mes)), "esta_en_sql_este_mes": idx in indices_sql, "deberia_salir_rojo": (mes_orig == str(int(mes))) and (idx not in indices_sql), }) return { "supabase_conectado": conectado, "error_conexion": error_conexion, "total_fechas_en_supabase": len(fechas), "indices_en_sql_este_mes": indices_sql, "analisis": analisis, } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/cobranza/detalle-todos") def get_cobranza_detalle_todos(ano: int, mes: int, sectorista: str = "TODOS"): try: return services.cobranza_detalle_todos(ano, mes, sectorista) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ── USUARIOS ─────────────────────────────────────────────────────────────── @app.get("/api/usuarios") def get_usuarios(): try: return services.usuarios_listar() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/usuarios/crear") def post_usuario_crear(payload: dict = Body(...)): try: return services.usuarios_crear( payload.get("email",""), payload.get("password",""), payload.get("nombre",""), payload.get("rol","COBRANZA")) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/usuarios/rol") def post_usuario_rol(payload: dict = Body(...)): try: return services.usuarios_actualizar_rol(payload.get("id",""), payload.get("rol","")) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/usuarios/eliminar") def post_usuario_eliminar(payload: dict = Body(...)): try: return services.usuarios_eliminar(payload.get("id","")) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ── VENDEDORES MANUALES (Comisiones) ─────────────────────────────────────── @app.get("/api/comisiones/vendedores") def get_vend_manuales(ano: int, mes: int): try: return services.vendedores_manuales_listar(ano, mes) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/comisiones/vendedores/crear") def post_vend_manual_crear(payload: dict = Body(...)): try: return services.vendedores_manuales_crear( payload.get("nombre",""), payload.get("descripcion",""), payload.get("fch_emision",""), payload.get("monto",0), payload.get("ano"), payload.get("mes")) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/comisiones/vendedores/eliminar") def post_vend_manual_eliminar(payload: dict = Body(...)): try: return services.vendedores_manuales_eliminar(payload.get("id","")) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/comisiones/override/guardar") def post_com_override_guardar(payload: dict = Body(...)): try: return services.comisiones_override_guardar(payload.get("registros", [])) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/comisiones/override/restaurar") def post_com_override_restaurar(payload: dict = Body(...)): try: return services.comisiones_override_restaurar(payload.get("num_matricula","")) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/comisiones/vendedores/actualizar") def post_vend_manual_actualizar(payload: dict = Body(...)): try: return services.vendedores_manuales_actualizar( payload.get("id",""), payload.get("descripcion",""), payload.get("fch_emision",""), payload.get("monto",0)) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/comisiones/vendedores/lote") def post_vend_manual_lote(payload: dict = Body(...)): try: return services.vendedores_manuales_guardar_lote( payload.get("nombre",""), payload.get("ano"), payload.get("mes"), payload.get("filas", [])) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/comisiones/detalle-todos") def get_comisiones_detalle_todos(ano: int, mes: int): try: return services.comisiones_detalle_todos(ano, mes) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/comisiones/config") def get_comisiones_config(ano: int = Query(...), mes: int = Query(...)): try: return services.comisiones_config_listar(ano, mes) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/comisiones/config/guardar") def post_comisiones_config(payload: dict = Body(...)): try: return services.comisiones_config_guardar(payload.get("ano"), payload.get("mes"), payload.get("filas", [])) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ── ASESORES (Chatwoot) ──────────────────────────────────────────────────── CHATWOOT_URL = "https://gestor.escueladerefrigeracion.edu.pe" ACCESS_TOKEN = "4anazHvZnvKLtup8biu5Zuoh" ACCOUNT_ID = "1" CHATWOOT_HEADERS = {"api_access_token": ACCESS_TOKEN, "Content-Type": "application/json"} @app.get("/api/asesores") def get_asesores(): import requests try: url = f"{CHATWOOT_URL}/api/v1/accounts/{ACCOUNT_ID}/agents" r = requests.get(url, headers=CHATWOOT_HEADERS, timeout=10) r.raise_for_status() return {"agentes": r.json()} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/asesores/estado") def set_asesor_estado(agent_id: int = Query(...), online: bool = Query(...)): import requests, time url = f"{CHATWOOT_URL}/api/v1/accounts/{ACCOUNT_ID}/agents/{agent_id}" estado = "online" if online else "offline" try: requests.patch(url, headers=CHATWOOT_HEADERS, json={"auto_offline": False}, timeout=10) time.sleep(0.5) r = requests.patch(url, headers=CHATWOOT_HEADERS, json={"availability_status": estado, "availability": estado}, timeout=10) r.raise_for_status() return {"status": "ok", "estado": estado} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) if __name__ == "__main__": uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=False)