Actualizar frontend/src/lib/api.js
This commit is contained in:
@@ -1,24 +1,12 @@
|
|||||||
// Cliente del backend FastAPI de Leads.
|
// Cliente del backend FastAPI de Leads (puerto 8001).
|
||||||
// 1) Si existe VITE_API_URL (archivo .env del frontend), se usa esa.
|
const BASE_URL = "http://localhost:8001";
|
||||||
// 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(/\/+$/, "");
|
|
||||||
|
|
||||||
// ── Cache en memoria (frontend) con TTL 15 min y "stale-while-revalidate" ──
|
// ── 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 TTL = 15 * 60 * 1000; // 15 minutos
|
||||||
const _cache = new Map(); // key -> { ts, data }
|
const _cache = new Map(); // key -> { ts, data }
|
||||||
const _enVuelo = new Map(); // key -> Promise (evita fetch duplicado)
|
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; }
|
function _genDe(k) { return _gen.get(k) || 0; }
|
||||||
|
|
||||||
// ── Persistencia en sessionStorage para que el cache SOBREVIVA al F5 ──
|
// ── 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";
|
const _SS_KEY = "leads_api_cache_v1";
|
||||||
function _persistir() {
|
function _persistir() {
|
||||||
try {
|
try {
|
||||||
@@ -43,6 +34,7 @@ function _persistir() {
|
|||||||
} catch (e) { /* ignorar */ }
|
} catch (e) { /* ignorar */ }
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// Guarda en el Map y persiste (una sola vía para no olvidar persistir).
|
||||||
function _guardar(key, data) {
|
function _guardar(key, data) {
|
||||||
_cache.set(key, { ts: Date.now(), data });
|
_cache.set(key, { ts: Date.now(), data });
|
||||||
_persistir();
|
_persistir();
|
||||||
@@ -59,13 +51,16 @@ async function _fetch(path, params) {
|
|||||||
return r.json();
|
return r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET con cache. Devuelve SIEMPRE una promesa que resuelve rápido si hay cache.
|
||||||
function get(path, params = {}) {
|
function get(path, params = {}) {
|
||||||
const key = _key(path, params);
|
const key = _key(path, params);
|
||||||
const hit = _cache.get(key);
|
const hit = _cache.get(key);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (hit) {
|
if (hit) {
|
||||||
|
// Hay algo cacheado -> devolverlo YA.
|
||||||
if (now - hit.ts >= TTL && !_enVuelo.has(key)) {
|
if (now - hit.ts >= TTL && !_enVuelo.has(key)) {
|
||||||
|
// Venció: refrescar en 2º plano (no bloquea, no muestra loader).
|
||||||
const g = _genDe(key);
|
const g = _genDe(key);
|
||||||
const p = _fetch(path, params)
|
const p = _fetch(path, params)
|
||||||
.then((data) => { if (_genDe(key) === g) _guardar(key, data); return data; })
|
.then((data) => { if (_genDe(key) === g) _guardar(key, data); return data; })
|
||||||
@@ -75,6 +70,7 @@ function get(path, params = {}) {
|
|||||||
return Promise.resolve(hit.data);
|
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);
|
if (_enVuelo.has(key)) return _enVuelo.get(key);
|
||||||
|
|
||||||
const g = _genDe(key);
|
const g = _genDe(key);
|
||||||
@@ -85,13 +81,17 @@ function get(path, params = {}) {
|
|||||||
return p;
|
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) {
|
function invalidar(...prefijos) {
|
||||||
const afecta = (k) => prefijos.some((p) => k.startsWith(p));
|
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 _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); } }
|
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 = []) {
|
async function post(path, body, invalidarPrefijos = []) {
|
||||||
const r = await fetch(`${BASE_URL}${path}`, {
|
const r = await fetch(`${BASE_URL}${path}`, {
|
||||||
method: "POST", headers: { "Content-Type": "application/json" },
|
method: "POST", headers: { "Content-Type": "application/json" },
|
||||||
@@ -103,6 +103,9 @@ async function post(path, body, invalidarPrefijos = []) {
|
|||||||
return data;
|
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"];
|
const _TODOS = ["/api/leads", "/api/vendedores", "/api/roas", "/api/otros-general", "/api/programa", "/api/alertas"];
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
@@ -136,21 +139,36 @@ export const api = {
|
|||||||
method: "POST", headers: { "Content-Type": "application/json" },
|
method: "POST", headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ codigos, asesores, estados }),
|
body: JSON.stringify({ codigos, asesores, estados }),
|
||||||
}).then((r) => { if (!r.ok) throw new Error("Error al obtener base"); return r.json(); }),
|
}).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"),
|
conjuntosSinSede: () => get("/api/conjuntos-sin-sede"),
|
||||||
guardarConjuntoSede: (cambios) =>
|
guardarConjuntoSede: (cambios) =>
|
||||||
post("/api/conjuntos-sin-sede/guardar", { cambios }, ["/api/conjuntos-sin-sede", ..._TODOS]),
|
post("/api/conjuntos-sin-sede/guardar", { cambios }, ["/api/conjuntos-sin-sede", ..._TODOS]),
|
||||||
borrarConjuntoSede: (conjunto) =>
|
borrarConjuntoSede: (conjunto) =>
|
||||||
post("/api/conjuntos-sin-sede/borrar", { conjunto }, ["/api/conjuntos-sin-sede", ..._TODOS]),
|
post("/api/conjuntos-sin-sede/borrar", { conjunto }, ["/api/conjuntos-sin-sede", ..._TODOS]),
|
||||||
ultimaActualizacion: () => get("/api/ultima-actualizacion"),
|
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 () => {
|
refrescarTodo: async () => {
|
||||||
const r = await fetch(`${BASE_URL}/api/cache/refresh`, { method: "POST" });
|
const r = await fetch(`${BASE_URL}/api/cache/refresh`, { method: "POST" });
|
||||||
if (!r.ok) throw new Error("Error al actualizar");
|
if (!r.ok) throw new Error("Error al actualizar");
|
||||||
await r.json();
|
await r.json(); // espera a que el backend TERMINE de rehacer todo
|
||||||
_cache.clear();
|
_cache.clear(); // solo AHORA se invalida el cache del cliente
|
||||||
_persistir();
|
_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", {});
|
const hr = await _fetch("/api/ultima-actualizacion", {});
|
||||||
_guardar(_key("/api/ultima-actualizacion", {}), hr);
|
_guardar(_key("/api/ultima-actualizacion", {}), hr);
|
||||||
return hr;
|
return hr; // { hora: "..." }
|
||||||
},
|
},
|
||||||
conjuntosSinPauta: () => get("/api/conjuntos-sin-pauta"),
|
conjuntosSinPauta: () => get("/api/conjuntos-sin-pauta"),
|
||||||
usoPauta: (pauta, excluir) => get("/api/pauta/uso", { pauta, excluir: excluir || "" }),
|
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 },
|
post("/api/curso/guardar-pauta", { num_indice, pauta, conjunto, contar },
|
||||||
["/api/conjuntos-sin-pauta", ..._TODOS]),
|
["/api/conjuntos-sin-pauta", ..._TODOS]),
|
||||||
alertasClasif: () => get("/api/leads/alertas"),
|
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"),
|
campanias: () => get("/api/campanias"),
|
||||||
agregarCampania: (datos) =>
|
agregarCampania: (datos) =>
|
||||||
post("/api/campanias/agregar", datos,
|
post("/api/campanias/agregar", datos,
|
||||||
|
|||||||
Reference in New Issue
Block a user