Actualizar frontend/src/pages/Cobranza.jsx

This commit is contained in:
2026-09-29 16:37:22 -05:00
parent 3e92c19d4f
commit 449bd4c305

View File

@@ -339,8 +339,8 @@ export default function Cobranza() {
// al menos un grupo expandido, o siempre en PROGRAMA. // al menos un grupo expandido, o siempre en PROGRAMA.
const conColsExtra = esPrograma || (expandidas.size > 0); const conColsExtra = esPrograma || (expandidas.size > 0);
const headers = !conColsExtra const headers = !conColsExtra
? ["GRUPO","CTA X COB. MES ANT.","COB. MES ANT.","% ANT","CTA X COB EN CURSO","COB. EN CURSO","% MES","CTA X COB TOTAL","COB. TOTAL","% TOTAL","SALDO","SALDO POR RETIRAR","OPC."] ? ["GRUPO","CTA X COB. MES ANT.","COB. MES ANT.","% ANT","CTA X COB EN CURSO","COB. EN CURSO","% MES","CTA X COB TOTAL","COB. TOTAL","% TOTAL","SALDO","RETIROS","DIFERENCIA","OPC."]
: [(esPrograma?"PROGRAMA":"GRUPO"),"FREC.","N° CUOTA","VENCIMIENTO","CTA X COB. MES ANT.","COB. MES ANT.","% ANT","CTA X COB EN CURSO","COB. EN CURSO","% MES","CTA X COB TOTAL","COB. TOTAL","% TOTAL","SALDO","SALDO POR RETIRAR","OPC."]; : [(esPrograma?"PROGRAMA":"GRUPO"),"FREC.","N° CUOTA","VENCIMIENTO","CTA X COB. MES ANT.","COB. MES ANT.","% ANT","CTA X COB EN CURSO","COB. EN CURSO","% MES","CTA X COB TOTAL","COB. TOTAL","% TOTAL","SALDO","RETIROS","DIFERENCIA","OPC."];
// Anchos para columnas redimensionables (1ª columna más ancha, resto estándar) // Anchos para columnas redimensionables (1ª columna más ancha, resto estándar)
const anchosCob = headers.map((h, i) => i === 0 ? (conColsExtra ? 280 : 200) : (h === "FREC." || h.startsWith("%") || h === "OPC." ? 80 : 120)); const anchosCob = headers.map((h, i) => i === 0 ? (conColsExtra ? 280 : 200) : (h === "FREC." || h.startsWith("%") || h === "OPC." ? 80 : 120));
@@ -598,7 +598,7 @@ export default function Cobranza() {
<tbody> <tbody>
{datos.map((fRaw, ri) => { {datos.map((fRaw, ri) => {
const f = filaLayout(fRaw); // layout normalizado (15 o 12 cols) const f = filaLayout(fRaw); // layout normalizado (15 o 12 cols)
const nCols = headers.length - 1; // sin OPC const nCols = headers.length - 2; // sin RETIROS (calculada) ni OPC
const sedeKey = String(f[0]).toUpperCase(); const sedeKey = String(f[0]).toUpperCase();
const abiertaSede = !esPrograma && expandidas.has(sedeKey); const abiertaSede = !esPrograma && expandidas.has(sedeKey);
// Hijos a mostrar al expandir: en SEDE los programas de la sede; // Hijos a mostrar al expandir: en SEDE los programas de la sede;
@@ -609,7 +609,14 @@ export default function Cobranza() {
const av = !String(fila[LAY.ant[2]]||"").trim() || ["-","—"].includes(String(fila[LAY.ant[2]]).trim()); const av = !String(fila[LAY.ant[2]]||"").trim() || ["-","—"].includes(String(fila[LAY.ant[2]]).trim());
const mv = !String(fila[LAY.mes[2]]||"").trim() || ["-","—"].includes(String(fila[LAY.mes[2]]).trim()); const mv = !String(fila[LAY.mes[2]]||"").trim() || ["-","—"].includes(String(fila[LAY.mes[2]]).trim());
const tv = !String(fila[LAY.tot[2]]||"").trim() || ["-","—"].includes(String(fila[LAY.tot[2]]).trim()); const tv = !String(fila[LAY.tot[2]]||"").trim() || ["-","—"].includes(String(fila[LAY.tot[2]]).trim());
return fila.slice(0, nCols).map((v, ci) => { // RETIROS (calculada) = SALDO − SALDO POR RETIRAR de la propia fila.
const retiros = toMonto(fila[LAY.saldo]) - toMonto(fila[LAY.saldoRet]);
const celdaRetiros = (
<td key="retiros" style={{textAlign:"center",whiteSpace:"nowrap",color:"#7c3aed",fontWeight:700, backgroundColor: bg}}>
{"S/ " + retiros.toLocaleString("es-PE",{maximumFractionDigits:0})}
</td>
);
const celdas = fila.slice(0, nCols).map((v, ci) => {
const isPct = LAY.ant[2]===ci || LAY.mes[2]===ci || LAY.tot[2]===ci; const isPct = LAY.ant[2]===ci || LAY.mes[2]===ci || LAY.tot[2]===ci;
let val = v; let val = v;
if ((ci===LAY.ant[0]||ci===LAY.ant[1]||ci===LAY.ant[2]) && av) val = "—"; if ((ci===LAY.ant[0]||ci===LAY.ant[1]||ci===LAY.ant[2]) && av) val = "—";
@@ -642,6 +649,7 @@ export default function Cobranza() {
</td> </td>
); );
}); });
return [...celdas, celdaRetiros];
}; };
return ( return (
@@ -688,6 +696,7 @@ export default function Cobranza() {
if (ci===LAY.tot[2]) return ratio(T.oTot,T.cTot); if (ci===LAY.tot[2]) return ratio(T.oTot,T.cTot);
if (ci===LAY.saldo) return money(T.saldo); if (ci===LAY.saldo) return money(T.saldo);
if (ci===LAY.saldoRet) return money(T.saldoRet); if (ci===LAY.saldoRet) return money(T.saldoRet);
if (ci===LAY.saldoRet+1) return money(T.saldo - T.saldoRet); // RETIROS (calculada)
return ""; return "";
}; };
return ( return (
@@ -703,10 +712,12 @@ export default function Cobranza() {
boxShadow:`inset -1px 0 0 0 #cbd5e1${ci===FIJAS_MAIN-1?", 3px 0 5px -2px rgba(0,0,0,0.18)":""}, inset 0 1px 0 0 #cbd5e1` } boxShadow:`inset -1px 0 0 0 #cbd5e1${ci===FIJAS_MAIN-1?", 3px 0 5px -2px rgba(0,0,0,0.18)":""}, inset 0 1px 0 0 #cbd5e1` }
: { position:"sticky", bottom:0, zIndex:9, backgroundColor:"#eff6ff", : { position:"sticky", bottom:0, zIndex:9, backgroundColor:"#eff6ff",
boxShadow:"inset 0 1px 0 0 #cbd5e1" }; boxShadow:"inset 0 1px 0 0 #cbd5e1" };
const esRetiros = ci===LAY.saldoRet+1;
return ( return (
<td key={ci} className={ci===0?"col-name":""} <td key={ci} className={ci===0?"col-name":""}
style={{textAlign: ci===0?"left":"center",whiteSpace:"nowrap", style={{textAlign: ci===0?"left":"center",whiteSpace:"nowrap",
color: ci===LAY.saldoRet ? "#dc2626" : undefined, ...st}}> color: ci===LAY.saldoRet ? "#dc2626" : (esRetiros ? "#7c3aed" : undefined),
fontWeight: esRetiros ? 700 : undefined, ...st}}>
{isPct ? <MiniBar valStr={val} /> : val} {isPct ? <MiniBar valStr={val} /> : val}
</td> </td>
); );
@@ -841,8 +852,8 @@ function Tarjetas({ kpis, retiro, loading }) {
<Row k="Cta x Cobrar" v={`S/ ${cta.toLocaleString("es-PE",{maximumFractionDigits:0})}`} /> <Row k="Cta x Cobrar" v={`S/ ${cta.toLocaleString("es-PE",{maximumFractionDigits:0})}`} />
<Row k="Cobrado" v={`S/ ${cob.toLocaleString("es-PE",{maximumFractionDigits:0})}`} /> <Row k="Cobrado" v={`S/ ${cob.toLocaleString("es-PE",{maximumFractionDigits:0})}`} />
<Row k="Saldo" v={`S/ ${saldo.toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color={saldo>0?"#dc2626":"#10b981"} /> <Row k="Saldo" v={`S/ ${saldo.toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color={saldo>0?"#dc2626":"#10b981"} />
<Row k="Saldo por Retirar" v={`S/ ${(saldoRetiro||0).toLocaleString("es-PE",{maximumFractionDigits:0})}`} color="#dc2626" /> <Row k="Retiros" v={`S/ ${(saldoRetiro||0).toLocaleString("es-PE",{maximumFractionDigits:0})}`} color="#dc2626" />
<Row k="Retiros" v={`S/ ${(saldo-(saldoRetiro||0)).toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color="#7c3aed" /> <Row k="Diferencia" v={`S/ ${(saldo-(saldoRetiro||0)).toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color="#7c3aed" />
{cta>0 && ( {cta>0 && (
<div style={{marginTop:10}}> <div style={{marginTop:10}}>
<div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}> <div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}>
@@ -892,8 +903,8 @@ function TarjetasResumen({ r, retiro }) {
<Row k="Cta x Cobrar" v={`S/ ${cta.toLocaleString("es-PE",{maximumFractionDigits:0})}`} /> <Row k="Cta x Cobrar" v={`S/ ${cta.toLocaleString("es-PE",{maximumFractionDigits:0})}`} />
<Row k="Cobrado" v={`S/ ${cob.toLocaleString("es-PE",{maximumFractionDigits:0})}`} /> <Row k="Cobrado" v={`S/ ${cob.toLocaleString("es-PE",{maximumFractionDigits:0})}`} />
<Row k="Saldo" v={`S/ ${saldo.toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color={saldo>0?"#dc2626":"#10b981"} /> <Row k="Saldo" v={`S/ ${saldo.toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color={saldo>0?"#dc2626":"#10b981"} />
<Row k="Saldo por Retirar" v={`S/ ${(saldoRetiro||0).toLocaleString("es-PE",{maximumFractionDigits:0})}`} color="#dc2626" /> <Row k="Retiros" v={`S/ ${(saldoRetiro||0).toLocaleString("es-PE",{maximumFractionDigits:0})}`} color="#dc2626" />
<Row k="Retiros" v={`S/ ${(saldo-(saldoRetiro||0)).toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color="#7c3aed" /> <Row k="Diferencia" v={`S/ ${(saldo-(saldoRetiro||0)).toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color="#7c3aed" />
{cta>0 && ( {cta>0 && (
<div style={{marginTop:8}}> <div style={{marginTop:8}}>
<div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}> <div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}>
@@ -1136,7 +1147,7 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, progFiltro
}; };
useEffect(() => { cargarPorRetirar(); }, [ano, mes]); useEffect(() => { cargarPorRetirar(); }, [ano, mes]);
const COLS = ["PROGRAMA","ALUMNO","N° CUOTA","F. VENC.","CTA ANT.","COB ANT.","% ANT","CTA MES","COB MES","% MES","CTA TOT","COB TOT","% TOT","SALDO","INFO"]; const COLS = ["PROGRAMA","ALUMNO","N° CUOTA","F. VENC.","CTA ANT.","COB ANT.","% ANT","CTA MES","COB MES","% MES","CTA TOT","COB TOT","% TOT","SALDO","DIFERENCIA","INFO"];
// Alumno seleccionado para el pop-up de INFO (estado + observaciones) // Alumno seleccionado para el pop-up de INFO (estado + observaciones)
const [infoAlumno, setInfoAlumno] = useState(null); const [infoAlumno, setInfoAlumno] = useState(null);
@@ -1256,7 +1267,7 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, progFiltro
Ver solo por retirar Ver solo por retirar
</label> </label>
</div> </div>
<div className="table-wrap" style={{overflow:"auto", maxHeight:"60vh"}}> <div className="table-wrap" style={{overflow:"auto", maxHeight:"52vh"}}>
<table className="cob-table" {...cols.tableProps}> <table className="cob-table" {...cols.tableProps}>
<cols.ColGroup /> <cols.ColGroup />
<thead><tr>{COLS.map((c,i)=>{ <thead><tr>{COLS.map((c,i)=>{
@@ -1264,7 +1275,7 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, progFiltro
const stH = fija const stH = fija
? stickyCol(i,{esHeader:true,bg:"#334155"}) ? stickyCol(i,{esHeader:true,bg:"#334155"})
: { position:"sticky", top:0, zIndex:12, backgroundColor:"#334155" }; : { position:"sticky", top:0, zIndex:12, backgroundColor:"#334155" };
const ordenable = i !== 14; const ordenable = i !== 14 && i !== 15; // RETIROS (14, calculada) e INFO (15) no ordenables
return <th key={c} onClick={()=>clickOrden(i)} return <th key={c} onClick={()=>clickOrden(i)}
style={{...stH, position:stH.position||"relative", cursor: ordenable?"pointer":"default", userSelect:"none"}}> style={{...stH, position:stH.position||"relative", cursor: ordenable?"pointer":"default", userSelect:"none"}}>
{c}{ordenable && _flecha(ordCol===i, ordDir)}<cols.Resizer index={i} /></th>; {c}{ordenable && _flecha(ordCol===i, ordDir)}<cols.Resizer index={i} /></th>;
@@ -1282,6 +1293,9 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, progFiltro
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) const programa = f[14] || "-"; // programa del alumno (extra del backend)
const retira = porRetirar.has(String(f[0])); // alumno POR RETIRAR const retira = porRetirar.has(String(f[0])); // alumno POR RETIRAR
// RETIROS (calculada) = SALDO − SALDO POR RETIRAR. Si el alumno está POR
// RETIRAR, todo su saldo es "por retirar" ⇒ RETIROS = 0; si no, = su saldo.
const retirosFila = retira ? 0 : saldo;
return ( return (
<tr key={i} className={deuda?"row-deuda":""} <tr key={i} className={deuda?"row-deuda":""}
style={retira ? { textDecoration:"line-through", textDecorationColor:"#dc2626", style={retira ? { textDecoration:"line-through", textDecorationColor:"#dc2626",
@@ -1315,6 +1329,10 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, progFiltro
</td> </td>
); );
})} })}
{/* RETIROS (calculada) = SALDO − SALDO POR RETIRAR */}
<td style={{textAlign:"center",whiteSpace:"nowrap",color:"#7c3aed",fontWeight:700}}>
{"S/ " + retirosFila.toLocaleString("es-PE",{maximumFractionDigits:0})}
</td>
{/* Botón INFO por alumno (estado + observaciones) */} {/* Botón INFO por alumno (estado + observaciones) */}
<td style={{textAlign:"center"}}> <td style={{textAlign:"center"}}>
<button <button
@@ -1337,6 +1355,10 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, progFiltro
if (alumnos.length===0) return null; if (alumnos.length===0) return null;
const sum = (j)=>alumnos.reduce((s,f)=>s+toMonto(f[j]),0); const sum = (j)=>alumnos.reduce((s,f)=>s+toMonto(f[j]),0);
const t = {4:sum(4),5:sum(5),7:sum(7),8:sum(8),10:sum(10),11:sum(11),13:sum(13)}; const t = {4:sum(4),5:sum(5),7:sum(7),8:sum(8),10:sum(10),11:sum(11),13:sum(13)};
// RETIROS total (calculada) = SALDO total − SALDO POR RETIRAR total.
// SALDO POR RETIRAR = saldo de los alumnos marcados POR RETIRAR.
const saldoPorRetirarTot = alumnos.reduce((s,f)=> s + (porRetirar.has(String(f[0])) ? toMonto(f[13]) : 0), 0);
const retirosTot = t[13] - saldoPorRetirarTot;
const r = (cob,cta)=>cta>0?`${Math.round(cob/cta*100)}%`:""; const r = (cob,cta)=>cta>0?`${Math.round(cob/cta*100)}%`:"";
const celda = (j)=>{ const celda = (j)=>{
if (j===0) return "TOTAL GENERAL"; if (j===0) return "TOTAL GENERAL";
@@ -1369,6 +1391,10 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, progFiltro
</td> </td>
); );
})} })}
{/* RETIROS total (calculada) */}
<td style={{position:"sticky",bottom:0,zIndex:8,backgroundColor:"#eff6ff",padding:"11px 8px",textAlign:"center",whiteSpace:"nowrap",color:"#7c3aed",fontWeight:700,boxShadow:"inset 0 1px 0 0 #cbd5e1"}}>
{"S/ " + retirosTot.toLocaleString("es-PE",{maximumFractionDigits:0})}
</td>
<td style={{position:"sticky",bottom:0,zIndex:8,backgroundColor:"#eff6ff",boxShadow:"inset 0 1px 0 0 #cbd5e1"}}></td> <td style={{position:"sticky",bottom:0,zIndex:8,backgroundColor:"#eff6ff",boxShadow:"inset 0 1px 0 0 #cbd5e1"}}></td>
</tr> </tr>
); );