Actualizar frontend/src/pages/Ocupabilidad.jsx
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user