Diagnóstico y fix SQL Server: SERVERNAME, TDS_Version, Puerto explicitos

This commit is contained in:
desoladorxx
2026-07-22 16:11:54 -05:00
commit cd18c4ca5e
48 changed files with 8389 additions and 0 deletions

View File

@@ -0,0 +1,760 @@
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)
useEffect(() => {
const id = setInterval(() => setReload((n) => n + 1), 15 * 60 * 1000);
return () => clearInterval(id);
}, []);
// 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(() => 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 (
<div>
<h1 className="page-title">🎯 Leads</h1>
<Filters>
<Select label="Programa" value={tipoProg} options={optTipo} onChange={setTipoProg} />
<Select label="Sede" value={sede} options={optSede} onChange={setSede} />
<Select label="Año" value={ano} options={optAnos} onChange={setAno} />
<Select label="Mes" value={mes} options={optMes} onChange={setMes} />
<Select label="Día" value={dia} options={optDia} onChange={setDia} />
</Filters>
{loading ? <Loader text="Cargando leads..." /> :
error ? <ErrorBox msg={error} /> :
!k ? <ErrorBox msg="Sin datos" /> :
<>
{/* ── FILA DE KPIs (4 tarjetas compuestas, como el PBI) ── */}
<div className="kpis">
{/* Tarjeta 1: Leads Recibidos / Procesados / % */}
<div className="kpi-multi">
<KpiRow ico="🧲" label="Leads Recibidos" value={k.leads_recibidos} />
<KpiRow ico="🛠️" label="Leads Procesados" value={k.leads_procesados} />
<KpiRow ico="↻" label="% Porcentaje Procesados" value={fmtPct(k.pct_procesados)} />
</div>
{/* Tarjeta 2: Procesados / Contactados / % */}
<div className="kpi-multi">
<KpiRow ico="🔧" label="Leads Procesados" value={k.leads_procesados} />
<KpiRow ico="📞" label="Total Contactados" value={k.leads_contactados} />
<KpiRow ico="↻" label="% Contactabilidad" value={fmtPct(k.pct_contactados)} />
</div>
{/* Tarjeta 3: Matriculados / Matrículas mes / Ocupabilidad */}
<div className="kpi-multi">
<KpiRow ico="🎓" label="Total matriculados" value={k.total_matriculados} />
<KpiRow ico="📅" label="Matrículas en los Cursos del Mes" value={k.matriculas_mes} />
<KpiRow ico="💱" label="Ocupabilidad en los Cursos del Mes" value={fmtPct(k.ocupabilidad)} />
</div>
{/* Tarjeta 4: Cursos programados / reprog / susp / iniciados */}
<div className="kpi-multi">
<KpiRow ico="📚" label="Cursos Programados" value={k.cursos_programados} />
<KpiRow ico="🔁" label="Cursos Reprogramados" value={k.cursos_reprogramados} />
<KpiRow ico="⛔" label="Cursos Suspendidos" value={k.cursos_suspendidos} />
<KpiRow ico="🚀" label="Cursos Iniciados" value={k.cursos_iniciados} />
</div>
</div>
{/* ── FILA INFERIOR: gráfico + tabla ── */}
<div style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr", gap: 16, marginTop: 16 }}>
<div className="card">
<div className="card-title">Matrículas por Día</div>
<ResponsiveContainer width="100%" height={320}>
<ComposedChart data={data.matriculas_por_dia} margin={{ top: 24, right: 24, left: -6, bottom: 8 }}>
<defs>
<linearGradient id="gradMat" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.25} />
<stop offset="100%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#eef2f7" vertical={false} />
<XAxis dataKey="dia" tick={{ fontSize: 10, fill: "#94a3b8" }} interval={0}
axisLine={{ stroke: "#e2e8f0" }} tickLine={false} />
<YAxis tick={{ fontSize: 11, fill: "#94a3b8" }} allowDecimals={false}
axisLine={false} tickLine={false} width={32} />
<Tooltip content={<TooltipMatriculas />} cursor={{ stroke: "#cbd5e1", strokeDasharray: "4 4" }} />
<Area type="monotone" dataKey="cantidad" stroke="none" fill="url(#gradMat)" />
<Line type="monotone" dataKey="cantidad" stroke="#2563eb" strokeWidth={2.5}
dot={{ r: 3, fill: "#fff", stroke: "#2563eb", strokeWidth: 2 }}
activeDot={{ r: 5, fill: "#2563eb", stroke: "#fff", strokeWidth: 2 }}>
<LabelList dataKey="cantidad" position="top" fontSize={10} fill="#64748b"
formatter={(v) => (v > 0 ? v : "")} />
</Line>
</ComposedChart>
</ResponsiveContainer>
</div>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
<div className="table-wrap" style={{ border: "none", maxHeight: 360, overflowY: "auto" }}>
<table>
<thead>
<tr>
<th className="col-name" style={{ position: "sticky", top: 0, zIndex: 2 }}>Estado/Objeción</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Leads Procesados</th>
</tr>
</thead>
<tbody>
{data.estado_objecion.filas.map((f) => {
const abierto = !!expandido[f.estado];
const tieneAsesores = (f.asesores || []).length > 0;
return (
<Fragment key={f.estado}>
<tr>
<td className="col-name">
{tieneAsesores && (
<button
onClick={() => setExpandido((p) => ({ ...p, [f.estado]: !p[f.estado] }))}
style={{ border: "none", background: "transparent", cursor: "pointer",
fontWeight: 700, marginRight: 6, color: "#2563eb", fontSize: 14 }}>
{abierto ? "" : "+"}
</button>
)}
{f.estado}
</td>
<td>{f.cantidad}</td>
</tr>
{abierto && (f.asesores || []).map((a) => {
const keyA = f.estado + "|" + a.asesor;
const abiertoA = !!expAsesor[keyA];
const tieneTel = (a.telefonos || []).length > 0;
return (
<Fragment key={keyA}>
<tr style={{ background: "#f8fafc" }}>
<td className="col-name" style={{ paddingLeft: 30, color: "#475569", fontSize: 12 }}>
{tieneTel && (
<button
onClick={() => setExpAsesor((p) => ({ ...p, [keyA]: !p[keyA] }))}
style={{ border: "none", background: "transparent", cursor: "pointer",
fontWeight: 700, marginRight: 6, color: "#7c3aed", fontSize: 13 }}>
{abiertoA ? "" : "+"}
</button>
)}
{a.asesor}
</td>
<td style={{ color: "#475569", fontSize: 12 }}>{a.cantidad}</td>
</tr>
{abiertoA && (a.telefonos || []).map((t, i) => (
<tr key={keyA + "-" + t + "-" + i} style={{ background: "#fcfdff" }}>
<td className="col-name" style={{ paddingLeft: 60, color: "#94a3b8", fontSize: 11 }}>📞 {t}</td>
<td></td>
</tr>
))}
</Fragment>
);
})}
</Fragment>
);
})}
<tr className="total-row">
<td className="col-name">Total</td>
<td>{data.estado_objecion.total}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{/* ── FILA 2: Leads por Programa (área) + Tabla por Sede ── */}
<div style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr", gap: 16, marginTop: 16 }}>
<div className="card">
<div className="card-title">Leads por Programa</div>
<ResponsiveContainer width="100%" height={320}>
<AreaChart data={data.leads_por_dia} margin={{ top: 10, right: 24, left: -6, bottom: 8 }}>
<defs>
<linearGradient id="gradTot" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#60a5fa" stopOpacity={0.55} />
<stop offset="100%" stopColor="#60a5fa" stopOpacity={0.05} />
</linearGradient>
<linearGradient id="gradProc" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#1e40af" stopOpacity={0.6} />
<stop offset="100%" stopColor="#1e40af" stopOpacity={0.1} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#eef2f7" vertical={false} />
<XAxis dataKey="dia" tick={{ fontSize: 10, fill: "#94a3b8" }} interval={0}
axisLine={{ stroke: "#e2e8f0" }} tickLine={false} />
<YAxis tick={{ fontSize: 11, fill: "#94a3b8" }} allowDecimals={false}
axisLine={false} tickLine={false} width={32} />
<Tooltip contentStyle={{ borderRadius: 10, border: "1px solid #e2e8f0",
boxShadow: "0 6px 20px rgba(0,0,0,0.1)", fontSize: 12 }} />
<Legend iconType="circle" wrapperStyle={{ fontSize: 12, paddingTop: 4 }} />
<Area type="linear" dataKey="totales" name="Leads Totales" stroke="#60a5fa"
fill="url(#gradTot)" strokeWidth={2.5}
dot={{ r: 2.5, fill: "#60a5fa", stroke: "#fff", strokeWidth: 1 }}
activeDot={{ r: 5 }} />
<Area type="linear" dataKey="procesados" name="Leads Procesados" stroke="#1e40af"
fill="url(#gradProc)" strokeWidth={2.5}
dot={{ r: 2.5, fill: "#1e40af", stroke: "#fff", strokeWidth: 1 }}
activeDot={{ r: 5 }} />
</AreaChart>
</ResponsiveContainer>
</div>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
<div className="table-wrap" style={{ border: "none", maxHeight: 360, overflowY: "auto" }}>
<table>
<thead>
<tr>
<th className="col-name" style={{ position: "sticky", top: 0, zIndex: 2, minWidth: 150 }}>Sede</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Importe Gastado</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Resultados</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Recibidos</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Procesados</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Matr.</th>
<th style={{ position: "sticky", top: 0, zIndex: 2, minWidth: 130, whiteSpace: "nowrap" }}>Valor Venta</th>
</tr>
</thead>
<tbody>
{data.tabla_pauta.filas.map((f) => {
const abierto = !!expSede[f.sede];
return (
<Fragment key={f.sede}>
<tr>
<td className="col-name">
<button onClick={() => setExpSede((p) => ({ ...p, [f.sede]: !p[f.sede] }))}
style={{ border: "none", background: "transparent", cursor: "pointer",
fontWeight: 700, marginRight: 6, color: "#2563eb", fontSize: 14 }}>
{abierto ? "" : "+"}
</button>
{f.sede}
</td>
<td style={{ fontWeight: 600 }}>{f.importe ? `$${Number(f.importe).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—"}</td>
<td>{f.resultados}</td>
<td>{f.recibidos}</td>
<td>{f.procesados}</td>
<td>{f.matriculas}</td>
<td>S/ {Number(f.inversion).toLocaleString("es-PE")}</td>
</tr>
{abierto && f.subfilas.map((s) => {
const keyT = f.sede + "|" + s.tipo;
const abiertoT = !!expTipo[keyT];
const tieneCod = (s.codigos || []).length > 0;
return (
<Fragment key={keyT}>
<tr style={{ background: "#f8fafc" }}>
<td className="col-name" style={{ paddingLeft: 30, color: "#475569", fontSize: 12 }}>
{tieneCod && (
<button onClick={() => setExpTipo((p) => ({ ...p, [keyT]: !p[keyT] }))}
style={{ border: "none", background: "transparent", cursor: "pointer",
fontWeight: 700, marginRight: 6, color: "#7c3aed", fontSize: 13 }}>
{abiertoT ? "" : "+"}
</button>
)}
{s.tipo}
</td>
<td style={{ color: "#475569", fontSize: 12, fontWeight: 600 }}>{s.importe ? `$${Number(s.importe).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—"}</td>
<td style={{ color: "#475569", fontSize: 12 }}>{s.resultados}</td>
<td style={{ color: "#475569", fontSize: 12 }}>{s.recibidos}</td>
<td style={{ color: "#475569", fontSize: 12 }}>{s.procesados}</td>
<td style={{ color: "#475569", fontSize: 12 }}>{s.matriculas}</td>
<td style={{ color: "#475569", fontSize: 12 }}>S/ {Number(s.inversion).toLocaleString("es-PE")}</td>
</tr>
{abiertoT && (s.codigos || []).map((cd) => (
<tr key={keyT + "-" + cd.codigo} style={{ background: "#fcfdff" }}>
<td className="col-name" style={{ paddingLeft: 56, color: "#94a3b8", fontSize: 11 }}>cód. {cd.codigo}</td>
<td style={{ color: "#94a3b8", fontSize: 11 }}>{cd.importe ? `$${Number(cd.importe).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—"}</td>
<td style={{ color: "#94a3b8", fontSize: 11 }}>{cd.resultados ? cd.resultados : "—"}</td>
<td style={{ color: "#94a3b8", fontSize: 11 }}>{cd.recibidos}</td>
<td style={{ color: "#94a3b8", fontSize: 11 }}>{cd.procesados}</td>
<td style={{ color: "#94a3b8", fontSize: 11 }}>{cd.matriculas}</td>
<td style={{ color: "#94a3b8", fontSize: 11 }}>S/ {Number(cd.inversion).toLocaleString("es-PE")}</td>
</tr>
))}
</Fragment>
);
})}
</Fragment>
);
})}
<tr className="total-row">
<td className="col-name">Total</td>
<td>{`$${Number(data.tabla_pauta.total.importe || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`}</td>
<td>{data.tabla_pauta.total.resultados}</td>
<td>{data.tabla_pauta.total.recibidos}</td>
<td>{data.tabla_pauta.total.procesados}</td>
<td>{data.tabla_pauta.total.matriculas}</td>
<td>S/ {Number(data.tabla_pauta.total.inversion).toLocaleString("es-PE")}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{/* ── FILA 3: Matriz por Curso (num_indice) ── */}
<div className="card" style={{ marginTop: 16, padding: 0, overflow: "hidden" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between",
padding: "14px 16px 6px", gap: 12, flexWrap: "wrap" }}>
<div className="card-title" style={{ padding: 0 }}>Detalle por Curso</div>
<div style={{ display: "flex", alignItems: "center", gap: 8,
background: "#f8fafc", border: "1px solid #e2e8f0",
borderRadius: 10, padding: "5px 8px" }}>
<span style={{ fontSize: 10, fontWeight: 700, color: "#94a3b8",
textTransform: "uppercase", letterSpacing: ".5px", marginRight: 2 }}>
Ver
</span>
{[["TOTAL", verTotal, setVerTotal], ["ASIGNADO", verAsig, setVerAsig]].map(([txt, val, set]) => (
<label key={txt} onClick={() => set((v) => !v)}
style={{
display: "flex", alignItems: "center", gap: 7, cursor: "pointer",
padding: "5px 12px", borderRadius: 8, userSelect: "none",
fontSize: 12, fontWeight: 700, letterSpacing: ".3px",
border: val ? "1px solid #1e3a5f" : "1px solid #e2e8f0",
background: val ? "#eef2f9" : "#fff",
color: val ? "#1e3a5f" : "#64748b",
transition: "all .12s ease",
}}>
<span style={{
width: 16, height: 16, borderRadius: 5, flexShrink: 0,
display: "flex", alignItems: "center", justifyContent: "center",
border: val ? "none" : "1.5px solid #cbd5e1",
background: val ? "#1e3a5f" : "#fff",
}}>
{val && (
<svg width="10" height="10" viewBox="0 0 24 24" fill="none"
stroke="#fff" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
)}
</span>
{txt}
</label>
))}
</div>
</div>
<div className="table-wrap" style={{ border: "none", maxHeight: 420, overflow: "auto" }}>
<table className="matriz-grid" {...colsMat.tableProps}>
<colsMat.ColGroup />
<thead>
<tr>
{colsVisibles.map((col, i) => (
<th key={col.key} style={{ position: "sticky", top: 0, zIndex: 2,
textAlign: i === 0 ? "left" : "center" }}>{col.label}<colsMat.Resizer index={i} /></th>
))}
</tr>
</thead>
<tbody>
{data.matriz_cursos.filas.map((c) => {
const abierto = !!expMat[c.num_indice];
return (
<Fragment key={c.num_indice}>
<tr>
{colsVisibles.map((col) => col.prog ? (
<td key={col.key} className="col-name" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{(c.subfilas_canal && c.subfilas_canal.length > 0) && (
<span
onClick={() => setExpMat((p) => ({ ...p, [c.num_indice]: !p[c.num_indice] }))}
style={{ cursor: "pointer", fontWeight: 700, color: "#1e40af",
marginRight: 6, userSelect: "none", display: "inline-block", width: 14 }}
title="Ver por canal (Pauta / Web / Otros)">
{abierto ? "" : "+"}
</span>
)}
{c.personalizado}
</td>
) : col.ver ? (
<td key={col.key} style={{ textAlign: "center" }}>
<button onClick={() => setModalCurso(c)}
style={{ border: "1px solid #1e3a5f", background: "#eef2f9", color: "#1e3a5f",
borderRadius: 7, padding: "3px 12px", fontSize: 12, fontWeight: 700,
cursor: "pointer" }}>
Ver
</button>
</td>
) : (
<td key={col.key} style={col.bold ? { fontWeight: 600 } : undefined}>
{col.render ? col.render(c) : c[col.key]}
</td>
))}
</tr>
{abierto && (c.subfilas_canal || []).map((sf) => (
<tr key={c.num_indice + sf.canal} style={{ background: "#f8fafc" }}>
{colsVisibles.map((col) => {
if (col.prog) return (
<td key={col.key} className="col-name" style={{ paddingLeft: 30, color: "#475569", fontSize: 12 }}>{sf.canal}</td>
);
if (col.ver) return <td key={col.key}></td>;
// 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 (
<td key={col.key} style={{ color: "#94a3b8", fontSize: 12 }}></td>
);
const val = sf[col.key];
return (
<td key={col.key} style={{ color: "#475569", fontSize: 12 }}>{val === undefined ? "—" : val}</td>
);
})}
</tr>
))}
</Fragment>
);
})}
</tbody>
</table>
</div>
</div>
</>}
{/* ── Pop-up "Ver": num_indice, pauta y conjuntos de anuncios ── */}
{modalCurso && (
<div onClick={cerrarModal}
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,0.55)",
display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000 }}>
<div onClick={(e) => 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" }}>
<div style={{ background: "#1e3a5f", color: "#fff", padding: "16px 22px",
borderTopLeftRadius: 14, borderTopRightRadius: 14,
display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div style={{ fontWeight: 700, fontSize: 15 }}>{modalCurso.personalizado}</div>
<button onClick={cerrarModal}
style={{ background: "transparent", border: "none", color: "#fff", fontSize: 22,
cursor: "pointer", lineHeight: 1 }}>×</button>
</div>
<div style={{ padding: "18px 22px" }}>
<div style={{ display: "flex", gap: 24, marginBottom: 18, alignItems: "flex-start" }}>
<div>
<div style={{ fontSize: 11, color: "#94a3b8", fontWeight: 700, textTransform: "uppercase" }}>Num Índice</div>
<div style={{ fontSize: 18, fontWeight: 700, color: "#0f172a" }}>{modalCurso.num_indice || "—"}</div>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 11, color: "#94a3b8", fontWeight: 700, textTransform: "uppercase" }}>Pauta</div>
{editando ? (
<input value={editPauta} onChange={(e) => 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%" }} />
) : (
<div style={{ fontSize: 18, fontWeight: 700, color: "#1e40af" }}>{modalCurso.pauta || "—"}</div>
)}
</div>
</div>
{editando ? (
<>
<div style={{ fontSize: 11, color: "#94a3b8", fontWeight: 700, textTransform: "uppercase", marginBottom: 6 }}>
Vincular Conjunto de Anuncio (opcional)
</div>
<select value={editConj} onChange={(e) => setEditConj(e.target.value)}
style={{ padding: "8px 10px", border: "1px solid #cbd5e1", borderRadius: 8,
fontSize: 13, width: "100%", background: "#fff" }}>
<option value=""> Ninguno </option>
{conjOpc.map((cj) => <option key={cj} value={cj}>{cj}</option>)}
</select>
{/* Switch: contar SI/NO */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between",
marginTop: 18, padding: "12px 14px", background: "#f8fafc",
border: "1px solid #e2e8f0", borderRadius: 10 }}>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: "#0f172a" }}>Mostrar en la lista</div>
<div style={{ fontSize: 12, color: "#64748b" }}>
{editContar === "SI" ? "Este programa aparece en el Detalle por Curso" : "Este programa quedará oculto"}
</div>
</div>
<div onClick={() => {
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 }}>
<div style={{ width: 20, height: 20, borderRadius: "50%", background: "#fff",
position: "absolute", top: 3, left: editContar === "SI" ? 23 : 3,
transition: "left .15s", boxShadow: "0 1px 3px rgba(0,0,0,0.3)" }} />
</div>
</div>
{avisoContar && editContar === "NO" && (
<div style={{ marginTop: 10, padding: "10px 12px", background: "#fffbeb",
border: "1px solid #fde68a", borderRadius: 8, fontSize: 12.5, color: "#92400e" }}>
Al poner <b>NO</b> y guardar, este programa ya no aparecerá en la lista.
Solo podrás volver a añadirlo desde Configuración.
</div>
)}
{msgGuardar && <div style={{ marginTop: 10, fontSize: 13, color: msgGuardar.startsWith("✓") ? "#047857" : "#b91c1c" }}>{msgGuardar}</div>}
<div style={{ display: "flex", gap: 8, marginTop: 16, justifyContent: "flex-end" }}>
<button onClick={() => { setEditando(false); setMsgGuardar(""); setAvisoContar(false); }}
style={{ padding: "8px 16px", border: "1px solid #cbd5e1", background: "#fff",
borderRadius: 8, fontSize: 13, fontWeight: 600, cursor: "pointer" }}>
Cancelar
</button>
{(() => {
const cambioContar = editContar !== (modalCurso.contar || "SI");
const puedeGuardar = editPauta.trim() || cambioContar;
return (
<button disabled={guardando || !puedeGuardar}
onClick={async () => {
setMsgGuardar("");
try {
// Verificar uso solo si se ingresó pauta
if (editPauta.trim()) {
const uso = await api.usoPauta(editPauta.trim(), modalCurso.num_indice);
if (uso.en_uso) { setAvisoUso(uso); return; }
}
await hacerGuardado();
} catch (e) { setMsgGuardar("Error: " + e.message); }
}}
style={{ padding: "8px 18px", border: "none", background: "#1e3a5f", color: "#fff",
borderRadius: 8, fontSize: 13, fontWeight: 700,
cursor: guardando ? "wait" : "pointer", opacity: (!puedeGuardar || guardando) ? 0.6 : 1 }}>
{guardando ? "Guardando..." : "Guardar"}
</button>
);
})()}
</div>
</>
) : (
<>
<div style={{ fontSize: 11, color: "#94a3b8", fontWeight: 700, textTransform: "uppercase", marginBottom: 8 }}>
Conjuntos de Anuncios ({(modalCurso.conjuntos || []).length})
</div>
{(modalCurso.conjuntos || []).length === 0 ? (
<div style={{ color: "#94a3b8", fontSize: 13 }}>Sin conjuntos vinculados a esta pauta.</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{modalCurso.conjuntos.map((cj, i) => (
<div key={i} style={{ background: "#f8fafc", border: "1px solid #e2e8f0",
borderRadius: 8, padding: "8px 12px", fontSize: 13, color: "#334155" }}>
{cj}
</div>
))}
</div>
)}
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: 16 }}>
<button onClick={() => {
setEditando(true); setEditPauta(modalCurso.pauta || ""); setEditConj("");
setEditContar(modalCurso.contar || "SI"); setAvisoContar(false); setMsgGuardar("");
api.conjuntosSinPauta().then((r) => setConjOpc(r.conjuntos || [])).catch(() => setConjOpc([]));
}}
style={{ padding: "8px 18px", border: "1px solid #1e3a5f", background: "#eef2f9",
color: "#1e3a5f", borderRadius: 8, fontSize: 13, fontWeight: 700, cursor: "pointer" }}>
Editar
</button>
</div>
</>
)}
</div>
</div>
</div>
)}
{/* ── Advertencia: la pauta ya está en uso ── */}
{avisoUso && (
<div style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,0.6)",
display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1100 }}>
<div style={{ background: "#fff", borderRadius: 14, width: 440, maxWidth: "92vw",
maxHeight: "82vh", overflow: "auto", boxShadow: "0 20px 60px rgba(0,0,0,0.4)" }}>
<div style={{ background: "#b45309", color: "#fff", padding: "14px 20px",
borderTopLeftRadius: 14, borderTopRightRadius: 14, fontWeight: 700, fontSize: 15 }}>
Esta pauta ya está en uso
</div>
<div style={{ padding: "18px 22px" }}>
<div style={{ fontSize: 13, color: "#334155", marginBottom: 12 }}>
La pauta <b>{editPauta}</b> ya está asignada a:
</div>
{(avisoUso.cursos || []).length > 0 && (
<div style={{ marginBottom: 12 }}>
<div style={{ fontSize: 11, color: "#94a3b8", fontWeight: 700, textTransform: "uppercase", marginBottom: 4 }}>Programas</div>
{avisoUso.cursos.map((cu, i) => (
<div key={i} style={{ background: "#fffbeb", border: "1px solid #fde68a",
borderRadius: 8, padding: "6px 10px", fontSize: 12, color: "#92400e", marginBottom: 4 }}>
{cu.dsc_det_programa || "(sin nombre)"} <span style={{ color: "#b45309" }}>· índice {cu.num_indice}</span>
</div>
))}
</div>
)}
{(avisoUso.conjuntos || []).length > 0 && (
<div style={{ marginBottom: 12 }}>
<div style={{ fontSize: 11, color: "#94a3b8", fontWeight: 700, textTransform: "uppercase", marginBottom: 4 }}>Conjuntos de anuncios</div>
{avisoUso.conjuntos.map((cj, i) => (
<div key={i} style={{ background: "#f8fafc", border: "1px solid #e2e8f0",
borderRadius: 8, padding: "6px 10px", fontSize: 12, color: "#334155", marginBottom: 4 }}>
{cj}
</div>
))}
</div>
)}
<div style={{ fontSize: 13, color: "#0f172a", fontWeight: 600, marginTop: 10 }}>
¿Estás seguro de continuar?
</div>
<div style={{ display: "flex", gap: 8, marginTop: 16, justifyContent: "flex-end" }}>
<button onClick={() => setAvisoUso(null)}
style={{ padding: "8px 18px", border: "1px solid #cbd5e1", background: "#fff",
borderRadius: 8, fontSize: 13, fontWeight: 600, cursor: "pointer" }}>
No
</button>
<button onClick={hacerGuardado}
style={{ padding: "8px 20px", border: "none", background: "#b45309", color: "#fff",
borderRadius: 8, fontSize: 13, fontWeight: 700, cursor: "pointer" }}>
, guardar
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}
// 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 (
<div style={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 10,
padding: "10px 12px", boxShadow: "0 6px 20px rgba(0,0,0,0.12)", minWidth: 220 }}>
<div style={{ fontWeight: 700, fontSize: 12, color: "#1e293b", marginBottom: 2 }}>
Día {label} {p.cantidad} matrícula(s)
</div>
{detalle.length === 0 ? (
<div style={{ fontSize: 11, color: "#94a3b8" }}>Sin matrículas</div>
) : detalle.map((d) => (
<div key={d.tipo} style={{ display: "flex", alignItems: "center", gap: 6, margin: "4px 0" }}>
<span style={{ fontSize: 10, color: "#475569", width: 90, textAlign: "right",
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{d.tipo}</span>
<div style={{ flex: 1, background: "#eff6ff", borderRadius: 3, height: 14, position: "relative" }}>
<div style={{ width: `${(d.cantidad / max) * 100}%`, background: "#60a5fa",
height: "100%", borderRadius: 3 }} />
</div>
<span style={{ fontSize: 11, fontWeight: 700, color: "#1e40af", width: 18 }}>{d.cantidad}</span>
</div>
))}
</div>
);
}
function KpiRow({ ico, label, value }) {
return (
<div className="kpi-row">
<span className="ico">{ico}</span>
<div className="txt">
<div className="label">{label}</div>
<div className="value">{value}</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,331 @@
import { useState, useEffect, useMemo, Fragment } from "react";
import { api } from "../lib/api";
import { Loader, ErrorBox, Filters, Select } from "../components/UI";
const MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"];
export default function OtrosGeneral() {
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 [filtros, setFiltros] = useState({ anos: [2026] });
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [expAlways, setExpAlways] = useState({});
const [expAlwaysProg, setExpAlwaysProg] = useState({});
const [expWeb, setExpWeb] = useState({});
const [expAsig, setExpAsig] = useState({});
const [reload, setReload] = useState(0);
useEffect(() => {
api.leadsFiltros().then(setFiltros).catch(() => {});
}, []);
// Auto-refresco cada 15 min (sin F5)
useEffect(() => {
const id = setInterval(() => setReload((n) => n + 1), 15 * 60 * 1000);
return () => clearInterval(id);
}, []);
useEffect(() => {
let activo = true;
if (!data) setLoading(true); // spinner solo en la primera carga
setError(null);
api.otrosGeneral(ano, mes, dia)
.then((res) => { if (activo) { setData(res); setLoading(false); } })
.catch((e) => { if (activo) { setError(e.message); setLoading(false); } });
return () => { activo = false; };
}, [ano, mes, dia, reload]);
const optAnos = useMemo(() => (filtros.anos || []).map(String), [filtros]);
const optMes = useMemo(() => MESES.map((m,i)=>({ value: String(i+1), label: m })), []);
const optDia = useMemo(() => ["TODOS", ...Array.from({length:31}, (_,i)=>String(i+1))], []);
const money = (v) => `$${Number(v).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const sol = (v) => `S/ ${Number(v || 0).toLocaleString("en-US", { maximumFractionDigits: 0 })}`;
return (
<div>
<h1 className="page-title">📊 Otros General</h1>
<Filters>
<Select label="Año" value={ano} options={optAnos} onChange={setAno} />
<Select label="Mes" value={mes} options={optMes} onChange={setMes} />
<Select label="Día" value={dia} options={optDia} onChange={setDia} />
</Filters>
{loading ? <Loader text="Cargando..." /> :
error ? <ErrorBox msg={error} /> :
!data ? <ErrorBox msg="Sin datos" /> :
<>
{/* ── Inversión Publicitaria por Sede (Always On) ── */}
{data.matriz_always && data.matriz_always.filas && (
<div className="card" style={{ marginTop: 4, padding: 0, overflow: "hidden" }}>
<div className="card-title" style={{ padding: "14px 16px 6px" }}>Inversión Publicitaria por Sede (Always On)</div>
<div className="table-wrap" style={{ border: "none", maxHeight: 420, overflow: "auto" }}>
<table>
<thead>
<tr>
<th style={{ textAlign: "left" }}>Sede</th>
<th>Importe Gastado</th>
<th>Resultados</th>
<th>Leads Recibidos</th>
<th>Leads Procesados</th>
<th>Matrículas</th>
<th>Valor Venta</th>
</tr>
</thead>
<tbody>
{data.matriz_always.filas.map((f) => {
const abierto = !!expAlways[f.sede];
return (
<Fragment key={f.sede}>
<tr>
<td className="col-name">
<span
onClick={() => setExpAlways((p) => ({ ...p, [f.sede]: !p[f.sede] }))}
style={{ cursor: "pointer", fontWeight: 700, color: "#1e40af",
marginRight: 6, userSelect: "none", display: "inline-block", width: 14 }}
title="Ver por programa">
{abierto ? "" : "+"}
</span>
{f.sede}
</td>
<td style={{ fontWeight: 600 }}>{money(f.importe)}</td>
<td>{f.resultados}</td>
<td>{f.nuevos}</td>
<td>{f.nuevos_asig}</td>
<td style={{ fontWeight: 600 }}>{f.matriculas}</td>
<td style={{ fontWeight: 600 }}>{sol(f.venta)}</td>
</tr>
{abierto && f.subfilas.map((s) => {
const kProg = f.sede + "|" + s.programa;
const abiertoP = !!expAlwaysProg[kProg];
return (
<Fragment key={kProg}>
<tr style={{ background: "#f8fafc" }}>
<td className="col-name" style={{ paddingLeft: 28, color: "#475569" }}>
<span
onClick={() => setExpAlwaysProg((p) => ({ ...p, [kProg]: !p[kProg] }))}
style={{ cursor: "pointer", fontWeight: 700, color: "#1e40af",
marginRight: 6, userSelect: "none", display: "inline-block", width: 14 }}
title="Ver por pauta">
{abiertoP ? "" : "+"}
</span>
{s.programa}
</td>
<td style={{ color: "#475569", fontWeight: 600 }}>{money(s.importe)}</td>
<td style={{ color: "#475569" }}>{s.resultados}</td>
<td style={{ color: "#475569" }}>{s.nuevos}</td>
<td style={{ color: "#475569" }}>{s.nuevos_asig}</td>
<td style={{ color: "#475569", fontWeight: 600 }}>{s.matriculas}</td>
<td style={{ color: "#475569", fontWeight: 600 }}>{sol(s.venta)}</td>
</tr>
{abiertoP && (s.pautas || []).map((pt) => (
<tr key={kProg + pt.pauta} style={{ background: "#eef2f7" }}>
<td className="col-name" style={{ paddingLeft: 56, color: "#64748b", fontSize: 12 }}>Pauta {pt.pauta}</td>
<td style={{ color: "#64748b", fontSize: 12 }}>{money(pt.importe)}</td>
<td style={{ color: "#64748b", fontSize: 12 }}>{pt.resultados}</td>
<td style={{ color: "#64748b", fontSize: 12 }}>{pt.nuevos}</td>
<td style={{ color: "#64748b", fontSize: 12 }}>{pt.nuevos_asig}</td>
<td style={{ color: "#64748b", fontSize: 12 }}>{pt.matriculas}</td>
<td style={{ color: "#64748b", fontSize: 12 }}>{sol(pt.venta)}</td>
</tr>
))}
</Fragment>
);
})}
</Fragment>
);
})}
{data.matriz_always.total && (
<tr className="total-row">
<td className="col-name">Total</td>
<td>{money(data.matriz_always.total.importe)}</td>
<td>{data.matriz_always.total.resultados}</td>
<td>{data.matriz_always.total.nuevos}</td>
<td>{data.matriz_always.total.nuevos_asig}</td>
<td>{data.matriz_always.total.matriculas}</td>
<td>{sol(data.matriz_always.total.venta)}</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
{/* ── Leads Web Formulario por Sede ── */}
{data.matriz_webform && data.matriz_webform.filas && (
<div className="card" style={{ marginTop: 16, padding: 0, overflow: "hidden" }}>
<div className="card-title" style={{ padding: "14px 16px 6px" }}>Leads Web Formulario por Sede</div>
<div className="table-wrap" style={{ border: "none", maxHeight: 420, overflow: "auto" }}>
<table>
<thead>
<tr>
<th className="col-name" style={{ position: "sticky", top: 0, zIndex: 2, minWidth: 150 }}>Sede</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Leads Recibidos</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Leads Procesados</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Matrículas</th>
<th style={{ position: "sticky", top: 0, zIndex: 2, minWidth: 130, whiteSpace: "nowrap" }}>Valor Venta</th>
</tr>
</thead>
<tbody>
{data.matriz_webform.filas.map((f) => {
const abierto = !!expWeb[f.sede];
return (
<Fragment key={f.sede}>
<tr>
<td className="col-name">
<button onClick={() => setExpWeb((p) => ({ ...p, [f.sede]: !p[f.sede] }))}
style={{ border: "none", background: "transparent", cursor: "pointer",
fontWeight: 700, marginRight: 6, color: "#2563eb", fontSize: 14 }}>
{abierto ? "" : "+"}
</button>
{f.sede}
</td>
<td>{f.recibidos}</td>
<td>{f.procesados}</td>
<td style={{ fontWeight: 600 }}>{f.matriculas}</td>
<td style={{ fontWeight: 600 }}>{sol(f.venta)}</td>
</tr>
{abierto && f.subfilas.map((s) => (
<tr key={f.sede + s.programa} style={{ background: "#f8fafc" }}>
<td className="col-name" style={{ paddingLeft: 30, color: "#475569", fontSize: 12 }}>{s.programa}</td>
<td style={{ color: "#475569", fontSize: 12 }}>{s.recibidos}</td>
<td style={{ color: "#475569", fontSize: 12 }}>{s.procesados}</td>
<td style={{ color: "#475569", fontSize: 12 }}>{s.matriculas}</td>
<td style={{ color: "#475569", fontSize: 12 }}>{sol(s.venta)}</td>
</tr>
))}
</Fragment>
);
})}
{data.matriz_webform.total && (
<tr className="total-row">
<td className="col-name">Total</td>
<td>{data.matriz_webform.total.recibidos}</td>
<td>{data.matriz_webform.total.procesados}</td>
<td>{data.matriz_webform.total.matriculas}</td>
<td>{sol(data.matriz_webform.total.venta)}</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
{/* ── Leads Asignados por Asesor y Día (pivot) ── */}
{data.matriz_asignados && data.matriz_asignados.filas && (
<div className="card" style={{ marginTop: 16, padding: 0, overflow: "hidden" }}>
<div className="card-title" style={{ padding: "14px 16px 6px" }}>Leads Asignados por Asesor y Día</div>
<div className="table-wrap" style={{ border: "none", maxHeight: 460, overflow: "auto" }}>
<table>
<thead>
<tr>
<th className="col-name" style={{ position: "sticky", top: 0, left: 0, zIndex: 3, minWidth: 180, background: "#1e3a5f" }}>Asesor</th>
{data.matriz_asignados.dias.map((d) => (
<th key={d} style={{ position: "sticky", top: 0, zIndex: 2 }}>{d}</th>
))}
<th style={{ position: "sticky", top: 0, zIndex: 2, minWidth: 70 }}>Total</th>
</tr>
</thead>
<tbody>
{data.matriz_asignados.filas.map((f) => {
const abierto = !!expAsig[f.asesor];
return (
<Fragment key={f.asesor}>
<tr>
<td className="col-name">
<button onClick={() => setExpAsig((p) => ({ ...p, [f.asesor]: !p[f.asesor] }))}
style={{ border: "none", background: "transparent", cursor: "pointer",
fontWeight: 700, marginRight: 6, color: "#2563eb", fontSize: 14 }}>
{abierto ? "" : "+"}
</button>
{f.asesor}
</td>
{data.matriz_asignados.dias.map((d) => (
<td key={d} style={{ color: f.por_dia[d] ? "#0f172a" : "#cbd5e1" }}>{f.por_dia[d] || ""}</td>
))}
<td style={{ fontWeight: 700 }}>{f.total}</td>
</tr>
{abierto && (
<tr>
<td colSpan={data.matriz_asignados.dias.length + 2} style={{ background: "#f8fafc", padding: "8px 14px" }}>
{Object.keys(f.tels_dia).length === 0
? <span style={{ color: "#94a3b8" }}></span>
: Object.entries(f.tels_dia).map(([d, tels]) => (
<div key={d} style={{ marginBottom: 6 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: "#475569" }}>Día {d} ({tels.length}): </span>
<span style={{ fontSize: 12, color: "#64748b" }}>{tels.join(", ")}</span>
</div>
))}
</td>
</tr>
)}
</Fragment>
);
})}
<tr className="total-row">
<td className="col-name">Total</td>
{data.matriz_asignados.dias.map((d) => (
<td key={d}>{data.matriz_asignados.total_por_dia[d] || ""}</td>
))}
<td>{data.matriz_asignados.total}</td>
</tr>
</tbody>
</table>
</div>
</div>
)}
{/* ── Plantillas WhatsApp (cobradas) por Plantilla ── */}
{data.matriz_plantillas && data.matriz_plantillas.filas && (
<div className="card" style={{ marginTop: 16, padding: 0, overflow: "hidden" }}>
<div className="card-title" style={{ padding: "14px 16px 6px" }}>Plantillas WhatsApp (cobradas)</div>
<div className="table-wrap" style={{ border: "none", maxHeight: 460, overflow: "auto" }}>
<table>
<thead>
<tr>
<th className="col-name" style={{ position: "sticky", top: 0, zIndex: 2, minWidth: 220 }}>Plantilla</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Enviadas</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Respondidas</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Asignadas</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Matrículas</th>
<th style={{ position: "sticky", top: 0, zIndex: 2, minWidth: 130, whiteSpace: "nowrap" }}>Valor Venta</th>
</tr>
</thead>
<tbody>
{data.matriz_plantillas.filas.map((f) => (
<tr key={f.plantilla}>
<td className="col-name">{f.plantilla}</td>
<td style={{ fontWeight: 600 }}>{f.enviadas}</td>
<td>{f.respondidas}</td>
<td>{f.asignadas}</td>
<td style={{ fontWeight: 600 }}>{f.matriculas}</td>
<td style={{ fontWeight: 600 }}>{sol(f.venta)}</td>
</tr>
))}
{data.matriz_plantillas.total && (
<tr className="total-row">
<td className="col-name">Total</td>
<td>{data.matriz_plantillas.total.enviadas}</td>
<td>{data.matriz_plantillas.total.respondidas}</td>
<td>{data.matriz_plantillas.total.asignadas}</td>
<td>{data.matriz_plantillas.total.matriculas}</td>
<td>{sol(data.matriz_plantillas.total.venta)}</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
</>}
</div>
);
}