Actualizar dominios a dashboardapi y dashboardv1

This commit is contained in:
Administrator
2026-08-29 12:21:21 -05:00
parent 191b5bb1ba
commit 54581c21ab
77 changed files with 7679 additions and 11728 deletions

View File

@@ -1,483 +1,290 @@
# 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
"""API REST del Dashboard de LEADS (FastAPI)."""
from fastapi import FastAPI, Query, HTTPException, Body
from fastapi.middleware.cors import CORSMiddleware
from datetime import datetime
import uvicorn
import os
from dotenv import load_dotenv
# Cargar variables de entorno desde .env
load_dotenv()
import services
from cache_manager import start_background_refresh, cache_invalidate, cache_stats
from cache_manager import start_background_refresh, cache_stats
app = FastAPI(title="Dashboard API", version="1.0")
app = FastAPI(title="Dashboard Leads API", version="1.0")
# Orígenes CORS configurables
cors_env = os.getenv("CORS_ORIGINS", "*")
origins = [o.strip() for o in cors_env.split(",") if o.strip() and o.strip() != "*"]
# 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_origins=origins if origins else ["*"],
allow_origin_regex=".*",
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)
# Precarga en SEGUNDO PLANO (hilo aparte) para no bloquear el arranque del server.
import threading
def _precarga_bg():
print("[startup] Precargando datos de Leads (2do plano)...")
try:
services.precargar_todo()
print("[startup] Precarga completa.")
except Exception as e:
print(f"[startup] Precarga falló: {e}")
threading.Thread(target=_precarga_bg, daemon=True).start()
# Otros General en un hilo aparte (sus consultas son mas pesadas)
def _precarga_otros_bg():
try:
services.precargar_otros_general()
except Exception as e:
print(f"[startup] Precarga Otros falló: {e}")
threading.Thread(target=_precarga_otros_bg, daemon=True).start()
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():
def stats():
return cache_stats()
@app.post("/api/cache/refresh")
def cache_refresh():
def 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/leads/filtros")
def get_filtros():
try:
return services.opciones_filtros()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@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 (para el aviso 'ya cargó todo')."""
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"),
@app.get("/api/leads")
def get_leads(
ano: str = Query("TODOS"),
mes: str = Query("TODOS"),
dia: 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"),
con_overrides: bool = Query(False)):
try:
return services.ventas(ano, mes, sede, programa, con_overrides)
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)
return services.leads_dashboard(ano, mes, dia, programa, sede)
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"),
sede_filtro: str = Query("TODOS"),
prog_filtro: str = Query("TODOS"),
venc_hasta: str = Query("TODOS"),
@app.get("/api/vendedores")
def get_vendedores(
ano: str = Query("TODOS"),
mes: str = Query("TODOS"),
dia: str = Query("TODOS"),
):
try:
return services.cobranza(ano, mes, sectorista, agrupacion, sede_filtro, prog_filtro, venc_hasta)
return services.vendedores_dashboard(ano, mes, dia)
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"),
prog_filtro: str = Query("TODOS"),
sede_filtro: str = Query("TODOS"),
venc_hasta: str = Query("TODOS"),
@app.get("/api/roas")
def get_roas(
ano: str = Query("TODOS"),
mes: str = Query("TODOS"),
dia: str = Query("TODOS"),
):
try:
return services.cobranza_detalle(grupo, ano, mes, sectorista, agrupacion, prog_filtro, sede_filtro, venc_hasta)
return services.roas_dashboard(ano, mes, dia)
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"),
@app.get("/api/conjuntos-sin-sede")
def get_conjuntos_sin_sede():
try:
return services.conjuntos_sin_sede()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/conjuntos-sin-sede/guardar")
def post_conjunto_sede(body: dict = Body(...)):
try:
cambios = body.get("cambios") or []
return services.guardar_conjunto_sede(cambios)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/conjuntos-sin-sede/borrar")
def post_borrar_conjunto_sede(body: dict = Body(...)):
try:
return services.borrar_conjunto_sede(body.get("conjunto") or "")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/otros-general")
def get_otros_general(
ano: str = Query("TODOS"),
mes: str = Query("TODOS"),
dia: str = Query("TODOS"),
):
try:
return services.rentabilidad(ano, mes, sede, programa)
return services.otros_general(ano, mes, dia)
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(...),
):
@app.get("/api/conjuntos-sin-pauta")
def get_conjuntos_sin_pauta():
try:
return services.rentabilidad_detalle(programa, ano, mes)
return {"conjuntos": services.conjuntos_sin_pauta()}
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(...),
):
@app.get("/api/programas-ocultos")
def get_programas_ocultos():
try:
return services.rentabilidad_costos(programa, ano, mes)
return services.programas_ocultos()
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")):
@app.post("/api/programas-ocultos/encender")
def post_encender_programas(body: dict = Body(...)):
try:
return services.saldo_pendiente(tipo_cuota)
nis = body.get("num_indices") or []
return services.encender_programas(nis)
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(...)):
@app.get("/api/programas-disponibles")
def get_programas_disponibles():
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)")
return services.programas_disponibles()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/leyenda-anuncios")
def get_leyenda():
try:
return services.leyenda_anuncios()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/leyenda-anuncios/guardar")
def post_leyenda(body: dict = Body(...)):
try:
cambios = body.get("cambios") or []
return services.guardar_leyenda(cambios)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ── LEYENDA (campanias de Supabase): listar / agregar / borrar ──
@app.get("/api/campanias")
def get_campanias():
try:
return {"filas": services.campanias_listar()}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/campanias/agregar")
def post_campania(body: dict = Body(...)):
try:
return services.campania_agregar(body or {})
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/campanias/editar")
def post_editar_campania(body: dict = Body(...)):
try:
cid = body.get("id")
return services.campania_editar(cid, body or {})
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/campanias/borrar")
def post_borrar_campania(body: dict = Body(...)):
try:
return services.campania_borrar(body.get("id"))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/pauta/uso")
def get_uso_pauta(pauta: str = Query(...), excluir: str = Query("")):
try:
return services.uso_de_pauta(pauta, excluir)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/curso/guardar-pauta")
def post_guardar_pauta(body: dict = Body(...)):
try:
ni = str(body.get("num_indice", "")).strip()
pa = str(body.get("pauta", "")).strip()
cj = body.get("conjunto") or None
contar = body.get("contar") # "SI" / "NO" / None
if not ni:
raise HTTPException(status_code=400, detail="num_indice es obligatorio")
return services.guardar_edicion_curso(ni, pa, cj, contar)
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(...)):
@app.get("/api/ultima-actualizacion")
def get_ultima_actualizacion():
try:
nombres = payload.get("programas", [])
return {"mapa": services.clasificar_programas(nombres)}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/cobranza/clasificar-full")
def post_clasificar_full(payload: dict = Body(...)):
"""Devuelve {programa: {sede, filtro}} usando el JSON REPLACE_META."""
try:
nombres = payload.get("programas", [])
return {"mapa": services.clasificar_programas_full(nombres)}
return services.ultima_actualizacion()
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."""
@app.get("/api/leads/alertas")
def get_alertas():
"""Valores de programa/sede en cartera_junta que NO están en el diccionario."""
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,
}
return services.alertas()
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"):
@app.get("/api/debug/cursos")
def debug_cursos(ano: str = "2026", mes: str = "1"):
"""Diagnóstico: lista cursos del periodo con inscritos, tipo y meta."""
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/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.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))
@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/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}
return services.debug_cursos(ano, mes)
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)
uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=False)