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

139 lines
6.7 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 Roas() {
const hoy = new Date();
const [ano, setAno] = useState(String(hoy.getFullYear()));
const [mes, setMes] = useState(String(hoy.getMonth() + 1));
const [filtros, setFiltros] = useState({ anos: [2026] });
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [expand, setExpand] = useState({}); // sede -> abierto/cerrado
const [expandP, setExpandP] = useState({}); // "sede|prog" -> abierto/cerrado
const [reload, setReload] = useState(0);
useEffect(() => { api.leadsFiltros().then(setFiltros).catch(() => {}); }, []);
useEffect(() => {
const id = setInterval(() => setReload((n) => n + 1), 15 * 60 * 1000);
const onActualizar = () => setReload((n) => n + 1); // recarga al guardar en Config
window.addEventListener("roas-actualizar", onActualizar);
window.addEventListener("datos-actualizar", onActualizar);
return () => { clearInterval(id);
window.removeEventListener("roas-actualizar", onActualizar);
window.removeEventListener("datos-actualizar", onActualizar); };
}, []);
useEffect(() => {
let activo = true;
if (!data) setLoading(true);
setError(null);
api.roas(ano, mes, "TODOS")
.then((res) => { if (activo) { setData(res); setLoading(false); } })
.catch((e) => { if (activo) { setError(e.message); setLoading(false); } });
return () => { activo = false; };
}, [ano, mes, 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 soles = (v) => "$" + Number(v || 0).toLocaleString("es-PE", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const num = (v) => Number(v || 0).toLocaleString("es-PE");
return (
<div>
<h1 className="page-title">📈 ROAS</h1>
<Filters>
<Select label="Año" value={ano} options={optAnos} onChange={setAno} />
<Select label="Mes" value={mes} options={optMes} onChange={setMes} />
</Filters>
{loading ? <Loader text="Cargando ROAS..." /> :
error ? <ErrorBox msg={error} /> :
!data ? <ErrorBox msg="Sin datos" /> :
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
<div className="card-title" style={{ padding: "14px 16px 6px" }}>Inversión por Sede</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: 160 }}>Sede</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Importe Gastado</th>
<th style={{ position: "sticky", top: 0, zIndex: 2 }}>Resultados</th>
</tr>
</thead>
<tbody>
{(data.filas || []).map((f) => {
const subs = f.programas || [];
const abierto = !!expand[f.sede];
return (
<Fragment key={f.sede}>
<tr>
<td className="col-name">
{subs.length > 0 && (
<button onClick={() => setExpand((e) => ({ ...e, [f.sede]: !e[f.sede] }))}
style={{ border: "none", background: "transparent", cursor: "pointer",
color: "#2563eb", fontWeight: 700, fontSize: 15, marginRight: 8, width: 16 }}>
{abierto ? "" : "+"}
</button>
)}
{f.sede}
</td>
<td style={{ fontWeight: 600 }}>{soles(f.importe)}</td>
<td>{num(f.resultados)}</td>
</tr>
{abierto && subs.map((p) => {
const claveP = f.sede + "|" + p.programa;
const abiertoP = !!expandP[claveP];
const conjs = p.conjuntos || [];
return (
<Fragment key={claveP}>
<tr style={{ background: "#f8fafc" }}>
<td className="col-name" style={{ paddingLeft: 24, color: "#475569", fontSize: 13 }}>
{conjs.length > 0 && (
<button onClick={() => setExpandP((e) => ({ ...e, [claveP]: !e[claveP] }))}
style={{ border: "none", background: "transparent", cursor: "pointer",
color: "#2563eb", fontWeight: 700, fontSize: 14, marginRight: 8, width: 14 }}>
{abiertoP ? "" : "+"}
</button>
)}
{p.programa}
</td>
<td style={{ color: "#475569" }}>{soles(p.importe)}</td>
<td style={{ color: "#475569" }}>{num(p.resultados)}</td>
</tr>
{abiertoP && conjs.map((c) => (
<tr key={claveP + "-" + c.conjunto} style={{ background: "#eef2f7" }}>
<td className="col-name" title={c.conjunto} style={{ paddingLeft: 60, color: "#94a3b8", fontSize: 12.5,
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: 280 }}>{c.conjunto}</td>
<td style={{ color: "#94a3b8", fontSize: 12.5 }}>{soles(c.importe)}</td>
<td style={{ color: "#94a3b8", fontSize: 12.5 }}>{num(c.resultados)}</td>
</tr>
))}
</Fragment>
);
})}
</Fragment>
);
})}
{data.total && (
<tr className="total-row">
<td className="col-name">Total</td>
<td>{soles(data.total.importe)}</td>
<td>{num(data.total.resultados)}</td>
</tr>
)}
</tbody>
</table>
</div>
</div>}
</div>
);
}