Actualizar frontend/src/pages/Cobranza.jsx

This commit is contained in:
2026-09-23 10:42:24 -05:00
parent 7f624a6428
commit 0fb44d046c

View File

@@ -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 (
<div className="filter-group" ref={ref} style={{ position: "relative", minWidth }}>
<label>{label}</label>
<button type="button" onClick={() => setAbierto((v) => !v)}
style={{ height: 38, width: "100%", textAlign: "left", padding: "0 12px",
border: "1px solid #cbd5e1", borderRadius: 8, background: "#fff",
cursor: "pointer", fontSize: 13, display: "flex", alignItems: "center",
justifyContent: "space-between", gap: 8 }}>
<span style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{texto}</span>
<span style={{ color: "#94a3b8", fontSize: 11 }}>▼</span>
</button>
{abierto && (
<div style={{ position: "absolute", top: "100%", left: 0, right: 0, zIndex: 50, marginTop: 4,
background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8,
boxShadow: "0 8px 24px rgba(0,0,0,0.12)", padding: 6, maxHeight: 300, overflowY: "auto" }}>
{opciones.length === 0 && <div style={{ padding: 8, color: "#94a3b8", fontSize: 13 }}>Sin opciones.</div>}
{opciones.map((o) => {
const on = seleccion.includes(o);
return (
<label key={o} style={{ display: "flex", alignItems: "center", gap: 8,
padding: "6px 8px", borderRadius: 6, cursor: "pointer", fontSize: 13,
background: on ? "#eff6ff" : "transparent" }}>
<input type="checkbox" checked={on} onChange={() => toggle(o)} />
{o}
</label>
);
})}
</div>
)}
</div>
);
}
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() {
<Filters>
<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="Agrupación" value={agrupacion} options={AGRUPACIONES} onChange={(v)=>{setAgrupacion(v);setSede("TODOS");setFrec("TODOS");setSedeFiltro("TODOS");setProgFiltro("TODOS");}} minWidth={150} />
<SelectBonito label="Sectorista" value={sectorista} options={sectoristas} onChange={setSectorista} minWidth={250} />
<SelectBonito label="Agrupación" value={agrupacion} options={AGRUPACIONES} onChange={(v)=>{setAgrupacion(v);setSede("TODOS");setFrec("TODOS");setSedesSel([]);setProgsSel([]);}} minWidth={150} />
<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="Frecuencia" value={frec} options={FRECS} onChange={setFrec} minWidth={140} />}
{(agrupacion==="SEDE"||agrupacion==="ASESOR") && <SelectBonito label="Sede" value={sedeFiltro} options={SEDES} onChange={(v)=>{setSedeFiltro(v);setExpandidas(new Set());}} minWidth={140} />}
{(agrupacion==="SEDE"||agrupacion==="ASESOR") && <SelectBonito label="Programa" value={progFiltro} options={PROGRAMAS_FILTRO} onChange={(v)=>{setProgFiltro(v);setExpandidas(new Set());}} minWidth={150} />}
{(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} />}
<div style={{display:"flex",gap:8,alignItems:"flex-end",marginLeft:"auto"}}>
<div style={{position:"relative"}}>
<button className="btn btn-ghost"