Solucionado submodulos y subiendo codigo real
This commit is contained in:
410
backend/main.py
Normal file
410
backend/main.py
Normal file
@@ -0,0 +1,410 @@
|
||||
# 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.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))
|
||||
|
||||
|
||||
# ── 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)
|
||||
Reference in New Issue
Block a user