From d94c4c6a0d19847aedd0a2d0d17f68aef104c5ec Mon Sep 17 00:00:00 2001 From: Administrator Date: Fri, 4 Sep 2026 15:59:03 -0500 Subject: [PATCH] =?UTF-8?q?A=C3=B1adir=20frontend/src/pages/Alertas.jsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Alertas.jsx | 208 +++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 frontend/src/pages/Alertas.jsx diff --git a/frontend/src/pages/Alertas.jsx b/frontend/src/pages/Alertas.jsx new file mode 100644 index 0000000..9d90137 --- /dev/null +++ b/frontend/src/pages/Alertas.jsx @@ -0,0 +1,208 @@ +import { useState, useEffect } from "react"; +import { api } from "../lib/api"; + +export default function Alertas() { + const [abierto, setAbierto] = useState(null); + const [filtro, setFiltro] = useState("critico"); // nivel mostrado + const [datos, setDatos] = useState([]); + const [iniciados, setIniciados] = useState([]); + const [cargando, setCargando] = useState(true); + const [error, setError] = useState(""); + + const cargar = (intento = 0) => { + api.alertas() + .then((r) => { + const items = (r && r.items) || []; + setDatos(items); + setIniciados((r && r.iniciados) || []); + setError(""); + setCargando(false); + // Si llego vacio y aun no reintentamos mucho, el backend quiza no habia + // terminado de cargar la data: reintentar en 3s (hasta 5 veces). + if (items.length === 0 && intento < 5) { + setTimeout(() => cargar(intento + 1), 3000); + } + }) + .catch(() => { + setError("No se pudieron cargar las alertas."); + setCargando(false); + if (intento < 5) setTimeout(() => cargar(intento + 1), 3000); + }); + }; + useEffect(() => { + cargar(); + const h = () => cargar(); + window.addEventListener("datos-actualizar", h); + return () => window.removeEventListener("datos-actualizar", h); + }, []); + + const criticos = datos.filter((a) => a.nivel === "critico"); + const adv = datos.filter((a) => a.nivel === "advertencia"); + const oks = datos.filter((a) => a.nivel === "ok"); + // "iniciados" es un filtro más: muestra los programas que ya iniciaron. + const lista = filtro === "iniciados" ? iniciados : datos.filter((a) => a.nivel === filtro); + + const colorNivel = (n) => n === "critico" ? "#E24B4A" : n === "advertencia" ? "#EF9F27" : "#0f766e"; + const bgNivel = (n) => n === "critico" ? "#fdecec" : n === "advertencia" ? "#fdf3e3" : "#f0fdfa"; + const txtNivel = (n) => n === "critico" ? "#a32d2d" : n === "advertencia" ? "#854f0b" : "#0f766e"; + const etiqNivel = (n) => n === "critico" ? "Crítico" : n === "advertencia" ? "Advertencia" : "En rumbo"; + + return ( +
+

⚠️ Alertas

+

+ Cursos próximos a iniciar (≤ 30 días) con pauta activa. El estado se define por las + matrículas frente a lo esperado según los días que faltan. Usa "Ya iniciados" para ver + los programas que iniciaron en los últimos 30 días. +

+ + {cargando && ( +
+ Cargando alertas… +
+ )} + {error && ( +
{error}
+ )} + + {!cargando && !error && (<> + {/* Contadores: los 3 niveles a la izquierda, "Ya iniciados" a la derecha */} +
+
+ {[ + { key: "critico", l: "Críticos", v: criticos.length, c: "#E24B4A", bg: "#fdecec" }, + { key: "advertencia", l: "Advertencias", v: adv.length, c: "#EF9F27", bg: "#fdf3e3" }, + { key: "ok", l: "En buen rumbo", v: oks.length, c: "#0f766e", bg: "#f0fdfa" }, + ].map((k) => ( +
{ setFiltro(k.key); setAbierto(null); }} + style={{ background: k.bg, borderRadius: 12, padding: "12px 16px", cursor: "pointer", + border: filtro === k.key ? `2px solid ${k.c}` : "2px solid transparent", + transition: "border .15s ease" }}> +
{k.l}
+
{k.v}
+
+ ))} +
+ {/* Botón "Ya iniciados" a la derecha */} +
{ setFiltro("iniciados"); setAbierto(null); }} + style={{ background: "#eaf1fb", borderRadius: 12, padding: "12px 20px", cursor: "pointer", + display: "flex", flexDirection: "column", justifyContent: "center", minWidth: 150, + border: filtro === "iniciados" ? "2px solid #2563eb" : "2px solid transparent", + transition: "border .15s ease" }}> +
🕓 Ya iniciados
+
{iniciados.length}
+
+
+ + {/* Encabezado de la lista cuando es "Ya iniciados" */} + {filtro === "iniciados" && ( +

+ Análisis de los programas que iniciaron entre ayer y hace 30 días. +

+ )} + + {/* Lista (segun el filtro elegido) */} +
+ {lista.length === 0 && ( +
+ {filtro === "iniciados" ? "No hay programas iniciados en los últimos 30 días." : "No hay cursos en esta categoría."} +
+ )} + {lista.map((a) => renderTarjeta(a))} +
+ )} +
+ ); + + function renderTarjeta(a) { + const open = abierto === a.id; + return ( +
+
setAbierto(open ? null : a.id)} + style={{ padding: "14px 16px", cursor: "pointer" }}> +
+ + {a.curso} + {a.suspendido && ( + + ⛔ SUSPENDIDO + + )} + + + + {etiqNivel(a.nivel)} · {a.ya_inicio ? `inició hace ${a.dias_desde} días` : `inicia en ${a.dias_inicio} días`} + + {open ? "▲" : "▼"} + +
+
{a.resumen}
+
+ + {open && ( +
+ {/* Métricas con color por estado (rojo/amarillo/verde) */} +
+ {(a.metricas || []).map((m) => { + const c = m.est === "rojo" ? "#E24B4A" : m.est === "amarillo" ? "#EF9F27" : "#0f766e"; + const bg = m.est === "rojo" ? "#fdecec" : m.est === "amarillo" ? "#fdf3e3" : "#f0fdfa"; + const tc = m.est === "rojo" ? "#a32d2d" : m.est === "amarillo" ? "#854f0b" : "#0f766e"; + return ( +
+
+ {m.l} +
+
{m.v}
+
{m.sub}
+
+ ); + })} +
+ {/* Análisis dividido en Óptimo / En observación / Crítico */} +
+
Análisis
+ {(a.grupos || []).filter((g) => g.items.length > 0).map((g) => { + const c = g.color === "rojo" ? "#a32d2d" : g.color === "amarillo" ? "#854f0b" : "#0f766e"; + return ( +
+
{g.titulo}
+
    + {g.items.map((t, i) => ( +
  • + › + {t} +
  • + ))} +
+
+ ); + })} + {a.cruce && ( +
+ Cruce por teléfono: {a.cruce} +
+ )} + {a.reco && ( +
+ Recomendación: {a.reco} +
+ )} +
+
+ )} +
+ ); + } +}