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
{loading ?
:
error ?
:
!k ?
:
<>
{/* ── FILA DE KPIs (4 tarjetas compuestas, como el PBI) ── */}
{/* Tarjeta 1: Leads Recibidos / Procesados / % */}
{/* Tarjeta 2: Procesados / Contactados / % */}
{/* Tarjeta 3: Matriculados / Matrículas mes / Ocupabilidad */}
{/* Tarjeta 4: Cursos programados / reprog / susp / iniciados */}
{/* ── FILA INFERIOR: gráfico + tabla ── */}
Matrículas por Día
} cursor={{ stroke: "#cbd5e1", strokeDasharray: "4 4" }} />
(v > 0 ? v : "")} />
| Estado/Objeción |
Leads Procesados |
{(data.estado_objecion?.filas || []).map((f) => {
const abierto = !!expandido[f.estado];
const tieneAsesores = (f.asesores || []).length > 0;
return (
|
{tieneAsesores && (
)}
{f.estado}
|
{f.cantidad} |
{abierto && (f.asesores || []).map((a) => {
const keyA = f.estado + "|" + a.asesor;
const abiertoA = !!expAsesor[keyA];
const tieneTel = (a.telefonos || []).length > 0;
return (
|
{tieneTel && (
)}
{a.asesor}
|
{a.cantidad} |
{abiertoA && (a.telefonos || []).map((t, i) => (
| 📞 {t} |
|
))}
);
})}
);
})}
| Total |
{data.estado_objecion.total} |
{/* ── FILA 2: Leads por Programa (área) + Tabla por Sede ── */}
| Sede |
Importe Gastado |
Resultados |
Recibidos |
Procesados |
Matr. |
Valor Venta |
{(data.tabla_pauta?.filas || []).map((f) => {
const abierto = !!expSede[f.sede];
return (
|
{f.sede}
|
{f.importe ? `$${Number(f.importe).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—"} |
{f.resultados} |
{f.recibidos} |
{f.procesados} |
{f.matriculas} |
S/ {Number(f.inversion || 0).toLocaleString("es-PE")} |
{abierto && (f.subfilas || []).map((s) => {
const keyT = f.sede + "|" + s.tipo;
const abiertoT = !!expTipo[keyT];
const tieneCod = (s.codigos || []).length > 0;
return (
|
{tieneCod && (
)}
{s.tipo}
|
{s.importe ? `$${Number(s.importe).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—"} |
{s.resultados} |
{s.recibidos} |
{s.procesados} |
{s.matriculas} |
S/ {Number(s.inversion || 0).toLocaleString("es-PE")} |
{abiertoT && (s.codigos || []).map((cd) => (
| cód. {cd.codigo} |
{cd.importe ? `$${Number(cd.importe).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—"} |
{cd.resultados ? cd.resultados : "—"} |
{cd.recibidos} |
{cd.procesados} |
{cd.matriculas} |
S/ {Number(cd.inversion || 0).toLocaleString("es-PE")} |
))}
);
})}
);
})}
| Total |
{`$${Number(data.tabla_pauta.total.importe || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`} |
{data.tabla_pauta.total.resultados} |
{data.tabla_pauta.total.recibidos} |
{data.tabla_pauta.total.procesados} |
{data.tabla_pauta.total.matriculas} |
S/ {Number(data.tabla_pauta.total.inversion).toLocaleString("es-PE")} |
{/* ── FILA 3: Matriz por Curso (num_indice) ── */}
Detalle por Curso
Ver
{[["TOTAL", verTotal, setVerTotal], ["ASIGNADO", verAsig, setVerAsig]].map(([txt, val, set]) => (
))}
{colsVisibles.map((col, i) => (
| {col.label} |
))}
{(data.matriz_cursos?.filas || []).map((c) => {
const abierto = !!expMat[c.num_indice];
return (
{colsVisibles.map((col) => col.prog ? (
|
{(c.subfilas_canal && c.subfilas_canal.length > 0) && (
setExpMat((p) => ({ ...p, [c.num_indice]: !p[c.num_indice] }))}
style={{ cursor: "pointer", fontWeight: 700, color: "#2563eb",
marginRight: 6, userSelect: "none", display: "inline-block", width: 14 }}
title="Ver por canal (Pauta / Web / Otros)">
{abierto ? "−" : "+"}
)}
{c.personalizado}
|
) : col.ver ? (
|
) : (
{col.render ? col.render(c) : c[col.key]}
|
))}
{abierto && (c.subfilas_canal || []).map((sf) => (
{colsVisibles.map((col) => {
if (col.prog) return (
| {sf.canal} |
);
if (col.ver) return | ;
// Estas columnas -> "-" en subfilas (Importe y Leads Recibidos/Procesados)
const GUION = ["importe_pauta", "importe_pauta_mes",
"leads_nuevos", "leads_nuevos_asesor",
"leads_nuevos_mes", "leads_nuevos_mes_asesor"];
if (GUION.includes(col.key)) return (
— |
);
const val = sf[col.key];
return (
{val === undefined ? "—" : val} |
);
})}
))}
);
})}
>}
{/* ── Pop-up "Ver": num_indice, pauta y conjuntos de anuncios ── */}
{modalCurso && (
e.stopPropagation()}
style={{ background: "#fff", borderRadius: 16, width: 560, maxWidth: "94vw",
maxHeight: "86vh", overflow: "auto", boxShadow: "0 24px 70px rgba(0,0,0,0.4)",
fontFamily: "system-ui, -apple-system, 'Segoe UI', sans-serif" }}>
{modalCurso.personalizado}
Num Índice
{modalCurso.num_indice || "—"}
Pauta
{editando ? (
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) => (
))}
);
}
function KpiRow({ ico, label, value }) {
return (
);
}