From 2ea839c52927ae8b89ce9d6152f542489d7d98f4 Mon Sep 17 00:00:00 2001 From: Administrator Date: Tue, 22 Sep 2026 15:41:29 -0500 Subject: [PATCH] Actualizar frontend/src/pages/Retiros.jsx --- frontend/src/pages/Retiros.jsx | 131 ++++++++++++++++++++++++++++----- 1 file changed, 112 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/Retiros.jsx b/frontend/src/pages/Retiros.jsx index 1cdf8b6..3f3510c 100644 --- a/frontend/src/pages/Retiros.jsx +++ b/frontend/src/pages/Retiros.jsx @@ -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() {
- ({value:i+1,label:m}))} - onChange={(v)=>setMes(+v)} /> - + setAno(+v)} minWidth={110} /> + ({value:i+1,label:m}))} valor={mes} onChange={(v)=>setMes(+v)} minWidth={160} /> + +
{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 ( +
+ + + {abierto && ( +
+ {opts.map((o) => { + const on = String(o.value) === String(valor); + return ( +
{ 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} +
+ ); + })} +
+ )} +
+ ); +} + +// 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 ( +
+ + + {abierto && ( +
+ {opciones.length === 0 &&
Sin opciones.
} + {opciones.map((o) => { + const on = seleccion.includes(o); + return ( + + ); + })} +
+ )} +
+ ); +}