Actualizar frontend/src/pages/Retiros.jsx
This commit is contained in:
@@ -60,8 +60,12 @@ const COLS = [
|
||||
{ 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: "_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)
|
||||
const INFO_CAMPOS = [
|
||||
{ k: "n_asistencia", t: "N° ASISTENCIA", tipo: "number" },
|
||||
@@ -112,6 +116,24 @@ export default function Retiros() {
|
||||
// Exportar
|
||||
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) => {
|
||||
setErrGuardar(null);
|
||||
setForm({ ...INFO_VACIO(), ...Object.fromEntries(INFO_CAMPOS.map((c) => [c.k, f[c.k] ?? ""])) });
|
||||
@@ -209,7 +231,7 @@ export default function Retiros() {
|
||||
setExportando(true);
|
||||
try {
|
||||
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},
|
||||
fill:{fgColor:{rgb:"1E40AF"}}, alignment:{horizontal:"center",vertical:"center"} };
|
||||
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="Sectorista" opciones={sectoristas} seleccion={sectoristasSel} onChange={setSectoristasSel} plural="sectoristas" minWidth={250} />
|
||||
</Filters>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 18 }}>
|
||||
{msgTotal && (
|
||||
<span style={{ fontSize: 12.5, fontWeight: 600,
|
||||
<span style={{ fontSize: 12.5, fontWeight: 600, alignSelf: "center",
|
||||
color: msgTotal.err ? "#dc2626" : "#15803d" }}>
|
||||
{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",
|
||||
style={{ height: 40, padding: "0 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",
|
||||
style={{ height: 40, padding: "0 18px", borderRadius: 9, border: "1px solid #cbd5e1",
|
||||
background: "#fff", color: "#334155", fontWeight: 700, fontSize: 13.5,
|
||||
cursor: (exportando || !filasFiltradas.length) ? "default" : "pointer",
|
||||
opacity: (exportando || !filasFiltradas.length) ? .6 : 1, whiteSpace: "nowrap" }}>
|
||||
@@ -281,7 +303,7 @@ export default function Retiros() {
|
||||
</button>
|
||||
<button onClick={guardarTotal} disabled={guardandoTotal}
|
||||
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%)",
|
||||
color: "#fff", fontWeight: 700, fontSize: 13.5,
|
||||
cursor: guardandoTotal ? "default" : "pointer",
|
||||
@@ -312,7 +334,7 @@ export default function Retiros() {
|
||||
<thead>
|
||||
<tr>
|
||||
{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",
|
||||
overflow: "hidden", textOverflow: "ellipsis",
|
||||
borderRight: "1px solid #cbd5e1",
|
||||
@@ -320,7 +342,7 @@ export default function Retiros() {
|
||||
// Encabezado de las columnas manuales en verde
|
||||
background: c.vacio ? "#7a9d8f" : 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>
|
||||
))}
|
||||
</tr>
|
||||
@@ -332,8 +354,24 @@ export default function Retiros() {
|
||||
</td></tr>
|
||||
)}
|
||||
{filasFiltradas.map((f, i) => (
|
||||
<tr key={f.matricula + i}>
|
||||
<tr key={f.matricula + i}
|
||||
style={f.origen_buscador ? { background: "#eff6ff" } : undefined}>
|
||||
{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) {
|
||||
return (
|
||||
<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,
|
||||
borderRight: "1px solid #e2e8f0",
|
||||
borderBottom: "1px solid #e2e8f0",
|
||||
// Fondo verde suave para las celdas de columnas manuales
|
||||
background: c.vacio ? "#eef4f1" : undefined,
|
||||
// Fondo verde suave para columnas manuales; si la fila viene
|
||||
// del buscador, dejar transparente para que se vea el azul.
|
||||
background: f.origen_buscador ? "transparent" : (c.vacio ? "#eef4f1" : undefined),
|
||||
}}>
|
||||
{val}
|
||||
</td>
|
||||
@@ -387,6 +426,31 @@ export default function Retiros() {
|
||||
})}
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
@@ -429,6 +493,43 @@ export default function Retiros() {
|
||||
</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 && (
|
||||
<div onClick={() => setInfo(null)}
|
||||
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);
|
||||
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, [])));
|
||||
await Promise.all(cambios.map((c) => api.alumnoEstadoGuardar(c.mat, ano, mes, c.estado, [], "BUSCADOR_RETIROS")));
|
||||
setOrig({ ...estados }); // ya quedó guardado
|
||||
if (onCambio) onCambio(); // recargar la tabla principal de Retiros
|
||||
setMsg({ ok: true, txt: `✅ ${cambios.length} cambio(s) guardado(s).` });
|
||||
|
||||
Reference in New Issue
Block a user