From c0c19d8d40994879b23af8312effe6cfa3a59999 Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 7 Oct 2026 12:03:11 -0500 Subject: [PATCH] Actualizar frontend/src/lib/api.js --- frontend/src/lib/api.js | 70 ++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index e6dbd30..d1065bb 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -1,24 +1,12 @@ -// 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 -const API_LOCAL = "http://localhost:8001"; -const API_SERVIDOR = "https://api-leads.escueladerefrigeracion.lat"; - -function _baseUrlPorDefecto() { - if (typeof window === "undefined") return API_LOCAL; - const h = window.location.hostname; - return h === "localhost" || h === "127.0.0.1" || h === "::1" - ? API_LOCAL - : API_SERVIDOR; -} - -const BASE_URL = ( - import.meta.env.VITE_API_URL || _baseUrlPorDefecto() -).replace(/\/+$/, ""); +// Cliente del backend FastAPI de Leads (puerto 8001). +const BASE_URL = "http://localhost:8001"; // ── 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) @@ -26,6 +14,9 @@ const _gen = new Map(); // key -> nº de generación (para i function _genDe(k) { return _gen.get(k) || 0; } // ── Persistencia en sessionStorage para que el cache SOBREVIVA al F5 ── +// Al recargar la página, el Map en memoria se pierde; aquí lo rehidratamos +// desde sessionStorage, así tras F5 los datos aparecen al instante (y si +// vencieron, se refrescan solos en 2º plano). const _SS_KEY = "leads_api_cache_v1"; function _persistir() { try { @@ -43,6 +34,7 @@ function _persistir() { } catch (e) { /* ignorar */ } })(); +// Guarda en el Map y persiste (una sola vía para no olvidar persistir). function _guardar(key, data) { _cache.set(key, { ts: Date.now(), data }); _persistir(); @@ -59,13 +51,16 @@ async function _fetch(path, params) { return r.json(); } +// 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 g = _genDe(key); const p = _fetch(path, params) .then((data) => { if (_genDe(key) === g) _guardar(key, data); return data; }) @@ -75,6 +70,7 @@ function get(path, params = {}) { 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 g = _genDe(key); @@ -85,13 +81,17 @@ function get(path, params = {}) { return p; } +// Invalida entradas del cache cuyo path empiece con alguno de los prefijos dados. +// Tambien elimina las peticiones EN VUELO de esas keys, para que una respuesta +// vieja no vuelva a cachear el dato antiguo despues de invalidar. function invalidar(...prefijos) { const afecta = (k) => prefijos.some((p) => k.startsWith(p)); for (const k of _cache.keys()) { if (afecta(k)) _cache.delete(k); } for (const k of _enVuelo.keys()) { if (afecta(k)) { _gen.set(k, _genDe(k) + 1); _enVuelo.delete(k); } } - _persistir(); + _persistir(); // reflejar la invalidación también en sessionStorage } +// 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" }, @@ -103,6 +103,9 @@ async function post(path, body, invalidarPrefijos = []) { return data; } +// Prefijos de TODOS los apartados que dependen de la config de los mini-apartados. +// Cualquier guardado (leyenda, sedes, programas, etc.) invalida estos para que el +// cambio se refleje en segundo plano en todo el dashboard. const _TODOS = ["/api/leads", "/api/vendedores", "/api/roas", "/api/otros-general", "/api/programa", "/api/alertas"]; export const api = { @@ -136,21 +139,36 @@ export const api = { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ codigos, asesores, estados }), }).then((r) => { if (!r.ok) throw new Error("Error al obtener base"); return r.json(); }), + // SUBIR BASE + subirBaseEtiquetas: () => _fetch("/api/subir-base/etiquetas", {}), + subirBaseAsesores: () => _fetch("/api/subir-base/asesores", {}), + subirBaseEnviar: (datos) => + fetch(`${BASE_URL}/api/subir-base/enviar`, { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify(datos), + }).then((r) => { if (!r.ok) throw new Error("Error al subir base"); return r.json(); }), conjuntosSinSede: () => get("/api/conjuntos-sin-sede"), guardarConjuntoSede: (cambios) => post("/api/conjuntos-sin-sede/guardar", { cambios }, ["/api/conjuntos-sin-sede", ..._TODOS]), borrarConjuntoSede: (conjunto) => post("/api/conjuntos-sin-sede/borrar", { conjunto }, ["/api/conjuntos-sin-sede", ..._TODOS]), ultimaActualizacion: () => get("/api/ultima-actualizacion"), + // 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(); - _cache.clear(); - _persistir(); + await r.json(); // espera a que el backend TERMINE de rehacer todo + _cache.clear(); // solo AHORA se invalida el cache del cliente + _persistir(); // reflejar el vaciado también en sessionStorage + // 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", {}); _guardar(_key("/api/ultima-actualizacion", {}), hr); - return hr; + return hr; // { hora: "..." } }, conjuntosSinPauta: () => get("/api/conjuntos-sin-pauta"), usoPauta: (pauta, excluir) => get("/api/pauta/uso", { pauta, excluir: excluir || "" }), @@ -167,6 +185,8 @@ export const api = { post("/api/curso/guardar-pauta", { num_indice, pauta, conjunto, contar }, ["/api/conjuntos-sin-pauta", ..._TODOS]), alertasClasif: () => 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, @@ -177,4 +197,4 @@ export const api = { borrarCampania: (id) => post("/api/campanias/borrar", { id }, ["/api/campanias", "/api/leads", "/api/vendedores", "/api/roas", "/api/otros-general", "/api/programa"]), -}; \ No newline at end of file +};