Añadir frontend/src/pages/Retiros.jsx
This commit is contained in:
407
frontend/src/pages/Retiros.jsx
Normal file
407
frontend/src/pages/Retiros.jsx
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
// src/pages/Retiros.jsx
|
||||||
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
import { api } from "../lib/api";
|
||||||
|
import { Loader, ErrorBox, Filters, Select } from "../components/UI";
|
||||||
|
import { useColumnasAjustables } from "../lib/useColumnasAjustables";
|
||||||
|
|
||||||
|
const MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"];
|
||||||
|
const ANOS = [2024, 2025, 2026];
|
||||||
|
|
||||||
|
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 },
|
||||||
|
{ k: "vendedor", t: "ASESOR COMERCIAL", al: "left", w: 190, elip: true },
|
||||||
|
{ k: "programa", t: "PROGRAMA", al: "left", w: 260, elip: true },
|
||||||
|
{ k: "cod_detalle", t: "COD. DETALLE", al: "center", w: 100 },
|
||||||
|
{ k: "fch_inicio", t: "FECHA INICIO", al: "center", w: 110, fecha: true },
|
||||||
|
{ k: "alumno", t: "ALUMNO", al: "left", w: 220, elip: true },
|
||||||
|
{ k: "num_cuotas_pagadas", t: "N° CUOTAS PAGADAS", al: "center", w: 110 },
|
||||||
|
{ k: "num_cuotas_vencidas", t: "N° CUOTAS VENCIDAS", al: "center", w: 110 },
|
||||||
|
{ k: "fch_vencimiento", t: "FECHA DE VENCIMIENTO", al: "center", w: 140, fecha: true },
|
||||||
|
{ 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 },
|
||||||
|
{ 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 },
|
||||||
|
{ k: "hasta_sesion", t: "ÚLTIMA SESIÓN DE ASISTENCIA", al: "center", w: 230, vacio: true },
|
||||||
|
{ k: "fch_ult_resp", t: "FECHA DE ULTIMA RESPUESTA DE ALUMNO", al: "center", w: 280, vacio: true },
|
||||||
|
{ k: "fch_envio_correo", t: "FECHA DE ENVIO DE CORREO", al: "center", w: 192, vacio: true },
|
||||||
|
{ k: "retiro_wsp_class", t: "¿SE RETIRÓ DE WHATSAPP Y CLASSROOM?", al: "center", w: 288, vacio: true },
|
||||||
|
{ k: "motivo_retiro", t: "MOTIVO DE RETIRO", al: "left", w: 360, 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: "_info", t: "INFO", al: "center", w: 80, info: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const INFO_CAMPOS = [
|
||||||
|
{ k: "n_asistencia", t: "N° ASISTENCIA", tipo: "number" },
|
||||||
|
{ k: "n_faltas", t: "N° FALTAS", tipo: "number" },
|
||||||
|
{ k: "ultimo_dia_asist", t: "ÚLTIMO DÍA DE ASISTENCIA", tipo: "date" },
|
||||||
|
{ k: "hasta_sesion", t: "ÚLTIMA SESIÓN DE ASISTENCIA", tipo: "text" },
|
||||||
|
{ k: "fch_ult_resp", t: "FECHA DE ÚLTIMA RESPUESTA DE ALUMNO", tipo: "date" },
|
||||||
|
{ k: "fch_envio_correo", t: "FECHA DE ENVÍO DE CORREO", tipo: "date" },
|
||||||
|
{ k: "retiro_wsp_class", t: "¿SE RETIRÓ DE GRUPO WHATSAPP Y CLASSROOM?", tipo: "sino" },
|
||||||
|
{ k: "motivo_retiro", t: "MOTIVO DE RETIRO", tipo: "text" },
|
||||||
|
{ k: "detalle_retiro", t: "DETALLE DE RETIRO", tipo: "textarea" },
|
||||||
|
];
|
||||||
|
|
||||||
|
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 [sede, setSede] = useState("TODOS");
|
||||||
|
const [sectorista, setSectorista] = useState("TODOS");
|
||||||
|
|
||||||
|
const [filas, setFilas] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const [info, setInfo] = useState(null);
|
||||||
|
const [form, setForm] = useState(INFO_VACIO());
|
||||||
|
const [guardando, setGuardando] = useState(false);
|
||||||
|
const [errGuardar, setErrGuardar] = useState(null);
|
||||||
|
|
||||||
|
const [guardandoTotal, setGuardandoTotal] = useState(false);
|
||||||
|
const [msgTotal, setMsgTotal] = useState(null);
|
||||||
|
const [exportando, setExportando] = useState(false);
|
||||||
|
|
||||||
|
const abrirInfo = (f) => {
|
||||||
|
setErrGuardar(null);
|
||||||
|
setForm({ ...INFO_VACIO(), ...Object.fromEntries(INFO_CAMPOS.map((c) => [c.k, f[c.k] ?? ""])) });
|
||||||
|
setInfo(f);
|
||||||
|
};
|
||||||
|
const setCampo = (k, v) => setForm((prev) => ({ ...prev, [k]: v }));
|
||||||
|
|
||||||
|
const guardarInfo = async () => {
|
||||||
|
if (!info || guardando) return;
|
||||||
|
setGuardando(true); setErrGuardar(null);
|
||||||
|
try {
|
||||||
|
await api.retirosInfoGuardar({ matricula: info.matricula, ano, mes, ...form });
|
||||||
|
setFilas((prev) => prev.map((f) =>
|
||||||
|
f.matricula === info.matricula ? { ...f, ...form } : f));
|
||||||
|
setInfo(null);
|
||||||
|
} catch (e) {
|
||||||
|
setErrGuardar(e.message || "No se pudo guardar");
|
||||||
|
} finally {
|
||||||
|
setGuardando(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const guardarTotal = async () => {
|
||||||
|
if (guardandoTotal) return;
|
||||||
|
setGuardandoTotal(true); setMsgTotal(null);
|
||||||
|
try {
|
||||||
|
const r = await api.retirosSnapshotGuardar({ ano, mes, filas });
|
||||||
|
setMsgTotal({ err: false, txt: `Guardado: ${r.guardados} registro(s) de ${MESES[mes-1]} ${ano}.` });
|
||||||
|
} catch (e) {
|
||||||
|
setMsgTotal({ err: true, txt: e.message || "No se pudo guardar" });
|
||||||
|
} finally {
|
||||||
|
setGuardandoTotal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let activo = true;
|
||||||
|
setLoading(true); setError(null); setMsgTotal(null);
|
||||||
|
api.retiros(ano, mes)
|
||||||
|
.then((r) => { if (activo) { setFilas(r.filas || []); setLoading(false); } })
|
||||||
|
.catch((e) => { if (activo) { setError(e.message); setLoading(false); } });
|
||||||
|
return () => { activo = false; };
|
||||||
|
}, [ano, mes]);
|
||||||
|
|
||||||
|
const sectoristas = useMemo(() => {
|
||||||
|
const s = [...new Set(filas.map((f) => f.asesor).filter(Boolean))].sort();
|
||||||
|
return ["TODOS", ...s];
|
||||||
|
}, [filas]);
|
||||||
|
|
||||||
|
const sedes = useMemo(() => {
|
||||||
|
const s = [...new Set(filas.map((f) => f.sede).filter(Boolean))].sort();
|
||||||
|
return ["TODOS", ...s];
|
||||||
|
}, [filas]);
|
||||||
|
|
||||||
|
const filasFiltradas = useMemo(() => {
|
||||||
|
return filas.filter((f) => {
|
||||||
|
if (sede !== "TODOS" && f.sede !== sede) return false;
|
||||||
|
if (sectorista !== "TODOS" && f.asesor !== sectorista) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [filas, sede, sectorista]);
|
||||||
|
|
||||||
|
const fmtFecha = (s) => {
|
||||||
|
if (!s) return "-";
|
||||||
|
const [y, m, d] = String(s).split("-");
|
||||||
|
return (y && m && d) ? `${d}/${m}/${y}` : s;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cols = useColumnasAjustables(COLS.map((c) => c.w));
|
||||||
|
|
||||||
|
async function exportar() {
|
||||||
|
if (exportando) return;
|
||||||
|
const base = filasFiltradas;
|
||||||
|
if (!base.length) { alert("No hay datos para exportar con los filtros actuales."); return; }
|
||||||
|
setExportando(true);
|
||||||
|
try {
|
||||||
|
const XLSX = await import("xlsx-js-style");
|
||||||
|
const COLS_EXP = COLS.filter((c) => !c.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 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}; });
|
||||||
|
base.forEach((f, ri) => {
|
||||||
|
const r = ri + 1;
|
||||||
|
COLS_EXP.forEach((c, ci) => {
|
||||||
|
const ref = XLSX.utils.encode_cell({r, c:ci});
|
||||||
|
const st = c.vacio ? { s: manualStyle } : {};
|
||||||
|
const raw = f[c.k];
|
||||||
|
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 };
|
||||||
|
} 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)) {
|
||||||
|
ws[ref] = { v: raw ? fmtFecha(raw) : "", t:"s", ...st };
|
||||||
|
} else {
|
||||||
|
ws[ref] = { v: (raw ?? "") === "" ? "" : String(raw), t:"s", ...st };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ws["!ref"] = XLSX.utils.encode_range({ s:{r:0,c:0}, e:{r:base.length, c:COLS_EXP.length-1} });
|
||||||
|
ws["!cols"] = COLS_EXP.map((c) => ({ wch: Math.max(10, Math.round(c.w/7)) }));
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, "Retiros");
|
||||||
|
const suf = `${MESES[mes-1]}_${ano}` + (sede!=="TODOS"?`_${sede}`:"") + (sectorista!=="TODOS"?`_${sectorista}`:"");
|
||||||
|
XLSX.writeFile(wb, `retiros_${suf}.xlsx`);
|
||||||
|
} finally {
|
||||||
|
setExportando(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="page-title">🚪 Retiros</h1>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
|
||||||
|
<Filters>
|
||||||
|
<Select label="Año" value={ano} options={ANOS} onChange={(v)=>setAno(+v)} />
|
||||||
|
<Select label="Mes" value={mes}
|
||||||
|
options={MESES.map((m,i)=>({value:i+1,label:m}))}
|
||||||
|
onChange={(v)=>setMes(+v)} />
|
||||||
|
<Select label="Sede" value={sede} options={sedes} onChange={setSede} />
|
||||||
|
<Select label="Sectorista" value={sectorista} options={sectoristas} onChange={setSectorista} width={250} />
|
||||||
|
</Filters>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||||
|
{msgTotal && (
|
||||||
|
<span style={{ fontSize: 12.5, fontWeight: 600,
|
||||||
|
color: msgTotal.err ? "#dc2626" : "#15803d" }}>
|
||||||
|
{msgTotal.txt}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<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",
|
||||||
|
background: "#fff", color: "#334155", fontWeight: 700, fontSize: 13.5,
|
||||||
|
cursor: (exportando || !filasFiltradas.length) ? "default" : "pointer",
|
||||||
|
opacity: (exportando || !filasFiltradas.length) ? .6 : 1, whiteSpace: "nowrap" }}>
|
||||||
|
{exportando ? "Exportando…" : "⬇️ Exportar"}
|
||||||
|
</button>
|
||||||
|
<button onClick={guardarTotal} disabled={guardandoTotal}
|
||||||
|
title="Guarda todas las columnas del mes seleccionado en Supabase"
|
||||||
|
style={{ padding: "9px 20px", borderRadius: 9, border: "none",
|
||||||
|
background: "linear-gradient(135deg, #6f9284 0%, #86a99a 100%)",
|
||||||
|
color: "#fff", fontWeight: 700, fontSize: 13.5,
|
||||||
|
cursor: guardandoTotal ? "default" : "pointer",
|
||||||
|
opacity: guardandoTotal ? .7 : 1,
|
||||||
|
boxShadow: "0 4px 12px rgba(111,146,132,.35)", whiteSpace: "nowrap" }}>
|
||||||
|
{guardandoTotal ? "Guardando…" : "💾 GUARDAR"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? <Loader text="Cargando retiros..." /> :
|
||||||
|
error ? <ErrorBox msg={error} /> :
|
||||||
|
<>
|
||||||
|
<div style={{ fontSize: 13, color: "#1e40af", fontWeight: 600, margin: "10px 0 8px" }}>
|
||||||
|
{filasFiltradas.length} alumno(s) por retirar
|
||||||
|
</div>
|
||||||
|
<div className="table-wrap" style={{ overflow: "auto", maxHeight: "65vh" }}>
|
||||||
|
<table className="cob-table" {...cols.tableProps}>
|
||||||
|
<cols.ColGroup />
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{COLS.map((c, i) => (
|
||||||
|
<th key={c.k} title={c.t}
|
||||||
|
style={{ textAlign: "center", position: "relative", whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden", textOverflow: "ellipsis",
|
||||||
|
borderRight: "1px solid #cbd5e1",
|
||||||
|
background: c.vacio ? "#7a9d8f" : undefined,
|
||||||
|
color: c.vacio ? "#fff" : undefined }}>
|
||||||
|
{c.t}<cols.Resizer index={i} />
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filasFiltradas.length === 0 && (
|
||||||
|
<tr><td colSpan={COLS.length} style={{ textAlign: "center", color: "#94a3b8", padding: 24 }}>
|
||||||
|
No hay alumnos por retirar con estos filtros.
|
||||||
|
</td></tr>
|
||||||
|
)}
|
||||||
|
{filasFiltradas.map((f, i) => (
|
||||||
|
<tr key={f.matricula + i}>
|
||||||
|
{COLS.map((c) => {
|
||||||
|
if (c.info) {
|
||||||
|
return (
|
||||||
|
<td key={c.k} style={{ textAlign: "center", borderRight: "1px solid #e2e8f0", borderBottom: "1px solid #e2e8f0" }}>
|
||||||
|
<button onClick={() => abrirInfo(f)}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.background = "#f8fafc"; e.currentTarget.style.borderColor = "#94a3b8"; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.background = "#fff"; e.currentTarget.style.borderColor = "#cbd5e1"; }}
|
||||||
|
style={{ background: "#fff", color: "#334155", border: "1px solid #cbd5e1",
|
||||||
|
borderRadius: 6, padding: "4px 12px", fontSize: 11.5,
|
||||||
|
fontWeight: 700, letterSpacing: .3, cursor: "pointer",
|
||||||
|
transition: "background .15s, border-color .15s" }}>
|
||||||
|
INFO
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let val = f[c.k];
|
||||||
|
if (c.vacio) {
|
||||||
|
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))
|
||||||
|
val = fmtFecha(raw);
|
||||||
|
else val = raw;
|
||||||
|
}
|
||||||
|
else if (c.fecha) val = f[c.k] ? fmtFecha(f[c.k]) : "-";
|
||||||
|
else if (c.money) val = "S/ " + Number(f[c.k] || 0).toLocaleString("es-PE", { maximumFractionDigits: 0 });
|
||||||
|
else if (c.k === "num_cuotas_pagadas" || c.k === "num_cuotas_vencidas") val = f[c.k] ?? 0;
|
||||||
|
else val = val || "-";
|
||||||
|
const rojo = (c.k === "num_cuotas_vencidas" && (f.num_cuotas_vencidas ?? 0) > 0)
|
||||||
|
|| (c.k === "fch_vencimiento" && f.fch_vencimiento)
|
||||||
|
|| (c.money && Number(f[c.k] || 0) > 0);
|
||||||
|
const bold = c.k === "num_cuotas_pagadas" || c.k === "num_cuotas_vencidas" || c.money;
|
||||||
|
return (
|
||||||
|
<td key={c.k}
|
||||||
|
title={String(val ?? "")}
|
||||||
|
style={{
|
||||||
|
textAlign: c.al,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
fontWeight: bold ? 700 : undefined,
|
||||||
|
color: rojo ? "#dc2626" : undefined,
|
||||||
|
borderRight: "1px solid #e2e8f0",
|
||||||
|
borderBottom: "1px solid #e2e8f0",
|
||||||
|
background: c.vacio ? "#eef4f1" : undefined,
|
||||||
|
}}>
|
||||||
|
{val}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>}
|
||||||
|
|
||||||
|
{info && (
|
||||||
|
<div onClick={() => setInfo(null)}
|
||||||
|
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.55)",
|
||||||
|
backdropFilter: "blur(2px)", display: "flex", alignItems: "center",
|
||||||
|
justifyContent: "center", zIndex: 1000, padding: 16 }}>
|
||||||
|
<div onClick={(e) => e.stopPropagation()}
|
||||||
|
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" }}>
|
||||||
|
<div style={{ background: "linear-gradient(135deg, #6f9284 0%, #86a99a 100%)",
|
||||||
|
color: "#fff", padding: "18px 22px",
|
||||||
|
display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<div style={{ 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>
|
||||||
|
<div style={{ fontWeight: 700, fontSize: 16, letterSpacing: .2 }}>Información de retiro</div>
|
||||||
|
<div style={{ fontSize: 12.5, opacity: .85, marginTop: 2 }}>{info.alumno || info.matricula}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setInfo(null)}
|
||||||
|
style={{ background: "rgba(255,255,255,.12)", color: "#fff", border: "none",
|
||||||
|
width: 32, height: 32, borderRadius: 8, fontSize: 20, cursor: "pointer",
|
||||||
|
lineHeight: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: 22, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16,
|
||||||
|
overflow: "auto", background: "#f8fafc" }}>
|
||||||
|
{INFO_CAMPOS.map((c) => {
|
||||||
|
const full = c.tipo === "textarea" || c.k === "hasta_sesion" || c.k === "motivo_retiro";
|
||||||
|
return (
|
||||||
|
<div key={c.k} style={{ gridColumn: full ? "1 / -1" : "auto" }}>
|
||||||
|
<label style={{ display: "block", fontSize: 11.5, fontWeight: 700,
|
||||||
|
color: "#475569", marginBottom: 6, textTransform: "uppercase",
|
||||||
|
letterSpacing: .3 }}>{c.t}</label>
|
||||||
|
{c.tipo === "sino" ? (
|
||||||
|
<select value={form[c.k]} onChange={(e) => setCampo(c.k, e.target.value)}
|
||||||
|
onFocus={onFoco} onBlur={offFoco} style={inpStyle}>
|
||||||
|
<option value="">—</option>
|
||||||
|
<option value="SI">SI</option>
|
||||||
|
<option value="NO">NO</option>
|
||||||
|
</select>
|
||||||
|
) : c.tipo === "textarea" ? (
|
||||||
|
<textarea value={form[c.k]} onChange={(e) => setCampo(c.k, e.target.value)}
|
||||||
|
onFocus={onFoco} onBlur={offFoco}
|
||||||
|
rows={3} style={{ ...inpStyle, resize: "vertical" }} />
|
||||||
|
) : (
|
||||||
|
<input type={c.tipo} value={form[c.k]}
|
||||||
|
onChange={(e) => setCampo(c.k, e.target.value)}
|
||||||
|
onFocus={onFoco} onBlur={offFoco} style={inpStyle} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: "16px 22px", display: "flex", justifyContent: "flex-end",
|
||||||
|
alignItems: "center", gap: 10,
|
||||||
|
borderTop: "1px solid #e2e8f0", background: "#fff" }}>
|
||||||
|
{errGuardar && (
|
||||||
|
<span style={{ color: "#dc2626", fontSize: 12.5, fontWeight: 600, marginRight: "auto" }}>
|
||||||
|
{errGuardar}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button onClick={() => setInfo(null)} disabled={guardando}
|
||||||
|
style={{ padding: "9px 18px", borderRadius: 9, border: "1px solid #cbd5e1",
|
||||||
|
background: "#fff", color: "#475569", fontWeight: 600, fontSize: 13.5,
|
||||||
|
cursor: guardando ? "default" : "pointer", opacity: guardando ? .6 : 1 }}>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button onClick={guardarInfo} disabled={guardando}
|
||||||
|
style={{ padding: "9px 20px", borderRadius: 9, border: "none",
|
||||||
|
background: "linear-gradient(135deg, #6f9284 0%, #86a99a 100%)",
|
||||||
|
color: "#fff", fontWeight: 700, fontSize: 13.5,
|
||||||
|
cursor: guardando ? "default" : "pointer", opacity: guardando ? .7 : 1,
|
||||||
|
boxShadow: "0 4px 12px rgba(111,146,132,.35)" }}>
|
||||||
|
{guardando ? "Guardando…" : "Guardar"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inpStyle = {
|
||||||
|
width: "100%", padding: "9px 11px", border: "1px solid #cbd5e1",
|
||||||
|
borderRadius: 9, fontSize: 13.5, boxSizing: "border-box", outline: "none",
|
||||||
|
background: "#fff", transition: "border-color .15s, box-shadow .15s",
|
||||||
|
};
|
||||||
|
const onFoco = (e) => { e.target.style.borderColor = "#7a9d8f"; e.target.style.boxShadow = "0 0 0 3px rgba(122,157,143,.18)"; };
|
||||||
|
const offFoco = (e) => { e.target.style.borderColor = "#cbd5e1"; e.target.style.boxShadow = "none"; };
|
||||||
Reference in New Issue
Block a user