Actualizar frontend/src/pages/Retiros.jsx

This commit is contained in:
2026-09-22 15:41:29 -05:00
parent 1f2f35d523
commit 2ea839c529

View File

@@ -1,5 +1,5 @@
// src/pages/Retiros.jsx
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect, useMemo, useRef } from "react";
import { api } from "../lib/api";
import { Loader, ErrorBox, Filters, Select } from "../components/UI";
import { useColumnasAjustables } from "../lib/useColumnasAjustables";
@@ -81,8 +81,8 @@ export default function Retiros() {
const hoy = new Date();
const [ano, setAno] = useState(hoy.getFullYear());
const [mes, setMes] = useState(hoy.getMonth() + 1); // mes actual (sin TODOS)
const [sede, setSede] = useState("TODOS");
const [sectorista, setSectorista] = useState("TODOS");
const [sedesSel, setSedesSel] = useState([]); // multi; vacío = todas
const [sectoristasSel, setSectoristasSel] = useState([]); // multi; vacío = todos
const [ordCol, setOrdCol] = useState(null); // key de la columna ordenada
const [ordDir, setOrdDir] = useState(null);
const clickOrden = (k, esInfo) => {
@@ -166,30 +166,28 @@ export default function Retiros() {
return () => { activo = false; };
}, [ano, mes]);
// Opciones de sectorista (de los datos cargados)
// Opciones de sectorista (de los datos cargados) — sin "TODOS" (checks)
const sectoristas = useMemo(() => {
const s = [...new Set(filas.map((f) => f.asesor).filter(Boolean))].sort();
return ["TODOS", ...s];
return [...new Set(filas.map((f) => f.asesor).filter(Boolean))].sort();
}, [filas]);
// Opciones de sede (de los datos cargados)
// Opciones de sede (de los datos cargados) — sin "TODOS" (checks)
const sedes = useMemo(() => {
const s = [...new Set(filas.map((f) => f.sede).filter(Boolean))].sort();
return ["TODOS", ...s];
return [...new Set(filas.map((f) => f.sede).filter(Boolean))].sort();
}, [filas]);
// El backend ya trae solo el mes; aquí solo se aplican Sede y Sectorista (cliente).
// El backend ya trae solo el mes; aquí solo se aplican Sede y Sectorista (cliente, multi).
const filasFiltradas = useMemo(() => {
let base = filas.filter((f) => {
if (sede !== "TODOS" && f.sede !== sede) return false;
if (sectorista !== "TODOS" && f.asesor !== sectorista) return false;
if (sedesSel.length && !sedesSel.includes(f.sede)) return false;
if (sectoristasSel.length && !sectoristasSel.includes(f.asesor)) return false;
return true;
});
if (ordCol != null && ordDir) {
base = [...base].sort((a, b) => _cmp(a[ordCol], b[ordCol], ordDir));
}
return base;
}, [filas, sede, sectorista, ordCol, ordDir]);
}, [filas, sedesSel, sectoristasSel, ordCol, ordDir]);
// Fecha yyyy-mm-dd -> dd/mm/yyyy
const fmtFecha = (s) => {
@@ -252,12 +250,10 @@ export default function Retiros() {
<div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
<Filters>
<Select label="Año" value={ano} options={ANOS} onChange={(v)=>setAno(+v)} />
<Select label="Mes" value={mes}
options={MESES.map((m,i)=>({value:i+1,label:m}))}
onChange={(v)=>setMes(+v)} />
<Select label="Sede" value={sede} options={sedes} onChange={setSede} />
<Select label="Sectorista" value={sectorista} options={sectoristas} onChange={setSectorista} width={250} />
<SelectUnico label="Año" opciones={ANOS} valor={ano} onChange={(v)=>setAno(+v)} minWidth={110} />
<SelectUnico label="Mes" opciones={MESES.map((m,i)=>({value:i+1,label:m}))} valor={mes} onChange={(v)=>setMes(+v)} minWidth={160} />
<ListaMulti label="Sede" opciones={sedes} seleccion={sedesSel} onChange={setSedesSel} plural="sedes" minWidth={160} />
<ListaMulti label="Sectorista" opciones={sectoristas} seleccion={sectoristasSel} onChange={setSectoristasSel} plural="sectoristas" minWidth={250} />
</Filters>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
{msgTotal && (
@@ -521,3 +517,100 @@ const inpStyle = {
};
const onFoco = (e) => { e.target.style.borderColor = "#7a9d8f"; e.target.style.boxShadow = "0 0 0 3px rgba(122,157,143,.18)"; };
const offFoco = (e) => { e.target.style.borderColor = "#cbd5e1"; e.target.style.boxShadow = "none"; };
// Desplegable de UNA sola opción (Año/Mes) con diseño bonito. Opciones: strings o {value,label}.
function SelectUnico({ label, opciones, valor, onChange, minWidth = 150 }) {
const [abierto, setAbierto] = useState(false);
const ref = useRef(null);
const opts = opciones.map((o) => (typeof o === "object" ? o : { value: o, label: String(o) }));
const actual = opts.find((o) => String(o.value) === String(valor));
useEffect(() => {
const fuera = (e) => { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); };
document.addEventListener("mousedown", fuera);
return () => document.removeEventListener("mousedown", fuera);
}, []);
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" }}>
{actual ? actual.label : "—"}
</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" }}>
{opts.map((o) => {
const on = String(o.value) === String(valor);
return (
<div key={o.value} onClick={() => { onChange(o.value); setAbierto(false); }}
style={{ padding: "8px 10px", borderRadius: 6, cursor: "pointer", fontSize: 13,
fontWeight: on ? 700 : 500, color: on ? "#1e3a5f" : "#334155",
background: on ? "#eff6ff" : "transparent" }}
onMouseEnter={(e) => { if (!on) e.currentTarget.style.background = "#f8fafc"; }}
onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = "transparent"; }}>
{o.label}
</div>
);
})}
</div>
)}
</div>
);
}
// Filtro multi-selección con casillas (Sede, Sectorista). 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>
);
}