Files
dashboard-leads/frontend/src/pages/OtrosGeneral.jsx

335 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) + recarga al guardar en Config
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); };
}, []);
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(() => [{ 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 money = (v) => `$${Number(v || 0).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 className="col-name" style={{ position: "sticky", top: 0, zIndex: 2 }}>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 }}>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 }}>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: "#2563eb",
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: "#2563eb",
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>
);
}