Actualizar frontend/src/pages/Cobranza.jsx
This commit is contained in:
@@ -155,9 +155,8 @@ export default function Cobranza() {
|
|||||||
}, [alumnos, esPrograma, sede, frec]);
|
}, [alumnos, esPrograma, sede, frec]);
|
||||||
|
|
||||||
function grupoDe(a) {
|
function grupoDe(a) {
|
||||||
if (agrupacion === "SEDE") return a.sede;
|
// El programa detallado del alumno, en TODOS los filtros (Sede, Asesor, Programa).
|
||||||
if (agrupacion === "ASESOR") return ""; // el sectorista ya filtra; no hay sub-grupo por alumno
|
return a.programa;
|
||||||
return a.programa; // PROGRAMA
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exportar el DETALLE DE ALUMNOS (rápido: usa el dataset ya cargado, sin N consultas)
|
// Exportar el DETALLE DE ALUMNOS (rápido: usa el dataset ya cargado, sin N consultas)
|
||||||
@@ -169,10 +168,7 @@ export default function Cobranza() {
|
|||||||
|
|
||||||
const toDate = (v) => {
|
const toDate = (v) => {
|
||||||
if (!v || v === "-") return "";
|
if (!v || v === "-") return "";
|
||||||
const s = String(v).trim().replace(/\//g,"-");
|
return String(v).trim();
|
||||||
const p = s.split("-");
|
|
||||||
if (p.length === 3 && p[2].length === 4) return new Date(+p[2], +p[1]-1, +p[0]);
|
|
||||||
return String(v);
|
|
||||||
};
|
};
|
||||||
const pct = (cob, cta) => cta > 0 ? cob/cta : null;
|
const pct = (cob, cta) => cta > 0 ? cob/cta : null;
|
||||||
|
|
||||||
@@ -210,7 +206,6 @@ export default function Cobranza() {
|
|||||||
const st = cellStyle(c);
|
const st = cellStyle(c);
|
||||||
if ([5,6,8,9,11,12,14].includes(c)) ws[ref] = { v:Number(v)||0, t:"n", z:moneyFmt, ...(st?{s:st}:{}) };
|
if ([5,6,8,9,11,12,14].includes(c)) ws[ref] = { v:Number(v)||0, t:"n", z:moneyFmt, ...(st?{s:st}:{}) };
|
||||||
else if ([7,10,13].includes(c)) ws[ref] = (v===null) ? { v:"-", t:"s", ...(st?{s:st}:{}) } : { v:Number(v), t:"n", z:pctFmt, ...(st?{s:st}:{}) };
|
else if ([7,10,13].includes(c)) ws[ref] = (v===null) ? { v:"-", t:"s", ...(st?{s:st}:{}) } : { v:Number(v), t:"n", z:pctFmt, ...(st?{s:st}:{}) };
|
||||||
else if (c===4 && v instanceof Date) ws[ref] = { v, t:"d", z:dateFmt };
|
|
||||||
else ws[ref] = { v: v ?? "", t:"s" };
|
else ws[ref] = { v: v ?? "", t:"s" };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -560,7 +555,11 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
|
|||||||
return () => { activo = false; };
|
return () => { activo = false; };
|
||||||
}, [grupo, ano, mes, sectorista, agrupacion]);
|
}, [grupo, ano, mes, sectorista, agrupacion]);
|
||||||
|
|
||||||
const COLS = ["MATRÍCULA","ALUMNO","N° CUOTA","F. VENC.","CTA ANT.","COB ANT.","% ANT","CTA MES","COB MES","% MES","CTA TOT","COB TOT","% TOT","SALDO"];
|
const COLS = ["PROGRAMA","ALUMNO","N° CUOTA","F. VENC.","CTA ANT.","COB ANT.","% ANT","CTA MES","COB MES","% MES","CTA TOT","COB TOT","% TOT","SALDO"];
|
||||||
|
|
||||||
|
// Columnas redimensionables (igual que la tabla principal): PROGRAMA y ALUMNO más anchas
|
||||||
|
const anchosDet = COLS.map((h, i) => i === 0 ? 240 : (i === 1 ? 220 : (h.startsWith("%") ? 80 : 110)));
|
||||||
|
const cols = useColumnasAjustables(anchosDet);
|
||||||
|
|
||||||
// Resumen para las 3 tarjetas (siempre visibles) — suma del detalle de alumnos
|
// Resumen para las 3 tarjetas (siempre visibles) — suma del detalle de alumnos
|
||||||
const resumen = useMemo(() => {
|
const resumen = useMemo(() => {
|
||||||
@@ -574,6 +573,17 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
|
|||||||
return { cAnt,obAnt,cMes,obMes,cTot,obTot };
|
return { cAnt,obAnt,cMes,obMes,cTot,obTot };
|
||||||
}, [filas]);
|
}, [filas]);
|
||||||
|
|
||||||
|
// Conteo de alumnos: total, ya pagaron (saldo 0) y por pagar (saldo > 0)
|
||||||
|
const conteo = useMemo(() => {
|
||||||
|
let total=0, pagaron=0, deben=0;
|
||||||
|
filas.forEach((f) => {
|
||||||
|
if (String(f[0]).toUpperCase() === "TOTAL") return;
|
||||||
|
total++;
|
||||||
|
if (toMonto(f[13]) > 0.01) deben++; else pagaron++;
|
||||||
|
});
|
||||||
|
return { total, pagaron, deben };
|
||||||
|
}, [filas]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title={`📊 Estado de Cuenta — ${grupo}`} onClose={onClose} width={1500}>
|
<Modal title={`📊 Estado de Cuenta — ${grupo}`} onClose={onClose} width={1500}>
|
||||||
{loading ? <Loader text="Cargando estado de cuenta..." /> :
|
{loading ? <Loader text="Cargando estado de cuenta..." /> :
|
||||||
@@ -582,9 +592,28 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
|
|||||||
<div style={{marginBottom:16}}>
|
<div style={{marginBottom:16}}>
|
||||||
<TarjetasResumen r={resumen} />
|
<TarjetasResumen r={resumen} />
|
||||||
</div>
|
</div>
|
||||||
<div className="table-wrap">
|
{/* Etiquetas de conteo de alumnos */}
|
||||||
<table className="cob-table" style={{minWidth:1000}}>
|
<div style={{display:"flex",gap:10,marginBottom:14,flexWrap:"wrap"}}>
|
||||||
<thead><tr>{COLS.map((c)=><th key={c}>{c}</th>)}</tr></thead>
|
<span style={{display:"inline-flex",alignItems:"center",gap:6,padding:"6px 14px",
|
||||||
|
borderRadius:999,background:"#eff6ff",color:"#1e40af",fontSize:13,fontWeight:700,
|
||||||
|
border:"1px solid #bfdbfe"}}>
|
||||||
|
👥 Total: {conteo.total} alumno{conteo.total!==1?"s":""}
|
||||||
|
</span>
|
||||||
|
<span style={{display:"inline-flex",alignItems:"center",gap:6,padding:"6px 14px",
|
||||||
|
borderRadius:999,background:"#ecfdf5",color:"#047857",fontSize:13,fontWeight:700,
|
||||||
|
border:"1px solid #a7f3d0"}}>
|
||||||
|
✅ Pagaron: {conteo.pagaron}
|
||||||
|
</span>
|
||||||
|
<span style={{display:"inline-flex",alignItems:"center",gap:6,padding:"6px 14px",
|
||||||
|
borderRadius:999,background:"#fef2f2",color:"#b91c1c",fontSize:13,fontWeight:700,
|
||||||
|
border:"1px solid #fecaca"}}>
|
||||||
|
⏳ Por pagar: {conteo.deben}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="table-wrap" style={{overflowX:"auto"}}>
|
||||||
|
<table className="cob-table" {...cols.tableProps}>
|
||||||
|
<cols.ColGroup />
|
||||||
|
<thead><tr>{COLS.map((c,i)=><th key={c} style={{position:"relative"}}>{c}<cols.Resizer index={i} /></th>)}</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL").map((f, i) => {
|
{filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL").map((f, i) => {
|
||||||
const saldo = toMonto(f[13]);
|
const saldo = toMonto(f[13]);
|
||||||
@@ -593,10 +622,12 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
|
|||||||
const antVacio = !String(f[6]||"").trim() || ["-","—"].includes(String(f[6]).trim());
|
const antVacio = !String(f[6]||"").trim() || ["-","—"].includes(String(f[6]).trim());
|
||||||
const mesVacio = !String(f[9]||"").trim() || ["-","—"].includes(String(f[9]).trim());
|
const mesVacio = !String(f[9]||"").trim() || ["-","—"].includes(String(f[9]).trim());
|
||||||
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)
|
||||||
return (
|
return (
|
||||||
<tr key={i} className={deuda?"row-deuda":""}>
|
<tr key={i} className={deuda?"row-deuda":""}>
|
||||||
{f.map((v, j) => {
|
{f.slice(0, 14).map((v, j) => {
|
||||||
let val = v;
|
// Columna 0: mostrar PROGRAMA en vez de la matrícula
|
||||||
|
let val = (j===0) ? programa : v;
|
||||||
if ((j===4||j===5||j===6) && antVacio) val = "—";
|
if ((j===4||j===5||j===6) && antVacio) val = "—";
|
||||||
if ((j===7||j===8||j===9) && mesVacio) val = "—";
|
if ((j===7||j===8||j===9) && mesVacio) val = "—";
|
||||||
if ((j===10||j===11||j===12) && totVacio) val = "—";
|
if ((j===10||j===11||j===12) && totVacio) val = "—";
|
||||||
@@ -606,11 +637,15 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
|
|||||||
if ([4,5,6].includes(j)) cart = "cart-ant";
|
if ([4,5,6].includes(j)) cart = "cart-ant";
|
||||||
else if ([7,8,9].includes(j)) cart = "cart-mes";
|
else if ([7,8,9].includes(j)) cart = "cart-mes";
|
||||||
else if ([10,11,12].includes(j)) cart = "cart-tot";
|
else if ([10,11,12].includes(j)) cart = "cart-tot";
|
||||||
const clss = ((j===1?"col-name":"") + (cart?` ${cart}`:"")).trim();
|
const clss = (((j===0||j===1)?"col-name":"") + (cart?` ${cart}`:"")).trim();
|
||||||
|
const esTexto = (j===0||j===1);
|
||||||
return (
|
return (
|
||||||
<td key={j} className={clss}
|
<td key={j} className={clss}
|
||||||
style={{textAlign:j===1?"left":(isMonto?"right":"center"),
|
title={esTexto ? String(val) : undefined}
|
||||||
|
style={{textAlign:esTexto?"left":(isMonto?"right":"center"),
|
||||||
whiteSpace:"nowrap",
|
whiteSpace:"nowrap",
|
||||||
|
overflow:esTexto?"hidden":undefined,
|
||||||
|
textOverflow:esTexto?"ellipsis":undefined,
|
||||||
color:j===13&&deuda?"#dc2626":undefined,
|
color:j===13&&deuda?"#dc2626":undefined,
|
||||||
fontWeight:j===13?700:undefined}}>
|
fontWeight:j===13?700:undefined}}>
|
||||||
{isPct ? (val==="—"?<span style={{color:"#cbd5e1"}}>—</span>:<MiniBar valStr={val} />) : fmtMoneda(val)}
|
{isPct ? (val==="—"?<span style={{color:"#cbd5e1"}}>—</span>:<MiniBar valStr={val} />) : fmtMoneda(val)}
|
||||||
|
|||||||
Reference in New Issue
Block a user