diff --git a/frontend/src/pages/Cobranza.jsx b/frontend/src/pages/Cobranza.jsx index 62ee409..3f25618 100644 --- a/frontend/src/pages/Cobranza.jsx +++ b/frontend/src/pages/Cobranza.jsx @@ -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 ( {puedeExpandir && ( toggleSede(String(fila[0]))} @@ -502,12 +522,12 @@ export default function Cobranza() { onClick={() => setModal({ grupo: fRaw[0] })}>👁️ Ver - {abiertaSede && programasDeSede(fRaw[0]).map((fp, pi) => ( + {abiertaSede && hijos.map((fp, pi) => ( {renderCeldas(fp, {indent:true, bg:"#f8fafc"})} + onClick={()=>setModal({ grupo: fp[0], agrupacion: "PROGRAMA", ...(esAsesor ? { sectorista: fRaw[0] } : {}) })}>👁️ Ver ))} @@ -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 ( + style={{textAlign: ci===0?"left":"center",whiteSpace:"nowrap", + color: ci===LAY.saldoRet ? "#dc2626" : undefined, ...st}}> {isPct ? : val} ); @@ -569,7 +591,7 @@ export default function Cobranza() { {modal && ( setModal(null)} /> )}