Actualizar frontend/src/pages/Retiros.jsx
This commit is contained in:
@@ -7,6 +7,33 @@ import { useColumnasAjustables } from "../lib/useColumnasAjustables";
|
||||
const MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"];
|
||||
const ANOS = [2024, 2025, 2026];
|
||||
|
||||
// ── Ordenamiento (detecta número/monto, fecha yyyy-mm-dd o dd/mm/yyyy, o texto) ──
|
||||
function _valorOrden(v) {
|
||||
const s = String(v ?? "").trim();
|
||||
if (s === "" || s === "—" || s === "-") return { t: "vacio", n: 0, s: "" };
|
||||
const iso = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
||||
if (iso) { const [, y, m, d] = iso; return { t: "fecha", n: new Date(+y, +m - 1, +d).getTime(), s }; }
|
||||
const dmy = s.match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})$/);
|
||||
if (dmy) { const [, d, m, y] = dmy; return { t: "fecha", n: new Date(+y, +m - 1, +d).getTime(), s }; }
|
||||
const limpio = s.replace(/S\/|%|,/g, "").replace(/\s/g, "").trim();
|
||||
if (limpio !== "" && !isNaN(Number(limpio))) return { t: "num", n: Number(limpio), s };
|
||||
return { t: "texto", n: 0, s: s.toLowerCase() };
|
||||
}
|
||||
function _cmp(a, b, dir) {
|
||||
const mult = dir === "asc" ? 1 : -1;
|
||||
const va = _valorOrden(a), vb = _valorOrden(b);
|
||||
if (va.t === "vacio" && vb.t === "vacio") return 0;
|
||||
if (va.t === "vacio") return 1;
|
||||
if (vb.t === "vacio") return -1;
|
||||
if (va.t === "texto" || vb.t === "texto") return mult * va.s.localeCompare(vb.s, "es");
|
||||
return mult * (va.n - vb.n);
|
||||
}
|
||||
function _flecha(activo, dir) {
|
||||
if (!activo) return <span style={{ opacity: 0.3, marginLeft: 4 }}>⇅</span>;
|
||||
return <span style={{ marginLeft: 4 }}>{dir === "asc" ? "▲" : "▼"}</span>;
|
||||
}
|
||||
|
||||
// Columnas de la tabla: key, título, alineación y ancho inicial.
|
||||
const COLS = [
|
||||
{ k: "fch_matricula", t: "FECHA MATRÍCULA", al: "center", w: 120, fecha: true },
|
||||
{ k: "asesor", t: "SECTORISTA", al: "left", w: 190, elip: true },
|
||||
@@ -21,6 +48,7 @@ const COLS = [
|
||||
{ k: "monto_venc_ant", t: "MONTO VENCIDOS MES ANTERIOR", al: "center", w: 150, money: true },
|
||||
{ k: "monto_venc_cur", t: "MONTO VENCIDO MES EN CURSO", al: "center", w: 150, money: true },
|
||||
{ k: "monto_dejo_pagar", t: "MONTO DEJO DE PAGAR", al: "center", w: 150, money: true },
|
||||
// Columnas para llenar manualmente (por ahora vacías)
|
||||
{ k: "n_asistencia", t: "N° ASISTENCIA", al: "center", w: 130, vacio: true },
|
||||
{ k: "n_faltas", t: "N° FALTAS", al: "center", w: 90, vacio: true },
|
||||
{ k: "ultimo_dia_asist", t: "ULTIMO DIA DE ASISTENCIA", al: "center", w: 210, vacio: true },
|
||||
@@ -34,6 +62,7 @@ const COLS = [
|
||||
{ k: "_info", t: "INFO", al: "center", w: 80, info: true },
|
||||
];
|
||||
|
||||
// Campos editables del popup INFO (los 9 manuales)
|
||||
const INFO_CAMPOS = [
|
||||
{ k: "n_asistencia", t: "N° ASISTENCIA", tipo: "number" },
|
||||
{ k: "n_faltas", t: "N° FALTAS", tipo: "number" },
|
||||
@@ -51,9 +80,17 @@ const INFO_VACIO = () => Object.fromEntries(INFO_CAMPOS.map((c) => [c.k, ""]));
|
||||
export default function Retiros() {
|
||||
const hoy = new Date();
|
||||
const [ano, setAno] = useState(hoy.getFullYear());
|
||||
const [mes, setMes] = useState(hoy.getMonth() + 1);
|
||||
const [mes, setMes] = useState(hoy.getMonth() + 1); // mes actual (sin TODOS)
|
||||
const [sede, setSede] = useState("TODOS");
|
||||
const [sectorista, setSectorista] = useState("TODOS");
|
||||
const [ordCol, setOrdCol] = useState(null); // key de la columna ordenada
|
||||
const [ordDir, setOrdDir] = useState(null);
|
||||
const clickOrden = (k, esInfo) => {
|
||||
if (esInfo) return;
|
||||
if (ordCol !== k) { setOrdCol(k); setOrdDir("asc"); }
|
||||
else if (ordDir === "asc") setOrdDir("desc");
|
||||
else { setOrdCol(null); setOrdDir(null); }
|
||||
};
|
||||
|
||||
const [filas, setFilas] = useState([]);
|
||||
const [guardado, setGuardado] = useState(false); // ¿mostrando snapshot guardado?
|
||||
@@ -61,13 +98,18 @@ export default function Retiros() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const [info, setInfo] = useState(null);
|
||||
// Popup INFO
|
||||
const [info, setInfo] = useState(null); // fila abierta (o null)
|
||||
const [form, setForm] = useState(INFO_VACIO());
|
||||
const [guardando, setGuardando] = useState(false);
|
||||
const [errGuardar, setErrGuardar] = useState(null);
|
||||
|
||||
// GUARDAR total (snapshot del mes)
|
||||
const [guardandoTotal, setGuardandoTotal] = useState(false);
|
||||
const [msgTotal, setMsgTotal] = useState(null);
|
||||
const [confirmReemplazo, setConfirmReemplazo] = useState(false);
|
||||
|
||||
// Exportar
|
||||
const [exportando, setExportando] = useState(false);
|
||||
|
||||
const abrirInfo = (f) => {
|
||||
@@ -82,6 +124,7 @@ export default function Retiros() {
|
||||
setGuardando(true); setErrGuardar(null);
|
||||
try {
|
||||
await api.retirosInfoGuardar({ matricula: info.matricula, ano, mes, ...form });
|
||||
// reflejar en memoria (sin recargar -> sin egress extra)
|
||||
setFilas((prev) => prev.map((f) =>
|
||||
f.matricula === info.matricula ? { ...f, ...form } : f));
|
||||
setInfo(null);
|
||||
@@ -94,8 +137,16 @@ export default function Retiros() {
|
||||
|
||||
const guardarTotal = async () => {
|
||||
if (guardandoTotal) return;
|
||||
// Si el mes YA tiene datos guardados, abrir el pop-up de confirmación.
|
||||
if (guardado) { setConfirmReemplazo(true); return; }
|
||||
await _hacerGuardadoTotal();
|
||||
};
|
||||
|
||||
const _hacerGuardadoTotal = async () => {
|
||||
setConfirmReemplazo(false);
|
||||
setGuardandoTotal(true); setMsgTotal(null);
|
||||
try {
|
||||
// Respeta solo Año y Mes: se guardan TODAS las filas del mes cargado (ignora Sede/Sectorista)
|
||||
const r = await api.retirosSnapshotGuardar({ ano, mes, filas });
|
||||
setMsgTotal({ err: false, txt: `Guardado: ${r.guardados} registro(s) de ${MESES[mes-1]} ${ano}.` });
|
||||
} catch (e) {
|
||||
@@ -105,6 +156,7 @@ 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;
|
||||
setLoading(true); setError(null); setMsgTotal(null);
|
||||
@@ -114,32 +166,42 @@ export default function Retiros() {
|
||||
return () => { activo = false; };
|
||||
}, [ano, mes]);
|
||||
|
||||
// Opciones de sectorista (de los datos cargados)
|
||||
const sectoristas = useMemo(() => {
|
||||
const s = [...new Set(filas.map((f) => f.asesor).filter(Boolean))].sort();
|
||||
return ["TODOS", ...s];
|
||||
}, [filas]);
|
||||
|
||||
// Opciones de sede (de los datos cargados)
|
||||
const sedes = useMemo(() => {
|
||||
const s = [...new Set(filas.map((f) => f.sede).filter(Boolean))].sort();
|
||||
return ["TODOS", ...s];
|
||||
}, [filas]);
|
||||
|
||||
// El backend ya trae solo el mes; aquí solo se aplican Sede y Sectorista (cliente).
|
||||
const filasFiltradas = useMemo(() => {
|
||||
return filas.filter((f) => {
|
||||
let base = filas.filter((f) => {
|
||||
if (sede !== "TODOS" && f.sede !== sede) return false;
|
||||
if (sectorista !== "TODOS" && f.asesor !== sectorista) return false;
|
||||
return true;
|
||||
});
|
||||
}, [filas, sede, sectorista]);
|
||||
if (ordCol != null && ordDir) {
|
||||
base = [...base].sort((a, b) => _cmp(a[ordCol], b[ordCol], ordDir));
|
||||
}
|
||||
return base;
|
||||
}, [filas, sede, sectorista, ordCol, ordDir]);
|
||||
|
||||
// Fecha yyyy-mm-dd -> dd/mm/yyyy
|
||||
const fmtFecha = (s) => {
|
||||
if (!s) return "-";
|
||||
const [y, m, d] = String(s).split("-");
|
||||
return (y && m && d) ? `${d}/${m}/${y}` : s;
|
||||
};
|
||||
|
||||
// Columnas con ancho ajustable manualmente (arrastrar el borde)
|
||||
const cols = useColumnasAjustables(COLS.map((c) => c.w));
|
||||
|
||||
// Exportar la tabla principal a Excel (respeta TODOS los filtros: Año/Mes/Sede/Sectorista)
|
||||
async function exportar() {
|
||||
if (exportando) return;
|
||||
const base = filasFiltradas;
|
||||
@@ -147,10 +209,10 @@ export default function Retiros() {
|
||||
setExportando(true);
|
||||
try {
|
||||
const XLSX = await import("xlsx-js-style");
|
||||
const COLS_EXP = COLS.filter((c) => !c.info);
|
||||
const COLS_EXP = COLS.filter((c) => !c.info); // sin la columna INFO
|
||||
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"}} };
|
||||
const manualStyle = { fill:{fgColor:{rgb:"EEF4F1"}} }; // celdas manuales verde suave
|
||||
const moneyFmt = '"S/" #,##0';
|
||||
const ws = {};
|
||||
COLS_EXP.forEach((c, ci) => { ws[XLSX.utils.encode_cell({r:0,c:ci})] = {v:c.t, t:"s", s:headerStyle}; });
|
||||
@@ -163,7 +225,7 @@ export default function Retiros() {
|
||||
if (c.money) {
|
||||
ws[ref] = { v: Number(raw)||0, t:"n", z:moneyFmt, ...st };
|
||||
} else if (c.fecha) {
|
||||
ws[ref] = { v: raw ? fmtFecha(raw) : "", t:"s", ...st };
|
||||
ws[ref] = { v: raw ? fmtFecha(raw) : "", t:"s", ...st }; // texto dd/mm/aaaa
|
||||
} else if (c.k === "num_cuotas_pagadas" || c.k === "num_cuotas_vencidas") {
|
||||
ws[ref] = { v: raw ?? 0, t:"n", ...st };
|
||||
} else if (c.vacio && ["ultimo_dia_asist","fch_ult_resp","fch_envio_correo"].includes(c.k)) {
|
||||
@@ -245,13 +307,15 @@ export default function Retiros() {
|
||||
<thead>
|
||||
<tr>
|
||||
{COLS.map((c, i) => (
|
||||
<th key={c.k} title={c.t}
|
||||
<th key={c.k} title={c.t} onClick={() => clickOrden(c.k, c.info)}
|
||||
style={{ textAlign: "center", position: "relative", whiteSpace: "nowrap",
|
||||
overflow: "hidden", textOverflow: "ellipsis",
|
||||
borderRight: "1px solid #cbd5e1",
|
||||
cursor: c.info ? "default" : "pointer", userSelect: "none",
|
||||
// Encabezado de las columnas manuales en verde
|
||||
background: c.vacio ? "#7a9d8f" : undefined,
|
||||
color: c.vacio ? "#fff" : undefined }}>
|
||||
{c.t}<cols.Resizer index={i} />
|
||||
{c.t}{!c.info && _flecha(ordCol === c.k, ordDir)}<cols.Resizer index={i} />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
@@ -282,6 +346,7 @@ export default function Retiros() {
|
||||
}
|
||||
let val = f[c.k];
|
||||
if (c.vacio) {
|
||||
// Columnas manuales: muestran lo guardado (fechas en dd/mm/yyyy)
|
||||
const raw = f[c.k];
|
||||
if (raw === null || raw === undefined || raw === "") val = "";
|
||||
else if (["ultimo_dia_asist","fch_ult_resp","fch_envio_correo"].includes(c.k))
|
||||
@@ -308,6 +373,7 @@ 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,
|
||||
}}>
|
||||
{val}
|
||||
@@ -321,6 +387,43 @@ export default function Retiros() {
|
||||
</div>
|
||||
</>}
|
||||
|
||||
{confirmReemplazo && (
|
||||
<div onClick={() => setConfirmReemplazo(false)}
|
||||
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, #6f9284 0%, #86a99a 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 }}>Ya existen datos guardados</div>
|
||||
</div>
|
||||
<div style={{ padding: "22px", fontSize: 14, color: "#334155", lineHeight: 1.6 }}>
|
||||
Ya hay datos guardados de <b>{MESES[mes-1]} {ano}</b>.<br/>
|
||||
Si continúas, se <b style={{ color: "#b45309" }}>reemplazarán</b> por los datos actuales de la tabla.
|
||||
</div>
|
||||
<div style={{ padding: "0 22px 20px", display: "flex", justifyContent: "flex-end", gap: 10 }}>
|
||||
<button onClick={() => setConfirmReemplazo(false)}
|
||||
style={{ padding: "9px 18px", borderRadius: 9, border: "1px solid #cbd5e1",
|
||||
background: "#fff", color: "#475569", fontWeight: 600, fontSize: 13.5, cursor: "pointer" }}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button onClick={_hacerGuardadoTotal}
|
||||
style={{ padding: "9px 20px", borderRadius: 9, border: "none",
|
||||
background: "linear-gradient(135deg, #6f9284 0%, #86a99a 100%)",
|
||||
color: "#fff", fontWeight: 700, fontSize: 13.5, cursor: "pointer",
|
||||
boxShadow: "0 4px 12px rgba(111,146,132,.35)" }}>
|
||||
Sí, reemplazar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{info && (
|
||||
<div onClick={() => setInfo(null)}
|
||||
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.55)",
|
||||
@@ -330,6 +433,7 @@ export default function Retiros() {
|
||||
style={{ background: "#fff", borderRadius: 16, width: 600, maxWidth: "94vw",
|
||||
maxHeight: "90vh", overflow: "hidden", display: "flex", flexDirection: "column",
|
||||
boxShadow: "0 24px 70px rgba(15,23,42,.35)", border: "1px solid #e2e8f0" }}>
|
||||
{/* Cabecera */}
|
||||
<div style={{ background: "linear-gradient(135deg, #6f9284 0%, #86a99a 100%)",
|
||||
color: "#fff", padding: "18px 22px",
|
||||
display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
@@ -348,6 +452,7 @@ export default function Retiros() {
|
||||
lineHeight: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>×</button>
|
||||
</div>
|
||||
|
||||
{/* Formulario */}
|
||||
<div style={{ padding: 22, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16,
|
||||
overflow: "auto", background: "#f8fafc" }}>
|
||||
{INFO_CAMPOS.map((c) => {
|
||||
@@ -378,6 +483,7 @@ export default function Retiros() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Pie */}
|
||||
<div style={{ padding: "16px 22px", display: "flex", justifyContent: "flex-end",
|
||||
alignItems: "center", gap: 10,
|
||||
borderTop: "1px solid #e2e8f0", background: "#fff" }}>
|
||||
|
||||
Reference in New Issue
Block a user