Files

459 lines
17 KiB
Python

# 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))
# ── ESTADO / OBSERVACIONES POR ALUMNO (Cobranza) ───────────────────────────
@app.get("/api/alumno/estado")
def get_alumno_estado(matricula: str):
try:
return services.alumno_estado_obtener(matricula)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/alumno/estado/guardar")
def post_alumno_estado(payload: dict = Body(...)):
try:
return services.alumno_estado_guardar(
payload.get("matricula",""),
payload.get("estado","EN CURSO"),
payload.get("observaciones",[]),
payload.get("usuario"))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/alumnos/por-retirar")
def get_alumnos_por_retirar():
try:
return services.alumnos_por_retirar()
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)