From 5f0b8abf720e909ac907fcad699bc2a3ad4870af Mon Sep 17 00:00:00 2001 From: Administrator Date: Tue, 22 Sep 2026 10:12:33 -0500 Subject: [PATCH] Actualizar frontend/src/pages/Ocupabilidad.jsx --- frontend/src/pages/Ocupabilidad.jsx | 97 ++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/Ocupabilidad.jsx b/frontend/src/pages/Ocupabilidad.jsx index ca242bd..a9849f9 100644 --- a/frontend/src/pages/Ocupabilidad.jsx +++ b/frontend/src/pages/Ocupabilidad.jsx @@ -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 ⇅; + return {dir === "asc" ? "▲" : "▼"}; +} + 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() {
- {COLS.map((c, i) => )} + {COLS.map((c, i) => { + const ordenable = i !== COLS.length - 1; + return ( + + ); + })} {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 ( {loading ? : @@ -404,13 +481,15 @@ function ModalAlumnosCurso({ numIndice, programa, onClose }) {
{c}
clickOrden(i)}> + {c}{ordenable && _flecha(ordCol === i, ordDir)} +
- - - - + {["ALUMNO","ESTADO","CURSO ANTERIOR (TEAC/TERC)","ESTADO ANTERIOR"].map((h, i) => ( + + ))} - {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);
ALUMNOESTADOCURSO ANTERIOR (TEAC/TERC)ESTADO ANTERIOR clickOrden(i)} + style={{ textAlign: i === 0 || i === 2 ? "left" : "center", cursor:"pointer", userSelect:"none" }}> + {h}{_flecha(ordCol === i, ordDir)} +