// src/pages/Ocupabilidad.jsx
import { useState, useEffect, useMemo } from "react";
import { api } from "../lib/api";
import { Loader, ErrorBox, ProgressBar, Filters, Select } from "../components/UI";
import { useColumnasAjustables } from "../lib/useColumnasAjustables";
import Modal from "../components/Modal";
import { setVistaActual, onRefrescar } from "../lib/vistaActual";
const MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"];
const ANOS = [2024, 2025, 2026];
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);
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 cols = useColumnasAjustables([280, 110, 120, 110, 100, 120, 110, 120, 100, 100, 130, 140, 80]);
useEffect(() => {
let activo = true;
setVistaActual(`ocupabilidad:${ano}|${mes}|${sede}|${programa}`);
const cargar = () => {
setLoading(true); setError(null);
api.ocupabilidad(ano, mes, sede, programa)
.then((res) => { if (activo) { setDatos(res.datos || []); setLoading(false); } })
.catch((e) => { if (activo) { setError(e.message); setLoading(false); } });
};
cargar();
const off = onRefrescar(cargar);
return () => { activo = false; off(); };
}, [ano, mes, sede, programa]);
// 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]);
const kpis = useMemo(() => {
if (!datosVisibles.length) return null;
const esRepro = (d) => String(d.dias_para_inicio).trim().toUpperCase() === "REPROGRAMADO";
// Las filas REPROGRAMADO (inicios) no entran en las métricas (igual que escritorio)
const validos = datosVisibles.filter((d) => !esRepro(d));
const insc = validos.reduce((s, d) => s + (parseInt(d.Inscritos_Activos) || 0), 0);
const totalInsc = validos.reduce((s, d) => s + (parseInt(d.Inscritos_Totales) || 0), 0);
const retirados = validos.reduce((s, d) => s + (parseInt(d.Retirados ?? d.Inscritos_Retirados) || 0), 0);
const meta = validos.reduce((s, d) => s + (parseInt(d.Meta_Curso) || 0), 0);
return {
cursos: validos.length, // solo programados (sin reprogramados)
reprogramados: datosVisibles.filter(esRepro).length, // inicios rojos (REPROGRAMADO)
inscritos: totalInsc, // columna TOTAL INSCRITOS
meta,
avance: meta > 0 ? (totalInsc / meta * 100) : 0,
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),
};
}, [datosVisibles]);
const totales = useMemo(() => {
const t = { mes:0, total:0, pc:0, retirados:0, activos:0, refri:0, cont:0, meta:0 };
datosVisibles.forEach((d) => {
t.mes += parseInt(d.Inscritos_Mes)||0;
t.total += parseInt(d.Inscritos_Totales)||0;
t.pc += parseInt(d.Inscritos_PC)||0;
t.retirados += parseInt(d.Retirados ?? d.Inscritos_Retirados)||0;
t.activos += parseInt(d.Inscritos_Activos)||0;
t.refri += parseInt(d.Descuento)||0;
t.cont += parseInt(d.Inscritos_Continuidad)||0;
t.meta += parseInt(d.Meta_Curso)||0;
});
return t;
}, [datosVisibles]);
const KPI_CARDS = kpis ? [
["📚","Cursos Activos", kpis.cursos, "programas este mes"],
["🔁","Cursos Reprogramados", kpis.reprogramados, "inicios reprogramados"],
["👥","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"],
] : [];
async function exportarExcel() {
if (!datosVisibles.length) return;
// Cargar SheetJS desde CDN solo cuando se necesita
const XLSX = await import("xlsx");
const filas = datosVisibles.map((d) => ({
"PROGRAMA": d.programa_frecuencia || d.dsc_programa || "",
"FECHA INICIO": d.fch_inicio || "",
"DÍAS PARA INICIO": d.dias_para_inicio ?? "",
"TOTAL INSCRITOS": parseInt(d.Inscritos_Totales)||0,
"RETIRADOS": parseInt(d.Retirados ?? d.Inscritos_Retirados)||0,
"INSCRITOS EN CURSO": parseInt(d.Inscritos_Activos)||0,
"META INSCRITOS": parseInt(d.Meta_Curso)||0,
"AVANCE INSCRITOS": d.Avance_Inscritos || "",
"INSCRITOS MES": parseInt(d.Inscritos_Mes)||0,
"INSCRITOS P.C": parseInt(d.Inscritos_PC)||0,
"INSCRITOS REFRIPERU": parseInt(d.Descuento)||0,
"INSCRITOS CONTINUIDAD": parseInt(d.Inscritos_Continuidad)||0,
}));
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`);
}
return (
📊 Ocupabilidad
{loading ?
:
error ?
:
<>
{KPI_CARDS.map(([ico,label,val,sub]) => (
{ico}
{label}
{val}
{sub}
))}
{COLS.map((c, i) => | {c} | )}
{datosVisibles.map((d, i) => {
const prog = d.programa_frecuencia || d.dsc_programa || "";
const dias = parseInt(d.dias_para_inicio);
const pct = toNum(d.Avance_Inscritos);
// Inicio reprogramado (existe en Supabase pero no en SQL este mes) → fila roja
const esInicio = String(d.dias_para_inicio).trim().toUpperCase() === "REPROGRAMADO";
const cel = (v) => esInicio ? "-" : v;
return (
| {prog} |
{d.fch_inicio || ""} |
{d.dias_para_inicio ?? ""} |
{cel(parseInt(d.Inscritos_Totales)||0)} |
{cel(parseInt(d.Retirados ?? d.Inscritos_Retirados)||0)} |
{cel(parseInt(d.Inscritos_Activos)||0)} |
{cel(parseInt(d.Meta_Curso)||0)} |
{esInicio ? "-" : } |
{cel(parseInt(d.Inscritos_Mes)||0)} |
{cel(parseInt(d.Inscritos_PC)||0)} |
{cel(parseInt(d.Descuento)||0)} |
{cel(parseInt(d.Inscritos_Continuidad)||0)} |
{esInicio ? "-" : (
)}
|
);
})}
{datosVisibles.length > 0 && (
| TOTAL GENERAL |
— | — |
{totales.total} |
{totales.retirados} |
{totales.activos} |
{totales.meta} |
0 ? totales.total/totales.meta*100 : 0} /> |
{totales.mes} | {totales.pc} | {totales.refri} | {totales.cont} |
— |
)}
>}
{modalCurso && (
setModalCurso(null)}
/>
)}
);
}
function ModalAlumnosCurso({ numIndice, programa, onClose }) {
const [alumnos, setAlumnos] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let activo = true;
setLoading(true);
api.ocupabilidadAlumnos(numIndice)
.then((res) => { if (activo) { setAlumnos(res.alumnos || []); setLoading(false); } })
.catch(() => { if (activo) { setAlumnos([]); setLoading(false); } });
return () => { activo = false; };
}, [numIndice]);
const colorEstado = (e) => {
if (e === "ALU") return { bg:"#ecfdf5", fg:"#047857", bd:"#a7f3d0" };
if (e === "PRE") return { bg:"#eff6ff", fg:"#1e40af", bd:"#bfdbfe" };
if (e === "RET") return { bg:"#fef2f2", fg:"#b91c1c", bd:"#fecaca" };
return { bg:"#f1f5f9", fg:"#475569", bd:"#e2e8f0" };
};
const conteo = useMemo(() => {
const c = {};
alumnos.forEach((a) => { c[a.estado] = (c[a.estado] || 0) + 1; });
return c;
}, [alumnos]);
return (
{loading ? :
alumnos.length === 0 ? Sin alumnos.
:
<>
👥 Total: {alumnos.length} alumno{alumnos.length!==1?"s":""}
{["ALU","PRE","RET"].map((e)=> conteo[e] ? (
{e}: {conteo[e]}
) : null)}
| ALUMNO |
ESTADO |
CURSO ANTERIOR (TEAC/TERC) |
ESTADO ANTERIOR |
{alumnos.map((a, i) => {
const c = colorEstado(a.estado);
const tieneAnt = a.curso_anterior && a.curso_anterior !== "-";
const cAnt = colorEstado(a.estado_anterior);
return (
| {a.alumno} |
{a.estado}
|
{a.curso_anterior || "-"}
|
{tieneAnt ? (
{a.estado_anterior}
) : -}
|
);
})}
>}
);
}