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