Actualizar frontend/src/pages/Retiros.jsx

This commit is contained in:
2026-10-05 12:22:27 -05:00
parent 1125f91b8f
commit 920318a604

View File

@@ -60,8 +60,12 @@ const COLS = [
{ k: "detalle_retiro", t: "DETALLE DE RETIRO", al: "left", w: 400, vacio: true, elip: true }, { k: "detalle_retiro", t: "DETALLE DE RETIRO", al: "left", w: 400, vacio: true, elip: true },
{ k: "fch_retiro", t: "FECHA DE RETIRO", al: "center", w: 130, fecha: true }, { k: "fch_retiro", t: "FECHA DE RETIRO", al: "center", w: 130, fecha: true },
{ k: "_info", t: "INFO", al: "center", w: 80, info: true }, { k: "_info", t: "INFO", al: "center", w: 80, info: true },
{ k: "_quitar", t: "", al: "center", w: 56, quitar: true },
]; ];
// Columnas que se suman en la fila TOTAL GENERAL
const COLS_SUMA = ["num_cuotas_pagadas","num_cuotas_vencidas","monto_venc_ant","monto_venc_cur","monto_dejo_pagar"];
// Campos editables del popup INFO (los 9 manuales) // Campos editables del popup INFO (los 9 manuales)
const INFO_CAMPOS = [ const INFO_CAMPOS = [
{ k: "n_asistencia", t: "N° ASISTENCIA", tipo: "number" }, { k: "n_asistencia", t: "N° ASISTENCIA", tipo: "number" },
@@ -112,6 +116,24 @@ export default function Retiros() {
// Exportar // Exportar
const [exportando, setExportando] = useState(false); const [exportando, setExportando] = useState(false);
// Quitar alumno (pasar a EN CURSO) — con confirmación
const [quitar, setQuitar] = useState(null); // fila a quitar (o null)
const [quitando, setQuitando] = useState(false);
const confirmarQuitar = async () => {
if (!quitar || quitando) return;
setQuitando(true);
try {
await api.alumnoEstadoGuardar(quitar.matricula, ano, mes, "EN CURSO", [], "BUSCADOR_RETIROS");
setFilas((prev) => prev.filter((f) => f.matricula !== quitar.matricula));
setQuitar(null);
} catch (e) {
alert("No se pudo cambiar el estado: " + (e.message || ""));
} finally {
setQuitando(false);
}
};
const abrirInfo = (f) => { const abrirInfo = (f) => {
setErrGuardar(null); setErrGuardar(null);
setForm({ ...INFO_VACIO(), ...Object.fromEntries(INFO_CAMPOS.map((c) => [c.k, f[c.k] ?? ""])) }); setForm({ ...INFO_VACIO(), ...Object.fromEntries(INFO_CAMPOS.map((c) => [c.k, f[c.k] ?? ""])) });
@@ -209,7 +231,7 @@ export default function Retiros() {
setExportando(true); setExportando(true);
try { try {
const XLSX = await import("xlsx-js-style"); const XLSX = await import("xlsx-js-style");
const COLS_EXP = COLS.filter((c) => !c.info); // sin la columna INFO const COLS_EXP = COLS.filter((c) => !c.info && !c.quitar); // sin INFO ni X
const headerStyle = { font:{bold:true,color:{rgb:"FFFFFF"},sz:11}, const headerStyle = { font:{bold:true,color:{rgb:"FFFFFF"},sz:11},
fill:{fgColor:{rgb:"1E40AF"}}, alignment:{horizontal:"center",vertical:"center"} }; fill:{fgColor:{rgb:"1E40AF"}}, alignment:{horizontal:"center",vertical:"center"} };
const manualStyle = { fill:{fgColor:{rgb:"EEF4F1"}} }; // celdas manuales verde suave const manualStyle = { fill:{fgColor:{rgb:"EEF4F1"}} }; // celdas manuales verde suave
@@ -257,23 +279,23 @@ export default function Retiros() {
<ListaMulti label="Sede" opciones={sedes} seleccion={sedesSel} onChange={setSedesSel} plural="sedes" 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} /> <ListaMulti label="Sectorista" opciones={sectoristas} seleccion={sectoristasSel} onChange={setSectoristasSel} plural="sectoristas" minWidth={250} />
</Filters> </Filters>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}> <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 18 }}>
{msgTotal && ( {msgTotal && (
<span style={{ fontSize: 12.5, fontWeight: 600, <span style={{ fontSize: 12.5, fontWeight: 600, alignSelf: "center",
color: msgTotal.err ? "#dc2626" : "#15803d" }}> color: msgTotal.err ? "#dc2626" : "#15803d" }}>
{msgTotal.txt} {msgTotal.txt}
</span> </span>
)} )}
<button onClick={() => setBuscadorAbierto(true)} <button onClick={() => setBuscadorAbierto(true)}
title="Buscar un alumno y marcarlo POR RETIRAR" title="Buscar un alumno y marcarlo POR RETIRAR"
style={{ padding: "9px 18px", borderRadius: 9, border: "1px solid #cbd5e1", style={{ height: 40, padding: "0 18px", borderRadius: 9, border: "1px solid #cbd5e1",
background: "#fff", color: "#334155", fontWeight: 700, fontSize: 13.5, background: "#fff", color: "#334155", fontWeight: 700, fontSize: 13.5,
cursor: "pointer", whiteSpace: "nowrap" }}> cursor: "pointer", whiteSpace: "nowrap" }}>
🔍 Buscar Alumno 🔍 Buscar Alumno
</button> </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={{ height: 40, padding: "0 18px", borderRadius: 9, border: "1px solid #cbd5e1",
background: "#fff", color: "#334155", fontWeight: 700, fontSize: 13.5, background: "#fff", color: "#334155", fontWeight: 700, fontSize: 13.5,
cursor: (exportando || !filasFiltradas.length) ? "default" : "pointer", cursor: (exportando || !filasFiltradas.length) ? "default" : "pointer",
opacity: (exportando || !filasFiltradas.length) ? .6 : 1, whiteSpace: "nowrap" }}> opacity: (exportando || !filasFiltradas.length) ? .6 : 1, whiteSpace: "nowrap" }}>
@@ -281,7 +303,7 @@ export default function Retiros() {
</button> </button>
<button onClick={guardarTotal} disabled={guardandoTotal} <button onClick={guardarTotal} disabled={guardandoTotal}
title="Guarda todas las columnas del mes seleccionado en Supabase" title="Guarda todas las columnas del mes seleccionado en Supabase"
style={{ padding: "9px 20px", borderRadius: 9, border: "none", style={{ height: 40, padding: "0 20px", borderRadius: 9, border: "none",
background: "linear-gradient(135deg, #6f9284 0%, #86a99a 100%)", background: "linear-gradient(135deg, #6f9284 0%, #86a99a 100%)",
color: "#fff", fontWeight: 700, fontSize: 13.5, color: "#fff", fontWeight: 700, fontSize: 13.5,
cursor: guardandoTotal ? "default" : "pointer", cursor: guardandoTotal ? "default" : "pointer",
@@ -312,7 +334,7 @@ export default function Retiros() {
<thead> <thead>
<tr> <tr>
{COLS.map((c, i) => ( {COLS.map((c, i) => (
<th key={c.k} title={c.t} onClick={() => clickOrden(c.k, c.info)} <th key={c.k} title={c.t} onClick={() => clickOrden(c.k, c.info || c.quitar)}
style={{ textAlign: "center", position: "relative", whiteSpace: "nowrap", style={{ textAlign: "center", position: "relative", whiteSpace: "nowrap",
overflow: "hidden", textOverflow: "ellipsis", overflow: "hidden", textOverflow: "ellipsis",
borderRight: "1px solid #cbd5e1", borderRight: "1px solid #cbd5e1",
@@ -320,7 +342,7 @@ export default function Retiros() {
// Encabezado de las columnas manuales en verde // Encabezado de las columnas manuales en verde
background: c.vacio ? "#7a9d8f" : undefined, background: c.vacio ? "#7a9d8f" : undefined,
color: c.vacio ? "#fff" : undefined }}> color: c.vacio ? "#fff" : undefined }}>
{c.t}{!c.info && _flecha(ordCol === c.k, ordDir)}<cols.Resizer index={i} /> {c.t}{!c.info && !c.quitar && _flecha(ordCol === c.k, ordDir)}<cols.Resizer index={i} />
</th> </th>
))} ))}
</tr> </tr>
@@ -332,8 +354,24 @@ export default function Retiros() {
</td></tr> </td></tr>
)} )}
{filasFiltradas.map((f, i) => ( {filasFiltradas.map((f, i) => (
<tr key={f.matricula + i}> <tr key={f.matricula + i}
style={f.origen_buscador ? { background: "#eff6ff" } : undefined}>
{COLS.map((c) => { {COLS.map((c) => {
if (c.quitar) {
return (
<td key={c.k} style={{ textAlign: "center", borderRight: "1px solid #e2e8f0", borderBottom: "1px solid #e2e8f0" }}>
<button onClick={() => setQuitar(f)}
title="Pasar a EN CURSO (quitar de retiros)"
onMouseEnter={(e) => { e.currentTarget.style.background = "#fef2f2"; e.currentTarget.style.borderColor = "#f87171"; e.currentTarget.style.color = "#dc2626"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "#fff"; e.currentTarget.style.borderColor = "#cbd5e1"; e.currentTarget.style.color = "#94a3b8"; }}
style={{ background: "#fff", color: "#94a3b8", border: "1px solid #cbd5e1",
borderRadius: 6, width: 28, height: 28, fontSize: 15, lineHeight: 1,
fontWeight: 700, cursor: "pointer", transition: "all .15s" }}>
✕
</button>
</td>
);
}
if (c.info) { if (c.info) {
return ( return (
<td key={c.k} style={{ textAlign: "center", borderRight: "1px solid #e2e8f0", borderBottom: "1px solid #e2e8f0" }}> <td key={c.k} style={{ textAlign: "center", borderRight: "1px solid #e2e8f0", borderBottom: "1px solid #e2e8f0" }}>
@@ -378,8 +416,9 @@ export default function Retiros() {
color: rojo ? "#dc2626" : undefined, color: rojo ? "#dc2626" : undefined,
borderRight: "1px solid #e2e8f0", borderRight: "1px solid #e2e8f0",
borderBottom: "1px solid #e2e8f0", borderBottom: "1px solid #e2e8f0",
// Fondo verde suave para las celdas de columnas manuales // Fondo verde suave para columnas manuales; si la fila viene
background: c.vacio ? "#eef4f1" : undefined, // del buscador, dejar transparente para que se vea el azul.
background: f.origen_buscador ? "transparent" : (c.vacio ? "#eef4f1" : undefined),
}}> }}>
{val} {val}
</td> </td>
@@ -387,6 +426,31 @@ export default function Retiros() {
})} })}
</tr> </tr>
))} ))}
{filasFiltradas.length > 0 && (
<tr style={{ background: "#dbeafe", fontWeight: 800, position: "sticky", bottom: 0 }}>
{COLS.map((c, ci) => {
if (ci === 0) return (
<td key={c.k} style={{ textAlign: "left", padding: "10px 12px", color: "#1e3a5f",
borderRight: "1px solid #bfdbfe", borderTop: "2px solid #93c5fd" }}>
TOTAL GENERAL
</td>
);
if (COLS_SUMA.includes(c.k)) {
const tot = filasFiltradas.reduce((a, f) => a + (Number(f[c.k]) || 0), 0);
const val = c.money
? "S/ " + tot.toLocaleString("es-PE", { maximumFractionDigits: 0 })
: tot;
return (
<td key={c.k} style={{ textAlign: "center", color: "#1e3a5f",
borderRight: "1px solid #bfdbfe", borderTop: "2px solid #93c5fd" }}>
{val}
</td>
);
}
return <td key={c.k} style={{ borderRight: "1px solid #bfdbfe", borderTop: "2px solid #93c5fd" }} />;
})}
</tr>
)}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -429,6 +493,43 @@ export default function Retiros() {
</div> </div>
)} )}
{quitar && (
<div onClick={() => !quitando && setQuitar(null)}
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.55)",
backdropFilter: "blur(2px)", display: "flex", alignItems: "center",
justifyContent: "center", zIndex: 1100, padding: 16 }}>
<div onClick={(e) => e.stopPropagation()}
style={{ background: "#fff", borderRadius: 16, width: 440, maxWidth: "94vw",
overflow: "hidden", boxShadow: "0 24px 70px rgba(15,23,42,.35)", border: "1px solid #e2e8f0" }}>
<div style={{ background: "linear-gradient(135deg, #b45309 0%, #d97706 100%)",
color: "#fff", padding: "18px 22px", display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: "rgba(255,255,255,.15)",
display: "flex", alignItems: "center", justifyContent: "center", fontSize: 20 }}>↩️</div>
<div style={{ fontWeight: 700, fontSize: 16 }}>Pasar a EN CURSO</div>
</div>
<div style={{ padding: "22px", fontSize: 14, color: "#334155", lineHeight: 1.6 }}>
El alumno <b>{quitar.alumno || quitar.matricula}</b> pasará a <b style={{ color: "#15803d" }}>EN CURSO</b> y
dejará de aparecer en la tabla de Retiros de <b>{MESES[mes-1]} {ano}</b>.
</div>
<div style={{ padding: "0 22px 20px", display: "flex", justifyContent: "flex-end", gap: 10 }}>
<button onClick={() => setQuitar(null)} disabled={quitando}
style={{ padding: "9px 18px", borderRadius: 9, border: "1px solid #cbd5e1",
background: "#fff", color: "#475569", fontWeight: 600, fontSize: 13.5,
cursor: quitando ? "default" : "pointer", opacity: quitando ? .6 : 1 }}>
Cancelar
</button>
<button onClick={confirmarQuitar} disabled={quitando}
style={{ padding: "9px 20px", borderRadius: 9, border: "none",
background: "linear-gradient(135deg, #b45309 0%, #d97706 100%)",
color: "#fff", fontWeight: 700, fontSize: 13.5,
cursor: quitando ? "default" : "pointer", opacity: quitando ? .7 : 1 }}>
{quitando ? "Guardando…" : "Sí, pasar a EN CURSO"}
</button>
</div>
</div>
</div>
)}
{info && ( {info && (
<div onClick={() => setInfo(null)} <div onClick={() => setInfo(null)}
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.55)", style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.55)",
@@ -698,7 +799,7 @@ function ModalBuscarRetiro({ ano, mes, onClose, onCambio }) {
setGuardando(true); setMsg(null); setGuardando(true); setMsg(null);
try { try {
// Guardar en paralelo (rápido). Cada uno actualiza (matrícula, año, mes). // 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, []))); await Promise.all(cambios.map((c) => api.alumnoEstadoGuardar(c.mat, ano, mes, c.estado, [], "BUSCADOR_RETIROS")));
setOrig({ ...estados }); // ya quedó guardado setOrig({ ...estados }); // ya quedó guardado
if (onCambio) onCambio(); // recargar la tabla principal de Retiros if (onCambio) onCambio(); // recargar la tabla principal de Retiros
setMsg({ ok: true, txt: `✅ ${cambios.length} cambio(s) guardado(s).` }); setMsg({ ok: true, txt: `✅ ${cambios.length} cambio(s) guardado(s).` });