// 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

({value:i+1,label:m}))} onChange={(v) => setMes(+v)} />
{loading ? : error ? : <>
{KPI_CARDS.map(([ico,label,val,sub]) => (
{ico}
{label}
{val}
{sub}
))}
{COLS.map((c, i) => )} {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 ( ); })} {datosVisibles.length > 0 && ( )}
{c}
{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 ? "-" : ( )}
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)}
{alumnos.map((a, i) => { const c = colorEstado(a.estado); const tieneAnt = a.curso_anterior && a.curso_anterior !== "-"; const cAnt = colorEstado(a.estado_anterior); return ( ); })}
ALUMNO ESTADO CURSO ANTERIOR (TEAC/TERC) ESTADO ANTERIOR
{a.alumno} {a.estado} {a.curso_anterior || "-"} {tieneAnt ? ( {a.estado_anterior} ) : -}
}
); }