diff --git a/frontend/src/pages/Comisiones.jsx b/frontend/src/pages/Comisiones.jsx index 07f345d..be87c84 100644 --- a/frontend/src/pages/Comisiones.jsx +++ b/frontend/src/pages/Comisiones.jsx @@ -1,6 +1,6 @@ // src/pages/Comisiones.jsx -import { useState, useEffect, useMemo, useRef } from "react"; -import { api } from "../lib/api"; +import { useState, useEffect, useMemo, useRef, Fragment } from "react"; +import { api, BASE_URL } from "../lib/api"; import { Loader, ErrorBox, Filters, Select } from "../components/UI"; import Modal from "../components/Modal"; import { useColumnasAjustables } from "../lib/useColumnasAjustables"; @@ -120,7 +120,8 @@ export default function Comisiones() { const total = useMemo(() => filas.find((f) => f[0] === "TOTAL GENERAL"), [filas]); const datos = useMemo(() => { - let base = filas.filter((f) => f[0] !== "TOTAL GENERAL"); + // Ocultar vendedores con AVANCE TOTAL (f[7]) en 0 + let base = filas.filter((f) => f[0] !== "TOTAL GENERAL" && toNum(f[7]) > 0); if (ordCol != null && ordDir) { const idx = IDX_COM[ordCol]; base = [...base].sort((a, b) => _cmp(a[idx], b[idx], ordDir)); @@ -139,15 +140,36 @@ export default function Comisiones() { return Object.values(map); }, [manuales]); - // 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; + // Lista de asesores del mes (automáticos de la tabla + manuales), deduped, + // excluyendo "TOTAL GENERAL". Se pasa a ModalConfig para la matriz de metas. + const asesoresLista = useMemo(() => { + const vistos = new Set(); + const out = []; + const agregar = (nombre) => { + const n = String(nombre || "").trim(); + if (!n || n.toUpperCase() === "TOTAL GENERAL") return; + const key = n.toUpperCase(); + if (vistos.has(key)) return; + vistos.add(key); out.push(n); + }; + datos.forEach((f) => agregar(f[0])); + manualesAgrupados.forEach((g) => agregar(g.nombre)); + return out; + }, [datos, manualesAgrupados]); + + // Los vendedores manuales pertenecen a SEDE LIMA y PROGRAMA OTROS. + // Se muestran/cuentan cuando el filtro PERMITE LIMA (sede vacía o incluye LIMA) + // Y PERMITE OTROS (programa vacío o incluye OTROS). + const mostrarManuales = useMemo(() => { + const sedeOk = sedes.length === 0 || sedes.map((s)=>String(s).toUpperCase()).includes("LIMA"); + const progOk = programas.length === 0 || programas.map((p)=>String(p).toUpperCase()).includes("OTROS"); + return sedeOk && progOk; + }, [sedes, programas]); const totalAvance = useMemo(() => { let s = total ? toNum(total[7]) : 0; - if (sinFiltroSedeProg) manuales.forEach((m) => { s += toNum(m.monto); }); + if (mostrarManuales) manuales.forEach((m) => { s += toNum(m.monto); }); return s; - }, [total, manuales, sinFiltroSedeProg]); + }, [total, manuales, mostrarManuales]); const C = { textAlign: "center" }; // todas las columnas centradas @@ -163,6 +185,21 @@ export default function Comisiones() {
+ @@ -201,8 +238,8 @@ export default function Comisiones() { ))} - {/* Asesores manuales: solo cuando NO hay filtro de sede/programa */} - {sinFiltroSedeProg && manualesAgrupados.map((g) => ( + {/* Vendedores manuales (sede LIMA / programa OTROS): visibles si el filtro lo permite */} + {mostrarManuales && manualesAgrupados.map((g) => ( {g.nombre} —— @@ -252,7 +289,7 @@ export default function Comisiones() { )} {modalConfig && ( - setModalConfig(false)} onGuardado={cargarTabla} /> + setModalConfig(false)} onGuardado={cargarTabla} /> )} {modalBuscar && ( @@ -462,86 +499,190 @@ function ModalEditar({ vendedor, ano, mes, sede = "TODOS", programa = "TODOS", o ); } +// Editar vendedor manual = BUSCADOR DE FACTURAS. Busca por deudor o N° comprobante, +// muestra las facturas y las que marques (con su monto ya en soles) se guardan como +// filas del vendedor (suman en la tabla principal y en "Ver"). function ModalEditarManual({ nombre, ano, mes, filasActuales, onClose, onCambio }) { - // Todas las filas son locales hasta Guardar (se manda todo en lote) - const [rows, setRows] = useState([]); const [guardando, setGuardando] = useState(false); const [msg, setMsg] = useState(null); + const [facturas, setFacturas] = useState([]); + const [tcMes, setTcMes] = useState(null); + const [loading, setLoading] = useState(true); + const [q, setQ] = useState(""); + const [manualOpen, setManualOpen] = useState(false); + // num_comprobante -> factura seleccionada (para guardar). Inicia con lo ya guardado. + const [sel, setSel] = useState(() => { + const m = {}; + let man = 1; + (filasActuales || []).forEach((r) => { + const c = String(r.num_comprobante || "").trim(); + const key = c || ("MANUAL_" + (man++)); // sin comprobante → fila manual + m[key] = { + num_comprobante: c, dsc_deudor: r.descripcion || "", + fch_emision: r.fch_emision || "", monto_soles: toNum(r.monto), + cod_estado: c ? "" : "MANUAL", cod_moneda: "", imp_subtotal: toNum(r.monto), _yaGuardada: true, + }; + }); + return m; + }); useEffect(() => { - const inicial = (filasActuales || []).map((r) => ({ - descripcion: r.descripcion || "", fch_emision: r.fch_emision || "", - monto: String(toNum(r.monto)), - })); - // Siempre dejar al menos una fila para escribir - setRows(inicial.length ? inicial : [{ descripcion:"", fch_emision:"", monto:"" }]); - }, [filasActuales]); + let activo = true; + setLoading(true); + api.facturasBuscar(ano, mes) + .then((res) => { if (activo) { setFacturas(res.facturas || []); setTcMes(res.tc_mes || null); setLoading(false); } }) + .catch(() => { if (activo) { setFacturas([]); setLoading(false); } }); + return () => { activo = false; }; + }, [ano, mes]); - function cambiarRow(i, campo, valor) { - setRows((prev) => prev.map((r, idx) => idx === i ? { ...r, [campo]: valor } : r)); + const norm = (s) => String(s || "").toLowerCase(); + const filtradas = useMemo(() => { + const t = norm(q).trim(); + const base = facturas; + if (!t) return base.slice(0, 80); + return base.filter((f) => + norm(f.dsc_deudor).includes(t) || norm(f.num_comprobante).includes(t) + ).slice(0, 300); + }, [facturas, q]); + + function toggle(f) { + const c = String(f.num_comprobante || "").trim(); + if (!c) return; + setSel((p) => { + const n = { ...p }; + if (n[c]) delete n[c]; else n[c] = f; + return n; + }); } - function quitarFila(i) { - setRows((prev) => prev.filter((_, idx) => idx !== i)); - } - function agregarFila() { - setRows((prev) => [...prev, { descripcion:"", fch_emision:"", monto:"" }]); + + // Agrega un registro MANUAL (sin num_comprobante). Si es DOL, convierte a soles + // con el TC del mes (o el TC que el usuario escriba). Clave única "MANUAL_n". + function agregarManual({ dsc_deudor, fch_emision, cod_moneda, subtotal, tc }) { + const sub = toNum(subtotal); + let montoSoles = sub; + if (String(cod_moneda).toUpperCase() === "DOL") { + const t = toNum(tc) > 0 ? toNum(tc) : (toNum(tcMes) > 0 ? toNum(tcMes) : 1); + montoSoles = sub * t; + } + setSel((p) => { + let i = 1; + while (p["MANUAL_" + i]) i++; + return { ...p, ["MANUAL_" + i]: { + num_comprobante: "", dsc_deudor: dsc_deudor || "", fch_emision: fch_emision || "", + cod_estado: "MANUAL", cod_moneda: String(cod_moneda || "SOL").toUpperCase(), + imp_subtotal: sub, monto_soles: Math.round(montoSoles * 100) / 100, + } }; + }); + setManualOpen(false); } + const simboloMoneda = (m) => String(m).toUpperCase() === "DOL" ? "$" : "S/"; + const mesDe = (fch) => { + const s = String(fch || ""); + if (s.includes("-")) return s.split("-")[1] || ""; + if (s.includes("/")) return s.split("/")[1] || ""; + return ""; + }; + + const totalSel = useMemo( + () => Object.values(sel).reduce((a, f) => a + toNum(f.monto_soles), 0), + [sel]); + async function guardar() { setGuardando(true); setMsg(null); try { - // Solo filas con algún dato - const filas = rows - .filter((r) => r.descripcion.trim() || r.fch_emision || toNum(r.monto) > 0) - .map((r) => ({ descripcion: r.descripcion.trim(), fch_emision: r.fch_emision, monto: toNum(r.monto) })); - // Una sola llamada: reemplaza todo (si filas vacío → elimina el vendedor) + const filas = Object.values(sel).map((f) => ({ + descripcion: f.dsc_deudor || "", + fch_emision: f.fch_emision || "", + monto: toNum(f.monto_soles), + num_comprobante: String(f.num_comprobante || "").trim(), + })); await api.vendedoresManualesLote({ nombre, ano, mes, filas }); onCambio(); - onClose(); // cerrar tras guardar + onClose(); } catch (e) { setMsg({ ok:false, txt:"Error: " + e.message }); setGuardando(false); } } - const inp = { width:"100%", padding:"6px 8px", border:"1px solid #cbd5e1", borderRadius:6, fontSize:12, textAlign:"center", boxSizing:"border-box" }; - const ce = { textAlign:"center" }; + const thf = { background:"#334155", color:"#f1f5f9", padding:"8px 8px", fontSize:11, + fontWeight:700, textAlign:"center", whiteSpace:"nowrap", position:"sticky", top:0 }; + const tdf = { padding:"7px 8px", fontSize:12, borderBottom:"1px solid #f1f5f9", whiteSpace:"nowrap" }; return ( - -
- - - - - - - {rows.map((r, i) => ( - - - - - - - - ))} - -
VENDEDORDESCRIPCIÓNFECHA EMISIÓNMONTO (S/)ACCIÓN
{nombre}cambiarRow(i,"descripcion",e.target.value)} style={inp} />cambiarRow(i,"fch_emision",e.target.value)} style={inp} />cambiarRow(i,"monto",e.target.value)} style={inp} /> - -
+ +
+ setQ(e.target.value)} + placeholder="Buscar por deudor o N° de comprobante…" + style={{ flex:1, padding:"11px 14px", border:"1px solid #cbd5e1", + borderRadius:10, fontSize:14, boxSizing:"border-box", outline:"none" }} /> +
- + {manualOpen && ( + setManualOpen(false)} + /> + )} + +
+ {loading ? ( +
Cargando facturas…
+ ) : ( + + + + + + + + + + + + + + {filtradas.length === 0 && ( + + )} + {filtradas.map((f, i) => { + const c = String(f.num_comprobante || "").trim(); + const on = !!sel[c]; + return ( + + + + + + + + + + + + ); + })} + +
MESFECHA EMISIÓNN° FACTURADEUDORESTADOMONEDASUBTOTALMONTO (S/)
+ {q ? "Sin resultados." : "Escribe para buscar."} +
+ toggle(f)} style={{cursor:"pointer"}} /> + {mesDe(f.fch_emision)}{f.fch_emision}{f.num_comprobante}{f.dsc_deudor}{f.cod_estado}{simboloMoneda(f.cod_moneda)}{simboloMoneda(f.cod_moneda)} {toNum(f.imp_subtotal).toLocaleString("es-PE",{maximumFractionDigits:0})}S/ {toNum(f.monto_soles).toLocaleString("es-PE",{maximumFractionDigits:0})}
+ )} +
{msg && (
{msg.txt}
)} -
+ +
+ + Seleccionadas: {Object.keys(sel).length} · Total: S/ {totalSel.toLocaleString("es-PE",{maximumFractionDigits:0})} + @@ -551,59 +692,186 @@ function ModalEditarManual({ nombre, ano, mes, filasActuales, onClose, onCambio ); } -function ModalConfig({ ano, mes, onClose, onGuardado }) { - const TIPOS = ["TEAC", "TERC", "SEMINARIOS", "OTROS"]; - const [tc, setTc] = useState(""); // IMP_TC único del mes - const [filas, setFilas] = useState([]); // META y COMISIÓN por tipo +// Pop-up para agregar un registro MANUAL (como un comprobante, pero sin N°). +// Campos: Deudor, Fecha emisión, Moneda, Subtotal. Si DOL, se convierte a soles. +function ModalAgregarManualFactura({ tcMes, onAgregar, onClose }) { + const [deudor, setDeudor] = useState(""); + const [fecha, setFecha] = useState(""); + const [moneda, setMoneda] = useState("SOL"); + const [subtotal, setSubtotal] = useState(""); + const [tc, setTc] = useState(tcMes ? String(tcMes) : ""); + const [err, setErr] = useState(""); + + const esDol = moneda === "DOL"; + const montoPrev = (() => { + const s = toNum(subtotal); + if (!esDol) return s; + const t = toNum(tc) > 0 ? toNum(tc) : (toNum(tcMes) > 0 ? toNum(tcMes) : 1); + return s * t; + })(); + + function agregar() { + if (!deudor.trim()) { setErr("El deudor es obligatorio."); return; } + if (toNum(subtotal) <= 0) { setErr("El subtotal debe ser mayor a 0."); return; } + onAgregar({ dsc_deudor: deudor.trim(), fch_emision: fecha, cod_moneda: moneda, subtotal, tc }); + } + + const inp = { width:"100%", padding:"9px 12px", border:"1px solid #cbd5e1", borderRadius:8, fontSize:13, boxSizing:"border-box" }; + + return ( +
+
e.stopPropagation()} + style={{ background:"#fff", borderRadius:14, width:"min(440px,95vw)", padding:20, + boxShadow:"0 20px 60px rgba(0,0,0,.3)" }}> +
+

