diff --git a/frontend/src/pages/Cobranza.jsx b/frontend/src/pages/Cobranza.jsx index b6c4925..0301f6a 100644 --- a/frontend/src/pages/Cobranza.jsx +++ b/frontend/src/pages/Cobranza.jsx @@ -176,7 +176,8 @@ export default function Cobranza() { const [agrupacion, setAgrupacion] = useState("SEDE"); const [sectoristasSel, setSectoristasSel] = useState([]); // multi; vacío = 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. const [sedesSel, setSedesSel] = useState([]); const [progsSel, setProgsSel] = useState([]); @@ -210,10 +211,10 @@ export default function Cobranza() { useEffect(() => { let activo = true; - setVistaActual(`cobranza:${ano}|${mes}|${sectorista}|${agrupacion}|${sedeFiltro}|${progFiltro}|${vencHasta}`); + setVistaActual(`cobranza:${ano}|${mes}|${sectorista}|${agrupacion}|${sedeFiltro}|${progFiltro}|${vencHasta}|${frecFiltro}`); const cargar = () => { 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) => { if (!activo) return; setFilas(res.filas || []); @@ -225,7 +226,7 @@ export default function Cobranza() { cargar(); const off = onRefrescar(cargar); 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 useEffect(() => { @@ -251,7 +252,7 @@ export default function Cobranza() { // 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]); + [ano, mes, agrupacion, sectorista, sedeFiltro, progFiltro, vencHasta, frecFiltro]); // Cargar matrículas POR RETIRAR (para el "Saldo por Retirar" de las tarjetas) const [porRetirar, setPorRetirar] = useState(new Set()); @@ -266,7 +267,7 @@ export default function Cobranza() { useEffect(() => { if (agrupacion !== "SEDE") { setFilasPrograma([]); setExpandidas(new Set()); return; } let activo = true; - api.cobranza(ano, mes, sectorista, "PROGRAMA", sedeFiltro, progFiltro, vencHasta) + api.cobranza(ano, mes, sectorista, "PROGRAMA", sedeFiltro, progFiltro, vencHasta, frecFiltro) .then((res) => { if (!activo) return; const fp = (res.filas || []).filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL"); @@ -281,7 +282,7 @@ export default function Cobranza() { }) .catch(() => { if (activo) setFilasPrograma([]); }); 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 useEffect(() => { @@ -297,7 +298,8 @@ export default function Cobranza() { return () => { activo = false; }; }, [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(() => { let arr = filas.filter((f) => String(f[0]).toUpperCase() !== "TOTAL GENERAL"); if (esPrograma) { @@ -307,14 +309,13 @@ export default function Cobranza() { return s === sede.toUpperCase(); }); } - if (frec !== "TODOS") arr = arr.filter((f) => String(f[1]).toUpperCase() === frec); return arr; } // 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. return arr; - }, [filas, esPrograma, sede, frec, sedeMap]); + }, [filas, esPrograma, sede, sedeMap]); // 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. @@ -394,7 +395,7 @@ export default function Cobranza() { }); // 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) + api.cobranza(ano, mes, s, "PROGRAMA", sedeFiltro, progFiltro, vencHasta, frecFiltro) .then((res) => { const fp = (res.filas || []).filter((f)=>String(f[0]).toUpperCase()!=="TOTAL GENERAL"); setProgramasAsesor((prev) => ({ ...prev, [key]: fp })); @@ -423,8 +424,9 @@ export default function Cobranza() { let arr = alumnos; if (esPrograma) { 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 if (sedesSel.length) { arr = arr.filter((a) => { @@ -448,7 +450,7 @@ export default function Cobranza() { }); } return arr; - }, [alumnos, esPrograma, sede, frec, sedeFiltro, progFiltro, progClasif, vencHasta]); + }, [alumnos, esPrograma, sede, frecsSel, sedeFiltro, progFiltro, progClasif, vencHasta]); function grupoDe(a) { // El programa detallado del alumno, en TODOS los filtros (Sede, Asesor, Programa). @@ -550,10 +552,10 @@ export default function Cobranza() { setAno(+v)} minWidth={110} /> ({value:i+1,label:m}))} onChange={(v)=>setMes(+v)} minWidth={150} /> - {setAgrupacion(v);setSede("TODOS");setFrec("TODOS");setSedesSel([]);setProgsSel([]);}} minWidth={150} /> + {setAgrupacion(v);setSede("TODOS");setSedesSel([]);setProgsSel([]);}} minWidth={150} /> s!=="TODOS")} seleccion={sectoristasSel} onChange={setSectoristasSel} plural="sectoristas" minWidth={250} /> {esPrograma && } - {esPrograma && } + f!=="TODOS")} seleccion={frecsSel} onChange={(v)=>{setFrecsSel(v);setExpandidas(new Set());}} plural="frecuencias" minWidth={140} /> {(agrupacion==="SEDE"||agrupacion==="ASESOR") && {setSedesSel(v);setExpandidas(new Set());}} plural="sedes" minWidth={140} />} {(agrupacion==="SEDE"||agrupacion==="ASESOR") && {setProgsSel(v);setExpandidas(new Set());}} plural="programas" minWidth={150} />}
@@ -721,7 +723,7 @@ export default function Cobranza() { {modal && ( setModal(null)} /> )} @@ -840,6 +842,7 @@ function Tarjetas({ kpis, retiro, loading }) { 0?"#dc2626":"#10b981"} /> + {cta>0 && (
@@ -890,6 +893,7 @@ function TarjetasResumen({ r, retiro }) { 0?"#dc2626":"#10b981"} /> + {cta>0 && (
@@ -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 [loading, setLoading] = useState(true); const [porRetirar, setPorRetirar] = useState(new Set()); // matrículas POR RETIRAR @@ -1118,11 +1122,11 @@ function ModalEstadoCuenta({ grupo, ano, mes, sectorista, agrupacion, progFiltro useEffect(() => { let activo = 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); } }) .catch(() => { if (activo) { setFilas([]); setLoading(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 const cargarPorRetirar = () => {