Actualizar frontend/src/pages/Retiros.jsx

This commit is contained in:
2026-09-28 17:09:49 -05:00
parent 012a4e14ec
commit c1bcd4ea5b

View File

@@ -157,14 +157,16 @@ export default function Retiros() {
};
// Cargar los alumnos POR RETIRAR de (año, mes) con sus datos + info manual de ese mes
useEffect(() => {
let activo = true;
const cargar = () => {
setLoading(true); setError(null); setMsgTotal(null);
api.retiros(ano, mes)
.then((r) => { if (activo) { setFilas(r.filas || []); setGuardado(!!r.guardado); setGuardadoEn(r.guardado_en || null); setLoading(false); } })
.catch((e) => { if (activo) { setError(e.message); setLoading(false); } });
return () => { activo = false; };
}, [ano, mes]);
.then((r) => { setFilas(r.filas || []); setGuardado(!!r.guardado); setGuardadoEn(r.guardado_en || null); setLoading(false); })
.catch((e) => { setError(e.message); setLoading(false); });
};
useEffect(() => { cargar(); }, [ano, mes]);
// Buscador para agregar/quitar alumnos POR RETIRAR
const [buscadorAbierto, setBuscadorAbierto] = useState(false);
// Opciones de sectorista (de los datos cargados) — sin "TODOS" (checks)
const sectoristas = useMemo(() => {
@@ -262,6 +264,13 @@ export default function Retiros() {
{msgTotal.txt}
</span>
)}
<button onClick={() => setBuscadorAbierto(true)}
title="Buscar un alumno y marcarlo POR RETIRAR"
style={{ padding: "9px 18px", borderRadius: 9, border: "1px solid #cbd5e1",
background: "#fff", color: "#334155", fontWeight: 700, fontSize: 13.5,
cursor: "pointer", whiteSpace: "nowrap" }}>
🔍 Buscar Alumno
</button>
<button onClick={exportar} disabled={exportando || !filasFiltradas.length}
title="Exportar la tabla actual a Excel (respeta los filtros)"
style={{ padding: "9px 18px", borderRadius: 9, border: "1px solid #cbd5e1",
@@ -506,6 +515,14 @@ export default function Retiros() {
</div>
</div>
)}
{buscadorAbierto && (
<ModalBuscarRetiro
ano={ano} mes={mes}
onClose={() => setBuscadorAbierto(false)}
onCambio={cargar}
/>
)}
</div>
);
}
@@ -614,3 +631,197 @@ function ListaMulti({ label, opciones, seleccion, onChange, plural = "opciones",
</div>
);
}
// ── Buscador de alumnos para marcar POR RETIRAR ──────────────────────────────
// Usa comisionesDetalleTodos (cacheado: instantáneo tras la 1ª carga). Muestra
// Alumno, Vendedor, F. Matrícula, Programa, Inv. Neta y un Estado editable
// (POR RETIRAR / EN CURSO) que se guarda en Supabase por (matrícula, año, mes).
function ModalBuscarRetiro({ ano, mes, onClose, onCambio }) {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
const [q, setQ] = useState("");
const [orig, setOrig] = useState({}); // estado guardado en Supabase
const [estados, setEstados] = useState({}); // estado editado localmente
const [guardando, setGuardando] = useState(false);
const [msg, setMsg] = useState(null);
// Cargar TODOS los alumnos (cacheado) + set actual de POR RETIRAR del mes
useEffect(() => {
let activo = true;
setLoading(true);
Promise.all([
api.comisionesDetalleTodos(ano, mes),
api.alumnosPorRetirar(ano, mes),
]).then(([det, por]) => {
if (!activo) return;
setTodos(det.filas || []);
const set = {};
(por.matriculas || []).forEach((m) => { set[String(m)] = "POR RETIRAR"; });
setOrig(set);
setEstados(set);
setLoading(false);
}).catch(() => { if (activo) { setTodos([]); setLoading(false); } });
return () => { activo = false; };
}, [ano, mes]);
const norm = (s) => String(s || "").toLowerCase();
const filtrados = useMemo(() => {
const term = norm(q).trim();
const base = (todos || []).filter((f) => String(f[0]).toUpperCase() !== "TOTAL GENERAL");
if (!term) return base.slice(0, 60); // sin búsqueda: primeros 60
return base.filter((f) =>
norm(f[1]).includes(term) || norm(f[8]).includes(term) ||
norm(f[0]).includes(term) || norm(f[15]).includes(term)
).slice(0, 200);
}, [todos, q]);
// Solo cambia el estado LOCAL (no guarda hasta presionar el botón).
function cambiarEstado(mat, nuevo) {
setEstados((p) => ({ ...p, [mat]: nuevo }));
setMsg(null);
}
// Matrículas cuyo estado cambió respecto a lo guardado.
const cambios = useMemo(() => {
const keys = new Set([...Object.keys(orig), ...Object.keys(estados)]);
const out = [];
keys.forEach((m) => {
const a = orig[m] || "EN CURSO";
const b = estados[m] || "EN CURSO";
if (a !== b) out.push({ mat: m, estado: b });
});
return out;
}, [orig, estados]);
async function guardarCambios() {
if (!cambios.length) return;
setGuardando(true); setMsg(null);
try {
// Guardar en paralelo (rápido). Cada uno actualiza (matrícula, año, mes).
await Promise.all(cambios.map((c) => api.alumnoEstadoGuardar(c.mat, ano, mes, c.estado, [])));
setOrig({ ...estados }); // ya quedó guardado
if (onCambio) onCambio(); // recargar la tabla principal de Retiros
setMsg({ ok: true, txt: `✅ ${cambios.length} cambio(s) guardado(s).` });
} catch (e) {
setMsg({ ok: false, txt: "Error al guardar: " + e.message });
}
setGuardando(false);
}
const th = { background: "#334155", color: "#f1f5f9", padding: "9px 10px", fontSize: 11,
fontWeight: 700, textAlign: "left", whiteSpace: "nowrap", position: "sticky", top: 0 };
const td = { padding: "8px 10px", fontSize: 12.5, borderBottom: "1px solid #f1f5f9",
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" };
return (
<div onClick={onClose}
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.55)",
display: "flex", alignItems: "flex-start", justifyContent: "center",
zIndex: 1000, padding: "40px 20px", overflow: "auto" }}>
<div onClick={(e) => e.stopPropagation()}
style={{ background: "#fff", borderRadius: 16, width: "min(1100px, 96vw)",
boxShadow: "0 20px 60px rgba(0,0,0,.3)", overflow: "hidden" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between",
padding: "16px 20px", borderBottom: "1px solid #e2e8f0" }}>
<h3 style={{ margin: 0, fontSize: 17, color: "#1e3a5f", fontWeight: 800 }}>🔍 Buscar Alumno — Retiros</h3>
<button onClick={onClose}
style={{ border: "none", background: "#f1f5f9", borderRadius: 8, width: 32, height: 32,
cursor: "pointer", fontSize: 16, color: "#475569" }}>×</button>
</div>
<div style={{ padding: "14px 20px" }}>
<input autoFocus value={q} onChange={(e) => setQ(e.target.value)}
placeholder="Escribe nombre de alumno, programa, vendedor o matrícula…"
style={{ width: "100%", padding: "11px 14px", border: "1px solid #cbd5e1",
borderRadius: 10, fontSize: 14, boxSizing: "border-box", outline: "none" }} />
<div style={{ marginTop: 12, maxHeight: "60vh", overflow: "auto",
border: "1px solid #e2e8f0", borderRadius: 10 }}>
{loading ? (
<div style={{ padding: 30, textAlign: "center", color: "#94a3b8" }}>Cargando alumnos…</div>
) : (
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead><tr>
<th style={th}>ALUMNO</th><th style={th}>VENDEDOR</th>
<th style={th}>F. MATRÍCULA</th><th style={th}>PROGRAMA</th>
<th style={{ ...th, textAlign: "right" }}>INV. NETA</th>
<th style={{ ...th, textAlign: "center" }}>ESTADO</th>
</tr></thead>
<tbody>
{filtrados.length === 0 && (
<tr><td colSpan={6} style={{ padding: 24, textAlign: "center", color: "#94a3b8" }}>
Sin resultados.
</td></tr>
)}
{filtrados.map((f, i) => {
const mat = String(f[15]);
const est = estados[mat] || "EN CURSO";
const porRetirar = est === "POR RETIRAR";
return (
<tr key={i} style={porRetirar ? { background: "#fff7ed" } : undefined}>
<td style={{ ...td, fontWeight: 600, color: "#1e293b", maxWidth: 240 }}>{f[1]}</td>
<td style={{ ...td, color: "#475569", maxWidth: 160 }}>{f[0]}</td>
<td style={{ ...td, color: "#475569" }}>{f[2]}</td>
<td style={{ ...td, color: "#475569", maxWidth: 260 }}>{f[8]}</td>
<td style={{ ...td, textAlign: "right", color: "#1e293b" }}>{f[7]}</td>
<td style={{ ...td, textAlign: "center" }}>
<select value={est}
onChange={(e) => cambiarEstado(mat, e.target.value)}
style={{ padding: "5px 8px", borderRadius: 8, fontSize: 12, fontWeight: 700,
cursor: "pointer",
border: `1px solid ${porRetirar ? "#f59e0b" : "#cbd5e1"}`,
background: porRetirar ? "#fffbeb" : "#fff",
color: porRetirar ? "#b45309" : "#334155" }}>
<option value="EN CURSO">EN CURSO</option>
<option value="POR RETIRAR">POR RETIRAR</option>
</select>
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
{!loading && (
<div style={{ marginTop: 8, fontSize: 11.5, color: "#94a3b8" }}>
Al marcar POR RETIRAR, el alumno aparece en la tabla de Retiros con sus datos calculados. {q ? `${filtrados.length} resultado(s).` : "Escribe para buscar."}
</div>
)}
{/* Pie: mensaje + botón Guardar cambios */}
<div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 14 }}>
{msg && (
<span style={{ fontSize: 12.5, fontWeight: 600,
color: msg.ok ? "#15803d" : "#dc2626", marginRight: "auto" }}>
{msg.txt}
</span>
)}
{!msg && cambios.length > 0 && (
<span style={{ fontSize: 12.5, color: "#b45309", fontWeight: 600, marginRight: "auto" }}>
{cambios.length} cambio(s) sin guardar
</span>
)}
{!msg && cambios.length === 0 && <span style={{ marginRight: "auto" }} />}
<button onClick={onClose}
style={{ padding: "9px 18px", borderRadius: 9, border: "1px solid #cbd5e1",
background: "#fff", color: "#475569", fontWeight: 600, fontSize: 13.5,
cursor: "pointer" }}>
Cerrar
</button>
<button onClick={guardarCambios} disabled={guardando || cambios.length === 0}
style={{ padding: "9px 22px", borderRadius: 9, border: "none",
background: (guardando || cambios.length === 0)
? "#cbd5e1" : "linear-gradient(135deg, #6f9284 0%, #86a99a 100%)",
color: "#fff", fontWeight: 700, fontSize: 13.5,
cursor: (guardando || cambios.length === 0) ? "default" : "pointer",
boxShadow: (guardando || cambios.length === 0) ? "none" : "0 4px 12px rgba(111,146,132,.35)",
whiteSpace: "nowrap" }}>
{guardando ? "Guardando…" : "💾 Guardar cambios"}
</button>
</div>
</div>
</div>
</div>
);
}