Actualizar frontend/src/pages/Ventas.jsx
This commit is contained in:
@@ -14,6 +14,47 @@ const COLS = ["VENDEDOR","INSCRITOS","VENTA TOTAL","INSCRITOS P.C DEL MES","VENT
|
|||||||
const TIPOS = ["Venta Inscritos", "Venta P.C", "Venta Pendientes"];
|
const TIPOS = ["Venta Inscritos", "Venta P.C", "Venta Pendientes"];
|
||||||
const HEADERS_DETALLE = ["VENDEDOR","ALUMNO","F. MATRÍCULA","F. CANCELACIÓN","TIPO","CUOTA PROM.","DESC. ESPECIAL","INV. NETA"];
|
const HEADERS_DETALLE = ["VENDEDOR","ALUMNO","F. MATRÍCULA","F. CANCELACIÓN","TIPO","CUOTA PROM.","DESC. ESPECIAL","INV. NETA"];
|
||||||
|
|
||||||
|
// ── Ordenamiento de tablas (detecta número/soles, fecha dd-mm-yyyy o texto) ──
|
||||||
|
function _valorOrden(v) {
|
||||||
|
const s = String(v ?? "").trim();
|
||||||
|
if (s === "" || s === "—" || s === "-") return { t: "vacio", n: 0, s: "" };
|
||||||
|
// Fecha dd-mm-yyyy o dd/mm/yyyy
|
||||||
|
const mf = s.match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})$/);
|
||||||
|
if (mf) {
|
||||||
|
const [, d, m, y] = mf;
|
||||||
|
return { t: "fecha", n: new Date(+y, +m - 1, +d).getTime(), s };
|
||||||
|
}
|
||||||
|
// Número / soles / porcentaje (quita S/, comas, %, espacios)
|
||||||
|
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() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordena filas (arrays) por la columna idx. dir: "asc" | "desc". Vacíos al final.
|
||||||
|
function _ordenarFilas(filas, idx, dir) {
|
||||||
|
if (idx == null || !dir) return filas;
|
||||||
|
const mult = dir === "asc" ? 1 : -1;
|
||||||
|
return [...filas].sort((a, b) => {
|
||||||
|
const va = _valorOrden(a[idx]);
|
||||||
|
const vb = _valorOrden(b[idx]);
|
||||||
|
if (va.t === "vacio" && vb.t === "vacio") return 0;
|
||||||
|
if (va.t === "vacio") return 1; // vacíos siempre al final
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flecha del encabezado según el estado de orden de esa columna.
|
||||||
|
function _flecha(activo, dir) {
|
||||||
|
if (!activo) return <span style={{ opacity: 0.3, marginLeft: 4 }}>⇅</span>;
|
||||||
|
return <span style={{ marginLeft: 4 }}>{dir === "asc" ? "▲" : "▼"}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Ventas() {
|
export default function Ventas() {
|
||||||
const hoy = new Date();
|
const hoy = new Date();
|
||||||
const [ano, setAno] = useState(hoy.getFullYear());
|
const [ano, setAno] = useState(hoy.getFullYear());
|
||||||
@@ -24,8 +65,18 @@ export default function Ventas() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [modal, setModal] = useState(null); // { vendedor }
|
const [modal, setModal] = useState(null); // { vendedor }
|
||||||
|
const [ordCol, setOrdCol] = useState(null); // índice de columna ordenada
|
||||||
|
const [ordDir, setOrdDir] = useState(null); // "asc" | "desc"
|
||||||
const cols = useColumnasAjustables([200, 100, 130, 120, 130, 120, 130, 140, 100]);
|
const cols = useColumnasAjustables([200, 100, 130, 120, 130, 120, 130, 140, 100]);
|
||||||
|
|
||||||
|
// Click en encabezado: alterna asc -> desc -> sin orden.
|
||||||
|
const clickOrden = (i) => {
|
||||||
|
if (i === COLS.length - 1) return; // columna OPC. no se ordena
|
||||||
|
if (ordCol !== i) { setOrdCol(i); setOrdDir("asc"); }
|
||||||
|
else if (ordDir === "asc") setOrdDir("desc");
|
||||||
|
else { setOrdCol(null); setOrdDir(null); }
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let activo = true;
|
let activo = true;
|
||||||
setVistaActual(`ventas:${ano}|${mes}|${sede}|${programa}|False`);
|
setVistaActual(`ventas:${ano}|${mes}|${sede}|${programa}|False`);
|
||||||
@@ -41,7 +92,10 @@ export default function Ventas() {
|
|||||||
}, [ano, mes, sede, programa]);
|
}, [ano, mes, sede, programa]);
|
||||||
|
|
||||||
const total = useMemo(() => filas.find((f) => f[0] === "TOTAL GENERAL"), [filas]);
|
const total = useMemo(() => filas.find((f) => f[0] === "TOTAL GENERAL"), [filas]);
|
||||||
const datos = useMemo(() => filas.filter((f) => f[0] !== "TOTAL GENERAL"), [filas]);
|
const datos = useMemo(() => {
|
||||||
|
const base = filas.filter((f) => f[0] !== "TOTAL GENERAL");
|
||||||
|
return _ordenarFilas(base, ordCol, ordDir);
|
||||||
|
}, [filas, ordCol, ordDir]);
|
||||||
|
|
||||||
const KPIS = total ? [
|
const KPIS = total ? [
|
||||||
["👤","Vendedores", datos.length, "activos este mes"],
|
["👤","Vendedores", datos.length, "activos este mes"],
|
||||||
@@ -191,7 +245,15 @@ export default function Ventas() {
|
|||||||
<div className="table-wrap" style={{overflowX:"auto"}}>
|
<div className="table-wrap" style={{overflowX:"auto"}}>
|
||||||
<table {...cols.tableProps}>
|
<table {...cols.tableProps}>
|
||||||
<cols.ColGroup />
|
<cols.ColGroup />
|
||||||
<thead><tr>{COLS.map((c, i) => <th key={c} style={{position:"relative"}}>{c}<cols.Resizer index={i} /></th>)}</tr></thead>
|
<thead><tr>{COLS.map((c, i) => {
|
||||||
|
const ordenable = i !== COLS.length - 1;
|
||||||
|
return (
|
||||||
|
<th key={c} style={{position:"relative", cursor: ordenable ? "pointer" : "default", userSelect:"none"}}
|
||||||
|
onClick={() => clickOrden(i)}>
|
||||||
|
{c}{ordenable && _flecha(ordCol === i, ordDir)}<cols.Resizer index={i} />
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
})}</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{datos.map((f, i) => (
|
{datos.map((f, i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
@@ -231,6 +293,14 @@ function ModalDetalle({ vendedor, ano, mes, onClose }) {
|
|||||||
const [tipo, setTipo] = useState(TIPOS[0]);
|
const [tipo, setTipo] = useState(TIPOS[0]);
|
||||||
const [filas, setFilas] = useState([]);
|
const [filas, setFilas] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [ordCol, setOrdCol] = useState(null);
|
||||||
|
const [ordDir, setOrdDir] = useState(null);
|
||||||
|
|
||||||
|
const clickOrden = (i) => {
|
||||||
|
if (ordCol !== i) { setOrdCol(i); setOrdDir("asc"); }
|
||||||
|
else if (ordDir === "asc") setOrdDir("desc");
|
||||||
|
else { setOrdCol(null); setOrdDir(null); }
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let activo = true;
|
let activo = true;
|
||||||
@@ -243,6 +313,7 @@ function ModalDetalle({ vendedor, ano, mes, onClose }) {
|
|||||||
|
|
||||||
// Columnas redimensionables (igual que las tablas principales)
|
// Columnas redimensionables (igual que las tablas principales)
|
||||||
const colsDet = useColumnasAjustables([200, 260, 120, 130, 120, 120, 130, 120]);
|
const colsDet = useColumnasAjustables([200, 260, 120, 130, 120, 120, 130, 120]);
|
||||||
|
const filasOrd = useMemo(() => _ordenarFilas(filas, ordCol, ordDir), [filas, ordCol, ordDir]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title={`👤 ${vendedor}`} onClose={onClose} width={1500}>
|
<Modal title={`👤 ${vendedor}`} onClose={onClose} width={1500}>
|
||||||
@@ -254,9 +325,14 @@ function ModalDetalle({ vendedor, ano, mes, onClose }) {
|
|||||||
<div className="table-wrap" style={{overflowX:"auto"}}>
|
<div className="table-wrap" style={{overflowX:"auto"}}>
|
||||||
<table {...colsDet.tableProps}>
|
<table {...colsDet.tableProps}>
|
||||||
<colsDet.ColGroup />
|
<colsDet.ColGroup />
|
||||||
<thead><tr>{HEADERS_DETALLE.map((h, i) => <th key={h} style={{position:"relative", whiteSpace:"nowrap"}}>{h}<colsDet.Resizer index={i} /></th>)}</tr></thead>
|
<thead><tr>{HEADERS_DETALLE.map((h, i) => (
|
||||||
|
<th key={h} style={{position:"relative", whiteSpace:"nowrap", cursor:"pointer", userSelect:"none"}}
|
||||||
|
onClick={() => clickOrden(i)}>
|
||||||
|
{h}{_flecha(ordCol === i, ordDir)}<colsDet.Resizer index={i} />
|
||||||
|
</th>
|
||||||
|
))}</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filas.map((f, i) => (
|
{filasOrd.map((f, i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
{f.slice(0, HEADERS_DETALLE.length).map((v, j) => (
|
{f.slice(0, HEADERS_DETALLE.length).map((v, j) => (
|
||||||
<td key={j} className={j === 1 ? "col-name" : ""}
|
<td key={j} className={j === 1 ? "col-name" : ""}
|
||||||
|
|||||||
Reference in New Issue
Block a user