Actualizar frontend/src/pages/Comisiones.jsx
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// src/pages/Comisiones.jsx
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { api } from "../lib/api";
|
||||
import { Loader, ErrorBox, Filters, Select } from "../components/UI";
|
||||
import Modal from "../components/Modal";
|
||||
@@ -7,8 +7,8 @@ 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 SEDES = ["TODOS", "LIMA", "AREQUIPA", "PIURA", "TRUJILLO"];
|
||||
const PROGRAMAS = ["TODOS", "SEMINARIOS", "OTROS", "TEAC", "TERC"];
|
||||
const SEDES = ["LIMA", "AREQUIPA", "PIURA", "TRUJILLO"];
|
||||
const PROGRAMAS = ["SEMINARIOS", "OTROS", "TEAC", "TERC"];
|
||||
const COLS = ["VENDEDOR","INSCRITOS PAGADOS MES EN CURSO","MONTO PAGADO MES EN CURSO","INSCRITOS PAGADOS MES PASADO","MONTO PAGADO MES PASADO","AVANCE TOTAL","OPCIONES"];
|
||||
// Mostaza transparente: resalta filas de alumnos con edición (override en Supabase)
|
||||
const BG_EDITADO = "rgba(217,160,28,0.14)";
|
||||
@@ -59,8 +59,10 @@ export default function Comisiones() {
|
||||
const hoy = new Date();
|
||||
const [ano, setAno] = useState(hoy.getFullYear());
|
||||
const [mes, setMes] = useState(hoy.getMonth() + 1);
|
||||
const [sede, setSede] = useState("TODOS");
|
||||
const [programa, setPrograma] = useState("TODOS");
|
||||
const [sedes, setSedes] = useState([]); // vacío = todas
|
||||
const [programas, setProgramas] = useState([]); // vacío = todos
|
||||
const sedesKey = useMemo(() => [...sedes].sort().join(","), [sedes]);
|
||||
const programasKey = useMemo(() => [...programas].sort().join(","), [programas]);
|
||||
const [filas, setFilas] = useState([]);
|
||||
const [manualesAno, setManualesAno] = useState([]); // todos los del año (cache)
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -99,7 +101,7 @@ export default function Comisiones() {
|
||||
// Tabla de ventas (depende del mes, sede y programa) — Comisiones SÍ aplica overrides
|
||||
function cargarTabla() {
|
||||
setLoading(true); setError(null);
|
||||
api.ventas(ano, mes, sede, programa, true)
|
||||
api.ventas(ano, mes, sedesKey || "TODOS", programasKey || "TODOS", true)
|
||||
.then((res) => { setFilas(res.filas || []); setLoading(false); })
|
||||
.catch((e) => { setError(e.message); setLoading(false); });
|
||||
}
|
||||
@@ -107,11 +109,11 @@ export default function Comisiones() {
|
||||
useEffect(() => {
|
||||
let activo = true;
|
||||
setLoading(true); setError(null);
|
||||
api.ventas(ano, mes, sede, programa, true)
|
||||
api.ventas(ano, mes, sedesKey || "TODOS", programasKey || "TODOS", true)
|
||||
.then((res) => { if (activo) { setFilas(res.filas || []); setLoading(false); } })
|
||||
.catch((e) => { if (activo) { setError(e.message); setLoading(false); } });
|
||||
return () => { activo = false; };
|
||||
}, [ano, mes, sede, programa]);
|
||||
}, [ano, mes, sedesKey, programasKey]);
|
||||
|
||||
// Cargar manuales del año cuando cambia el año (o al inicio)
|
||||
useEffect(() => { cargarManuales(); }, [ano]);
|
||||
@@ -139,11 +141,13 @@ export default function Comisiones() {
|
||||
|
||||
// Total general de AVANCE TOTAL = ventas + montos manuales
|
||||
// (los manuales solo cuentan cuando NO hay filtro de sede/programa)
|
||||
// Los manuales solo cuentan cuando NO hay filtro de sede ni programa (todos).
|
||||
const sinFiltroSedeProg = sedes.length === 0 && programas.length === 0;
|
||||
const totalAvance = useMemo(() => {
|
||||
let s = total ? toNum(total[7]) : 0;
|
||||
if (sede==="TODOS" && programa==="TODOS") manuales.forEach((m) => { s += toNum(m.monto); });
|
||||
if (sinFiltroSedeProg) manuales.forEach((m) => { s += toNum(m.monto); });
|
||||
return s;
|
||||
}, [total, manuales, sede, programa]);
|
||||
}, [total, manuales, sinFiltroSedeProg]);
|
||||
|
||||
const C = { textAlign: "center" }; // todas las columnas centradas
|
||||
|
||||
@@ -152,10 +156,10 @@ export default function Comisiones() {
|
||||
<h1 className="page-title">🧮 Comisiones</h1>
|
||||
|
||||
<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="Programa" value={programa} options={PROGRAMAS} onChange={setPrograma} />
|
||||
<SelectUnico label="Año" opciones={ANOS} valor={ano} onChange={(v) => setAno(+v)} minWidth={110} />
|
||||
<SelectUnico label="Mes" opciones={MESES.map((m,i)=>({value:i+1,label:m}))} valor={mes} onChange={(v) => setMes(+v)} minWidth={160} />
|
||||
<ListaMulti label="Sede" opciones={SEDES} seleccion={sedes} onChange={setSedes} plural="sedes" minWidth={140} />
|
||||
<ListaMulti label="Programa" opciones={PROGRAMAS} seleccion={programas} onChange={setProgramas} plural="programas" minWidth={150} />
|
||||
<div style={{display:"flex",gap:8,alignItems:"flex-end",marginLeft:"auto"}}>
|
||||
<button className="btn btn-ghost" style={{height:38}} onClick={()=>setModalConfig(true)}>⚙️ TC y Metas</button>
|
||||
<button className="btn btn-ghost" style={{height:38}} onClick={()=>setModalBuscar(true)}>🔍 Buscar Alumno</button>
|
||||
@@ -198,7 +202,7 @@ export default function Comisiones() {
|
||||
))}
|
||||
|
||||
{/* Asesores manuales: solo cuando NO hay filtro de sede/programa */}
|
||||
{sede==="TODOS" && programa==="TODOS" && manualesAgrupados.map((g) => (
|
||||
{sinFiltroSedeProg && manualesAgrupados.map((g) => (
|
||||
<tr key={"m-"+g.nombre} style={{background:"#fffbeb"}}>
|
||||
<td className="col-name" style={C}>{g.nombre}</td>
|
||||
<td style={C}>—</td><td style={C}>—</td>
|
||||
@@ -229,14 +233,14 @@ export default function Comisiones() {
|
||||
|
||||
{modal && (
|
||||
<ModalDetalle vendedor={modal.vendedor} manual={modal.manual} ano={ano} mes={mes}
|
||||
sede={sede} programa={programa}
|
||||
sede={sedesKey || "TODOS"} programa={programasKey || "TODOS"}
|
||||
onClose={() => setModal(null)}
|
||||
onEliminar={() => { setModal(null); cargarManuales(); }} />
|
||||
)}
|
||||
|
||||
{modalEdit && (
|
||||
<ModalEditar vendedor={modalEdit.vendedor} ano={ano} mes={mes}
|
||||
sede={sede} programa={programa}
|
||||
sede={sedesKey || "TODOS"} programa={programasKey || "TODOS"}
|
||||
onClose={() => setModalEdit(null)} onGuardado={cargarTabla} />
|
||||
)}
|
||||
|
||||
@@ -655,6 +659,13 @@ function ModalBuscar({ ano, mes, onClose, onGuardado }) {
|
||||
const [guardando, setGuardando] = useState(false);
|
||||
const [msg, setMsg] = useState(null);
|
||||
const [confirmar, setConfirmar] = useState(null); // matrícula a restaurar
|
||||
const [ordPos, setOrdPos] = useState(null);
|
||||
const [ordDir, setOrdDir] = useState(null);
|
||||
const clickOrden = (pos) => {
|
||||
if (ordPos !== pos) { setOrdPos(pos); setOrdDir("asc"); }
|
||||
else if (ordDir === "asc") setOrdDir("desc");
|
||||
else { setOrdPos(null); setOrdDir(null); }
|
||||
};
|
||||
|
||||
function cargar() {
|
||||
setLoading(true);
|
||||
@@ -701,6 +712,13 @@ function ModalBuscar({ ano, mes, onClose, onGuardado }) {
|
||||
// Campos editables (mismos que el Editar normal): idx de fila → tipo
|
||||
const EDITABLES = { 2:"fecha", 7:"dinero", 10:"dinero", 11:"dinero", 9:"fecha", 3:"fecha", 5:"dinero", 12:"numero", 13:"dinero", 14:"texto" };
|
||||
|
||||
// Orden por columna — solo en modo VER (en EDITAR se mantiene el orden original).
|
||||
const resultadosOrd = useMemo(() => {
|
||||
if (modo !== "VER" || ordPos == null || !ordDir) return resultados;
|
||||
const idx = ORDEN[ordPos];
|
||||
return [...resultados].sort((a, b) => _cmp(a[idx], b[idx], ordDir));
|
||||
}, [resultados, modo, ordPos, ordDir]);
|
||||
|
||||
// Anchos iniciales por columna (VENDEDOR/ALUMNO/PROGRAMA más anchas); +ACCIÓN en modo EDITAR
|
||||
const anchosBuscar = [180, 220, 240, ...Array(HEADERS.length-3).fill(130), ...(modo==="EDITAR" ? [80] : [])];
|
||||
const colsBuscar = useColumnasAjustables(anchosBuscar);
|
||||
@@ -755,11 +773,16 @@ function ModalBuscar({ ano, mes, onClose, onGuardado }) {
|
||||
<table {...colsBuscar.tableProps}>
|
||||
<colsBuscar.ColGroup />
|
||||
<thead><tr>
|
||||
{HEADERS.map((h,hi)=><th key={h} style={{textAlign:"center",position:"relative"}}>{h}<colsBuscar.Resizer index={hi} /></th>)}
|
||||
{HEADERS.map((h,hi)=>(
|
||||
<th key={h} onClick={()=> modo==="VER" && clickOrden(hi)}
|
||||
style={{textAlign:"center",position:"relative", cursor: modo==="VER"?"pointer":"default", userSelect:"none"}}>
|
||||
{h}{modo==="VER" && _flecha(ordPos===hi, ordDir)}<colsBuscar.Resizer index={hi} />
|
||||
</th>
|
||||
))}
|
||||
{modo==="EDITAR" && <th style={{textAlign:"center",position:"relative"}}>ACCIÓN<colsBuscar.Resizer index={HEADERS.length} /></th>}
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{resultados.map((f, i) => {
|
||||
{resultadosOrd.map((f, i) => {
|
||||
const mat = f[15];
|
||||
return (
|
||||
<tr key={i} style={String(f[17])==="1" ? {background:BG_EDITADO} : undefined}>
|
||||
@@ -967,6 +990,20 @@ function ModalDetalle({ vendedor, manual, ano, mes, sede = "TODOS", programa = "
|
||||
|
||||
const totalInv = useMemo(() => filas.reduce((s,f)=>s+toNum(f[7]),0), [filas]);
|
||||
|
||||
// Orden por columna (usa el índice real ORDEN[pos]).
|
||||
const [ordPos, setOrdPos] = useState(null);
|
||||
const [ordDir, setOrdDir] = useState(null);
|
||||
const clickOrden = (pos) => {
|
||||
if (ordPos !== pos) { setOrdPos(pos); setOrdDir("asc"); }
|
||||
else if (ordDir === "asc") setOrdDir("desc");
|
||||
else { setOrdPos(null); setOrdDir(null); }
|
||||
};
|
||||
const filasOrd = useMemo(() => {
|
||||
if (ordPos == null || !ordDir) return filas;
|
||||
const idx = ORDEN[ordPos];
|
||||
return [...filas].sort((a, b) => _cmp(a[idx], b[idx], ordDir));
|
||||
}, [filas, ordPos, ordDir]);
|
||||
|
||||
return (
|
||||
<Modal title={`🧮 ${vendedor}`} onClose={onClose} width={1600}>
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
@@ -982,10 +1019,11 @@ function ModalDetalle({ vendedor, manual, ano, mes, sede = "TODOS", programa = "
|
||||
<thead>
|
||||
<tr>
|
||||
{HEADERS.map((h, i) => (
|
||||
<th key={h} style={{position:"relative", overflow:"hidden",
|
||||
<th key={h} onClick={()=>clickOrden(i)}
|
||||
style={{position:"relative", overflow:"hidden", cursor:"pointer", userSelect:"none",
|
||||
whiteSpace:"nowrap", textOverflow:"ellipsis"}}>
|
||||
{h}
|
||||
<span onMouseDown={(e)=>iniciarResize(e, i)}
|
||||
{h}{_flecha(ordPos===i, ordDir)}
|
||||
<span onMouseDown={(e)=>{e.stopPropagation(); iniciarResize(e, i);}}
|
||||
className="col-resizer"
|
||||
style={{position:"absolute", top:0, right:0, width:8, height:"100%",
|
||||
cursor:"col-resize", userSelect:"none", zIndex:5}} />
|
||||
@@ -994,7 +1032,7 @@ function ModalDetalle({ vendedor, manual, ano, mes, sede = "TODOS", programa = "
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filas.map((f, i) => (
|
||||
{filasOrd.map((f, i) => (
|
||||
<tr key={i} style={String(f[17])==="1" ? {background:BG_EDITADO} : undefined}>
|
||||
{ORDEN.map((idx, pos) => {
|
||||
const esTexto = pos === 0 || pos === 1 || pos === 2;
|
||||
@@ -1055,3 +1093,99 @@ function ConfirmPopup({ mensaje, onConfirmar, onCancelar }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desplegable de UNA sola opción con diseño bonito. Opciones: strings o {value,label}.
|
||||
function SelectUnico({ label, opciones, valor, onChange, minWidth = 150 }) {
|
||||
const [abierto, setAbierto] = useState(false);
|
||||
const ref = useRef(null);
|
||||
const opts = opciones.map((o) => (typeof o === "object" ? o : { value: o, label: String(o) }));
|
||||
const actual = opts.find((o) => String(o.value) === String(valor));
|
||||
useEffect(() => {
|
||||
const fuera = (e) => { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); };
|
||||
document.addEventListener("mousedown", fuera);
|
||||
return () => document.removeEventListener("mousedown", fuera);
|
||||
}, []);
|
||||
return (
|
||||
<div className="filter-group" ref={ref} style={{ position: "relative", minWidth }}>
|
||||
<label>{label}</label>
|
||||
<button type="button" onClick={() => setAbierto((v) => !v)}
|
||||
style={{ height: 38, width: "100%", textAlign: "left", padding: "0 12px",
|
||||
border: "1px solid #cbd5e1", borderRadius: 8, background: "#fff",
|
||||
cursor: "pointer", fontSize: 13, display: "flex", alignItems: "center",
|
||||
justifyContent: "space-between", gap: 8 }}>
|
||||
<span style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{actual ? actual.label : "—"}
|
||||
</span>
|
||||
<span style={{ color: "#94a3b8", fontSize: 11 }}>▼</span>
|
||||
</button>
|
||||
{abierto && (
|
||||
<div style={{ position: "absolute", top: "100%", left: 0, right: 0, zIndex: 50, marginTop: 4,
|
||||
background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8,
|
||||
boxShadow: "0 8px 24px rgba(0,0,0,0.12)", padding: 6, maxHeight: 300, overflowY: "auto" }}>
|
||||
{opts.map((o) => {
|
||||
const on = String(o.value) === String(valor);
|
||||
return (
|
||||
<div key={o.value} onClick={() => { onChange(o.value); setAbierto(false); }}
|
||||
style={{ padding: "8px 10px", borderRadius: 6, cursor: "pointer", fontSize: 13,
|
||||
fontWeight: on ? 700 : 500, color: on ? "#1e3a5f" : "#334155",
|
||||
background: on ? "#eff6ff" : "transparent" }}
|
||||
onMouseEnter={(e) => { if (!on) e.currentTarget.style.background = "#f8fafc"; }}
|
||||
onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = "transparent"; }}>
|
||||
{o.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Filtro multi-selección con casillas (Sede, Programa). Vacío = todos.
|
||||
function ListaMulti({ label, opciones, seleccion, onChange, plural = "opciones", minWidth = 150 }) {
|
||||
const [abierto, setAbierto] = useState(false);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
const fuera = (e) => { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); };
|
||||
document.addEventListener("mousedown", fuera);
|
||||
return () => document.removeEventListener("mousedown", fuera);
|
||||
}, []);
|
||||
const toggle = (v) => {
|
||||
const set = new Set(seleccion);
|
||||
set.has(v) ? set.delete(v) : set.add(v);
|
||||
onChange([...set]);
|
||||
};
|
||||
const texto = seleccion.length === 0 ? "— Elegir —"
|
||||
: seleccion.length === 1 ? seleccion[0]
|
||||
: `${seleccion.length} ${plural}`;
|
||||
return (
|
||||
<div className="filter-group" ref={ref} style={{ position: "relative", minWidth }}>
|
||||
<label>{label}</label>
|
||||
<button type="button" onClick={() => setAbierto((v) => !v)}
|
||||
style={{ height: 38, width: "100%", textAlign: "left", padding: "0 12px",
|
||||
border: "1px solid #cbd5e1", borderRadius: 8, background: "#fff",
|
||||
cursor: "pointer", fontSize: 13, display: "flex", alignItems: "center",
|
||||
justifyContent: "space-between", gap: 8 }}>
|
||||
<span style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{texto}</span>
|
||||
<span style={{ color: "#94a3b8", fontSize: 11 }}>▼</span>
|
||||
</button>
|
||||
{abierto && (
|
||||
<div style={{ position: "absolute", top: "100%", left: 0, right: 0, zIndex: 50, marginTop: 4,
|
||||
background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8,
|
||||
boxShadow: "0 8px 24px rgba(0,0,0,0.12)", padding: 6, maxHeight: 300, overflowY: "auto" }}>
|
||||
{opciones.map((o) => {
|
||||
const on = seleccion.includes(o);
|
||||
return (
|
||||
<label key={o} style={{ display: "flex", alignItems: "center", gap: 8,
|
||||
padding: "6px 8px", borderRadius: 6, cursor: "pointer", fontSize: 13,
|
||||
background: on ? "#eff6ff" : "transparent" }}>
|
||||
<input type="checkbox" checked={on} onChange={() => toggle(o)} />
|
||||
{o}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user