Actualizar frontend/src/pages/Ocupabilidad.jsx

This commit is contained in:
2026-09-22 10:12:33 -05:00
parent c9a8cc2229
commit 5f0b8abf72

View File

@@ -15,6 +15,30 @@ const COLS = ["PROGRAMA","FECHA INICIO","DÍAS PARA INICIO","TOTAL INSCRITOS","R
function toNum(v) { const n = parseFloat(String(v).replace("%","").trim()); return isNaN(n) ? 0 : n; }
// ── Ordenamiento (detecta número/soles/%, fecha dd-mm-yyyy o texto) ──
function _valorOrden(v) {
const s = String(v ?? "").trim();
if (s === "" || s === "—" || s === "-") return { t: "vacio", n: 0, s: "" };
const mf = s.match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})$/);
if (mf) { const [, d, m, y] = mf; return { t: "fecha", n: new Date(+y, +m - 1, +d).getTime(), s }; }
const limpio = s.replace(/S\/|%|,/g, "").replace(/\s/g, "").trim();
if (limpio !== "" && !isNaN(Number(limpio))) return { t: "num", n: Number(limpio), s };
return { t: "texto", n: 0, s: s.toLowerCase() };
}
function _cmp(a, b, dir) {
const mult = dir === "asc" ? 1 : -1;
const va = _valorOrden(a), vb = _valorOrden(b);
if (va.t === "vacio" && vb.t === "vacio") return 0;
if (va.t === "vacio") return 1;
if (vb.t === "vacio") return -1;
if (va.t === "texto" || vb.t === "texto") return mult * va.s.localeCompare(vb.s, "es");
return mult * (va.n - vb.n);
}
function _flecha(activo, dir) {
if (!activo) return <span style={{ opacity: 0.3, marginLeft: 4 }}>⇅</span>;
return <span style={{ marginLeft: 4 }}>{dir === "asc" ? "▲" : "▼"}</span>;
}
export default function Ocupabilidad() {
const hoy = new Date();
const [ano, setAno] = useState(hoy.getFullYear());
@@ -27,8 +51,35 @@ export default function Ocupabilidad() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [modalCurso, setModalCurso] = useState(null); // curso seleccionado para "Ver alumnos"
const [ordCol, setOrdCol] = useState(null);
const [ordDir, setOrdDir] = useState(null);
const cols = useColumnasAjustables([280, 110, 120, 110, 100, 120, 110, 120, 100, 100, 130, 140, 80]);
const clickOrden = (i) => {
if (i === COLS.length - 1) return; // columna OPC. no se ordena
if (ordCol !== i) { setOrdCol(i); setOrdDir("asc"); }
else if (ordDir === "asc") setOrdDir("desc");
else { setOrdCol(null); setOrdDir(null); }
};
// Valor de cada columna (por índice) para ordenar, sacado del objeto de fila.
const _valCol = (d, i) => {
switch (i) {
case 0: return d.programa_frecuencia || d.dsc_programa || "";
case 1: return d.fch_inicio || "";
case 2: return d.dias_para_inicio ?? "";
case 3: return parseInt(d.Inscritos_Totales) || 0;
case 4: return parseInt(d.Retirados ?? d.Inscritos_Retirados) || 0;
case 5: return parseInt(d.Inscritos_Activos) || 0;
case 6: return parseInt(d.Meta_Curso) || 0;
case 7: return toNum(d.Avance_Inscritos);
case 8: return parseInt(d.Inscritos_Mes) || 0;
case 9: return parseInt(d.Inscritos_PC) || 0;
case 10: return parseInt(d.Descuento) || 0;
case 11: return parseInt(d.Inscritos_Continuidad) || 0;
default: return "";
}
};
// Clave estable de meses (ordenados) para las dependencias del effect.
const mesesKey = useMemo(() => [...meses].sort((a, b) => a - b).join(","), [meses]);
@@ -57,9 +108,13 @@ export default function Ocupabilidad() {
// Toggle "Mostrar Reprogramados": si está apagado, ocultar filas de inicio
const datosVisibles = useMemo(() => {
if (mostrarRepro) return datos;
return datos.filter((d) => String(d.dias_para_inicio).trim().toUpperCase() !== "REPROGRAMADO");
}, [datos, mostrarRepro]);
let base = mostrarRepro ? datos
: datos.filter((d) => String(d.dias_para_inicio).trim().toUpperCase() !== "REPROGRAMADO");
if (ordCol != null && ordDir) {
base = [...base].sort((a, b) => _cmp(_valCol(a, ordCol), _valCol(b, ordCol), ordDir));
}
return base;
}, [datos, mostrarRepro, ordCol, ordDir]);
const kpis = useMemo(() => {
if (!datosVisibles.length) return null;
@@ -173,7 +228,15 @@ export default function Ocupabilidad() {
<div className="table-wrap" style={{overflow:"auto", maxHeight:"60vh"}}>
<table {...cols.tableProps}>
<cols.ColGroup />
<thead><tr>{COLS.map((c, i) => <th key={c} style={{position:"sticky",top:0,zIndex:2}}>{c}<cols.Resizer index={i} /></th>)}</tr></thead>
<thead><tr>{COLS.map((c, i) => {
const ordenable = i !== COLS.length - 1;
return (
<th key={c} style={{position:"sticky",top:0,zIndex:2, cursor: ordenable ? "pointer" : "default", userSelect:"none"}}
onClick={() => clickOrden(i)}>
{c}{ordenable && _flecha(ordCol === i, ordDir)}<cols.Resizer index={i} />
</th>
);
})}</tr></thead>
<tbody>
{datosVisibles.map((d, i) => {
const prog = d.programa_frecuencia || d.dsc_programa || "";
@@ -356,6 +419,14 @@ function MesesMulti({ label, meses, onChange }) {
function ModalAlumnosCurso({ numIndice, programa, onClose }) {
const [alumnos, setAlumnos] = useState([]);
const [loading, setLoading] = useState(true);
const [ordCol, setOrdCol] = useState(null);
const [ordDir, setOrdDir] = useState(null);
const CAMPOS_AL = ["alumno", "estado", "curso_anterior", "estado_anterior"];
const clickOrden = (i) => {
if (ordCol !== i) { setOrdCol(i); setOrdDir("asc"); }
else if (ordDir === "asc") setOrdDir("desc");
else { setOrdCol(null); setOrdDir(null); }
};
useEffect(() => {
let activo = true;
@@ -381,6 +452,12 @@ function ModalAlumnosCurso({ numIndice, programa, onClose }) {
return c;
}, [alumnos]);
const alumnosOrd = useMemo(() => {
if (ordCol == null || !ordDir) return alumnos;
const campo = CAMPOS_AL[ordCol];
return [...alumnos].sort((a, b) => _cmp(a[campo], b[campo], ordDir));
}, [alumnos, ordCol, ordDir]);
return (
<Modal title={`👥 Alumnos Inscritos — ${programa}`} onClose={onClose} width={1050}>
{loading ? <Loader text="Cargando alumnos..." /> :
@@ -404,13 +481,15 @@ function ModalAlumnosCurso({ numIndice, programa, onClose }) {
<div className="table-wrap" style={{overflowX:"auto"}}>
<table className="cob-table" style={{minWidth:900}}>
<thead><tr>
<th style={{textAlign:"left"}}>ALUMNO</th>
<th>ESTADO</th>
<th style={{textAlign:"left"}}>CURSO ANTERIOR (TEAC/TERC)</th>
<th>ESTADO ANTERIOR</th>
{["ALUMNO","ESTADO","CURSO ANTERIOR (TEAC/TERC)","ESTADO ANTERIOR"].map((h, i) => (
<th key={h} onClick={() => clickOrden(i)}
style={{ textAlign: i === 0 || i === 2 ? "left" : "center", cursor:"pointer", userSelect:"none" }}>
{h}{_flecha(ordCol === i, ordDir)}
</th>
))}
</tr></thead>
<tbody>
{alumnos.map((a, i) => {
{alumnosOrd.map((a, i) => {
const c = colorEstado(a.estado);
const tieneAnt = a.curso_anterior && a.curso_anterior !== "-";
const cAnt = colorEstado(a.estado_anterior);