// src/pages/Rentabilidad.jsx import { useState, useEffect, useMemo } from "react"; import { api } from "../lib/api"; import { Loader, ErrorBox, Filters, Select } from "../components/UI"; import Modal from "../components/Modal"; import { useColumnasAjustables } from "../lib/useColumnasAjustables"; 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","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 const BG_INI = "#eef4ff", BG_ACT = "#edfff6"; function toNum(v){ const n=parseFloat(String(v).replace("S/","").replace(/,/g,"").replace("%","").trim()); return isNaN(n)?0:n; } export default function Rentabilidad() { 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 [mostrarInscritos, setMostrarInscritos] = useState(true); // TOTAL, RETIRADOS, EN CURSO const [filas, setFilas] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [modalVer, setModalVer] = useState(null); // Índices de columnas TOTAL(2), RETIRADOS(3), EN CURSO(4) — se ocultan si el check está apagado const IDX_INSCRITOS = [2, 3, 4]; const ocultar = (i) => !mostrarInscritos && IDX_INSCRITOS.includes(i); const ANCHOS_BASE = [260, 110, 80, 90, 90, 110, 120, 120, 120, 110, 120, 120, 110, 80]; const anchosVisibles = ANCHOS_BASE.filter((_, i) => !ocultar(i)); const cols = useColumnasAjustables(anchosVisibles); const [modalCostos, setModalCostos] = useState(null); function cargar() { setLoading(true); setError(null); api.rentabilidad(ano, mes, sede, programa) .then((res) => { setFilas(res.filas || []); setLoading(false); }) .catch((e) => { setError(e.message); setLoading(false); }); } useEffect(() => { let activo = true; setLoading(true); setError(null); api.rentabilidad(ano, mes, sede, programa) .then((res) => { if (activo) { setFilas(res.filas || []); setLoading(false); } }) .catch((e) => { if (activo) { setError(e.message); setLoading(false); } }); return () => { activo = false; }; }, [ano, mes, sede, programa]); const datos = useMemo(() => filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL"), [filas]); const total = useMemo(() => filas.find((f)=>String(f[0]).toUpperCase()==="TOTAL GENERAL"), [filas]); const kpis = useMemo(() => { if (!total) return null; return { programas: datos.length, enCurso: total[4], ventaAct: total[10], costoAct: total[11], mbAct: total[12], }; }, [datos, total]); function bgCol(ci){ if([7,8,9].includes(ci))return BG_INI; if([10,11,12].includes(ci))return BG_ACT; return ""; } return (

📈 Rentabilidad

({value:i+1,label:m}))} onChange={(v)=>setMes(+v)} />
{loading ? : error ? : <> {kpis && (
{[["📚","Programas",kpis.programas,"este mes"], ["🎓","En Curso",kpis.enCurso,"alumnos activos"], ["💵","Venta Actual",kpis.ventaAct,"ingreso total"], ["💸","Costo Actual",kpis.costoAct,"gasto total"], ["📊","M.B. Actual",kpis.mbAct,"margen bruto"]].map(([ico,label,val,sub])=>(
{ico}
{label}
{val}
{sub}
))}
)}
{COLS.filter((_,i)=>!ocultar(i)).map((c, vi)=>)} {datos.map((f, ri) => ( {f.slice(0,13).map((v,ci)=>{ if (ocultar(ci)) return null; const isPct = ci===9||ci===12; return ( ); })} ))} {total && ( {total.slice(0,13).map((v,ci)=>( ocultar(ci) ? null : ))} )}
{c}
{v}
{v}
} {modalVer && setModalVer(null)} />} {modalCostos && setModalCostos(null)} onGuardado={cargar} />}
); } function ModalVerDetalle({ programa, ano, mes, onClose }) { const [filas, setFilas] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { let activo = true; setLoading(true); api.rentabilidadDetalle(programa, ano, mes) .then((res)=>{ if(activo){setFilas(res.filas||[]);setLoading(false);} }) .catch(()=>{ if(activo){setFilas([]);setLoading(false);} }); return ()=>{activo=false;}; }, [programa, ano, mes]); const COLS_DET = ["VENDEDOR","ALUMNO","F. MATRÍCULA","F. CANCELACIÓN","TIPO","VALOR CUOTA","PROM. DESC. E.","VALOR VENTA"]; return ( {loading ? : filas.length===0 ?
Sin alumnos.
:
{COLS_DET.map((c)=>)} {filas.map((f,i)=>( {f.map((v,j)=>( ))} ))}
{c}
=5?"right":(j===1?"left":"center")}}>{v}
}
); } function ModalCostos({ programa, ano, mes, onClose, onGuardado }) { const [costos, setCostos] = useState(null); const [loading, setLoading] = useState(true); const [guardando, setGuardando] = useState(false); const [msg, setMsg] = useState(null); // [keyGuardar, etiqueta, prefijoBackend] const CAMPOS = [["epp","COSTO EPP","epp"],["certificado","COSTO CERTIFICADO","cert"],["docente","COSTO DOCENTE","doc"],["marketing","COSTO MARKETING","mkt"],["consumibles","COSTO CONSUMIBLES","cons"]]; const [ini, setIni] = useState({}); const [act, setAct] = useState({}); useEffect(() => { let activo = true; setLoading(true); api.rentabilidadCostos(programa, ano, mes) .then((res)=>{ if(!activo) return; const c = res.costos || {}; setCostos(c); const gi={}, ga={}; CAMPOS.forEach(([k,,bp])=>{ gi[k]=+(c[`${bp}_inicial`]||0); ga[k]=+(c[`${bp}_actual`]||0); }); setIni(gi); setAct(ga); setLoading(false); }) .catch(()=>{ if(activo){setCostos(null);setLoading(false);} }); return ()=>{activo=false;}; }, [programa, ano, mes]); const totIni = Object.values(ini).reduce((a,b)=>a+(+b||0),0); const totAct = Object.values(act).reduce((a,b)=>a+(+b||0),0); async function guardar() { setGuardando(true); setMsg(null); try { await api.guardarCostos(costos.num_indice, ini, act); if (onGuardado) onGuardado(); // recarga la tabla principal onClose(); // cierra el modal } catch (e) { setMsg({ ok:false, txt:"❌ Error al guardar. Verifica Supabase." }); setGuardando(false); } } return ( {loading ? : !costos ?
Sin datos de costos.
: <>
{msg &&
{msg.txt}
}
}
); } function Columna({ titulo, color, campos, valores, setVal, total }) { return (
{titulo}
{campos.map(([k,label])=>(
setVal({...valores,[k]:+e.target.value})} style={{width:"100%",padding:"7px 10px",border:"1px solid #cbd5e1",borderRadius:8,fontSize:13}} />
))}
TOTAL: S/ {total.toLocaleString("es-PE",{maximumFractionDigits:0})}
); }