Actualizar frontend/src/pages/Cobranza.jsx

This commit is contained in:
2026-09-29 13:01:27 -05:00
parent 9a8f698ab8
commit f7dcfdb6c2

View File

@@ -176,7 +176,8 @@ export default function Cobranza() {
const [agrupacion, setAgrupacion] = useState("SEDE"); const [agrupacion, setAgrupacion] = useState("SEDE");
const [sectoristasSel, setSectoristasSel] = useState([]); // multi; vacío = todos const [sectoristasSel, setSectoristasSel] = useState([]); // multi; vacío = todos
const [sede, setSede] = useState("TODOS"); const [sede, setSede] = useState("TODOS");
const [frec, setFrec] = useState("TODOS"); const [frecsSel, setFrecsSel] = useState([]); // multi; vacío = todas
const frecFiltro = frecsSel.length ? frecsSel.join(",") : "TODOS";
// Filtros extra para la vista "Por Sede" (usan JSON REPLACE_META). Multi; vacío = todos. // Filtros extra para la vista "Por Sede" (usan JSON REPLACE_META). Multi; vacío = todos.
const [sedesSel, setSedesSel] = useState([]); const [sedesSel, setSedesSel] = useState([]);
const [progsSel, setProgsSel] = useState([]); const [progsSel, setProgsSel] = useState([]);
@@ -210,10 +211,10 @@ export default function Cobranza() {
useEffect(() => { useEffect(() => {
let activo = true; let activo = true;
setVistaActual(`cobranza:${ano}|${mes}|${sectorista}|${agrupacion}|${sedeFiltro}|${progFiltro}|${vencHasta}`); setVistaActual(`cobranza:${ano}|${mes}|${sectorista}|${agrupacion}|${sedeFiltro}|${progFiltro}|${vencHasta}|${frecFiltro}`);
const cargar = () => { const cargar = () => {
setLoading(true); setError(null); setLoading(true); setError(null);
api.cobranza(ano, mes, sectorista, agrupacion, sedeFiltro, progFiltro, vencHasta) api.cobranza(ano, mes, sectorista, agrupacion, sedeFiltro, progFiltro, vencHasta, frecFiltro)
.then((res) => { .then((res) => {
if (!activo) return; if (!activo) return;
setFilas(res.filas || []); setFilas(res.filas || []);
@@ -225,7 +226,7 @@ export default function Cobranza() {
cargar(); cargar();
const off = onRefrescar(cargar); const off = onRefrescar(cargar);
return () => { activo = false; off(); }; return () => { activo = false; off(); };
}, [ano, mes, agrupacion, sectorista, sedeFiltro, progFiltro, vencHasta]); }, [ano, mes, agrupacion, sectorista, sedeFiltro, progFiltro, vencHasta, frecFiltro]);
// Cargar TODOS los alumnos una sola vez (para exportar y buscar) — 1 consulta cacheada // Cargar TODOS los alumnos una sola vez (para exportar y buscar) — 1 consulta cacheada
useEffect(() => { useEffect(() => {
@@ -251,7 +252,7 @@ export default function Cobranza() {
// Al cambiar filtros/periodo/agrupación, limpiar el cache de programas por asesor // Al cambiar filtros/periodo/agrupación, limpiar el cache de programas por asesor
useEffect(() => { setProgramasAsesor({}); setExpandidas(new Set()); }, useEffect(() => { setProgramasAsesor({}); setExpandidas(new Set()); },
[ano, mes, agrupacion, sectorista, sedeFiltro, progFiltro, vencHasta]); [ano, mes, agrupacion, sectorista, sedeFiltro, progFiltro, vencHasta, frecFiltro]);
// 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());
@@ -266,7 +267,7 @@ export default function Cobranza() {
useEffect(() => { useEffect(() => {
if (agrupacion !== "SEDE") { setFilasPrograma([]); setExpandidas(new Set()); return; } if (agrupacion !== "SEDE") { setFilasPrograma([]); setExpandidas(new Set()); return; }
let activo = true; let activo = true;
api.cobranza(ano, mes, sectorista, "PROGRAMA", sedeFiltro, progFiltro, vencHasta) api.cobranza(ano, mes, sectorista, "PROGRAMA", sedeFiltro, progFiltro, vencHasta, frecFiltro)
.then((res) => { .then((res) => {
if (!activo) return; if (!activo) return;
const fp = (res.filas || []).filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL"); const fp = (res.filas || []).filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL");
@@ -281,7 +282,7 @@ export default function Cobranza() {
}) })
.catch(() => { if (activo) setFilasPrograma([]); }); .catch(() => { if (activo) setFilasPrograma([]); });
return () => { activo = false; }; return () => { activo = false; };
}, [agrupacion, ano, mes, sectorista, sedeFiltro, progFiltro, vencHasta]); }, [agrupacion, ano, mes, sectorista, sedeFiltro, progFiltro, vencHasta, frecFiltro]);
// Cargar clasificación de sede (sede.json) para los programas visibles // Cargar clasificación de sede (sede.json) para los programas visibles
useEffect(() => { useEffect(() => {
@@ -297,7 +298,8 @@ export default function Cobranza() {
return () => { activo = false; }; return () => { activo = false; };
}, [filas, esPrograma]); }, [filas, esPrograma]);
// Filtro sede/frecuencia (solo PROGRAMA) en cliente — usa sede.json // Filtro sede (solo PROGRAMA) en cliente — usa sede.json.
// La frecuencia ya la filtra el backend (por matrícula) en todas las agrupaciones.
const datosCalc = useMemo(() => { const datosCalc = useMemo(() => {
let arr = filas.filter((f) => String(f[0]).toUpperCase() !== "TOTAL GENERAL"); let arr = filas.filter((f) => String(f[0]).toUpperCase() !== "TOTAL GENERAL");
if (esPrograma) { if (esPrograma) {
@@ -307,14 +309,13 @@ export default function Cobranza() {
return s === sede.toUpperCase(); return s === sede.toUpperCase();
}); });
} }
if (frec !== "TODOS") arr = arr.filter((f) => String(f[1]).toUpperCase() === frec);
return arr; return arr;
} }
// Vista POR SEDE / POR ASESOR: el backend ya devuelve las filas filtradas por // Vista POR SEDE / POR ASESOR: el backend ya devuelve las filas filtradas por
// Sede y Programa (JSON REPLACE_META), así que se muestran tal cual. // Sede y Programa (JSON REPLACE_META), así que se muestran tal cual.
return arr; return arr;
}, [filas, esPrograma, sede, frec, sedeMap]); }, [filas, esPrograma, sede, sedeMap]);
// Mientras carga, mostrar la ÚLTIMA vista renderizada (congelada) hasta que llegue // Mientras carga, mostrar la ÚLTIMA vista renderizada (congelada) hasta que llegue
// la respuesta nueva. Así al filtrar solo se opaca lo anterior, sin recalcular a 0. // la respuesta nueva. Así al filtrar solo se opaca lo anterior, sin recalcular a 0.
@@ -394,7 +395,7 @@ export default function Cobranza() {
}); });
// En ASESOR: al abrir, cargar los programas de ese asesor (vista PROGRAMA filtrada por él) // En ASESOR: al abrir, cargar los programas de ese asesor (vista PROGRAMA filtrada por él)
if (esAsesor && abriendo && !programasAsesor[key]) { if (esAsesor && abriendo && !programasAsesor[key]) {
api.cobranza(ano, mes, s, "PROGRAMA", sedeFiltro, progFiltro, vencHasta) api.cobranza(ano, mes, s, "PROGRAMA", sedeFiltro, progFiltro, vencHasta, frecFiltro)
.then((res) => { .then((res) => {
const fp = (res.filas || []).filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL"); const fp = (res.filas || []).filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL");
setProgramasAsesor((prev) => ({ ...prev, [key]: fp })); setProgramasAsesor((prev) => ({ ...prev, [key]: fp }));
@@ -423,8 +424,9 @@ export default function Cobranza() {
let arr = alumnos; let arr = alumnos;
if (esPrograma) { if (esPrograma) {
if (sede !== "TODOS") arr = arr.filter((a) => String(a.sede).toUpperCase() === sede.toUpperCase()); if (sede !== "TODOS") arr = arr.filter((a) => String(a.sede).toUpperCase() === sede.toUpperCase());
if (frec !== "TODOS") arr = arr.filter((a) => String(a.frecuencia).toUpperCase() === frec);
} }
// Frecuencia multi (aplica en todas las agrupaciones, igual que el backend)
if (frecsSel.length) arr = arr.filter((a) => frecsSel.includes(String(a.frecuencia).toUpperCase()));
// Filtros Sede / Programa (REPLACE_META) — multi; aplican en Por Sede y Por Asesor // Filtros Sede / Programa (REPLACE_META) — multi; aplican en Por Sede y Por Asesor
if (sedesSel.length) { if (sedesSel.length) {
arr = arr.filter((a) => { arr = arr.filter((a) => {
@@ -448,7 +450,7 @@ export default function Cobranza() {
}); });
} }
return arr; return arr;
}, [alumnos, esPrograma, sede, frec, sedeFiltro, progFiltro, progClasif, vencHasta]); }, [alumnos, esPrograma, sede, frecsSel, sedeFiltro, progFiltro, progClasif, vencHasta]);
function grupoDe(a) { function grupoDe(a) {
// El programa detallado del alumno, en TODOS los filtros (Sede, Asesor, Programa). // El programa detallado del alumno, en TODOS los filtros (Sede, Asesor, Programa).
@@ -550,10 +552,10 @@ export default function Cobranza() {
<Filters> <Filters>
<SelectBonito label="Año" value={ano} options={ANOS} onChange={(v)=>setAno(+v)} minWidth={110} /> <SelectBonito label="Año" value={ano} options={ANOS} onChange={(v)=>setAno(+v)} minWidth={110} />
<SelectBonito label="Mes" value={mes} options={MESES.map((m,i)=>({value:i+1,label:m}))} onChange={(v)=>setMes(+v)} minWidth={150} /> <SelectBonito label="Mes" value={mes} options={MESES.map((m,i)=>({value:i+1,label:m}))} onChange={(v)=>setMes(+v)} minWidth={150} />
<SelectBonito label="Agrupación" value={agrupacion} options={AGRUPACIONES} onChange={(v)=>{setAgrupacion(v);setSede("TODOS");setFrec("TODOS");setSedesSel([]);setProgsSel([]);}} minWidth={150} /> <SelectBonito label="Agrupación" value={agrupacion} options={AGRUPACIONES} onChange={(v)=>{setAgrupacion(v);setSede("TODOS");setSedesSel([]);setProgsSel([]);}} minWidth={150} />
<ListaMulti label="Sectorista" opciones={(sectoristas||[]).filter((s)=>s!=="TODOS")} seleccion={sectoristasSel} onChange={setSectoristasSel} plural="sectoristas" minWidth={250} /> <ListaMulti label="Sectorista" opciones={(sectoristas||[]).filter((s)=>s!=="TODOS")} seleccion={sectoristasSel} onChange={setSectoristasSel} plural="sectoristas" minWidth={250} />
{esPrograma && <SelectBonito label="Sede" value={sede} options={SEDES} onChange={setSede} minWidth={140} />} {esPrograma && <SelectBonito label="Sede" value={sede} options={SEDES} onChange={setSede} minWidth={140} />}
{esPrograma && <SelectBonito label="Frecuencia" value={frec} options={FRECS} onChange={setFrec} minWidth={140} />} <ListaMulti label="Frecuencia" opciones={FRECS.filter((f)=>f!=="TODOS")} seleccion={frecsSel} onChange={(v)=>{setFrecsSel(v);setExpandidas(new Set());}} plural="frecuencias" minWidth={140} />
{(agrupacion==="SEDE"||agrupacion==="ASESOR") && <ListaMulti label="Sede" opciones={SEDES_MULTI} seleccion={sedesSel} onChange={(v)=>{setSedesSel(v);setExpandidas(new Set());}} plural="sedes" minWidth={140} />} {(agrupacion==="SEDE"||agrupacion==="ASESOR") && <ListaMulti label="Sede" opciones={SEDES_MULTI} seleccion={sedesSel} onChange={(v)=>{setSedesSel(v);setExpandidas(new Set());}} plural="sedes" minWidth={140} />}
{(agrupacion==="SEDE"||agrupacion==="ASESOR") && <ListaMulti label="Programa" opciones={PROGRAMAS_MULTI} seleccion={progsSel} onChange={(v)=>{setProgsSel(v);setExpandidas(new Set());}} plural="programas" minWidth={150} />} {(agrupacion==="SEDE"||agrupacion==="ASESOR") && <ListaMulti label="Programa" opciones={PROGRAMAS_MULTI} seleccion={progsSel} onChange={(v)=>{setProgsSel(v);setExpandidas(new Set());}} plural="programas" minWidth={150} />}
<div style={{display:"flex",gap:8,alignItems:"flex-end",marginLeft:"auto"}}> <div style={{display:"flex",gap:8,alignItems:"flex-end",marginLeft:"auto"}}>
@@ -721,7 +723,7 @@ export default function Cobranza() {
{modal && ( {modal && (
<ModalEstadoCuenta grupo={modal.grupo} ano={ano} mes={mes} <ModalEstadoCuenta grupo={modal.grupo} ano={ano} mes={mes}
sectorista={modal.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} frecFiltro={frecFiltro}
onClose={() => setModal(null)} /> onClose={() => setModal(null)} />
)} )}
@@ -840,6 +842,7 @@ function Tarjetas({ kpis, retiro, loading }) {
<Row k="Cobrado" v={`S/ ${cob.toLocaleString("es-PE",{maximumFractionDigits:0})}`} /> <Row k="Cobrado" v={`S/ ${cob.toLocaleString("es-PE",{maximumFractionDigits:0})}`} />
<Row k="Saldo" v={`S/ ${saldo.toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color={saldo>0?"#dc2626":"#10b981"} /> <Row k="Saldo" v={`S/ ${saldo.toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color={saldo>0?"#dc2626":"#10b981"} />
<Row k="Saldo por Retirar" v={`S/ ${(saldoRetiro||0).toLocaleString("es-PE",{maximumFractionDigits:0})}`} color="#dc2626" /> <Row k="Saldo por Retirar" v={`S/ ${(saldoRetiro||0).toLocaleString("es-PE",{maximumFractionDigits:0})}`} color="#dc2626" />
<Row k="Retiros" v={`S/ ${(saldo-(saldoRetiro||0)).toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color="#7c3aed" />
{cta>0 && ( {cta>0 && (
<div style={{marginTop:10}}> <div style={{marginTop:10}}>
<div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}> <div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}>
@@ -890,6 +893,7 @@ function TarjetasResumen({ r, retiro }) {
<Row k="Cobrado" v={`S/ ${cob.toLocaleString("es-PE",{maximumFractionDigits:0})}`} /> <Row k="Cobrado" v={`S/ ${cob.toLocaleString("es-PE",{maximumFractionDigits:0})}`} />
<Row k="Saldo" v={`S/ ${saldo.toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color={saldo>0?"#dc2626":"#10b981"} /> <Row k="Saldo" v={`S/ ${saldo.toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color={saldo>0?"#dc2626":"#10b981"} />
<Row k="Saldo por Retirar" v={`S/ ${(saldoRetiro||0).toLocaleString("es-PE",{maximumFractionDigits:0})}`} color="#dc2626" /> <Row k="Saldo por Retirar" v={`S/ ${(saldoRetiro||0).toLocaleString("es-PE",{maximumFractionDigits:0})}`} color="#dc2626" />
<Row k="Retiros" v={`S/ ${(saldo-(saldoRetiro||0)).toLocaleString("es-PE",{maximumFractionDigits:0})}`} bold color="#7c3aed" />
{cta>0 && ( {cta>0 && (
<div style={{marginTop:8}}> <div style={{marginTop:8}}>
<div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}> <div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}>
@@ -1093,7 +1097,7 @@ function ModalBuscador({ alumnos, agrupacion, sectorista, ano, mes, onClose }) {
); );
} }
function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, progFiltro = "TODOS", sedeFiltro = "TODOS", vencHasta = "TODOS", onClose }) { function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, progFiltro = "TODOS", sedeFiltro = "TODOS", vencHasta = "TODOS", frecFiltro = "TODOS", onClose }) {
const [filas, setFilas] = useState([]); const [filas, setFilas] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [porRetirar, setPorRetirar] = useState(new Set()); // matrículas POR RETIRAR const [porRetirar, setPorRetirar] = useState(new Set()); // matrículas POR RETIRAR
@@ -1118,11 +1122,11 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, progFiltro
useEffect(() => { useEffect(() => {
let activo = true; let activo = true;
setLoading(true); setLoading(true);
api.cobranzaDetalle(grupo, ano, mes, sectorista, agrupacion, progFiltro, sedeFiltro, vencHasta) api.cobranzaDetalle(grupo, ano, mes, sectorista, agrupacion, progFiltro, sedeFiltro, vencHasta, frecFiltro)
.then((res) => { if (activo) { setFilas(res.filas || []); setLoading(false); } }) .then((res) => { if (activo) { setFilas(res.filas || []); setLoading(false); } })
.catch(() => { if (activo) { setFilas([]); setLoading(false); } }); .catch(() => { if (activo) { setFilas([]); setLoading(false); } });
return () => { activo = false; }; return () => { activo = false; };
}, [grupo, ano, mes, sectorista, agrupacion, progFiltro, sedeFiltro, vencHasta]); }, [grupo, ano, mes, sectorista, agrupacion, progFiltro, sedeFiltro, vencHasta, frecFiltro]);
// Cargar (y recargar) las matrículas marcadas POR RETIRAR (de este mes) para tachar sus filas // Cargar (y recargar) las matrículas marcadas POR RETIRAR (de este mes) para tachar sus filas
const cargarPorRetirar = () => { const cargarPorRetirar = () => {