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