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,86 +1,150 @@
// src/lib/api.js
// Cliente para el backend FastAPI. Cambia BASE_URL si el backend corre en otra IP.
const BASE_URL = "http://localhost:8000";
// Cliente del backend FastAPI de Leads.
// 1) Si existe VITE_API_URL (archivo .env del frontend), se usa esa.
// 2) Si no, se decide por el host desde el que se abrió la página:
// localhost -> backend local http://localhost:8001
// cualquier otro -> API publicada del servidor
// Así funciona en desarrollo y en el servidor sin necesitar ningún .env.
const API_LOCAL = "http://localhost:8001";
const API_SERVIDOR = "https://dashboardapi.escueladerefrigeracion.edu.pe";
async function get(path, params = {}) {
function _baseUrlPorDefecto() {
if (typeof window === "undefined") return API_LOCAL;
const h = window.location.hostname;
if (h === "localhost" || h === "127.0.0.1" || h === "::1") {
return API_LOCAL;
}
const proto = window.location.protocol || "http:";
return `${proto}//dashboardapi.escueladerefrigeracion.edu.pe`;
}
const BASE_URL = (
import.meta.env.VITE_API_URL || _baseUrlPorDefecto()
).replace(/\/+$/, "");
// ── Cache en memoria (frontend) con TTL 15 min y "stale-while-revalidate" ──
// Guarda la respuesta de cada GET (por path+params). Al volver a pedir:
// - si hay dato vigente (< 15 min) -> lo devuelve al instante, SIN fetch.
// - si venció -> devuelve el viejo al instante Y refresca en 2º plano.
// Así, al cambiar de apartado y volver, NO se recarga; y a los 15 min se
// actualiza solo sin mostrar loader. Los POST invalidan el cache.
const TTL = 15 * 60 * 1000; // 15 minutos
const _cache = new Map(); // key -> { ts, data }
const _enVuelo = new Map(); // key -> Promise (evita fetch duplicado)
function _key(path, params) {
return path + "?" + new URLSearchParams(params).toString();
}
async function _fetch(path, params) {
const qs = new URLSearchParams(params).toString();
const url = `${BASE_URL}${path}${qs ? "?" + qs : ""}`;
const r = await fetch(url);
const r = await fetch(`${BASE_URL}${path}${qs ? "?" + qs : ""}`);
if (!r.ok) throw new Error(`Error ${r.status} en ${path}`);
return r.json();
}
async function postJson(path, body) {
// GET con cache. Devuelve SIEMPRE una promesa que resuelve rápido si hay cache.
function get(path, params = {}) {
const key = _key(path, params);
const hit = _cache.get(key);
const now = Date.now();
if (hit) {
// Hay algo cacheado -> devolverlo YA.
if (now - hit.ts >= TTL && !_enVuelo.has(key)) {
// Venció: refrescar en 2º plano (no bloquea, no muestra loader).
const p = _fetch(path, params)
.then((data) => { _cache.set(key, { ts: Date.now(), data }); return data; })
.finally(() => _enVuelo.delete(key));
_enVuelo.set(key, p);
}
return Promise.resolve(hit.data);
}
// No hay cache: si ya hay una petición en vuelo para esta key, reusarla.
if (_enVuelo.has(key)) return _enVuelo.get(key);
const p = _fetch(path, params)
.then((data) => { _cache.set(key, { ts: Date.now(), data }); return data; })
.finally(() => _enVuelo.delete(key));
_enVuelo.set(key, p);
return p;
}
// Invalida entradas del cache cuyo path empiece con alguno de los prefijos dados.
function invalidar(...prefijos) {
for (const k of _cache.keys()) {
if (prefijos.some((p) => k.startsWith(p))) _cache.delete(k);
}
}
// POST helper: hace el POST y luego invalida los caches afectados.
async function post(path, body, invalidarPrefijos = []) {
const r = await fetch(`${BASE_URL}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!r.ok) throw new Error(`Error ${r.status} en ${path}`);
return r.json();
}
async function post(path, params = {}) {
const qs = new URLSearchParams(params).toString();
const url = `${BASE_URL}${path}${qs ? "?" + qs : ""}`;
const r = await fetch(url, { method: "POST" });
if (!r.ok) throw new Error(`Error ${r.status} en ${path}`);
return r.json();
if (!r.ok) throw new Error(`Error al guardar en ${path}`);
const data = await r.json();
if (invalidarPrefijos.length) invalidar(...invalidarPrefijos);
return data;
}
export const api = {
periodoActual: () => get("/api/periodo-actual"),
leadsFiltros: () => get("/api/leads/filtros"),
leads: (ano = "TODOS", mes = "TODOS", dia = "TODOS", programa = "TODOS", sede = "TODOS") =>
get("/api/leads", { ano, mes, dia, programa, sede }),
otrosGeneral: (ano = "TODOS", mes = "TODOS", dia = "TODOS") =>
get("/api/otros-general", { ano, mes, dia }),
vendedores: (ano = "TODOS", mes = "TODOS", dia = "TODOS") =>
get("/api/vendedores", { ano, mes, dia }),
roas: (ano = "TODOS", mes = "TODOS", dia = "TODOS") =>
get("/api/roas", { ano, mes, dia }),
conjuntosSinSede: () => get("/api/conjuntos-sin-sede"),
guardarConjuntoSede: (cambios) =>
post("/api/conjuntos-sin-sede/guardar", { cambios }, ["/api/conjuntos-sin-sede", "/api/roas"]),
borrarConjuntoSede: (conjunto) =>
post("/api/conjuntos-sin-sede/borrar", { conjunto }, ["/api/conjuntos-sin-sede", "/api/roas"]),
ultimaActualizacion: () => get("/api/ultima-actualizacion"),
cacheRefresh: () => post("/api/cache/refresh"),
cacheRefreshManual: (clave) => post("/api/cache/refresh-manual", clave ? { clave } : {}),
precargaModulo: (modulo, ano) => post("/api/precarga/modulo", { modulo, ano }),
precargaEstado: (modulo) => get("/api/precarga/estado", { modulo }),
ocupabilidad: (ano, mes, sede = "TODOS", programa = "TODOS") =>
get("/api/ocupabilidad", { ano, mes, sede, programa }),
ocupabilidadAlumnos: (num_indice) =>
get("/api/ocupabilidad/alumnos", { num_indice }),
ventas: (ano, mes, sede = "TODOS", programa = "TODOS", con_overrides = false) => get("/api/ventas", { ano, mes, sede, programa, con_overrides }),
ventasDetalle: (vendedor, ano, mes, tipo, sede = "TODOS", programa = "TODOS") =>
get("/api/ventas/detalle", { vendedor, ano, mes, tipo, sede, programa }),
cobranza: (ano, mes, sectorista = "TODOS", agrupacion = "SEDE", sede_filtro = "TODOS", prog_filtro = "TODOS", venc_hasta = "TODOS") =>
get("/api/cobranza", { ano, mes, sectorista, agrupacion, sede_filtro, prog_filtro, venc_hasta }),
cobranzaDetalle: (grupo, ano, mes, sectorista = "TODOS", agrupacion = "PROGRAMA", prog_filtro = "TODOS", sede_filtro = "TODOS", venc_hasta = "TODOS") =>
get("/api/cobranza/detalle", { grupo, ano, mes, sectorista, agrupacion, prog_filtro, sede_filtro, venc_hasta }),
alumnoEstado: (matricula) => get("/api/alumno/estado", { matricula }),
alumnosPorRetirar: () => get("/api/alumnos/por-retirar"),
alumnoEstadoGuardar: (matricula, estado, observaciones, usuario) =>
postJson("/api/alumno/estado/guardar", { matricula, estado, observaciones, usuario }),
rentabilidad: (ano, mes, sede = "TODOS", programa = "TODOS") =>
get("/api/rentabilidad", { ano, mes, sede, programa }),
rentabilidadDetalle: (programa, ano, mes) =>
get("/api/rentabilidad/detalle", { programa, ano, mes }),
rentabilidadCostos: (programa, ano, mes) =>
get("/api/rentabilidad/costos", { programa, ano, mes }),
saldoPendiente: (tipo_cuota) => get("/api/saldo-pendiente", { tipo_cuota }),
asesores: () => get("/api/asesores"),
asesorEstado: (agent_id, online) =>
post("/api/asesores/estado", { agent_id, online }),
guardarCostos: (num_indice, inicial, actual) =>
postJson("/api/rentabilidad/costos/guardar", { num_indice, inicial, actual }),
clasificarSede: (programas) =>
postJson("/api/cobranza/clasificar-sede", { programas }),
clasificarFull: (programas) =>
postJson("/api/cobranza/clasificar-full", { programas }),
cobranzaDetalleTodos: (ano, mes, sectorista = "TODOS") =>
get("/api/cobranza/detalle-todos", { ano, mes, sectorista }),
usuariosListar: () => get("/api/usuarios"),
usuariosCrear: (email, password, nombre, rol) =>
postJson("/api/usuarios/crear", { email, password, nombre, rol }),
usuariosActualizarRol: (id, rol) => postJson("/api/usuarios/rol", { id, rol }),
usuariosEliminar: (id) => postJson("/api/usuarios/eliminar", { id }),
vendedoresManualesListar: (ano, mes) => get("/api/comisiones/vendedores", { ano, mes }),
vendedoresManualesCrear: (datos) => postJson("/api/comisiones/vendedores/crear", datos),
vendedoresManualesEliminar: (id) => postJson("/api/comisiones/vendedores/eliminar", { id }),
vendedoresManualesActualizar: (datos) => postJson("/api/comisiones/vendedores/actualizar", datos),
vendedoresManualesLote: (datos) => postJson("/api/comisiones/vendedores/lote", datos),
comisionesOverrideGuardar: (registros) => postJson("/api/comisiones/override/guardar", { registros }),
comisionesOverrideRestaurar: (num_matricula) => postJson("/api/comisiones/override/restaurar", { num_matricula }),
comisionesDetalleTodos: (ano, mes) => get("/api/comisiones/detalle-todos", { ano, mes }),
comisionesConfig: (ano, mes) => get("/api/comisiones/config", { ano, mes }),
comisionesConfigGuardar: (datos) => postJson("/api/comisiones/config/guardar", datos),
// Actualiza TODO manualmente (como si pasaran los 15 min), pero EN SEGUNDO
// PLANO: NO se toca el cache del cliente mientras el backend trabaja, así el
// usuario sigue viendo los datos viejos sin pantallas de carga durante los
// ~29s. Recien cuando el backend responde OK se limpia el cache del cliente,
// para que a partir de ahi todo muestre los datos nuevos de inmediato.
refrescarTodo: async () => {
const r = await fetch(`${BASE_URL}/api/cache/refresh`, { method: "POST" });
if (!r.ok) throw new Error("Error al actualizar");
await r.json(); // espera a que el backend TERMINE de rehacer todo
_cache.clear(); // solo AHORA se invalida el cache del cliente
// Trae la hora de actualizacion FRESCA (sin cache) y la deja cacheada, para
// que el sidebar muestre la hora del refresco manual recien hecho.
const hr = await _fetch("/api/ultima-actualizacion", {});
_cache.set(_key("/api/ultima-actualizacion", {}), { ts: Date.now(), data: hr });
return hr; // { hora: "..." }
},
conjuntosSinPauta: () => get("/api/conjuntos-sin-pauta"),
usoPauta: (pauta, excluir) => get("/api/pauta/uso", { pauta, excluir: excluir || "" }),
leyendaAnuncios: () => get("/api/leyenda-anuncios"),
programasDisponibles: () => get("/api/programas-disponibles"),
programasOcultos: () => get("/api/programas-ocultos"),
encenderProgramas: (num_indices) =>
post("/api/programas-ocultos/encender", { num_indices },
["/api/programas-ocultos", "/api/programas-disponibles", "/api/leads"]),
guardarLeyenda: (cambios) =>
post("/api/leyenda-anuncios/guardar", { cambios }, ["/api/leyenda-anuncios", "/api/roas"]),
guardarPauta: (num_indice, pauta, conjunto, contar) =>
post("/api/curso/guardar-pauta", { num_indice, pauta, conjunto, contar },
["/api/leads", "/api/conjuntos-sin-pauta", "/api/roas"]),
alertas: () => get("/api/leads/alertas"),
// Leyenda (campanias de Supabase). Al agregar/borrar, invalida TODOS los
// apartados que dependen de las leyendas para que el cambio se vea al instante.
campanias: () => get("/api/campanias"),
agregarCampania: (datos) =>
post("/api/campanias/agregar", datos,
["/api/campanias", "/api/leads", "/api/vendedores", "/api/roas", "/api/otros-general"]),
editarCampania: (id, datos) =>
post("/api/campanias/editar", { id, ...datos },
["/api/campanias", "/api/leads", "/api/vendedores", "/api/roas", "/api/otros-general"]),
borrarCampania: (id) =>
post("/api/campanias/borrar", { id },
["/api/campanias", "/api/leads", "/api/vendedores", "/api/roas", "/api/otros-general"]),
};