import { useState, useEffect, useMemo, Fragment } from "react"; import { api } from "../lib/api"; import { Loader, ErrorBox, Filters, Select } from "../components/UI"; import { useColumnasAjustables } from "../lib/useColumnasAjustables"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LabelList, AreaChart, Area, Legend, ComposedChart, } from "recharts"; const MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"]; export default function Leads() { const hoy = new Date(); const [ano, setAno] = useState(String(hoy.getFullYear())); const [mes, setMes] = useState(String(hoy.getMonth() + 1)); const [dia, setDia] = useState("TODOS"); const [tipoProg, setTipoProg] = useState("TODOS"); const [sede, setSede] = useState("TODOS"); const [filtros, setFiltros] = useState({ anos: [2026], tipos: [] }); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [expandido, setExpandido] = useState({}); // estado -> abierto const [expAsesor, setExpAsesor] = useState({}); // "estado|asesor" -> abierto const [expSede, setExpSede] = useState({}); // sede -> abierto (tabla pauta) const [expTipo, setExpTipo] = useState({}); // "sede|tipo" -> abierto (códigos) const [expMat, setExpMat] = useState({}); // num_indice -> abierto (subfilas canal) const [modalCurso, setModalCurso] = useState(null); // curso seleccionado para el pop-up "Ver" const [editando, setEditando] = useState(false); // modo edición del pop-up const [editPauta, setEditPauta] = useState(""); // valor pauta en edición const [editConj, setEditConj] = useState(""); // conjunto elegido const [editContar, setEditContar] = useState("SI"); // switch contar SI/NO const [avisoContar, setAvisoContar] = useState(false);// mostró la alerta al cambiar a NO const [conjOpc, setConjOpc] = useState([]); // conjuntos sin pauta (desplegable) const [guardando, setGuardando] = useState(false); const [msgGuardar, setMsgGuardar] = useState(""); const [reload, setReload] = useState(0); const [avisoUso, setAvisoUso] = useState(null); // {cursos, conjuntos} si la pauta ya se usa // Vista de columnas de la matriz: TOTAL y/o ASIGNADO. Si ninguno → ambos (todo). const [verTotal, setVerTotal] = useState(true); const [verAsig, setVerAsig] = useState(true); // Definición de columnas de la matriz. modo: "fija" | "total" | "asig". // key = campo en la fila; label = encabezado; w = ancho; render opcional. const _fmtMoney = (v) => v ? `$${Number(v).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—"; const COLS_MAT = [ { key: "personalizado", label: "Programa", w: 340, modo: "fija", prog: true }, { key: "fecha_inicio", label: "Fecha Inicio", w: 110, modo: "fija" }, { key: "importe_pauta", label: "Importe Pauta", w: 120, modo: "fija", bold: true, render: (c) => _fmtMoney(c.importe_pauta) }, { key: "importe_pauta_mes", label: "Importe Pauta en el Mes", w: 150, modo: "fija", bold: true, render: (c) => _fmtMoney(c.importe_pauta_mes) }, { key: "cartera_total", label: "Cartera Total", w: 110, modo: "total" }, { key: "cartera_total_asig", label: "Cartera Total Asig.", w: 130, modo: "asig", bold: true }, { key: "leads_nuevos", label: "L. Recibidos", w: 120, modo: "total" }, { key: "leads_nuevos_asesor", label: "L. Procesados", w: 130, modo: "asig" }, { key: "leads_nuevos_mes", label: "L. Recibidos del Mes", w: 150, modo: "total" }, { key: "leads_nuevos_mes_asesor", label: "L. Procesados del Mes", w: 160, modo: "asig" }, { key: "matriculas_no_iden", label: "Matriculas no iden.", w: 140, modo: "fija", bold: true }, { key: "mat_leads_nuevos", label: "Matriculas Leads Nuevos", w: 160, modo: "fija", bold: true }, { key: "mat_leads_antiguos", label: "Matriculas Leads Antiguos", w: 170, modo: "fija", bold: true }, { key: "__ver", label: "", w: 70, modo: "fija", ver: true }, ]; // Si desmarcan ambos → mostrar todo (como si ambos activos) const _t = verTotal || (!verTotal && !verAsig); const _a = verAsig || (!verTotal && !verAsig); const colsVisibles = COLS_MAT.filter( (c) => c.modo === "fija" || (c.modo === "total" && _t) || (c.modo === "asig" && _a) ); // Columnas redimensionables de la matriz (según columnas visibles) const colsMat = useColumnasAjustables(colsVisibles.map((c) => c.w)); // Cargar opciones de filtros una vez useEffect(() => { api.leadsFiltros().then(setFiltros).catch(() => {}); }, []); // Auto-refresco: cada 15 min vuelve a pedir datos frescos (sin que el usuario haga F5) // y al recibir "datos-actualizar" (tras guardar en Config) recarga al instante. useEffect(() => { const id = setInterval(() => setReload((n) => n + 1), 15 * 60 * 1000); const onActualizar = () => setReload((n) => n + 1); window.addEventListener("datos-actualizar", onActualizar); return () => { clearInterval(id); window.removeEventListener("datos-actualizar", onActualizar); }; }, []); // Cargar dashboard cuando cambian los filtros (igual que el PBI) useEffect(() => { let activo = true; // Mostrar spinner solo si aún no hay datos (primera carga). En recargas/auto- // refresco, actualiza en silencio para no tapar la tabla. if (!data) setLoading(true); setError(null); api.leads(ano, mes, dia, tipoProg, sede) .then((res) => { if (activo) { setData(res); setLoading(false); } }) .catch((e) => { if (activo) { setError(e.message); setLoading(false); } }); return () => { activo = false; }; }, [ano, mes, dia, tipoProg, sede, reload]); const k = data?.kpis; const fmtPct = (v) => `${Number(v ?? 0).toFixed(2)} %`; // Cierra el pop-up y resetea el modo edición (para que reabra limpio) function cerrarModal() { setModalCurso(null); setEditando(false); setAvisoUso(null); setAvisoContar(false); setMsgGuardar(""); } // Guarda la pauta/conjunto (llamado tras confirmar o si la pauta no está en uso) async function hacerGuardado() { setGuardando(true); setMsgGuardar(""); setAvisoUso(null); try { await api.guardarPauta(modalCurso.num_indice, editPauta.trim(), editConj || null, editContar); setMsgGuardar("✓ Guardado. Refrescando datos..."); setTimeout(() => { setModalCurso(null); setEditando(false); setReload((n) => n + 1); }, 800); } catch (e) { setMsgGuardar("Error al guardar: " + e.message); } finally { setGuardando(false); } } // opciones de filtros const optAnos = useMemo(() => (filtros.anos || []).map(String), [filtros]); const optMes = useMemo(() => [{ value: "TODOS", label: "TODOS" }, ...MESES.map((m,i)=>({ value: String(i+1), label: m }))], []); const optDia = useMemo(() => ["TODOS", ...Array.from({length:31}, (_,i)=>String(i+1))], []); const optTipo = useMemo(() => ["TODOS", "TEAC", "TERC", "SEMINARIOS", "OTROS"], []); const optSede = useMemo(() => ["TODOS", "LIMA", "AREQUIPA", "TRUJILLO", "PIURA"], []); return (

