Actualizar frontend/src/pages/Cobranza.jsx

This commit is contained in:
2026-07-03 15:26:07 -05:00
parent b3eaed0863
commit 27787b97be

View File

@@ -155,9 +155,8 @@ export default function Cobranza() {
}, [alumnos, esPrograma, sede, frec]); }, [alumnos, esPrograma, sede, frec]);
function grupoDe(a) { function grupoDe(a) {
if (agrupacion === "SEDE") return a.sede; // El programa detallado del alumno, en TODOS los filtros (Sede, Asesor, Programa).
if (agrupacion === "ASESOR") return ""; // el sectorista ya filtra; no hay sub-grupo por alumno return a.programa;
return a.programa; // PROGRAMA
} }
// Exportar el DETALLE DE ALUMNOS (rápido: usa el dataset ya cargado, sin N consultas) // Exportar el DETALLE DE ALUMNOS (rápido: usa el dataset ya cargado, sin N consultas)
@@ -169,10 +168,7 @@ export default function Cobranza() {
const toDate = (v) => { const toDate = (v) => {
if (!v || v === "-") return ""; if (!v || v === "-") return "";
const s = String(v).trim().replace(/\//g,"-"); return String(v).trim();
const p = s.split("-");
if (p.length === 3 && p[2].length === 4) return new Date(+p[2], +p[1]-1, +p[0]);
return String(v);
}; };
const pct = (cob, cta) => cta > 0 ? cob/cta : null; const pct = (cob, cta) => cta > 0 ? cob/cta : null;
@@ -210,7 +206,6 @@ export default function Cobranza() {
const st = cellStyle(c); const st = cellStyle(c);
if ([5,6,8,9,11,12,14].includes(c)) ws[ref] = { v:Number(v)||0, t:"n", z:moneyFmt, ...(st?{s:st}:{}) }; if ([5,6,8,9,11,12,14].includes(c)) ws[ref] = { v:Number(v)||0, t:"n", z:moneyFmt, ...(st?{s:st}:{}) };
else if ([7,10,13].includes(c)) ws[ref] = (v===null) ? { v:"-", t:"s", ...(st?{s:st}:{}) } : { v:Number(v), t:"n", z:pctFmt, ...(st?{s:st}:{}) }; else if ([7,10,13].includes(c)) ws[ref] = (v===null) ? { v:"-", t:"s", ...(st?{s:st}:{}) } : { v:Number(v), t:"n", z:pctFmt, ...(st?{s:st}:{}) };
else if (c===4 && v instanceof Date) ws[ref] = { v, t:"d", z:dateFmt };
else ws[ref] = { v: v ?? "", t:"s" }; else ws[ref] = { v: v ?? "", t:"s" };
}); });
}); });
@@ -560,7 +555,11 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
return () => { activo = false; }; return () => { activo = false; };
}, [grupo, ano, mes, sectorista, agrupacion]); }, [grupo, ano, mes, sectorista, agrupacion]);
const COLS = ["MATRÍCULA","ALUMNO","N° CUOTA","F. VENC.","CTA ANT.","COB ANT.","% ANT","CTA MES","COB MES","% MES","CTA TOT","COB TOT","% TOT","SALDO"]; const COLS = ["PROGRAMA","ALUMNO","N° CUOTA","F. VENC.","CTA ANT.","COB ANT.","% ANT","CTA MES","COB MES","% MES","CTA TOT","COB TOT","% TOT","SALDO"];
// Columnas redimensionables (igual que la tabla principal): PROGRAMA y ALUMNO más anchas
const anchosDet = COLS.map((h, i) => i === 0 ? 240 : (i === 1 ? 220 : (h.startsWith("%") ? 80 : 110)));
const cols = useColumnasAjustables(anchosDet);
// Resumen para las 3 tarjetas (siempre visibles) — suma del detalle de alumnos // Resumen para las 3 tarjetas (siempre visibles) — suma del detalle de alumnos
const resumen = useMemo(() => { const resumen = useMemo(() => {
@@ -574,6 +573,17 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
return { cAnt,obAnt,cMes,obMes,cTot,obTot }; return { cAnt,obAnt,cMes,obMes,cTot,obTot };
}, [filas]); }, [filas]);
// Conteo de alumnos: total, ya pagaron (saldo 0) y por pagar (saldo > 0)
const conteo = useMemo(() => {
let total=0, pagaron=0, deben=0;
filas.forEach((f) => {
if (String(f[0]).toUpperCase() === "TOTAL") return;
total++;
if (toMonto(f[13]) > 0.01) deben++; else pagaron++;
});
return { total, pagaron, deben };
}, [filas]);
return ( return (
<Modal title={`📊 Estado de Cuenta — ${grupo}`} onClose={onClose} width={1500}> <Modal title={`📊 Estado de Cuenta — ${grupo}`} onClose={onClose} width={1500}>
{loading ? <Loader text="Cargando estado de cuenta..." /> : {loading ? <Loader text="Cargando estado de cuenta..." /> :
@@ -582,9 +592,28 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
<div style={{marginBottom:16}}> <div style={{marginBottom:16}}>
<TarjetasResumen r={resumen} /> <TarjetasResumen r={resumen} />
</div> </div>
<div className="table-wrap"> {/* Etiquetas de conteo de alumnos */}
<table className="cob-table" style={{minWidth:1000}}> <div style={{display:"flex",gap:10,marginBottom:14,flexWrap:"wrap"}}>
<thead><tr>{COLS.map((c)=><th key={c}>{c}</th>)}</tr></thead> <span style={{display:"inline-flex",alignItems:"center",gap:6,padding:"6px 14px",
borderRadius:999,background:"#eff6ff",color:"#1e40af",fontSize:13,fontWeight:700,
border:"1px solid #bfdbfe"}}>
👥 Total: {conteo.total} alumno{conteo.total!==1?"s":""}
</span>
<span style={{display:"inline-flex",alignItems:"center",gap:6,padding:"6px 14px",
borderRadius:999,background:"#ecfdf5",color:"#047857",fontSize:13,fontWeight:700,
border:"1px solid #a7f3d0"}}>
Pagaron: {conteo.pagaron}
</span>
<span style={{display:"inline-flex",alignItems:"center",gap:6,padding:"6px 14px",
borderRadius:999,background:"#fef2f2",color:"#b91c1c",fontSize:13,fontWeight:700,
border:"1px solid #fecaca"}}>
Por pagar: {conteo.deben}
</span>
</div>
<div className="table-wrap" style={{overflowX:"auto"}}>
<table className="cob-table" {...cols.tableProps}>
<cols.ColGroup />
<thead><tr>{COLS.map((c,i)=><th key={c} style={{position:"relative"}}>{c}<cols.Resizer index={i} /></th>)}</tr></thead>
<tbody> <tbody>
{filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL").map((f, i) => { {filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL").map((f, i) => {
const saldo = toMonto(f[13]); const saldo = toMonto(f[13]);
@@ -593,10 +622,12 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
const antVacio = !String(f[6]||"").trim() || ["-","—"].includes(String(f[6]).trim()); const antVacio = !String(f[6]||"").trim() || ["-","—"].includes(String(f[6]).trim());
const mesVacio = !String(f[9]||"").trim() || ["-","—"].includes(String(f[9]).trim()); const mesVacio = !String(f[9]||"").trim() || ["-","—"].includes(String(f[9]).trim());
const totVacio = !String(f[12]||"").trim() || ["-","—"].includes(String(f[12]).trim()); const totVacio = !String(f[12]||"").trim() || ["-","—"].includes(String(f[12]).trim());
const programa = f[14] || "-"; // programa del alumno (extra del backend)
return ( return (
<tr key={i} className={deuda?"row-deuda":""}> <tr key={i} className={deuda?"row-deuda":""}>
{f.map((v, j) => { {f.slice(0, 14).map((v, j) => {
let val = v; // Columna 0: mostrar PROGRAMA en vez de la matrícula
let val = (j===0) ? programa : v;
if ((j===4||j===5||j===6) && antVacio) val = "—"; if ((j===4||j===5||j===6) && antVacio) val = "—";
if ((j===7||j===8||j===9) && mesVacio) val = "—"; if ((j===7||j===8||j===9) && mesVacio) val = "—";
if ((j===10||j===11||j===12) && totVacio) val = "—"; if ((j===10||j===11||j===12) && totVacio) val = "—";
@@ -606,11 +637,15 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
if ([4,5,6].includes(j)) cart = "cart-ant"; if ([4,5,6].includes(j)) cart = "cart-ant";
else if ([7,8,9].includes(j)) cart = "cart-mes"; else if ([7,8,9].includes(j)) cart = "cart-mes";
else if ([10,11,12].includes(j)) cart = "cart-tot"; else if ([10,11,12].includes(j)) cart = "cart-tot";
const clss = ((j===1?"col-name":"") + (cart?` ${cart}`:"")).trim(); const clss = (((j===0||j===1)?"col-name":"") + (cart?` ${cart}`:"")).trim();
const esTexto = (j===0||j===1);
return ( return (
<td key={j} className={clss} <td key={j} className={clss}
style={{textAlign:j===1?"left":(isMonto?"right":"center"), title={esTexto ? String(val) : undefined}
style={{textAlign:esTexto?"left":(isMonto?"right":"center"),
whiteSpace:"nowrap", whiteSpace:"nowrap",
overflow:esTexto?"hidden":undefined,
textOverflow:esTexto?"ellipsis":undefined,
color:j===13&&deuda?"#dc2626":undefined, color:j===13&&deuda?"#dc2626":undefined,
fontWeight:j===13?700:undefined}}> fontWeight:j===13?700:undefined}}>
{isPct ? (val==="—"?<span style={{color:"#cbd5e1"}}></span>:<MiniBar valStr={val} />) : fmtMoneda(val)} {isPct ? (val==="—"?<span style={{color:"#cbd5e1"}}></span>:<MiniBar valStr={val} />) : fmtMoneda(val)}