Actualizar frontend/src/pages/Rentabilidad.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","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."];
|
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
|
// í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.
|
// meses = array de meses seleccionados (multi). 'mes' (primero) se usa en modales/export.
|
||||||
const [meses, setMeses] = useState([hoy.getMonth() + 1]);
|
const [meses, setMeses] = useState([hoy.getMonth() + 1]);
|
||||||
const mes = meses.length ? Math.min(...meses) : (hoy.getMonth() + 1);
|
const mes = meses.length ? Math.min(...meses) : (hoy.getMonth() + 1);
|
||||||
const [sede, setSede] = useState("TODOS");
|
const [sedes, setSedes] = useState([]); // multi; vacío = todas (filtro en front)
|
||||||
const [programa, setPrograma] = useState("TODOS");
|
const [programas, setProgramas] = useState([]); // multi; vacío = todos (filtro en front)
|
||||||
const [mostrarInscritos, setMostrarInscritos] = useState(true); // TOTAL, RETIRADOS, EN CURSO
|
const [mostrarInscritos, setMostrarInscritos] = useState(true); // TOTAL, RETIRADOS, EN CURSO
|
||||||
const [filas, setFilas] = useState([]);
|
const [filas, setFilas] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -213,8 +213,8 @@ export default function Rentabilidad() {
|
|||||||
try {
|
try {
|
||||||
// Traer ocupabilidad y rentabilidad frescas del mismo periodo/filtros
|
// Traer ocupabilidad y rentabilidad frescas del mismo periodo/filtros
|
||||||
const [ocu, rent] = await Promise.all([
|
const [ocu, rent] = await Promise.all([
|
||||||
api.ocupabilidad(ano, mes, sede, programa),
|
api.ocupabilidad(ano, mes, sedeArg, progArg),
|
||||||
api.rentabilidad(ano, mes, sede, programa),
|
api.rentabilidad(ano, mes, sedeArg, progArg),
|
||||||
]);
|
]);
|
||||||
const ocuPorProg = {};
|
const ocuPorProg = {};
|
||||||
(ocu.datos || []).forEach((d) => {
|
(ocu.datos || []).forEach((d) => {
|
||||||
@@ -444,7 +444,7 @@ export default function Rentabilidad() {
|
|||||||
XLSX.utils.book_append_sheet(wb, wsd, nombreHoja(prog));
|
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`);
|
XLSX.writeFile(wb, `rentabilidad_${suf}.xlsx`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert("No se pudo exportar: " + (e.message || e));
|
alert("No se pudo exportar: " + (e.message || e));
|
||||||
@@ -455,23 +455,27 @@ export default function Rentabilidad() {
|
|||||||
|
|
||||||
function cargar() {
|
function cargar() {
|
||||||
setLoading(true); setError(null);
|
setLoading(true); setError(null);
|
||||||
api.rentabilidad(ano, mes, sede, programa)
|
api.rentabilidad(ano, mes, sedeArg, progArg)
|
||||||
.then((res) => { setFilas(res.filas || []); setLoading(false); })
|
.then((res) => { setFilas(res.filas || []); setLoading(false); })
|
||||||
.catch((e) => { setError(e.message); setLoading(false); });
|
.catch((e) => { setError(e.message); setLoading(false); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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]);
|
||||||
|
const sedeArg = sedesKey || "TODOS";
|
||||||
|
const progArg = programasKey || "TODOS";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let activo = true;
|
let activo = true;
|
||||||
const lista = mesesKey ? mesesKey.split(",").map(Number) : [];
|
const lista = mesesKey ? mesesKey.split(",").map(Number) : [];
|
||||||
setVistaActual(`rentabilidad:${ano}|${mesesKey}|${sede}|${programa}`);
|
setVistaActual(`rentabilidad:${ano}|${mesesKey}|${sedeArg}|${progArg}`);
|
||||||
const cargarSeguro = () => {
|
const cargarSeguro = () => {
|
||||||
setLoading(true); setError(null);
|
setLoading(true); setError(null);
|
||||||
if (!lista.length) { setFilas([]); setLoading(false); return; }
|
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).
|
// 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) => {
|
.then((resArr) => {
|
||||||
if (!activo) return;
|
if (!activo) return;
|
||||||
const juntos = [];
|
const juntos = [];
|
||||||
@@ -486,7 +490,7 @@ export default function Rentabilidad() {
|
|||||||
cargarSeguro();
|
cargarSeguro();
|
||||||
const off = onRefrescar(cargarSeguro);
|
const off = onRefrescar(cargarSeguro);
|
||||||
return () => { activo = false; off(); };
|
return () => { activo = false; off(); };
|
||||||
}, [ano, mesesKey, sede, programa]);
|
}, [ano, mesesKey, sedeArg, progArg]);
|
||||||
|
|
||||||
const datos = useMemo(() => {
|
const datos = useMemo(() => {
|
||||||
let base = filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL");
|
let base = filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL");
|
||||||
@@ -530,8 +534,8 @@ export default function Rentabilidad() {
|
|||||||
<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" style={{marginLeft:"auto"}}>
|
<div className="filter-group" style={{marginLeft:"auto"}}>
|
||||||
<label>Inscritos</label>
|
<label>Inscritos</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}}>
|
||||||
@@ -711,14 +715,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);
|
||||||
@@ -911,3 +907,52 @@ function Columna({ titulo, color, campos, valores, setVal, total }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<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, 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user