diff --git a/frontend/src/pages/Comisiones.jsx b/frontend/src/pages/Comisiones.jsx index b1b4748..07f345d 100644 --- a/frontend/src/pages/Comisiones.jsx +++ b/frontend/src/pages/Comisiones.jsx @@ -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() {

🧮 Comisiones

- ({value:i+1,label:m}))} onChange={(v) => setMes(+v)} /> - + setAno(+v)} minWidth={110} /> + ({value:i+1,label:m}))} valor={mes} onChange={(v) => setMes(+v)} minWidth={160} /> + +
@@ -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) => ( {g.nombre} —— @@ -229,14 +233,14 @@ export default function Comisiones() { {modal && ( setModal(null)} onEliminar={() => { setModal(null); cargarManuales(); }} /> )} {modalEdit && ( 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 }) { - {HEADERS.map((h,hi)=>)} + {HEADERS.map((h,hi)=>( + + ))} {modo==="EDITAR" && } - {resultados.map((f, i) => { + {resultadosOrd.map((f, i) => { const mat = f[15]; return ( @@ -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 (
@@ -982,10 +1019,11 @@ function ModalDetalle({ vendedor, manual, ano, mes, sede = "TODOS", programa = "
{HEADERS.map((h, i) => ( - - {filas.map((f, i) => ( + {filasOrd.map((f, i) => ( {ORDEN.map((idx, pos) => { const esTexto = pos === 0 || pos === 1 || pos === 2; @@ -1055,3 +1093,99 @@ function ConfirmPopup({ mensaje, onConfirmar, onCancelar }) { ); } + +// 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 ( +
+ + + {abierto && ( +
+ {opts.map((o) => { + const on = String(o.value) === String(valor); + return ( +
{ 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} +
+ ); + })} +
+ )} +
+ ); +} + +// 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 ( +
+ + + {abierto && ( +
+ {opciones.map((o) => { + const on = seleccion.includes(o); + return ( + + ); + })} +
+ )} +
+ ); +}
{h} modo==="VER" && clickOrden(hi)} + style={{textAlign:"center",position:"relative", cursor: modo==="VER"?"pointer":"default", userSelect:"none"}}> + {h}{modo==="VER" && _flecha(ordPos===hi, ordDir)} + ACCIÓN
clickOrden(i)} + style={{position:"relative", overflow:"hidden", cursor:"pointer", userSelect:"none", whiteSpace:"nowrap", textOverflow:"ellipsis"}}> - {h} - iniciarResize(e, i)} + {h}{_flecha(ordPos===i, ordDir)} + {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 = "