Actualizar frontend/src/pages/Cobranza.jsx

This commit is contained in:
2026-08-31 15:00:11 -05:00
parent 01ef6a7c16
commit 0891a5a7e8

View File

@@ -69,13 +69,15 @@ export default function Cobranza() {
// Expansión sede -> programas (solo en agrupación SEDE)
const [filasPrograma, setFilasPrograma] = useState([]); // vista "Por Programa" (para expandir)
const [sedeMapPrograma, setSedeMapPrograma] = useState({}); // programa -> sede
const [expandidas, setExpandidas] = useState(new Set()); // sedes abiertas
const [expandidas, setExpandidas] = useState(new Set()); // sedes/asesores abiertos
// Programas por asesor (se cargan bajo demanda al expandir un asesor)
const [programasAsesor, setProgramasAsesor] = useState({}); // { NOMBRE_ASESOR: [filas PROGRAMA] }
const esPrograma = agrupacion === "PROGRAMA";
// índices según agrupación
const IDX = esPrograma
? { saldo:13, ant:[4,5,6], mes:[7,8,9], tot:[10,11,12] }
: { saldo:10, ant:[1,2,3], mes:[4,5,6], tot:[7,8,9] };
? { saldo:13, saldoRet:14, ant:[4,5,6], mes:[7,8,9], tot:[10,11,12] }
: { saldo:10, saldoRet:11, ant:[1,2,3], mes:[4,5,6], tot:[7,8,9] };
useEffect(() => {
let activo = true;
@@ -118,6 +120,10 @@ export default function Cobranza() {
return () => { activo = false; };
}, [alumnos]);
// Al cambiar filtros/periodo/agrupación, limpiar el cache de programas por asesor
useEffect(() => { setProgramasAsesor({}); setExpandidas(new Set()); },
[ano, mes, agrupacion, sectorista, sedeFiltro, progFiltro, vencHasta]);
// Cargar matrículas POR RETIRAR (para el "Saldo por Retirar" de las tarjetas)
const [porRetirar, setPorRetirar] = useState(new Set());
useEffect(() => {
@@ -199,12 +205,12 @@ export default function Cobranza() {
}, [datos, IDX]);
const esAsesor = agrupacion === "ASESOR";
// Las 3 columnas extra (FREC/N°CUOTA/VENC) SOLO se muestran en SEDE cuando hay
// al menos una sede expandida, o siempre en PROGRAMA. En ASESOR nunca.
const conColsExtra = esPrograma || (!esAsesor && expandidas.size > 0);
// Las 3 columnas extra (FREC/N°CUOTA/VENC) se muestran en SEDE o ASESOR cuando hay
// al menos un grupo expandido, o siempre en PROGRAMA.
const conColsExtra = esPrograma || (expandidas.size > 0);
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","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","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","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","SALDO POR RETIRAR","OPC."];
// 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));
@@ -212,8 +218,8 @@ export default function Cobranza() {
// Índices de cartera según el layout de columnas de la tabla actual.
const LAY = conColsExtra
? { saldo:13, ant:[4,5,6], mes:[7,8,9], tot:[10,11,12] }
: { saldo:10, ant:[1,2,3], mes:[4,5,6], tot:[7,8,9] };
? { saldo:13, saldoRet:14, ant:[4,5,6], mes:[7,8,9], tot:[10,11,12] }
: { saldo:10, saldoRet:11, ant:[1,2,3], mes:[4,5,6], tot:[7,8,9] };
function cartClase(ci) {
if (LAY.ant.includes(ci)) return "cart-ant";
@@ -242,20 +248,30 @@ export default function Cobranza() {
// Normaliza una fila de SEDE al layout activo. Si conColsExtra (hay expansión),
// inserta FREC/N°CUOTA/VENC vacías tras el grupo. Si no, la deja tal cual.
const filaLayout = (f) => {
if (esPrograma) return f; // ya viene con las 15 columnas
if (esAsesor || !conColsExtra) return f; // layout simple (12 cols)
// SEDE con expansión: [grupo, "","","", cta_ant,cob_ant,%ant, cta_mes,... saldo]
return [f[0], "", "", "", f[1],f[2],f[3], f[4],f[5],f[6], f[7],f[8],f[9], f[10]];
if (esPrograma) return f; // ya viene con todas las columnas
if (!conColsExtra) return f; // layout simple (13 cols: ...saldo, saldo_ret, OPC)
// SEDE/ASESOR con expansión: [grupo, "","","", cta_ant..., saldo, saldo_ret]
return [f[0], "", "", "", f[1],f[2],f[3], f[4],f[5],f[6], f[7],f[8],f[9], f[10], f[11]];
};
// Expandir/contraer una sede (agrupación SEDE)
// Expandir/contraer un grupo (sede o asesor).
const toggleSede = (s) => {
const key = String(s).toUpperCase();
const abriendo = !expandidas.has(key);
setExpandidas((prev) => {
const n = new Set(prev);
n.has(key) ? n.delete(key) : n.add(key);
return n;
});
// En ASESOR: al abrir, cargar los programas de ese asesor (vista PROGRAMA filtrada por él)
if (esAsesor && abriendo && !programasAsesor[key]) {
api.cobranza(ano, mes, s, "PROGRAMA", sedeFiltro, progFiltro, vencHasta)
.then((res) => {
const fp = (res.filas || []).filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL");
setProgramasAsesor((prev) => ({ ...prev, [key]: fp }));
})
.catch(() => setProgramasAsesor((prev) => ({ ...prev, [key]: [] })));
}
};
// sede/filtro de un programa (desde REPLACE_META). sedeMapPrograma[nombre]={sede,filtro}
@@ -453,7 +469,10 @@ export default function Cobranza() {
const f = filaLayout(fRaw); // layout normalizado (15 o 12 cols)
const nCols = headers.length - 1; // sin OPC
const sedeKey = String(f[0]).toUpperCase();
const abiertaSede = !esPrograma && !esAsesor && expandidas.has(sedeKey);
const abiertaSede = !esPrograma && expandidas.has(sedeKey);
// Hijos a mostrar al expandir: en SEDE los programas de la sede;
// en ASESOR los programas del asesor (cargados bajo demanda).
const hijos = esAsesor ? (programasAsesor[sedeKey] || []) : programasDeSede(fRaw[0]);
const renderCeldas = (fila, {indent=false, bg="#fff"}={}) => {
const av = !String(fila[LAY.ant[2]]||"").trim() || ["-","—"].includes(String(fila[LAY.ant[2]]).trim());
@@ -467,7 +486,7 @@ export default function Cobranza() {
if ((ci===LAY.tot[0]||ci===LAY.tot[1]||ci===LAY.tot[2]) && tv) val = "—";
const cartCls = cartClase(ci);
const clss = (ci===0?"col-name":"") + (cartCls?` ${cartCls}`:"");
const puedeExpandir = (!esPrograma && !esAsesor && ci===0 && !indent);
const puedeExpandir = (!esPrograma && ci===0 && !indent);
const abierta = puedeExpandir && expandidas.has(String(fila[0]).toUpperCase());
return (
<td key={ci} className={clss.trim()}
@@ -476,7 +495,8 @@ export default function Cobranza() {
paddingLeft: (ci===0 && indent)?28:undefined,
overflow: ci===0?"hidden":undefined,
textOverflow: ci===0?"ellipsis":undefined,
color: indent?"#475569":undefined,
color: ci===LAY.saldoRet ? "#dc2626" : (indent?"#475569":undefined),
fontWeight: ci===LAY.saldoRet ? 700 : undefined,
...stickyMain(ci, {bg})}}>
{puedeExpandir && (
<span onClick={()=>toggleSede(String(fila[0]))}
@@ -502,12 +522,12 @@ export default function Cobranza() {
onClick={() => setModal({ grupo: fRaw[0] })}>👁️ Ver</button>
</td>
</tr>
{abiertaSede && programasDeSede(fRaw[0]).map((fp, pi) => (
{abiertaSede && hijos.map((fp, pi) => (
<tr key={"p"+pi} className="fila-prog" style={{background:"#f8fafc"}}>
{renderCeldas(fp, {indent:true, bg:"#f8fafc"})}
<td>
<button className="btn btn-ghost" style={{padding:"3px 8px",fontSize:10}}
onClick={()=>setModal({ grupo: fp[0], agrupacion: "PROGRAMA" })}>👁️ Ver</button>
onClick={()=>setModal({ grupo: fp[0], agrupacion: "PROGRAMA", ...(esAsesor ? { sectorista: fRaw[0] } : {}) })}>👁️ Ver</button>
</td>
</tr>
))}
@@ -520,7 +540,7 @@ export default function Cobranza() {
const T = { cAnt:suma(IDX.ant[0]), oAnt:suma(IDX.ant[1]),
cMes:suma(IDX.mes[0]), oMes:suma(IDX.mes[1]),
cTot:suma(IDX.tot[0]), oTot:suma(IDX.tot[1]),
saldo:suma(IDX.saldo) };
saldo:suma(IDX.saldo), saldoRet:suma(IDX.saldoRet) };
const ratio = (cob,cta)=> cta>0 ? `${Math.round(cob/cta*100)}%` : "";
const money = (n)=> "S/ " + n.toLocaleString("es-PE",{maximumFractionDigits:0});
// valor por columna del layout LAY
@@ -536,6 +556,7 @@ export default function Cobranza() {
if (ci===LAY.tot[1]) return money(T.oTot);
if (ci===LAY.tot[2]) return ratio(T.oTot,T.cTot);
if (ci===LAY.saldo) return money(T.saldo);
if (ci===LAY.saldoRet) return money(T.saldoRet);
return "";
};
return (
@@ -553,7 +574,8 @@ export default function Cobranza() {
boxShadow:"inset 0 1px 0 0 #cbd5e1" };
return (
<td key={ci} className={ci===0?"col-name":""}
style={{textAlign: ci===0?"left":"center",whiteSpace:"nowrap",...st}}>
style={{textAlign: ci===0?"left":"center",whiteSpace:"nowrap",
color: ci===LAY.saldoRet ? "#dc2626" : undefined, ...st}}>
{isPct ? <MiniBar valStr={val} /> : val}
</td>
);
@@ -569,7 +591,7 @@ export default function Cobranza() {
{modal && (
<ModalEstadoCuenta grupo={modal.grupo} ano={ano} mes={mes}
sectorista={sectorista} agrupacion={modal.agrupacion || agrupacion}
sectorista={modal.sectorista || sectorista} agrupacion={modal.agrupacion || agrupacion}
progFiltro={progFiltro} sedeFiltro={sedeFiltro} vencHasta={vencHasta}
onClose={() => setModal(null)} />
)}