diff --git a/frontend/src/pages/Cobranza.jsx b/frontend/src/pages/Cobranza.jsx
index 1947c7c..7adfcd0 100644
--- a/frontend/src/pages/Cobranza.jsx
+++ b/frontend/src/pages/Cobranza.jsx
@@ -10,6 +10,8 @@ const MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO"
const ANOS = [2024, 2025, 2026];
const AGRUPACIONES = [{value:"SEDE",label:"Por Sede"},{value:"ASESOR",label:"Por Asesor"}];
const SEDES = ["TODOS","LIMA","AREQUIPA","PIURA","TRUJILLO"];
+const SEDES_MULTI = ["LIMA","AREQUIPA","PIURA","TRUJILLO"];
+const PROGRAMAS_MULTI = ["TEAC","TERC","SEMINARIOS","OTROS"];
const FRECS = ["TODOS","DOM","SAB","NOC","MAN","VIR","TAR"];
const PROGRAMAS_FILTRO = ["TODOS","TEAC","TERC","SEMINARIOS","OTROS"];
@@ -68,6 +70,56 @@ function SelectBonito({ label, value, options, onChange, minWidth = 150 }) {
);
}
+// Filtro multi-selección con casillas (Sectorista, Sede, Programa). Vacío = todos.
+function ListaMulti({ label, opciones, seleccion, onChange, plural = "opciones", minWidth = 150 }) {
+ const [abierto, setAbierto] = useState(false);
+ const ref = useRef(null);
+ useEffect(() => {
+ const fuera = (e) => { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); };
+ document.addEventListener("mousedown", fuera);
+ return () => document.removeEventListener("mousedown", fuera);
+ }, []);
+ const toggle = (v) => {
+ const set = new Set(seleccion);
+ set.has(v) ? set.delete(v) : set.add(v);
+ onChange([...set]);
+ };
+ const texto = seleccion.length === 0 ? "— Todos —"
+ : seleccion.length === 1 ? seleccion[0]
+ : `${seleccion.length} ${plural}`;
+ return (
+
+
+
+ {abierto && (
+
+ {opciones.length === 0 &&
Sin opciones.
}
+ {opciones.map((o) => {
+ const on = seleccion.includes(o);
+ return (
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
function toMonto(s){ const n = parseFloat(String(s).replace("S/","").replace(/,/g,"").trim()); return isNaN(n)?0:n; }
function toPct(s){ const n = parseFloat(String(s).replace("%","").trim()); return isNaN(n)?0:n; }
@@ -122,12 +174,16 @@ export default function Cobranza() {
const [ano, setAno] = useState(hoy.getFullYear());
const [mes, setMes] = useState(hoy.getMonth() + 1);
const [agrupacion, setAgrupacion] = useState("SEDE");
- const [sectorista, setSectorista] = useState("TODOS");
+ const [sectoristasSel, setSectoristasSel] = useState([]); // multi; vacío = todos
const [sede, setSede] = useState("TODOS");
const [frec, setFrec] = useState("TODOS");
- // Filtros extra para la vista "Por Sede" (usan JSON REPLACE_META)
- const [sedeFiltro, setSedeFiltro] = useState("TODOS");
- const [progFiltro, setProgFiltro] = useState("TODOS");
+ // Filtros extra para la vista "Por Sede" (usan JSON REPLACE_META). Multi; vacío = todos.
+ const [sedesSel, setSedesSel] = useState([]);
+ const [progsSel, setProgsSel] = useState([]);
+ // Claves string (coma) para dependencias y para pasar al backend. Vacío = "TODOS".
+ const sectorista = sectoristasSel.length ? sectoristasSel.join(",") : "TODOS";
+ const sedeFiltro = sedesSel.length ? sedesSel.join(",") : "TODOS";
+ const progFiltro = progsSel.length ? progsSel.join(",") : "TODOS";
const [filas, setFilas] = useState([]);
const [sectoristas, setSectoristas] = useState(["TODOS"]);
const [loading, setLoading] = useState(true);
@@ -357,7 +413,7 @@ export default function Cobranza() {
return filasPrograma.filter((fp) => {
const ip = infoProg(fp[0]);
if (String(ip.sede).toUpperCase() !== key) return false;
- if (progFiltro !== "TODOS" && String(ip.filtro).toUpperCase() !== progFiltro) return false;
+ if (progsSel.length && !progsSel.includes(String(ip.filtro).toUpperCase())) return false;
return true;
});
};
@@ -369,15 +425,15 @@ export default function Cobranza() {
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);
}
- // Filtros Sede / Programa (REPLACE_META) — aplican en Por Sede y Por Asesor
- if (sedeFiltro !== "TODOS") {
+ // Filtros Sede / Programa (REPLACE_META) — multi; aplican en Por Sede y Por Asesor
+ if (sedesSel.length) {
arr = arr.filter((a) => {
const s = (progClasif[String(a.programa)]?.sede || a.sede || "LIMA");
- return String(s).toUpperCase() === sedeFiltro.toUpperCase();
+ return sedesSel.includes(String(s).toUpperCase());
});
}
- if (progFiltro !== "TODOS") {
- arr = arr.filter((a) => String(progClasif[String(a.programa)]?.filtro || "").toUpperCase() === progFiltro);
+ if (progsSel.length) {
+ arr = arr.filter((a) => progsSel.includes(String(progClasif[String(a.programa)]?.filtro || "").toUpperCase()));
}
// Filtro por F. Vencimiento (<=). Cuotas sin fecha se incluyen siempre.
if (vencHasta !== "TODOS") {
@@ -494,12 +550,12 @@ 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");setSedeFiltro("TODOS");setProgFiltro("TODOS");}} minWidth={150} />
-
+ {setAgrupacion(v);setSede("TODOS");setFrec("TODOS");setSedesSel([]);setProgsSel([]);}} minWidth={150} />
+ s!=="TODOS")} seleccion={sectoristasSel} onChange={setSectoristasSel} plural="sectoristas" minWidth={250} />
{esPrograma && }
{esPrograma && }
- {(agrupacion==="SEDE"||agrupacion==="ASESOR") && {setSedeFiltro(v);setExpandidas(new Set());}} minWidth={140} />}
- {(agrupacion==="SEDE"||agrupacion==="ASESOR") && {setProgFiltro(v);setExpandidas(new Set());}} minWidth={150} />}
+ {(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} />}