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
|
// Cargar los alumnos POR RETIRAR de (año, mes) con sus datos + info manual de ese mes
|
||||||
useEffect(() => {
|
const cargar = () => {
|
||||||
let activo = true;
|
|
||||||
setLoading(true); setError(null); setMsgTotal(null);
|
setLoading(true); setError(null); setMsgTotal(null);
|
||||||
api.retiros(ano, mes)
|
api.retiros(ano, mes)
|
||||||
.then((r) => { if (activo) { setFilas(r.filas || []); setGuardado(!!r.guardado); setGuardadoEn(r.guardado_en || null); setLoading(false); } })
|
.then((r) => { setFilas(r.filas || []); setGuardado(!!r.guardado); setGuardadoEn(r.guardado_en || null); setLoading(false); })
|
||||||
.catch((e) => { if (activo) { setError(e.message); setLoading(false); } });
|
.catch((e) => { setError(e.message); setLoading(false); });
|
||||||
return () => { activo = false; };
|
};
|
||||||
}, [ano, mes]);
|
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)
|
// Opciones de sectorista (de los datos cargados) — sin "TODOS" (checks)
|
||||||
const sectoristas = useMemo(() => {
|
const sectoristas = useMemo(() => {
|
||||||
@@ -262,6 +264,13 @@ export default function Retiros() {
|
|||||||
{msgTotal.txt}
|
{msgTotal.txt}
|
||||||
</span>
|
</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}
|
<button onClick={exportar} disabled={exportando || !filasFiltradas.length}
|
||||||
title="Exportar la tabla actual a Excel (respeta los filtros)"
|
title="Exportar la tabla actual a Excel (respeta los filtros)"
|
||||||
style={{ padding: "9px 18px", borderRadius: 9, border: "1px solid #cbd5e1",
|
style={{ padding: "9px 18px", borderRadius: 9, border: "1px solid #cbd5e1",
|
||||||
@@ -506,6 +515,14 @@ export default function Retiros() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{buscadorAbierto && (
|
||||||
|
<ModalBuscarRetiro
|
||||||
|
ano={ano} mes={mes}
|
||||||
|
onClose={() => setBuscadorAbierto(false)}
|
||||||
|
onCambio={cargar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -614,3 +631,197 @@ function ListaMulti({ label, opciones, seleccion, onChange, plural = "opciones",
|
|||||||
</div>
|
</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