diff --git a/frontend/src/pages/Ocupabilidad.jsx b/frontend/src/pages/Ocupabilidad.jsx
index 1fbe444..ca242bd 100644
--- a/frontend/src/pages/Ocupabilidad.jsx
+++ b/frontend/src/pages/Ocupabilidad.jsx
@@ -1,7 +1,7 @@
// src/pages/Ocupabilidad.jsx
-import { useState, useEffect, useMemo } from "react";
+import { useState, useEffect, useMemo, useRef } from "react";
import { api } from "../lib/api";
-import { Loader, ErrorBox, ProgressBar, Filters, Select } from "../components/UI";
+import { Loader, ErrorBox, ProgressBar, Filters } from "../components/UI";
import { useColumnasAjustables } from "../lib/useColumnasAjustables";
import Modal from "../components/Modal";
import { setVistaActual, onRefrescar } from "../lib/vistaActual";
@@ -12,34 +12,48 @@ const SEDES = ["TODOS", "LIMA", "AREQUIPA", "PIURA", "TRUJILLO"];
const PROGRAMAS = ["TODOS", "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."];
+
function toNum(v) { const n = parseFloat(String(v).replace("%","").trim()); return isNaN(n) ? 0 : n; }
export default function Ocupabilidad() {
const hoy = new Date();
const [ano, setAno] = useState(hoy.getFullYear());
- const [mes, setMes] = useState(hoy.getMonth() + 1);
+ // meses = array de meses seleccionados (1-12). Permite elegir varios.
+ const [meses, setMeses] = useState([hoy.getMonth() + 1]);
const [sede, setSede] = useState("TODOS");
const [programa, setPrograma] = useState("TODOS");
const [mostrarRepro, setMostrarRepro] = useState(true);
const [datos, setDatos] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
- const [modalCurso, setModalCurso] = useState(null);
+ const [modalCurso, setModalCurso] = useState(null); // curso seleccionado para "Ver alumnos"
const cols = useColumnasAjustables([280, 110, 120, 110, 100, 120, 110, 120, 100, 100, 130, 140, 80]);
+ // Clave estable de meses (ordenados) para las dependencias del effect.
+ const mesesKey = useMemo(() => [...meses].sort((a, b) => a - b).join(","), [meses]);
+
useEffect(() => {
let activo = true;
- setVistaActual(`ocupabilidad:${ano}|${mes}|${sede}|${programa}`);
+ const lista = mesesKey ? mesesKey.split(",").map(Number) : [];
+ setVistaActual(`ocupabilidad:${ano}|${mesesKey}|${sede}|${programa}`);
const cargar = () => {
setLoading(true); setError(null);
- api.ocupabilidad(ano, mes, sede, programa)
- .then((res) => { if (activo) { setDatos(res.datos || []); setLoading(false); } })
+ if (!lista.length) { setDatos([]); setLoading(false); return; }
+ // Una llamada por cada mes seleccionado; se juntan todas las filas.
+ Promise.all(lista.map((m) => api.ocupabilidad(ano, m, sede, programa)))
+ .then((resArr) => {
+ if (!activo) return;
+ const juntos = [];
+ resArr.forEach((res) => (res.datos || []).forEach((d) => juntos.push(d)));
+ setDatos(juntos);
+ setLoading(false);
+ })
.catch((e) => { if (activo) { setError(e.message); setLoading(false); } });
};
cargar();
const off = onRefrescar(cargar);
return () => { activo = false; off(); };
- }, [ano, mes, sede, programa]);
+ }, [ano, mesesKey, sede, programa]);
// Toggle "Mostrar Reprogramados": si está apagado, ocultar filas de inicio
const datosVisibles = useMemo(() => {
@@ -62,6 +76,7 @@ export default function Ocupabilidad() {
inscritos: totalInsc, // columna TOTAL INSCRITOS
meta,
avance: meta > 0 ? (totalInsc / meta * 100) : 0,
+ retirados,
desercion: totalInsc > 0 ? (retirados / totalInsc * 100) : 0,
sobreMeta: validos.filter((d) => (parseInt(d.Inscritos_Totales)||0) >= (parseInt(d.Meta_Curso)||1)).length,
pc: validos.reduce((s, d) => s + (parseInt(d.Inscritos_PC) || 0), 0),
@@ -89,9 +104,13 @@ export default function Ocupabilidad() {
["👥","Total Inscritos", kpis.inscritos, "alumnos en curso"],
["🎯","Meta Total", kpis.meta, "inscritos objetivo"],
["📈","Avance Inscritos", `${kpis.avance.toFixed(1)}%`, "total inscritos / meta"],
- ["📉","Deserción", `${kpis.desercion.toFixed(1)}%`, "retirados / total inscritos"],
+ ["📉","Deserción", `${kpis.desercion.toFixed(1)}%`, `${kpis.retirados} retirados / ${kpis.inscritos} inscritos`],
] : [];
+ // Estilo para inmovilizar la fila TOTAL GENERAL abajo (sticky), igual que Cobranza.
+ const ST = { position: "sticky", bottom: 0, zIndex: 9, backgroundColor: "#eff6ff",
+ boxShadow: "inset 0 1px 0 0 #cbd5e1" };
+
async function exportarExcel() {
if (!datosVisibles.length) return;
// Cargar SheetJS desde CDN solo cuando se necesita
@@ -113,7 +132,7 @@ export default function Ocupabilidad() {
const ws = XLSX.utils.json_to_sheet(filas);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Ocupabilidad");
- XLSX.writeFile(wb, `cursos_${ano}_${mes}.xlsx`);
+ XLSX.writeFile(wb, `cursos_${ano}_${mesesKey.replace(/,/g, "-")}.xlsx`);
}
return (
@@ -121,10 +140,10 @@ export default function Ocupabilidad() {
📊 Ocupabilidad
-