Dashboard Leads - version limpia para servidor (.env y config de despliegue restaurados)
This commit is contained in:
138
frontend/src/pages/Roas.jsx
Normal file
138
frontend/src/pages/Roas.jsx
Normal file
@@ -0,0 +1,138 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user