Actualizar frontend/src/pages/Cobranza.jsx
This commit is contained in:
@@ -459,12 +459,14 @@ function ModalBuscador({ alumnos, agrupacion, sectorista, onClose }) {
|
||||
const [texto, setTexto] = useState("");
|
||||
const [query, setQuery] = useState("");
|
||||
const [porRetirar, setPorRetirar] = useState(new Set());
|
||||
const [infoAlumno, setInfoAlumno] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const cargarPorRetirar = () => {
|
||||
api.alumnosPorRetirar()
|
||||
.then((r)=>setPorRetirar(new Set((r.matriculas||[]).map(String))))
|
||||
.catch(()=>{});
|
||||
}, []);
|
||||
};
|
||||
useEffect(() => { cargarPorRetirar(); }, []);
|
||||
|
||||
const resultados = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
@@ -476,13 +478,37 @@ function ModalBuscador({ alumnos, agrupacion, sectorista, onClose }) {
|
||||
);
|
||||
}, [alumnos, query]);
|
||||
|
||||
const COLS = ["GRUPO","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","INFO"];
|
||||
|
||||
const anchosBusc = COLS.map((h,i) => {
|
||||
if (i===0) return 250;
|
||||
if (i===1) return 250;
|
||||
if (h==="N° CUOTA" || h==="F. VENC.") return 100;
|
||||
if (h==="INFO") return 70;
|
||||
if (h.startsWith("%")) return 80;
|
||||
return 110;
|
||||
});
|
||||
const cols = useColumnasAjustables(anchosBusc);
|
||||
|
||||
const FIJAS = 4;
|
||||
const stickyCol = (j, { esHeader = false, bg = "#fff" } = {}) => {
|
||||
if (j >= FIJAS) return {};
|
||||
let left = 0;
|
||||
for (let k = 0; k < j; k++) left += (cols.anchos[k] || 0);
|
||||
const ultima = j === FIJAS - 1;
|
||||
const lineaColor = esHeader ? "#475569" : "#e2e8f0";
|
||||
return {
|
||||
position: "sticky", left, zIndex: esHeader ? 20 : 10, backgroundColor: bg,
|
||||
borderRight: "none",
|
||||
boxShadow: `inset -1px 0 0 0 ${lineaColor}` + (ultima ? ", 3px 0 5px -2px rgba(0,0,0,0.18)" : ""),
|
||||
};
|
||||
};
|
||||
|
||||
const pctStr = (cob, cta) => cta > 0 ? `${Math.round(cob/cta*100)}%` : "";
|
||||
|
||||
return (
|
||||
<Modal title="🔍 Buscar Alumno" onClose={onClose} width={1200}>
|
||||
<Modal title="🔍 Buscar Alumno" onClose={onClose} width={1360}>
|
||||
<div style={{display:"flex",gap:8,marginBottom:16}}>
|
||||
<input
|
||||
autoFocus
|
||||
@@ -509,30 +535,35 @@ function ModalBuscador({ alumnos, agrupacion, sectorista, onClose }) {
|
||||
<div style={{fontSize:13,color:"#1e40af",fontWeight:600,marginBottom:8}}>
|
||||
{resultados.length} alumno(s) encontrado(s)
|
||||
</div>
|
||||
<div className="table-wrap">
|
||||
<table className="cob-table" style={{minWidth:1100}}>
|
||||
<thead><tr>{COLS.map((c)=><th key={c}>{c}</th>)}</tr></thead>
|
||||
<div className="table-wrap" style={{overflow:"auto", maxHeight:"60vh"}}>
|
||||
<table className="cob-table" {...cols.tableProps}>
|
||||
<cols.ColGroup />
|
||||
<thead><tr>{COLS.map((c,i)=>{
|
||||
const fija = i < FIJAS;
|
||||
const stH = fija ? stickyCol(i,{esHeader:true,bg:"#334155"})
|
||||
: { position:"sticky", top:0, zIndex:12, backgroundColor:"#334155" };
|
||||
return <th key={c} style={{...stH, position:stH.position||"relative"}}>{c}<cols.Resizer index={i} /></th>;
|
||||
})}</tr></thead>
|
||||
<tbody>
|
||||
{resultados.map((a,i)=>{
|
||||
const deuda = a.saldo > 0.01;
|
||||
const retira = porRetirar.has(String(a.matricula));
|
||||
const grp = agrupacion==="SEDE" ? a.sede : (agrupacion==="ASESOR" ? sectorista : a.programa);
|
||||
const bgFila = (i % 2 === 1) ? "#f8fafc" : "#fff";
|
||||
const celdas = [
|
||||
{v:grp, cls:"col-name", al:"left"},
|
||||
{v:a.matricula},
|
||||
{v:a.alumno, cls:"col-name", al:"left"},
|
||||
{v:a.programa, cls:"col-name", al:"left", texto:true},
|
||||
{v:a.alumno, cls:"col-name", al:"left", texto:true},
|
||||
{v:a.num_cuota},
|
||||
{v:a.fch_venc},
|
||||
{v:fmtMoneda("S/ "+a.cta_ant), al:"right", cart:"cart-ant"},
|
||||
{v:fmtMoneda("S/ "+a.cob_ant), al:"right", cart:"cart-ant"},
|
||||
{v:fmtMoneda("S/ "+a.cta_ant), cart:"cart-ant"},
|
||||
{v:fmtMoneda("S/ "+a.cob_ant), cart:"cart-ant"},
|
||||
{pct:pctStr(a.cob_ant,a.cta_ant), cart:"cart-ant"},
|
||||
{v:fmtMoneda("S/ "+a.cta_cur), al:"right", cart:"cart-mes"},
|
||||
{v:fmtMoneda("S/ "+a.cob_cur), al:"right", cart:"cart-mes"},
|
||||
{v:fmtMoneda("S/ "+a.cta_cur), cart:"cart-mes"},
|
||||
{v:fmtMoneda("S/ "+a.cob_cur), cart:"cart-mes"},
|
||||
{pct:pctStr(a.cob_cur,a.cta_cur), cart:"cart-mes"},
|
||||
{v:fmtMoneda("S/ "+a.cta_tot), al:"right", cart:"cart-tot"},
|
||||
{v:fmtMoneda("S/ "+a.cob_tot), al:"right", cart:"cart-tot"},
|
||||
{v:fmtMoneda("S/ "+a.cta_tot), cart:"cart-tot"},
|
||||
{v:fmtMoneda("S/ "+a.cob_tot), cart:"cart-tot"},
|
||||
{pct:pctStr(a.cob_tot,a.cta_tot), cart:"cart-tot"},
|
||||
{v:fmtMoneda("S/ "+a.saldo), al:"right", saldo:true},
|
||||
{v:fmtMoneda("S/ "+a.saldo), saldo:true},
|
||||
];
|
||||
return (
|
||||
<tr key={i} className={deuda?"row-deuda":""}
|
||||
@@ -540,12 +571,26 @@ function ModalBuscador({ alumnos, agrupacion, sectorista, onClose }) {
|
||||
textDecorationThickness:"2px" } : undefined}>
|
||||
{celdas.map((c,j)=>(
|
||||
<td key={j} className={((c.cls||"")+" "+(c.cart||"")).trim()}
|
||||
title={c.texto ? String(c.v) : undefined}
|
||||
style={{textAlign:c.al||"center",
|
||||
fontWeight:c.saldo?700:undefined,
|
||||
color:c.saldo&&deuda?"#dc2626":undefined}}>
|
||||
color:c.saldo&&deuda?"#dc2626":undefined,
|
||||
whiteSpace:"nowrap",
|
||||
overflow:c.texto?"hidden":undefined,
|
||||
textOverflow:c.texto?"ellipsis":undefined,
|
||||
...stickyCol(j,{bg:bgFila})}}>
|
||||
{c.pct!==undefined ? (c.pct ? <MiniBar valStr={c.pct} /> : "—") : c.v}
|
||||
</td>
|
||||
))}
|
||||
<td style={{textAlign:"center"}}>
|
||||
<button
|
||||
onClick={()=>setInfoAlumno({ matricula:a.matricula, alumno:a.alumno, programa:a.programa })}
|
||||
style={{padding:"3px 10px",fontSize:11,fontWeight:700,cursor:"pointer",
|
||||
border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#334155"}}
|
||||
title="Ver/editar estado y observaciones">
|
||||
INFO
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -554,6 +599,10 @@ function ModalBuscador({ alumnos, agrupacion, sectorista, onClose }) {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{infoAlumno && (
|
||||
<ModalInfoAlumno alumno={infoAlumno} onClose={()=>setInfoAlumno(null)}
|
||||
onGuardado={cargarPorRetirar} />
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -588,6 +637,21 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
|
||||
const anchosDet = COLS.map((h, i) => i === 0 ? 240 : (i === 1 ? 220 : (h==="INFO" ? 70 : h.startsWith("%") ? 80 : 110)));
|
||||
const cols = useColumnasAjustables(anchosDet);
|
||||
|
||||
// Congelar las 4 primeras columnas (PROGRAMA, ALUMNO, N° CUOTA, F. VENC.).
|
||||
const FIJAS = 4;
|
||||
const stickyCol = (j, { esHeader = false, bg = "#fff" } = {}) => {
|
||||
if (j >= FIJAS) return {};
|
||||
let left = 0;
|
||||
for (let k = 0; k < j; k++) left += (cols.anchos[k] || 0);
|
||||
const ultima = j === FIJAS - 1;
|
||||
const lineaColor = esHeader ? "#475569" : "#e2e8f0";
|
||||
return {
|
||||
position: "sticky", left, zIndex: esHeader ? 20 : 10, backgroundColor: bg,
|
||||
borderRight: "none",
|
||||
boxShadow: `inset -1px 0 0 0 ${lineaColor}` + (ultima ? ", 3px 0 5px -2px rgba(0,0,0,0.18)" : ""),
|
||||
};
|
||||
};
|
||||
|
||||
// Resumen para las 3 tarjetas (siempre visibles) — suma del detalle de alumnos
|
||||
const resumen = useMemo(() => {
|
||||
let cAnt=0,obAnt=0,cMes=0,obMes=0,cTot=0,obTot=0;
|
||||
@@ -666,28 +730,32 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
|
||||
Ver solo por retirar
|
||||
</label>
|
||||
</div>
|
||||
<div className="table-wrap" style={{overflowX:"auto"}}>
|
||||
<div className="table-wrap" style={{overflow:"auto", maxHeight:"60vh"}}>
|
||||
<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>
|
||||
<thead><tr>{COLS.map((c,i)=>{
|
||||
const fija = i < FIJAS;
|
||||
const stH = fija
|
||||
? stickyCol(i,{esHeader:true,bg:"#334155"})
|
||||
: { position:"sticky", top:0, zIndex:12, backgroundColor:"#334155" };
|
||||
return <th key={c} style={{...stH, position:stH.position||"relative"}}>{c}<cols.Resizer index={i} /></th>;
|
||||
})}</tr></thead>
|
||||
<tbody>
|
||||
{filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL")
|
||||
.filter((f)=> !soloRetirar || porRetirar.has(String(f[0])))
|
||||
.map((f, i) => {
|
||||
const saldo = toMonto(f[13]);
|
||||
const deuda = saldo > 0.01;
|
||||
// Regla "-": si %ANT vacío → CTA/COB ANT a "—" (igual MES y TOT)
|
||||
const antVacio = !String(f[6]||"").trim() || ["-","—"].includes(String(f[6]).trim());
|
||||
const mesVacio = !String(f[9]||"").trim() || ["-","—"].includes(String(f[9]).trim());
|
||||
const totVacio = !String(f[12]||"").trim() || ["-","—"].includes(String(f[12]).trim());
|
||||
const programa = f[14] || "-"; // programa del alumno (extra del backend)
|
||||
const retira = porRetirar.has(String(f[0])); // alumno POR RETIRAR
|
||||
const programa = f[14] || "-";
|
||||
const retira = porRetirar.has(String(f[0]));
|
||||
return (
|
||||
<tr key={i} className={deuda?"row-deuda":""}
|
||||
style={retira ? { textDecoration:"line-through", textDecorationColor:"#dc2626",
|
||||
textDecorationThickness:"2px" } : undefined}>
|
||||
{f.slice(0, 14).map((v, j) => {
|
||||
// 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===7||j===8||j===9) && mesVacio) val = "—";
|
||||
@@ -699,6 +767,7 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
|
||||
else if ([10,11,12].includes(j)) cart = "cart-tot";
|
||||
const clss = (((j===0||j===1)?"col-name":"") + (cart?` ${cart}`:"")).trim();
|
||||
const esTexto = (j===0||j===1);
|
||||
const bgFila = (i % 2 === 1) ? "#f8fafc" : "#fff";
|
||||
return (
|
||||
<td key={j} className={clss}
|
||||
title={esTexto ? String(val) : undefined}
|
||||
@@ -707,7 +776,8 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
|
||||
overflow:esTexto?"hidden":undefined,
|
||||
textOverflow:esTexto?"ellipsis":undefined,
|
||||
color:j===13&&deuda?"#dc2626":undefined,
|
||||
fontWeight:j===13?700:undefined}}>
|
||||
fontWeight:j===13?700:undefined,
|
||||
...stickyCol(j,{bg:bgFila})}}>
|
||||
{isPct ? (val==="—"?<span style={{color:"#cbd5e1"}}>—</span>:<MiniBar valStr={val} />) : fmtMoneda(val)}
|
||||
</td>
|
||||
);
|
||||
@@ -724,8 +794,9 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
{(() => {
|
||||
// TOTAL GENERAL del modal — solo alumnos visibles (respeta sede y filtro POR RETIRAR)
|
||||
const alumnos = filas.filter((f)=>String(f[0]).toUpperCase()!=="TOTAL")
|
||||
.filter((f)=> !soloRetirar || porRetirar.has(String(f[0])));
|
||||
if (alumnos.length===0) return null;
|
||||
@@ -746,18 +817,27 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, onClose })
|
||||
{Array.from({length:14}).map((_,j)=>{
|
||||
const isPct=[6,9,12].includes(j);
|
||||
const val=celda(j);
|
||||
const fija = j < FIJAS;
|
||||
let left = 0; for (let k=0;k<j;k++) left += (cols.anchos[k]||0);
|
||||
const st = {
|
||||
position:"sticky", bottom:0,
|
||||
zIndex: fija ? 15 : 8,
|
||||
backgroundColor:"#eff6ff",
|
||||
padding:"11px 8px",
|
||||
...(fija ? { left, boxShadow:`inset -1px 0 0 0 #cbd5e1${j===FIJAS-1?", 3px 0 5px -2px rgba(0,0,0,0.18)":""}, inset 0 1px 0 0 #cbd5e1` } : { boxShadow:"inset 0 1px 0 0 #cbd5e1" }),
|
||||
};
|
||||
return (
|
||||
<td key={j} className={j===0?"col-name":""}
|
||||
style={{textAlign:j===0?"left":"center",whiteSpace:"nowrap"}}>
|
||||
style={{textAlign:j===0?"left":"center",whiteSpace:"nowrap",...st}}>
|
||||
{isPct?<MiniBar valStr={val} />:val}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td></td>{/* columna INFO en la fila TOTAL */}
|
||||
<td style={{position:"sticky",bottom:0,zIndex:8,backgroundColor:"#eff6ff",boxShadow:"inset 0 1px 0 0 #cbd5e1"}}></td>
|
||||
</tr>
|
||||
);
|
||||
})()}
|
||||
</tbody>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</>}
|
||||
|
||||
Reference in New Issue
Block a user