Actualizar frontend/src/pages/Rentabilidad.jsx
This commit is contained in:
@@ -17,6 +17,30 @@ 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; }
|
||||
|
||||
// ── Ordenamiento (detecta monto S/, %, fecha dd-mm-yyyy, número o texto) ──
|
||||
function _valorOrden(v) {
|
||||
const s = String(v ?? "").trim();
|
||||
if (s === "" || s === "—" || s === "-") return { t: "vacio", n: 0, s: "" };
|
||||
const mf = s.match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})$/);
|
||||
if (mf) { const [, d, m, y] = mf; return { t: "fecha", n: new Date(+y, +m - 1, +d).getTime(), s }; }
|
||||
const limpio = s.replace(/S\/|%|,/g, "").replace(/\s/g, "").trim();
|
||||
if (limpio !== "" && !isNaN(Number(limpio))) return { t: "num", n: Number(limpio), s };
|
||||
return { t: "texto", n: 0, s: s.toLowerCase() };
|
||||
}
|
||||
function _cmp(a, b, dir) {
|
||||
const mult = dir === "asc" ? 1 : -1;
|
||||
const va = _valorOrden(a), vb = _valorOrden(b);
|
||||
if (va.t === "vacio" && vb.t === "vacio") return 0;
|
||||
if (va.t === "vacio") return 1;
|
||||
if (vb.t === "vacio") return -1;
|
||||
if (va.t === "texto" || vb.t === "texto") return mult * va.s.localeCompare(vb.s, "es");
|
||||
return mult * (va.n - vb.n);
|
||||
}
|
||||
function _flecha(activo, dir) {
|
||||
if (!activo) return <span style={{ opacity: 0.3, marginLeft: 4 }}>⇅</span>;
|
||||
return <span style={{ marginLeft: 4 }}>{dir === "asc" ? "▲" : "▼"}</span>;
|
||||
}
|
||||
|
||||
export default function Rentabilidad() {
|
||||
const hoy = new Date();
|
||||
const [ano, setAno] = useState(hoy.getFullYear());
|
||||
@@ -30,6 +54,14 @@ export default function Rentabilidad() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [modalVer, setModalVer] = useState(null);
|
||||
const [ordCol, setOrdCol] = useState(null); // índice REAL de columna
|
||||
const [ordDir, setOrdDir] = useState(null);
|
||||
const clickOrden = (i) => {
|
||||
if (i === 13) return; // columna OPC. no se ordena
|
||||
if (ordCol !== i) { setOrdCol(i); setOrdDir("asc"); }
|
||||
else if (ordDir === "asc") setOrdDir("desc");
|
||||
else { setOrdCol(null); setOrdDir(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);
|
||||
@@ -456,7 +488,11 @@ export default function Rentabilidad() {
|
||||
return () => { activo = false; off(); };
|
||||
}, [ano, mesesKey, sede, programa]);
|
||||
|
||||
const datos = useMemo(() => filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL"), [filas]);
|
||||
const datos = useMemo(() => {
|
||||
let base = filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL");
|
||||
if (ordCol != null && ordDir) base = [...base].sort((a,b)=>_cmp(a[ordCol], b[ordCol], ordDir));
|
||||
return base;
|
||||
}, [filas, ordCol, ordDir]);
|
||||
// TOTAL GENERAL recalculado en frontend (varios meses juntos). Suma columnas
|
||||
// sumables, recalcula MB% (=1-costo/venta), y promedia cuota/desc de las filas con valor.
|
||||
const total = useMemo(() => {
|
||||
@@ -529,7 +565,15 @@ export default function Rentabilidad() {
|
||||
<div className="table-wrap" style={{marginTop:16, overflow:"auto", maxHeight:"60vh"}}>
|
||||
<table {...cols.tableProps}>
|
||||
<cols.ColGroup />
|
||||
<thead><tr>{COLS.filter((_,i)=>!ocultar(i)).map((c, vi)=><th key={c} style={{position:"sticky",top:0,zIndex:2}}>{c}<cols.Resizer index={vi} /></th>)}</tr></thead>
|
||||
<thead><tr>{COLS.map((c,i)=>({c,i})).filter((o)=>!ocultar(o.i)).map((o, vi)=>{
|
||||
const ordenable = o.i !== 13;
|
||||
return (
|
||||
<th key={o.c} onClick={()=>clickOrden(o.i)}
|
||||
style={{position:"sticky",top:0,zIndex:2, cursor: ordenable?"pointer":"default", userSelect:"none"}}>
|
||||
{o.c}{ordenable && _flecha(ordCol===o.i, ordDir)}<cols.Resizer index={vi} />
|
||||
</th>
|
||||
);
|
||||
})}</tr></thead>
|
||||
<tbody>
|
||||
{datos.map((f, ri) => (
|
||||
<tr key={ri}>
|
||||
@@ -696,6 +740,13 @@ function MesesMulti({ label, meses, onChange }) {
|
||||
function ModalVerDetalle({ programa, ano, mes, resumen, onClose }) {
|
||||
const [filas, setFilas] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [ordCol, setOrdCol] = useState(null);
|
||||
const [ordDir, setOrdDir] = useState(null);
|
||||
const clickOrden = (i) => {
|
||||
if (ordCol !== i) { setOrdCol(i); setOrdDir("asc"); }
|
||||
else if (ordDir === "asc") setOrdDir("desc");
|
||||
else { setOrdCol(null); setOrdDir(null); }
|
||||
};
|
||||
useEffect(() => {
|
||||
let activo = true; setLoading(true);
|
||||
api.rentabilidadDetalle(programa, ano, mes)
|
||||
@@ -706,6 +757,15 @@ function ModalVerDetalle({ programa, ano, mes, resumen, onClose }) {
|
||||
|
||||
const COLS_DET = ["VENDEDOR","ALUMNO","F. MATRÍCULA","F. CANCELACIÓN","TIPO","VALOR CUOTA","PROM. DESC. E.","VALOR VENTA","ESTADO"];
|
||||
|
||||
// Ordena dejando la fila TOTAL GENERAL siempre al final.
|
||||
const filasOrd = useMemo(() => {
|
||||
if (ordCol == null || !ordDir) return filas;
|
||||
const tot = filas.filter((f)=>String(f[0]).toUpperCase()==="TOTAL GENERAL");
|
||||
const rest = filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL");
|
||||
rest.sort((a,b)=>_cmp(a[ordCol], b[ordCol], ordDir));
|
||||
return [...rest, ...tot];
|
||||
}, [filas, ordCol, ordDir]);
|
||||
|
||||
return (
|
||||
<Modal title={`👁️ Detalle — ${programa}`} onClose={onClose} width={1100}>
|
||||
{resumen && (
|
||||
@@ -720,9 +780,14 @@ function ModalVerDetalle({ programa, ano, mes, resumen, onClose }) {
|
||||
filas.length===0 ? <div style={{color:"#94a3b8",padding:20,textAlign:"center"}}>Sin alumnos.</div> :
|
||||
<div className="table-wrap" style={{overflowX:"auto", overflowY:"auto", maxHeight:"65vh"}}>
|
||||
<table style={{minWidth:1100, width:"max-content"}}>
|
||||
<thead><tr>{COLS_DET.map((c)=><th key={c} style={{whiteSpace:"nowrap", position:"sticky", top:0, zIndex:2}}>{c}</th>)}</tr></thead>
|
||||
<thead><tr>{COLS_DET.map((c,i)=>(
|
||||
<th key={c} onClick={()=>clickOrden(i)}
|
||||
style={{whiteSpace:"nowrap", position:"sticky", top:0, zIndex:2, cursor:"pointer", userSelect:"none"}}>
|
||||
{c}{_flecha(ordCol===i, ordDir)}
|
||||
</th>
|
||||
))}</tr></thead>
|
||||
<tbody>
|
||||
{filas.map((f,i)=>{
|
||||
{filasOrd.map((f,i)=>{
|
||||
const esTot = String(f[0]).toUpperCase()==="TOTAL GENERAL";
|
||||
const stTot = esTot ? {position:"sticky",bottom:0,zIndex:9,
|
||||
backgroundColor:"#dbeafe",fontWeight:700,color:"#1e3a5f",
|
||||
|
||||
Reference in New Issue
Block a user