From 5c585f29c5b51e6811c6d4c8e5573c325e644cad Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 23 Sep 2026 10:42:47 -0500 Subject: [PATCH] Actualizar frontend/src/pages/Rentabilidad.jsx --- frontend/src/pages/Rentabilidad.jsx | 87 ++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/frontend/src/pages/Rentabilidad.jsx b/frontend/src/pages/Rentabilidad.jsx index 794734a..ee91015 100644 --- a/frontend/src/pages/Rentabilidad.jsx +++ b/frontend/src/pages/Rentabilidad.jsx @@ -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 ANOS = [2024, 2025, 2026]; -const SEDES = ["TODOS","LIMA","AREQUIPA","PIURA","TRUJILLO"]; -const PROGRAMAS = ["TODOS","SEMINARIOS","OTROS","TEAC","TERC"]; +const SEDES = ["LIMA","AREQUIPA","PIURA","TRUJILLO"]; +const PROGRAMAS = ["SEMINARIOS","OTROS","TEAC","TERC"]; const COLS = ["PROGRAMA","FECHA INICIO","TOTAL","RETIRADOS","EN CURSO","CUOTA PROM.","PROM. DESC. E.","V. VENTA INICIAL","V. COSTO INICIAL","M.B. INICIAL %","V. VENTA ACTUAL","V. COSTO ACTUAL","M.B. ACTUAL %","OPC."]; // índices: inicial = 7,8,9 | actual = 10,11,12 @@ -47,8 +47,8 @@ export default function Rentabilidad() { // meses = array de meses seleccionados (multi). 'mes' (primero) se usa en modales/export. const [meses, setMeses] = useState([hoy.getMonth() + 1]); const mes = meses.length ? Math.min(...meses) : (hoy.getMonth() + 1); - const [sede, setSede] = useState("TODOS"); - const [programa, setPrograma] = useState("TODOS"); + const [sedes, setSedes] = useState([]); // multi; vacío = todas (filtro en front) + const [programas, setProgramas] = useState([]); // multi; vacío = todos (filtro en front) const [mostrarInscritos, setMostrarInscritos] = useState(true); // TOTAL, RETIRADOS, EN CURSO const [filas, setFilas] = useState([]); const [loading, setLoading] = useState(true); @@ -213,8 +213,8 @@ export default function Rentabilidad() { try { // Traer ocupabilidad y rentabilidad frescas del mismo periodo/filtros const [ocu, rent] = await Promise.all([ - api.ocupabilidad(ano, mes, sede, programa), - api.rentabilidad(ano, mes, sede, programa), + api.ocupabilidad(ano, mes, sedeArg, progArg), + api.rentabilidad(ano, mes, sedeArg, progArg), ]); const ocuPorProg = {}; (ocu.datos || []).forEach((d) => { @@ -444,7 +444,7 @@ export default function Rentabilidad() { XLSX.utils.book_append_sheet(wb, wsd, nombreHoja(prog)); }); - const suf = `${MESES[mes-1]}_${ano}` + (sede!=="TODOS"?`_${sede}`:"") + (programa!=="TODOS"?`_${programa}`:""); + const suf = `${MESES[mes-1]}_${ano}` + (sedesKey?`_${sedesKey}`:"") + (programasKey?`_${programasKey}`:""); XLSX.writeFile(wb, `rentabilidad_${suf}.xlsx`); } catch (e) { alert("No se pudo exportar: " + (e.message || e)); @@ -455,23 +455,27 @@ export default function Rentabilidad() { function cargar() { setLoading(true); setError(null); - api.rentabilidad(ano, mes, sede, programa) + api.rentabilidad(ano, mes, sedeArg, progArg) .then((res) => { setFilas(res.filas || []); setLoading(false); }) .catch((e) => { setError(e.message); setLoading(false); }); } // Clave estable de meses (ordenados) para las dependencias del effect. 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]); + const sedeArg = sedesKey || "TODOS"; + const progArg = programasKey || "TODOS"; useEffect(() => { let activo = true; const lista = mesesKey ? mesesKey.split(",").map(Number) : []; - setVistaActual(`rentabilidad:${ano}|${mesesKey}|${sede}|${programa}`); + setVistaActual(`rentabilidad:${ano}|${mesesKey}|${sedeArg}|${progArg}`); const cargarSeguro = () => { setLoading(true); setError(null); if (!lista.length) { setFilas([]); setLoading(false); return; } // Una llamada por cada mes; se juntan las filas de datos (sin la fila TOTAL de cada uno). - Promise.all(lista.map((m) => api.rentabilidad(ano, m, sede, programa))) + Promise.all(lista.map((m) => api.rentabilidad(ano, m, sedeArg, progArg))) .then((resArr) => { if (!activo) return; const juntos = []; @@ -486,7 +490,7 @@ export default function Rentabilidad() { cargarSeguro(); const off = onRefrescar(cargarSeguro); return () => { activo = false; off(); }; - }, [ano, mesesKey, sede, programa]); + }, [ano, mesesKey, sedeArg, progArg]); const datos = useMemo(() => { let base = filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL"); @@ -530,8 +534,8 @@ export default function Rentabilidad() { setAno(+v)} minWidth={110} /> - - + +
); } + +// Filtro multi-selección con casillas (Sede, Programa). Vacío = todos. +function ListaMulti({ label, opciones, seleccion, onChange, 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 ? "— Todos —" + : seleccion.length === 1 ? seleccion[0] + : `${seleccion.length} ${plural}`; + return ( +
+ + + {abierto && ( +
+ {opciones.map((o) => { + const on = seleccion.includes(o); + return ( + + ); + })} +
+ )} +
+ ); +}