459 lines
17 KiB
Python
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)
|