🎯 Leads

setEditPauta(e.target.value)} placeholder="Código de pauta" style={{ marginTop: 4, padding: "7px 10px", border: "1px solid #cbd5e1", borderRadius: 8, fontSize: 14, width: "100%" }} /> ) : (
{modalCurso.pauta || "—"}
)}
{editando ? ( <>
Vincular Conjunto de Anuncio (opcional)
{/* Switch: contar SI/NO */}
Mostrar en la lista
{editContar === "SI" ? "Este programa aparece en el Detalle por Curso" : "Este programa quedará oculto"}
{ const nuevo = editContar === "SI" ? "NO" : "SI"; setEditContar(nuevo); if (nuevo === "NO") setAvisoContar(true); }} style={{ width: 46, height: 26, borderRadius: 999, cursor: "pointer", background: editContar === "SI" ? "#2563eb" : "#cbd5e1", position: "relative", transition: "background .15s", flexShrink: 0 }}>
{avisoContar && editContar === "NO" && (
⚠️ Al poner NO y guardar, este programa ya no aparecerá en la lista. Solo podrás volver a añadirlo desde Configuración.
)} {msgGuardar &&
{msgGuardar}
}
{(() => { const cambioContar = editContar !== (modalCurso.contar || "SI"); const puedeGuardar = editPauta.trim() || cambioContar; return ( ); })()}
) : ( <>
Conjuntos de Anuncios ({(modalCurso.conjuntos || []).length})
{(modalCurso.conjuntos || []).length === 0 ? (
Sin conjuntos vinculados a esta pauta.
) : (
{modalCurso.conjuntos.map((cj, i) => (
{cj}
))}
)}
)}
)} {/* ── Advertencia: la pauta ya está en uso ── */} {avisoUso && (
⚠️ Esta pauta ya está en uso
La pauta {editPauta} ya está asignada a:
{(avisoUso.cursos || []).length > 0 && (
Programas
{avisoUso.cursos.map((cu, i) => (
{cu.dsc_det_programa || "(sin nombre)"} · índice {cu.num_indice}
))}
)} {(avisoUso.conjuntos || []).length > 0 && (
Conjuntos de anuncios
{avisoUso.conjuntos.map((cj, i) => (
{cj}
))}
)}
¿Estás seguro de continuar?
)} ); } // Tooltip personalizado: mini-gráfico de barras horizontales por tipo de programa function TooltipMatriculas({ active, payload, label }) { if (!active || !payload || !payload.length) return null; const p = payload[0].payload; const detalle = p.detalle || []; const max = Math.max(1, ...detalle.map((d) => d.cantidad)); return (
Día {label} — {p.cantidad} matrícula(s)
{detalle.length === 0 ? (
Sin matrículas
) : detalle.map((d) => (
{d.tipo}
{d.cantidad}
))}
); } function KpiRow({ ico, label, value }) { return (
{ico}
{label}
{value}
); }