Actualizar docker-compose.yml con sintaxis YAML corregida y puerto 80 para frontend
This commit is contained in:
@@ -1,150 +1,86 @@
|
||||
// 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";
|
||||
// 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";
|
||||
|
||||
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) {
|
||||
async function get(path, params = {}) {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
const r = await fetch(`${BASE_URL}${path}${qs ? "?" + qs : ""}`);
|
||||
const url = `${BASE_URL}${path}${qs ? "?" + qs : ""}`;
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`Error ${r.status} en ${path}`);
|
||||
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 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 = []) {
|
||||
async function postJson(path, body) {
|
||||
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 al guardar en ${path}`);
|
||||
const data = await r.json();
|
||||
if (invalidarPrefijos.length) invalidar(...invalidarPrefijos);
|
||||
return data;
|
||||
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();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
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"]),
|
||||
periodoActual: () => get("/api/periodo-actual"),
|
||||
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(); // 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"]),
|
||||
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),
|
||||
};
|
||||
|
||||
144
frontend/src/lib/auth.jsx
Normal file
144
frontend/src/lib/auth.jsx
Normal file
@@ -0,0 +1,144 @@
|
||||
// src/lib/auth.jsx
|
||||
import { createContext, useContext, useEffect, useState, useCallback } from "react";
|
||||
import { supabase } from "./supabase";
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [session, setSession] = useState(null);
|
||||
const [perfil, setPerfil] = useState(null);
|
||||
const [permisos, setPermisos] = useState([]);
|
||||
const [cargando, setCargando] = useState(true);
|
||||
|
||||
const cargarPerfil = useCallback(async (userId) => {
|
||||
try {
|
||||
const { data: perf, error: e1 } = await supabase
|
||||
.from("perfiles")
|
||||
.select("nombre, rol, activo")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
if (e1 || !perf || perf.activo === false) {
|
||||
setPerfil(null);
|
||||
setPermisos([]);
|
||||
return false;
|
||||
}
|
||||
const { data: perms } = await supabase
|
||||
.from("permisos")
|
||||
.select("modulo, puede_ver")
|
||||
.eq("rol", perf.rol);
|
||||
const modulos = (perms || [])
|
||||
.filter((p) => p.puede_ver)
|
||||
.map((p) => p.modulo);
|
||||
setPerfil({ nombre: perf.nombre, rol: perf.rol });
|
||||
setPermisos(modulos);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("[auth] cargarPerfil falló:", e);
|
||||
setPerfil(null);
|
||||
setPermisos([]);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let activo = true;
|
||||
let resuelto = false;
|
||||
|
||||
const terminar = () => {
|
||||
if (activo) { setCargando(false); resuelto = true; sessionStorage.removeItem("auth_reload"); }
|
||||
};
|
||||
|
||||
// Fuente principal: onAuthStateChange dispara INITIAL_SESSION al arrancar
|
||||
// con la sesión persistida (renueva token solo). Maneja login/logout en vivo.
|
||||
const { data: sub } = supabase.auth.onAuthStateChange(async (_evt, s) => {
|
||||
try {
|
||||
setSession(s);
|
||||
if (s?.user) await cargarPerfil(s.user.id);
|
||||
else { setPerfil(null); setPermisos([]); }
|
||||
} catch (e) {
|
||||
console.error("[auth] onAuthStateChange falló:", e);
|
||||
} finally {
|
||||
terminar();
|
||||
}
|
||||
});
|
||||
|
||||
// Respaldo: si el listener no disparó en 6s, intenta getSession una vez.
|
||||
// Si trae sesión la usa; si no, NO fuerza login (deja que el listener resuelva).
|
||||
const tRespaldo = setTimeout(async () => {
|
||||
if (resuelto || !activo) return;
|
||||
try {
|
||||
const { data } = await supabase.auth.getSession();
|
||||
const s = data?.session || null;
|
||||
if (s?.user && activo) {
|
||||
setSession(s);
|
||||
await cargarPerfil(s.user.id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[auth] getSession (respaldo) falló:", e);
|
||||
} finally {
|
||||
terminar();
|
||||
}
|
||||
}, 6000);
|
||||
|
||||
// Si a los 8s sigue sin resolver, recarga la página UNA vez (F5 automático)
|
||||
// en vez de mostrar el login pegado. sessionStorage evita bucle.
|
||||
const tSeguridad = setTimeout(() => {
|
||||
if (!activo || resuelto) return;
|
||||
if (!sessionStorage.getItem("auth_reload")) {
|
||||
sessionStorage.setItem("auth_reload", "1");
|
||||
window.location.reload();
|
||||
} else {
|
||||
setCargando(false);
|
||||
}
|
||||
}, 8000);
|
||||
|
||||
return () => {
|
||||
activo = false;
|
||||
clearTimeout(tRespaldo);
|
||||
clearTimeout(tSeguridad);
|
||||
sub?.subscription?.unsubscribe();
|
||||
};
|
||||
}, [cargarPerfil]);
|
||||
|
||||
const login = async (email, password) => {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
|
||||
if (error) return { ok: false, msg: traducirError(error.message) };
|
||||
const ok = await cargarPerfil(data.user.id);
|
||||
if (!ok) {
|
||||
await supabase.auth.signOut();
|
||||
return { ok: false, msg: "Tu usuario no tiene un perfil asignado. Contacta al administrador." };
|
||||
}
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
await supabase.auth.signOut();
|
||||
setPerfil(null);
|
||||
setPermisos([]);
|
||||
};
|
||||
|
||||
const value = {
|
||||
session,
|
||||
perfil,
|
||||
permisos,
|
||||
cargando,
|
||||
login,
|
||||
logout,
|
||||
autenticado: !!session && !!perfil,
|
||||
puedeVer: (modulo) => permisos.includes(modulo),
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error("useAuth debe usarse dentro de AuthProvider");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function traducirError(msg) {
|
||||
if (/invalid login credentials/i.test(msg)) return "Correo o contraseña incorrectos.";
|
||||
if (/email not confirmed/i.test(msg)) return "El correo no está confirmado.";
|
||||
return msg || "Error al iniciar sesión.";
|
||||
}
|
||||
15
frontend/src/lib/supabase.js
Normal file
15
frontend/src/lib/supabase.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// src/lib/supabase.js
|
||||
// Cliente de Supabase para autenticación (login).
|
||||
// IMPORTANTE: aquí va la clave PÚBLICA (anon), nunca la secreta (service_role).
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || "";
|
||||
const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY || "";
|
||||
|
||||
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
auth: {
|
||||
persistSession: true,
|
||||
autoRefreshToken: true,
|
||||
detectSessionInUrl: true,
|
||||
},
|
||||
});
|
||||
@@ -1,10 +1,11 @@
|
||||
// src/lib/useColumnasAjustables.jsx
|
||||
// Hook para columnas redimensionables (arrastrar el borde, estilo Excel).
|
||||
// Hook reutilizable para columnas redimensionables (estilo Excel) en cualquier tabla.
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useColumnasAjustables(anchosIniciales) {
|
||||
const [anchos, setAnchos] = useState(anchosIniciales);
|
||||
|
||||
// Si cambia el número de columnas (ej. Cobranza Sede↔Programa), reiniciar anchos
|
||||
useEffect(() => {
|
||||
if (anchos.length !== anchosIniciales.length) {
|
||||
setAnchos(anchosIniciales);
|
||||
@@ -28,6 +29,7 @@ export function useColumnasAjustables(anchosIniciales) {
|
||||
document.addEventListener("mouseup", onUp);
|
||||
}
|
||||
|
||||
// Devuelve los elementos <col> para el <colgroup> de la tabla
|
||||
function ColGroup() {
|
||||
return (
|
||||
<colgroup>
|
||||
@@ -36,6 +38,7 @@ export function useColumnasAjustables(anchosIniciales) {
|
||||
);
|
||||
}
|
||||
|
||||
// Devuelve el divisor arrastrable para poner dentro de cada <th>
|
||||
function Resizer({ index }) {
|
||||
return (
|
||||
<span
|
||||
@@ -48,6 +51,10 @@ export function useColumnasAjustables(anchosIniciales) {
|
||||
}
|
||||
|
||||
const anchoTotal = anchos.reduce((a, b) => a + b, 0);
|
||||
|
||||
// width:100% → llena el contenedor desde el inicio (sin huecos a la derecha).
|
||||
// minWidth:anchoTotal → si las columnas crecen más que el contenedor, aparece scroll.
|
||||
// tableLayout:fixed + colgroup → encabezados y celdas alineados.
|
||||
const tableProps = {
|
||||
style: { tableLayout: "fixed", width: "100%", minWidth: anchoTotal },
|
||||
};
|
||||
|
||||
23
frontend/src/lib/vistaActual.js
Normal file
23
frontend/src/lib/vistaActual.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// src/lib/vistaActual.js
|
||||
// Registro de la "vista actual" (clave de caché del backend) + bus de eventos
|
||||
// para que la página visible se recargue tras el refresco manual.
|
||||
let _clave = null;
|
||||
|
||||
export function setVistaActual(clave) {
|
||||
_clave = clave || null;
|
||||
}
|
||||
|
||||
export function getVistaActual() {
|
||||
return _clave;
|
||||
}
|
||||
|
||||
const EVENTO = "datos-refrescados";
|
||||
|
||||
export function emitirRefresco() {
|
||||
window.dispatchEvent(new Event(EVENTO));
|
||||
}
|
||||
|
||||
export function onRefrescar(cb) {
|
||||
window.addEventListener(EVENTO, cb);
|
||||
return () => window.removeEventListener(EVENTO, cb);
|
||||
}
|
||||
Reference in New Issue
Block a user