➕ Agregar manual

+ +
+
+
+ + setDeudor(e.target.value)} style={inp} placeholder="Empresa / nombre" autoFocus /> +
+
+ + setFecha(e.target.value)} style={inp} /> +
+
+
+ + +
+
+ + setSubtotal(e.target.value)} style={inp} placeholder="0" /> +
+
+ {esDol && ( +
+ + setTc(e.target.value)} style={inp} + placeholder={tcMes ? `TC del mes: ${tcMes}` : "TC"} /> +
+ )} +
+ Monto en soles: S/ {Math.round(montoPrev).toLocaleString("es-PE")} +
+ {err &&
{err}
} +
+ + +
+
+
+
+ ); +} + +function ModalConfig({ ano, mes, asesores = [], onClose, onGuardado }) { + // Grupos de la matriz de metas por asesor. + const GRUPOS = ["PT", "SEMINARIOS", "OTROS"]; + const vacio = () => ({ meta:"", bajo:"", sobre:"" }); + const [tc, setTc] = useState(""); // IMP_TC único del mes (se mantiene igual) + const [matriz, setMatriz] = useState({}); // { [asesor]: { PT:{meta,bajo,sobre}, SEMINARIOS:{...}, OTROS:{...} } } + const [jefe, setJefe] = useState(vacio()); // JEFE COMERCIAL (grupo TOTAL): una sola meta const [loading, setLoading] = useState(true); const [guardando, setGuardando] = useState(false); const [msg, setMsg] = useState(null); useEffect(() => { setLoading(true); - api.comisionesConfig(ano, mes) - .then((res) => { - const guardadas = res.config || []; - // TC: tomar el primero que tenga valor (es único por mes) - const tcGuardado = guardadas.find((x) => +x.imp_tc > 0); - setTc(tcGuardado ? String(tcGuardado.imp_tc) : ""); - const arr = TIPOS.map((t) => { - const g = guardadas.find((x) => x.tipo_programa === t) || {}; - return { tipo_programa: t, meta: g.meta ?? "", comision: g.comision ?? "" }; - }); - setFilas(arr); setLoading(false); - }) - .catch(() => { setTc(""); setFilas(TIPOS.map((t)=>({tipo_programa:t,meta:"",comision:""}))); setLoading(false); }); - }, [ano, mes]); + Promise.all([ + api.comisionesConfig(ano, mes).catch(() => ({ config: [] })), + api.comisionesMetas(ano, mes).catch(() => ({ metas: [] })), + ]).then(([cfg, met]) => { + // TC: tomar el primero que tenga valor (es único por mes) + const tcGuardado = (cfg.config || []).find((x) => +x.imp_tc > 0); + setTc(tcGuardado ? String(tcGuardado.imp_tc) : ""); - function cambiar(i, campo, val) { - setFilas((prev) => prev.map((f, idx) => idx === i ? { ...f, [campo]: val } : f)); + const guardadas = met.metas || []; + const buscar = (asesor, grupo) => + guardadas.find((x) => x.asesor === asesor && x.grupo === grupo); + const toStr = (g) => g ? { + meta: g.meta != null ? String(g.meta) : "", + bajo: g.pct_bajo_meta != null ? String(g.pct_bajo_meta) : "", + sobre: g.pct_sobre_meta != null ? String(g.pct_sobre_meta) : "", + } : vacio(); + + const m = {}; + asesores.forEach((a) => { + m[a] = {}; + GRUPOS.forEach((grp) => { m[a][grp] = toStr(buscar(a, grp)); }); + }); + setMatriz(m); + setJefe(toStr(buscar("JEFE COMERCIAL", "TOTAL"))); + setLoading(false); + }); + }, [ano, mes, asesores.join("|")]); + + function cambiar(asesor, grupo, campo, val) { + setMatriz((prev) => ({ + ...prev, + [asesor]: { ...prev[asesor], [grupo]: { ...(prev[asesor]?.[grupo] || vacio()), [campo]: val } }, + })); + } + function cambiarJefe(campo, val) { + setJefe((prev) => ({ ...prev, [campo]: val })); } async function guardar() { setGuardando(true); setMsg(null); try { - // El mismo TC del mes se guarda en todas las filas (es único por mes) - const payload = filas.map((f) => ({ - tipo_programa: f.tipo_programa, - imp_tc: toNum(tc), meta: toNum(f.meta), comision: toNum(f.comision), - })); - await api.comisionesConfigGuardar({ ano, mes, filas: payload }); + // (a) Guardar el TC como siempre (en comisiones_config_mensual). + await api.comisionesConfigGuardar({ + ano, mes, + filas: [{ tipo_programa: "TC", imp_tc: toNum(tc), meta: 0, comision: 0 }], + }); + + // (b) Armar filas de metas por asesor y grupo (solo las que tengan algún valor). + const filas = []; + const tieneValor = (c) => c && (c.meta !== "" || c.bajo !== "" || c.sobre !== ""); + asesores.forEach((a) => { + GRUPOS.forEach((grp) => { + const c = matriz[a]?.[grp]; + if (tieneValor(c)) { + filas.push({ asesor: a, grupo: grp, + meta: toNum(c.meta), pct_bajo_meta: toNum(c.bajo), pct_sobre_meta: toNum(c.sobre) }); + } + }); + }); + if (tieneValor(jefe)) { + filas.push({ asesor: "JEFE COMERCIAL", grupo: "TOTAL", + meta: toNum(jefe.meta), pct_bajo_meta: toNum(jefe.bajo), pct_sobre_meta: toNum(jefe.sobre) }); + } + await api.comisionesMetasGuardar({ ano, mes, filas }); + if (onGuardado) onGuardado(); // recarga la tabla principal onClose(); } catch (e) { setMsg({ ok:false, txt:"Error: " + e.message }); setGuardando(false); } } - const inp = { width:"100%", padding:"7px 9px", border:"1px solid #cbd5e1", borderRadius:6, fontSize:13, textAlign:"center", boxSizing:"border-box" }; + const inp = { width:"100%", padding:"6px 6px", border:"1px solid #cbd5e1", borderRadius:6, fontSize:12.5, textAlign:"center", boxSizing:"border-box" }; const MESES_N = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"]; + // Estilos de la matriz + const HDR_GRP = { background:"#B4C6E7", color:"#1e293b", fontWeight:700, fontSize:12.5, textAlign:"center", padding:"7px 6px", border:"1px solid #93a7cf" }; + const HDR_SUB = { background:"#DCE6F5", color:"#334155", fontWeight:600, fontSize:11.5, textAlign:"center", padding:"6px 5px", border:"1px solid #c3d2ea", whiteSpace:"nowrap" }; + const TD = { border:"1px solid #e2e8f0", padding:3, background:"#fff" }; + const TD_NAME = { border:"1px solid #e2e8f0", padding:"6px 10px", fontWeight:600, fontSize:12.5, color:"#1e293b", background:"#f8fafc", whiteSpace:"nowrap", position:"sticky", left:0, zIndex:1 }; + const SUBS = [["meta","META"],["bajo","% META"]]; + return ( - +
Configuración para {MESES_N[mes-1]} {ano}.
{loading ? : <> - {/* IMP_TC único del mes */} + {/* IMP_TC único del mes (SIN CAMBIOS) */}
@@ -613,33 +881,92 @@ function ModalConfig({ ano, mes, onClose, onGuardado }) { Se usará para convertir dólares este mes. Si lo dejas vacío, usa el TC de la base.
- {/* META y COMISIÓN por tipo de programa */} -
- - - - - - + {/* MATRIZ DE METAS POR ASESOR */} +
Metas por asesor
+ {asesores.length === 0 ? ( +
+ No hay asesores en este mes. +
+ ) : ( +
+
TIPO PROGRAMAMETA (S/)COMISIÓN (%)
+ + + {GRUPOS.map((grp) => ( + + {/* META más ancha */} + {/* % {/* % >META */} + + ))} + + + + + {GRUPOS.map((grp) => ( + + ))} + + + {GRUPOS.map((grp) => SUBS.map(([k, lbl]) => ( + + )))} + + - {filas.map((f, i) => ( - - - - + {asesores.map((a) => ( + + + {GRUPOS.map((grp) => SUBS.map(([k]) => { + const esPct = (k === "bajo" || k === "sobre"); + return ( + + ); + }))} ))}
ASESOR{grp}
{lbl}
{f.tipo_programa}cambiar(i,"meta",e.target.value)} style={inp} />cambiar(i,"comision",e.target.value)} style={inp} />
{a} +
+ cambiar(a, grp, k, e.target.value)} + style={inp} /> + {esPct && %} +
+
+ )} + + {/* JEFE COMERCIAL — una sola meta (grupo TOTAL) */} +
+
Jefe Comercial
+
+
JEFE COMERCIAL
+ {SUBS.map(([k, lbl]) => { + const esPct = (k === "bajo" || k === "sobre"); + return ( +
+ +
+ cambiarJefe(k, e.target.value)} + style={{...inp, width:esPct ? 90 : 130}} /> + {esPct && %} +
+
+ ); + })} +
+
} {msg && (
{msg.txt}
)} -
+
@@ -742,6 +1069,7 @@ function ModalBuscar({ ano, mes, onClose, onGuardado }) { await api.comisionesOverrideGuardar(registros); setEdits({}); if (onGuardado) onGuardado(); + cargar(); // recargar la tabla del modal para ver los valores nuevos setMsg({ ok:true, txt:"✅ Cambios guardados." }); setGuardando(false); } catch (e) { setMsg({ ok:false, txt:"Error: " + e.message }); setGuardando(false); } @@ -839,9 +1167,6 @@ function ModalBuscar({ ano, mes, onClose, onGuardado }) { function ModalAgregar({ ano, mes, onClose, onGuardado }) { const [nombre, setNombre] = useState(""); - const [descripcion, setDescripcion] = useState(""); - const [fchEmision, setFchEmision] = useState(""); - const [monto, setMonto] = useState(""); const [guardando, setGuardando] = useState(false); const [msg, setMsg] = useState(null); @@ -849,9 +1174,10 @@ function ModalAgregar({ ano, mes, onClose, onGuardado }) { if (!nombre.trim()) { setMsg("El nombre es obligatorio."); return; } setGuardando(true); setMsg(null); try { + // Solo se crea el vendedor (nombre). La descripción, fecha y monto se + // agregan luego desde el botón "Editar". await api.vendedoresManualesCrear({ - nombre: nombre.trim(), descripcion: descripcion.trim(), - fch_emision: fchEmision, monto: toNum(monto), ano, mes, + nombre: nombre.trim(), descripcion: "", fch_emision: "", monto: 0, ano, mes, }); onGuardado(); } catch (e) { @@ -867,19 +1193,9 @@ function ModalAgregar({ ano, mes, onClose, onGuardado }) {
- setNombre(e.target.value)} style={inp} placeholder="Nombre del vendedor" autoFocus /> -
-
- - setDescripcion(e.target.value)} style={inp} placeholder="Descripción" /> -
-
- - setFchEmision(e.target.value)} style={inp} /> -
-
- - setMonto(e.target.value)} style={inp} placeholder="0" /> + setNombre(e.target.value)} + onKeyDown={(e)=>{ if(e.key==="Enter" && !guardando) guardar(); }} + style={inp} placeholder="Nombre del vendedor" autoFocus />
{msg &&
{msg}
}
@@ -964,12 +1280,14 @@ function ModalDetalle({ vendedor, manual, ano, mes, sede = "TODOS", programa = " } // Orden solicitado de columnas - const HEADERS = ["VENDEDOR","ALUMNO","PROGRAMA","F. MATRÍCULA","INVERSIÓN NETA","SALDO MATRÍCULA","SALDO 1° CUOTA","FECHA INICIO","FECHA CANCELACIÓN 1","PROMEDIO CUOTA","DÍAS ANTICIPACIÓN","VALOR CUOTA ADICIONAL","SEDE"]; - const ORDEN = [0, 1, 8, 2, 7, 10, 11, 9, 3, 5, 12, 13, 14]; - const COL_MONTO = new Set([4, 5, 6, 9, 11]); + // VENDEDOR quitado de este pop-up (Ver). ORDEN mapea posición→índice real de la fila. + const HEADERS = ["ALUMNO","PROGRAMA","F. MATRÍCULA","INVERSIÓN NETA","SALDO MATRÍCULA","SALDO 1° CUOTA","FECHA INICIO","FECHA CANCELACIÓN 1","PROMEDIO CUOTA","DÍAS ANTICIPACIÓN","VALOR CUOTA ADICIONAL","SEDE"]; + const ORDEN = [1, 8, 2, 7, 10, 11, 9, 3, 5, 12, 13, 14]; + // COL_MONTO por POSICIÓN visible (INVERSIÓN NETA, SALDO MAT, SALDO 1°, PROMEDIO, VALOR ADIC) + const COL_MONTO = new Set([3, 4, 5, 8, 10]); // Anchos iniciales por columna (px) — el usuario los puede arrastrar - const ANCHOS_INI = [180, 240, 280, 110, 130, 130, 130, 110, 140, 120, 120, 150, 130]; + const ANCHOS_INI = [240, 280, 110, 130, 130, 130, 110, 140, 120, 120, 150, 130]; const [anchos, setAnchos] = useState(ANCHOS_INI); function iniciarResize(e, i) { @@ -1051,10 +1369,13 @@ function ModalDetalle({ vendedor, manual, ano, mes, sede = "TODOS", programa = " ))} - TOTAL GENERAL ({filas.length}) - - {fmt(totalInv)} - + {HEADERS.map((_, pos) => { + const base = { textAlign:"center", background:"#eff6ff", fontWeight:700, + color:"#1e40af", borderTop:"2px solid #bfdbfe" }; + if (pos === 0) return TOTAL GENERAL ({filas.length}); + if (pos === 3) return {fmt(totalInv)}; + return ; + })}