Actualizar frontend/src/pages/Ocupabilidad.jsx

This commit is contained in:
2026-09-22 15:39:51 -05:00
parent 797976d05a
commit 1f2f35d523

View File

@@ -8,8 +8,8 @@ import { setVistaActual, onRefrescar } from "../lib/vistaActual";
const MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"]; const MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"];
const ANOS = [2024, 2025, 2026]; const ANOS = [2024, 2025, 2026];
const SEDES = ["TODOS", "LIMA", "AREQUIPA", "PIURA", "TRUJILLO"]; const SEDES = ["LIMA", "AREQUIPA", "PIURA", "TRUJILLO"];
const PROGRAMAS = ["TODOS", "SEMINARIOS", "OTROS", "TEAC", "TERC"]; const PROGRAMAS = ["SEMINARIOS", "OTROS", "TEAC", "TERC"];
const COLS = ["PROGRAMA","FECHA INICIO","DÍAS PARA INICIO","TOTAL INSCRITOS","RETIRADOS","INSCRITOS EN CURSO","META INSCRITOS","AVANCE INSCRITOS","INSCRITOS MES","INSCRITOS P.C","INSCRITOS REFRIPERU","INSCRITOS CONTINUIDAD","OPC."]; const COLS = ["PROGRAMA","FECHA INICIO","DÍAS PARA INICIO","TOTAL INSCRITOS","RETIRADOS","INSCRITOS EN CURSO","META INSCRITOS","AVANCE INSCRITOS","INSCRITOS MES","INSCRITOS P.C","INSCRITOS REFRIPERU","INSCRITOS CONTINUIDAD","OPC."];
@@ -44,8 +44,8 @@ export default function Ocupabilidad() {
const [ano, setAno] = useState(hoy.getFullYear()); const [ano, setAno] = useState(hoy.getFullYear());
// meses = array de meses seleccionados (1-12). Permite elegir varios. // meses = array de meses seleccionados (1-12). Permite elegir varios.
const [meses, setMeses] = useState([hoy.getMonth() + 1]); const [meses, setMeses] = useState([hoy.getMonth() + 1]);
const [sede, setSede] = useState("TODOS"); const [sedes, setSedes] = useState([]); // vacío = muestra todo (casillas desmarcadas)
const [programa, setPrograma] = useState("TODOS"); const [programas, setProgramas] = useState([]); // vacío = muestra todo (casillas desmarcadas)
const [mostrarRepro, setMostrarRepro] = useState(true); const [mostrarRepro, setMostrarRepro] = useState(true);
const [datos, setDatos] = useState([]); const [datos, setDatos] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -82,16 +82,19 @@ export default function Ocupabilidad() {
// Clave estable de meses (ordenados) para las dependencias del effect. // Clave estable de meses (ordenados) para las dependencias del effect.
const mesesKey = useMemo(() => [...meses].sort((a, b) => a - b).join(","), [meses]); const mesesKey = useMemo(() => [...meses].sort((a, b) => a - b).join(","), [meses]);
const sedesKey = useMemo(() => [...sedes].sort().join(","), [sedes]);
const programasKey = useMemo(() => [...programas].sort().join(","), [programas]);
useEffect(() => { useEffect(() => {
let activo = true; let activo = true;
const lista = mesesKey ? mesesKey.split(",").map(Number) : []; const lista = mesesKey ? mesesKey.split(",").map(Number) : [];
setVistaActual(`ocupabilidad:${ano}|${mesesKey}|${sede}|${programa}`); setVistaActual(`ocupabilidad:${ano}|${mesesKey}`);
const cargar = () => { const cargar = () => {
setLoading(true); setError(null); setLoading(true); setError(null);
if (!lista.length) { setDatos([]); setLoading(false); return; } if (!lista.length) { setDatos([]); setLoading(false); return; }
// Una llamada por cada mes seleccionado; se juntan todas las filas. // Pedimos SIEMPRE "TODOS" (una llamada por mes, ya cacheada = rápido).
Promise.all(lista.map((m) => api.ocupabilidad(ano, m, sede, programa))) // El filtro por Sede/Programa se aplica en el frontend (instantáneo).
Promise.all(lista.map((m) => api.ocupabilidad(ano, m, "TODOS", "TODOS")))
.then((resArr) => { .then((resArr) => {
if (!activo) return; if (!activo) return;
const juntos = []; const juntos = [];
@@ -104,17 +107,20 @@ export default function Ocupabilidad() {
cargar(); cargar();
const off = onRefrescar(cargar); const off = onRefrescar(cargar);
return () => { activo = false; off(); }; return () => { activo = false; off(); };
}, [ano, mesesKey, sede, programa]); }, [ano, mesesKey]);
// Toggle "Mostrar Reprogramados": si está apagado, ocultar filas de inicio // Toggle "Mostrar Reprogramados": si está apagado, ocultar filas de inicio
const datosVisibles = useMemo(() => { const datosVisibles = useMemo(() => {
let base = mostrarRepro ? datos let base = mostrarRepro ? datos
: datos.filter((d) => String(d.dias_para_inicio).trim().toUpperCase() !== "REPROGRAMADO"); : datos.filter((d) => String(d.dias_para_inicio).trim().toUpperCase() !== "REPROGRAMADO");
// Filtro Sede/Programa en el frontend (instantáneo). Vacío = todo.
if (sedes.length) base = base.filter((d) => sedes.includes(String(d.Sede || "").toUpperCase()));
if (programas.length) base = base.filter((d) => programas.includes(String(d.Filtro_Programa || "").toUpperCase()));
if (ordCol != null && ordDir) { if (ordCol != null && ordDir) {
base = [...base].sort((a, b) => _cmp(_valCol(a, ordCol), _valCol(b, ordCol), ordDir)); base = [...base].sort((a, b) => _cmp(_valCol(a, ordCol), _valCol(b, ordCol), ordDir));
} }
return base; return base;
}, [datos, mostrarRepro, ordCol, ordDir]); }, [datos, mostrarRepro, sedes, programas, ordCol, ordDir]);
const kpis = useMemo(() => { const kpis = useMemo(() => {
if (!datosVisibles.length) return null; if (!datosVisibles.length) return null;
@@ -197,8 +203,8 @@ export default function Ocupabilidad() {
<Filters> <Filters>
<SelectBonito label="Año" value={ano} options={ANOS} onChange={(v) => setAno(+v)} minWidth={110} /> <SelectBonito label="Año" value={ano} options={ANOS} onChange={(v) => setAno(+v)} minWidth={110} />
<MesesMulti label="Mes" meses={meses} onChange={setMeses} /> <MesesMulti label="Mes" meses={meses} onChange={setMeses} />
<SelectBonito label="Sede" value={sede} options={SEDES} onChange={setSede} minWidth={140} /> <ListaMulti label="Sede" opciones={SEDES} seleccion={sedes} onChange={setSedes} plural="sedes" minWidth={140} />
<SelectBonito label="Programa" value={programa} options={PROGRAMAS} onChange={setPrograma} minWidth={150} /> <ListaMulti label="Programa" opciones={PROGRAMAS} seleccion={programas} onChange={setProgramas} plural="programas" minWidth={150} />
<div className="filter-group"> <div className="filter-group">
<label>Reprogramados</label> <label>Reprogramados</label>
<label style={{display:"flex",alignItems:"center",gap:6,height:38,cursor:"pointer",fontSize:13}}> <label style={{display:"flex",alignItems:"center",gap:6,height:38,cursor:"pointer",fontSize:13}}>
@@ -390,14 +396,6 @@ function MesesMulti({ label, meses, onChange }) {
background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8,
boxShadow: "0 8px 24px rgba(0,0,0,0.12)", padding: 6, minWidth: 180, boxShadow: "0 8px 24px rgba(0,0,0,0.12)", padding: 6, minWidth: 180,
maxHeight: 300, overflowY: "auto" }}> maxHeight: 300, overflowY: "auto" }}>
<div style={{ display: "flex", gap: 6, padding: "4px 6px 8px", borderBottom: "1px solid #f1f5f9", marginBottom: 4 }}>
<button type="button" onClick={() => onChange(MESES.map((_, i) => i + 1))}
style={{ flex: 1, fontSize: 11, padding: "4px 6px", border: "1px solid #e2e8f0",
borderRadius: 6, background: "#f8fafc", cursor: "pointer" }}>Todos</button>
<button type="button" onClick={() => onChange([])}
style={{ flex: 1, fontSize: 11, padding: "4px 6px", border: "1px solid #e2e8f0",
borderRadius: 6, background: "#f8fafc", cursor: "pointer" }}>Ninguno</button>
</div>
{MESES.map((m, i) => { {MESES.map((m, i) => {
const val = i + 1; const val = i + 1;
const on = meses.includes(val); const on = meses.includes(val);
@@ -416,6 +414,56 @@ function MesesMulti({ label, meses, onChange }) {
); );
} }
// Filtro multi-selección con casillas para listas de texto (Sede, Programa).
function ListaMulti({ label, opciones, seleccion, onChange, singular = "opción", plural = "opciones", minWidth = 150 }) {
const [abierto, setAbierto] = useState(false);
const ref = useRef(null);
useEffect(() => {
const fuera = (e) => { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); };
document.addEventListener("mousedown", fuera);
return () => document.removeEventListener("mousedown", fuera);
}, []);
const toggle = (v) => {
const set = new Set(seleccion);
set.has(v) ? set.delete(v) : set.add(v);
onChange([...set]);
};
const texto = seleccion.length === 0 ? "— Elegir —"
: seleccion.length === 1 ? seleccion[0]
: `${seleccion.length} ${plural}`;
return (
<div className="filter-group" ref={ref} style={{ position: "relative", minWidth }}>
<label>{label}</label>
<button type="button" onClick={() => setAbierto((v) => !v)}
style={{ height: 38, width: "100%", textAlign: "left", padding: "0 12px",
border: "1px solid #cbd5e1", borderRadius: 8, background: "#fff",
cursor: "pointer", fontSize: 13, display: "flex", alignItems: "center",
justifyContent: "space-between", gap: 8 }}>
<span style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{texto}</span>
<span style={{ color: "#94a3b8", fontSize: 11 }}>▼</span>
</button>
{abierto && (
<div style={{ position: "absolute", top: "100%", left: 0, right: 0, zIndex: 50, marginTop: 4,
background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8,
boxShadow: "0 8px 24px rgba(0,0,0,0.12)", padding: 6, minWidth: 160,
maxHeight: 300, overflowY: "auto" }}>
{opciones.map((o) => {
const on = seleccion.includes(o);
return (
<label key={o} style={{ display: "flex", alignItems: "center", gap: 8,
padding: "6px 8px", borderRadius: 6, cursor: "pointer", fontSize: 13,
background: on ? "#eff6ff" : "transparent" }}>
<input type="checkbox" checked={on} onChange={() => toggle(o)} />
{o}
</label>
);
})}
</div>
)}
</div>
);
}
function ModalAlumnosCurso({ numIndice, programa, onClose }) { function ModalAlumnosCurso({ numIndice, programa, onClose }) {
const [alumnos, setAlumnos] = useState([]); const [alumnos, setAlumnos] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);