Actualizar frontend/src/pages/Cobranza.jsx
This commit is contained in:
@@ -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)} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user