// ============================================================
// notifications.jsx — Campana de notificaciones (v2.5)
// ============================================================
// Antes la campana solo marcaba todo como leído, sin enseñar nada: el
// usuario que seguía una sección nunca veía los avisos de subidas, y se
// perdían los de "Nuevo acceso detectado" y las alertas de descargas
// masivas para los administradores. Además el contador solo se calculaba
// al entrar.
//
// La usan el Acervo (dashboard.jsx) y Administración (admin.jsx):
//
// ============================================================
const NOTIF_POLL_MS = 120000; // contador: cada 2 min
const NOTIF_ACTIVIDAD_MS = 10 * 60 * 1000; // …solo si hubo actividad en los últimos 10 min
function fechaNotificacion(valor) {
if (!valor) return "";
const d = new Date(String(valor).replace(" ", "T"));
if (isNaN(d.getTime())) return "";
return d.toLocaleString("es-MX", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
}
function NotificationsBell() {
const [abierto, setAbierto] = React.useState(false);
const [noLeidas, setNoLeidas] = React.useState(0);
const [lista, setLista] = React.useState(null); // null = sin cargar
const [cargando, setCargando] = React.useState(false);
const [error, setError] = React.useState(null);
const caja = React.useRef(null);
const contar = React.useCallback(function () {
return API.Notifications.list(true)
.then(function (r) { setNoLeidas((r && r.unread_count) || 0); })
.catch(function () {});
}, []);
// Contador: al montar, al volver a la pestaña y cada 2 minutos. Sin
// actividad reciente no se consulta: cada consulta mantiene viva la sesión
// en el servidor y no debe impedir el cierre por inactividad.
React.useEffect(() => {
contar();
const id = setInterval(function () {
if (document.visibilityState !== "visible") return;
let ultima = 0;
try { ultima = parseInt(localStorage.getItem("bib_last_activity") || "0", 10) || 0; } catch (e) {}
if (Date.now() - ultima > NOTIF_ACTIVIDAD_MS) return;
contar();
}, NOTIF_POLL_MS);
const alVolver = function () { if (document.visibilityState === "visible") contar(); };
document.addEventListener("visibilitychange", alVolver);
return function () {
clearInterval(id);
document.removeEventListener("visibilitychange", alVolver);
};
}, [contar]);
const cargar = function () {
setCargando(true);
setError(null);
API.Notifications.list(false)
.then(function (r) {
setLista((r && r.data) || []);
setNoLeidas((r && r.unread_count) || 0);
})
.catch(function (e) { setError((e && e.message) || "No se pudieron cargar las notificaciones."); })
.finally(function () { setCargando(false); });
};
const alternar = function () {
const nuevo = !abierto;
setAbierto(nuevo);
if (nuevo) cargar();
};
// Cerrar con un clic fuera o con Escape
React.useEffect(() => {
if (!abierto) return;
const fuera = function (e) { if (caja.current && !caja.current.contains(e.target)) setAbierto(false); };
const esc = function (e) { if (e.key === "Escape") setAbierto(false); };
document.addEventListener("mousedown", fuera);
document.addEventListener("keydown", esc);
return function () {
document.removeEventListener("mousedown", fuera);
document.removeEventListener("keydown", esc);
};
}, [abierto]);
const marcarLeida = function (n) {
if (n.read) return;
setLista(function (prev) {
return (prev || []).map(function (x) { return x.id === n.id ? Object.assign({}, x, { read: true }) : x; });
});
setNoLeidas(function (c) { return Math.max(0, c - 1); });
API.Notifications.read(n.id).catch(function () { cargar(); });
};
const marcarTodas = function () {
setLista(function (prev) {
return (prev || []).map(function (x) { return Object.assign({}, x, { read: true }); });
});
setNoLeidas(0);
API.Notifications.readAll().catch(function () { cargar(); });
};
const titulo = noLeidas > 0
? noLeidas + (noLeidas === 1 ? " notificación sin leer" : " notificaciones sin leer")
: "Notificaciones";
return (
{abierto && (
Notificaciones
{noLeidas > 0 && (
)}
{cargando && lista === null && (
Cargando…
)}
{error && (
{error}
)}
{!error && lista && lista.length === 0 && (
No tienes notificaciones.
)}
{!error && lista && lista.map(function (n) {
return (
);
})}
{lista && lista.length >= 50 && (
Se muestran las 50 más recientes.
)}
)}
);
}
window.NotificationsBell = NotificationsBell;