Actualizar frontend/src/pages/Ocupabilidad.jsx
This commit is contained in:
@@ -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; }
|
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() {
|
export default function Ocupabilidad() {
|
||||||
const hoy = new Date();
|
const hoy = new Date();
|
||||||
const [ano, setAno] = useState(hoy.getFullYear());
|
const [ano, setAno] = useState(hoy.getFullYear());
|
||||||
@@ -27,8 +51,35 @@ export default function Ocupabilidad() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [modalCurso, setModalCurso] = useState(null); // curso seleccionado para "Ver alumnos"
|
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 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.
|
// 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]);
|
||||||
|
|
||||||
@@ -57,9 +108,13 @@ export default function Ocupabilidad() {
|
|||||||
|
|
||||||
// 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(() => {
|
||||||
if (mostrarRepro) return datos;
|
let base = mostrarRepro ? datos
|
||||||
return datos.filter((d) => String(d.dias_para_inicio).trim().toUpperCase() !== "REPROGRAMADO");
|
: datos.filter((d) => String(d.dias_para_inicio).trim().toUpperCase() !== "REPROGRAMADO");
|
||||||
}, [datos, mostrarRepro]);
|
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(() => {
|
const kpis = useMemo(() => {
|
||||||
if (!datosVisibles.length) return null;
|
if (!datosVisibles.length) return null;
|
||||||
@@ -173,7 +228,15 @@ export default function Ocupabilidad() {
|
|||||||
<div className="table-wrap" style={{overflow:"auto", maxHeight:"60vh"}}>
|
<div className="table-wrap" style={{overflow:"auto", maxHeight:"60vh"}}>
|
||||||
<table {...cols.tableProps}>
|
<table {...cols.tableProps}>
|
||||||
<cols.ColGroup />
|
<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>
|
<tbody>
|
||||||
{datosVisibles.map((d, i) => {
|
{datosVisibles.map((d, i) => {
|
||||||
const prog = d.programa_frecuencia || d.dsc_programa || "";
|
const prog = d.programa_frecuencia || d.dsc_programa || "";
|
||||||
@@ -356,6 +419,14 @@ function MesesMulti({ label, meses, onChange }) {
|
|||||||
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);
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
let activo = true;
|
let activo = true;
|
||||||
@@ -381,6 +452,12 @@ function ModalAlumnosCurso({ numIndice, programa, onClose }) {
|
|||||||
return c;
|
return c;
|
||||||
}, [alumnos]);
|
}, [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 (
|
return (
|
||||||
<Modal title={`👥 Alumnos Inscritos — ${programa}`} onClose={onClose} width={1050}>
|
<Modal title={`👥 Alumnos Inscritos — ${programa}`} onClose={onClose} width={1050}>
|
||||||
{loading ? <Loader text="Cargando alumnos..." /> :
|
{loading ? <Loader text="Cargando alumnos..." /> :
|
||||||
@@ -404,13 +481,15 @@ function ModalAlumnosCurso({ numIndice, programa, onClose }) {
|
|||||||
<div className="table-wrap" style={{overflowX:"auto"}}>
|
<div className="table-wrap" style={{overflowX:"auto"}}>
|
||||||
<table className="cob-table" style={{minWidth:900}}>
|
<table className="cob-table" style={{minWidth:900}}>
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th style={{textAlign:"left"}}>ALUMNO</th>
|
{["ALUMNO","ESTADO","CURSO ANTERIOR (TEAC/TERC)","ESTADO ANTERIOR"].map((h, i) => (
|
||||||
<th>ESTADO</th>
|
<th key={h} onClick={() => clickOrden(i)}
|
||||||
<th style={{textAlign:"left"}}>CURSO ANTERIOR (TEAC/TERC)</th>
|
style={{ textAlign: i === 0 || i === 2 ? "left" : "center", cursor:"pointer", userSelect:"none" }}>
|
||||||
<th>ESTADO ANTERIOR</th>
|
{h}{_flecha(ordCol === i, ordDir)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{alumnos.map((a, i) => {
|
{alumnosOrd.map((a, i) => {
|
||||||
const c = colorEstado(a.estado);
|
const c = colorEstado(a.estado);
|
||||||
const tieneAnt = a.curso_anterior && a.curso_anterior !== "-";
|
const tieneAnt = a.curso_anterior && a.curso_anterior !== "-";
|
||||||
const cAnt = colorEstado(a.estado_anterior);
|
const cAnt = colorEstado(a.estado_anterior);
|
||||||
|
|||||||
Reference in New Issue
Block a user