Actualizar frontend/src/pages/Retiros.jsx
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user