Añadir frontend/src/pages/Alertas.jsx

This commit is contained in:
2026-09-04 15:59:03 -05:00
parent e1f0f37946
commit d94c4c6a0d

View File

@@ -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 (
<div>
<h1 className="page-title">⚠️ Alertas</h1>
<p style={{ color: "#64748b", fontSize: 13, marginTop: -8, marginBottom: 18 }}>
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.
</p>
{cargando && (
<div style={{ padding: 30, textAlign: "center", color: "#94a3b8", fontSize: 13 }}>
Cargando alertas…
</div>
)}
{error && (
<div style={{ padding: 16, background: "#fdecec", color: "#a32d2d", borderRadius: 8,
fontSize: 13, marginBottom: 16 }}>{error}</div>
)}
{!cargando && !error && (<>
{/* Contadores: los 3 niveles a la izquierda, "Ya iniciados" a la derecha */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "stretch",
gap: 12, marginBottom: 20, flexWrap: "wrap" }}>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 12, flex: 1, maxWidth: 620 }}>
{[
{ 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) => (
<div key={k.l} onClick={() => { 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" }}>
<div style={{ fontSize: 12, color: k.c, fontWeight: 700 }}>{k.l}</div>
<div style={{ fontSize: 26, fontWeight: 800, color: k.c }}>{k.v}</div>
</div>
))}
</div>
{/* Botón "Ya iniciados" a la derecha */}
<div onClick={() => { 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" }}>
<div style={{ fontSize: 12, color: "#2563eb", fontWeight: 700 }}>🕓 Ya iniciados</div>
<div style={{ fontSize: 26, fontWeight: 800, color: "#1e40af" }}>{iniciados.length}</div>
</div>
</div>
{/* Encabezado de la lista cuando es "Ya iniciados" */}
{filtro === "iniciados" && (
<p style={{ color: "#64748b", fontSize: 13, margin: "0 0 12px" }}>
Análisis de los programas que iniciaron entre ayer y hace 30 días.
</p>
)}
{/* Lista (segun el filtro elegido) */}
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{lista.length === 0 && (
<div style={{ padding: 30, textAlign: "center", color: "#94a3b8", fontSize: 13 }}>
{filtro === "iniciados" ? "No hay programas iniciados en los últimos 30 días." : "No hay cursos en esta categoría."}
</div>
)}
{lista.map((a) => renderTarjeta(a))}
</div>
</>)}
</div>
);
function renderTarjeta(a) {
const open = abierto === a.id;
return (
<div key={a.id} style={{ background: "#fff", border: "1px solid #eaeef4",
borderLeft: `4px solid ${colorNivel(a.nivel)}`, borderRadius: 8, overflow: "hidden" }}>
<div onClick={() => setAbierto(open ? null : a.id)}
style={{ padding: "14px 16px", cursor: "pointer" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 6 }}>
<span style={{ fontWeight: 700, color: "#0f172a", display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
{a.curso}
{a.suspendido && (
<span style={{ fontSize: 11, fontWeight: 800, letterSpacing: 0.3,
background: "#fde2e2", color: "#b91c1c", border: "1px solid #f5b5b5",
padding: "2px 9px", borderRadius: 999 }}>
⛔ SUSPENDIDO
</span>
)}
</span>
<span style={{ display: "flex", alignItems: "center", gap: 10 }}>
<span style={{ fontSize: 12, background: bgNivel(a.nivel), color: txtNivel(a.nivel),
padding: "2px 10px", borderRadius: 999, fontWeight: 700 }}>
{etiqNivel(a.nivel)} · {a.ya_inicio ? `inició hace ${a.dias_desde} días` : `inicia en ${a.dias_inicio} días`}
</span>
<span style={{ color: "#94a3b8", fontSize: 14 }}>{open ? "▲" : "▼"}</span>
</span>
</div>
<div style={{ fontSize: 13, color: "#64748b" }}>{a.resumen}</div>
</div>
{open && (
<div style={{ padding: "0 16px 16px", borderTop: "1px solid #f1f5f9" }}>
{/* Métricas con color por estado (rojo/amarillo/verde) */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(6,1fr)", gap: 10, margin: "14px 0" }}>
{(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 (
<div key={m.l} style={{ background: bg, borderRadius: 8, padding: "9px 12px",
borderLeft: `3px solid ${c}` }}>
<div style={{ fontSize: 11, color: tc, display: "flex", alignItems: "center", gap: 4 }}>
<span style={{ width: 7, height: 7, borderRadius: "50%", background: c }} />{m.l}
</div>
<div style={{ fontSize: 16, fontWeight: 800, color: tc }}>{m.v}</div>
<div style={{ fontSize: 10, color: "#94a3b8" }}>{m.sub}</div>
</div>
);
})}
</div>
{/* Análisis dividido en Óptimo / En observación / Crítico */}
<div style={{ background: bgNivel(a.nivel), borderRadius: 10, padding: "12px 14px" }}>
<div style={{ fontSize: 12, fontWeight: 800, color: txtNivel(a.nivel), marginBottom: 8,
textTransform: "uppercase", letterSpacing: 0.4 }}>Análisis</div>
{(a.grupos || []).filter((g) => g.items.length > 0).map((g) => {
const c = g.color === "rojo" ? "#a32d2d" : g.color === "amarillo" ? "#854f0b" : "#0f766e";
return (
<div key={g.titulo} style={{ marginBottom: 10 }}>
<div style={{ fontSize: 12, fontWeight: 800, color: c, marginBottom: 4 }}>{g.titulo}</div>
<ul style={{ margin: 0, paddingLeft: 0, listStyle: "none",
display: "flex", flexDirection: "column", gap: 5 }}>
{g.items.map((t, i) => (
<li key={i} style={{ fontSize: 13, color: "#334155", lineHeight: 1.55,
display: "flex", gap: 8 }}>
<span style={{ color: c, fontWeight: 800, flexShrink: 0 }}>›</span>
<span>{t}</span>
</li>
))}
</ul>
</div>
);
})}
{a.cruce && (
<div style={{ marginTop: 10, paddingTop: 10, borderTop: `1px solid ${txtNivel(a.nivel)}22`,
fontSize: 13, color: "#334155", lineHeight: 1.55 }}>
<span style={{ fontWeight: 800, color: txtNivel(a.nivel) }}>Cruce por teléfono: </span>{a.cruce}
</div>
)}
{a.reco && (
<div style={{ marginTop: 10, paddingTop: 10, borderTop: `1px solid ${txtNivel(a.nivel)}22`,
fontSize: 13, color: "#334155", lineHeight: 1.55 }}>
<span style={{ fontWeight: 800, color: txtNivel(a.nivel) }}>Recomendación: </span>{a.reco}
</div>
)}
</div>
</div>
)}
</div>
);
}
}