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

488 lines
27 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";
import {
AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, ReferenceLine,
} from "recharts";
const MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"];
export default function Programa({ inicial = null, onConsumirInicial } = {}) {
const _hoy = new Date();
const [ano, setAno] = useState(String(inicial?.ano ?? _hoy.getFullYear()));
const [mes, setMes] = useState(String(inicial?.mes ?? (_hoy.getMonth() + 1)));
const [programa, setPrograma] = useState("TODOS");
const [filtros, setFiltros] = useState({ anos: [2026] });
const [progOpc, setProgOpc] = useState([]); // lista de programas del periodo
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [recargando, setRecargando] = useState(false);
const [error, setError] = useState(null);
const [reload, setReload] = useState(0);
// Programa que se debe seleccionar automaticamente al llegar por enlace (Leads).
const [progPendiente, setProgPendiente] = useState(inicial?.programa || null);
// Expansion de la tabla Estado/Objecion (mismo patron que el apartado Leads)
const [expEstado, setExpEstado] = useState({}); // estado -> abierto
const [expEstAse, setExpEstAse] = useState({}); // "estado|asesor" -> abierto
useEffect(() => { api.leadsFiltros().then(setFiltros).catch(() => {}); }, []);
useEffect(() => {
const onActualizar = () => setReload((n) => n + 1);
window.addEventListener("datos-actualizar", onActualizar);
return () => window.removeEventListener("datos-actualizar", onActualizar);
}, []);
// Cargar la lista de programas cuando cambia año/mes; resetea el programa elegido.
// NO depende de 'reload' para no perder el programa seleccionado al guardar leyenda.
useEffect(() => {
api.programaOpciones(ano, mes)
.then((r) => setProgOpc(r.programas || []))
.catch(() => setProgOpc([]));
// Si venimos por enlace desde Leads, NO reseteamos a TODOS (el otro effect lo
// seleccionara cuando la lista de programas ya lo contenga).
if (!progPendiente) { setPrograma("TODOS"); setData(null); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ano, mes]);
// Al llegar por enlace: cuando la lista de programas ya incluye el pendiente,
// seleccionarlo una sola vez y consumir el estado inicial.
useEffect(() => {
if (!progPendiente) return;
if (progOpc.includes(progPendiente)) {
setPrograma(progPendiente);
setProgPendiente(null);
if (onConsumirInicial) onConsumirInicial();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [progOpc, progPendiente]);
// Al guardar leyenda (reload), refrescar la lista de programas SIN resetear el elegido.
useEffect(() => {
if (reload === 0) return;
api.programaOpciones(ano, mes)
.then((r) => setProgOpc(r.programas || []))
.catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reload]);
// Cargar datos del programa elegido
useEffect(() => {
if (programa === "TODOS") { setData(null); return; }
let activo = true;
if (data) setRecargando(true); else setLoading(true);
api.programa(ano, mes, programa)
.then((r) => { if (activo) { setData(r); setError(null); } })
.catch((e) => { if (activo) setError(e.message); })
.finally(() => { if (activo) { setLoading(false); setRecargando(false); } });
return () => { activo = false; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ano, mes, programa, reload]);
const num = (v) => Number(v || 0).toLocaleString("es-PE");
const optAnos = useMemo(() =>
(filtros.anos || [2026]).map((a) => ({ value: String(a), label: String(a) })), [filtros]);
const optMes = useMemo(() =>
MESES.map((m, i) => ({ value: String(i + 1), label: m })), []);
const optProg = useMemo(() =>
[{ value: "TODOS", label: "— Elige un programa —" },
...progOpc.map((p) => ({ value: p, label: p }))], [progOpc]);
return (
<div>
<h1 className="page-title">📘 Programa</h1>
<Filters>
<Select label="Año" value={ano} options={optAnos} onChange={setAno} />
<Select label="Mes" value={mes} options={optMes} onChange={setMes} />
<Select label="Programa" value={programa} options={optProg} onChange={setPrograma} />
</Filters>
{programa === "TODOS" ? (
<div style={{ padding: 50, textAlign: "center", color: "#94a3b8", fontSize: 14 }}>
Elige un programa del filtro para ver sus datos.
</div>
) : loading ? <Loader text="Cargando programa..." /> :
error ? <ErrorBox msg={error} /> :
!data ? <ErrorBox msg="Sin datos" /> :
<div style={{ opacity: recargando ? 0.45 : 1,
pointerEvents: recargando ? "none" : "auto",
transition: "opacity 0.25s ease" }}>
<Tarjetas r={data} num={num} />
{/* Gráfica (ancho de 3 tarjetas) + Estado/Objeción (ancho de 1) */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 14, marginTop: 4,
alignItems: "stretch" }}>
{/* Gráfica: Leads Recibidos / Procesados / Contactados por fecha */}
<div className="card" style={{ padding: "16px 18px 10px", minWidth: 0, gridColumn: "span 3",
height: 460, display: "flex", flexDirection: "column" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 4 }}>
<div className="card-title" style={{ margin: 0 }}>Leads por Fecha</div>
<div style={{ fontSize: 11.5, color: "#94a3b8" }}>{(data.serie || []).length} días con leads</div>
</div>
{(data.serie || []).length === 0 ? (
<div style={{ padding: 40, textAlign: "center", color: "#94a3b8", fontSize: 13 }}>
Sin leads para este programa.
</div>
) : (
<div style={{ width: "100%", overflowX: "auto", overflowY: "hidden", paddingBottom: 6 }}>
<div style={{ width: (data.serie.length * 24) + "px", minWidth: "100%", height: 380 }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data.serie} margin={{ top: 14, right: 26, left: 30, bottom: 24 }}>
<defs>
<linearGradient id="gRec" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.28} />
<stop offset="100%" stopColor="#3b82f6" stopOpacity={0.02} />
</linearGradient>
<linearGradient id="gProc" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#0d9488" stopOpacity={0.26} />
<stop offset="100%" stopColor="#0d9488" stopOpacity={0.02} />
</linearGradient>
<linearGradient id="gCont" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#8b5cf6" stopOpacity={0.24} />
<stop offset="100%" stopColor="#8b5cf6" stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="4 4" stroke="#f0f3f8" vertical={false} />
{/* Lineas verticales al cambiar de mes */}
{data.serie.map((p, i) =>
(i > 0 && p.mes !== data.serie[i - 1].mes) ? (
<ReferenceLine key={"rl" + i} x={p.fecha} stroke="#94a3b8"
strokeDasharray="4 3" strokeWidth={1.3} />
) : null
)}
{/* Eje de DIAS (arriba visible). dataKey=fecha (unico) pero muestra el dia. */}
<XAxis dataKey="fecha" interval={0} tick={{ fontSize: 9, fill: "#94a3b8" }}
tickFormatter={(v) => (v || "").slice(0, 2)}
axisLine={{ stroke: "#e2e8f0" }} tickLine={false} height={18} minTickGap={4} />
{/* Eje de MES (abajo, una vez por grupo) */}
<XAxis dataKey="mes" xAxisId="mes" axisLine={false} tickLine={false}
tick={<TickMes serie={data.serie} />} interval={0} height={16} />
<YAxis tick={{ fontSize: 11, fill: "#94a3b8" }} allowDecimals={false}
axisLine={false} tickLine={false} width={34} />
<Tooltip contentStyle={{ borderRadius: 12, border: "1px solid #e2e8f0",
boxShadow: "0 8px 24px rgba(0,0,0,0.12)", fontSize: 12.5, padding: "10px 12px" }}
labelStyle={{ fontWeight: 700, color: "#0f172a", marginBottom: 4 }}
labelFormatter={(v, p) => (p && p[0] ? p[0].payload.fecha : v)} />
<Area type="linear" dataKey="recibidos" name="Leads Recibidos" stroke="#3b82f6"
strokeWidth={2.4} fill="url(#gRec)" dot={false} activeDot={{ r: 5, strokeWidth: 2, stroke: "#fff" }} />
<Area type="linear" dataKey="procesados" name="Leads Procesados" stroke="#0d9488"
strokeWidth={2.4} fill="url(#gProc)" dot={false} activeDot={{ r: 5, strokeWidth: 2, stroke: "#fff" }} />
<Area type="linear" dataKey="contactados" name="Leads Contactados" stroke="#8b5cf6"
strokeWidth={2.4} fill="url(#gCont)" dot={false} activeDot={{ r: 5, strokeWidth: 2, stroke: "#fff" }} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Leyenda fija (fuera del scroll) */}
{(data.serie || []).length > 0 && (
<div style={{ display: "flex", justifyContent: "center", gap: 20, paddingTop: 8, fontSize: 12 }}>
{[["Leads Recibidos", "#3b82f6"], ["Leads Procesados", "#0d9488"], ["Leads Contactados", "#8b5cf6"]].map(([t, c]) => (
<span key={t} style={{ display: "inline-flex", alignItems: "center", gap: 6, color: "#475569" }}>
<span style={{ width: 10, height: 10, borderRadius: "50%", background: c }} />{t}
</span>
))}
</div>
)}
</div>
{/* Tabla Estado/Objeción (1/4) */}
<div className="card" style={{ padding: 0, overflow: "hidden", display: "flex", flexDirection: "column", height: 460 }}>
<div className="card-title" style={{ padding: "14px 16px 6px" }}>Estado/Objeción</div>
<div className="table-wrap" style={{ border: "none", flex: 1, overflow: "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</th>
</tr>
</thead>
<tbody>
{(data.matriz_etiq || []).length === 0 ? (
<tr><td colSpan={2} style={{ padding: 30, textAlign: "center", color: "#94a3b8" }}>Sin datos</td></tr>
) : (data.matriz_etiq || []).map((e) => {
const abierto = !!expEstado[e.estado];
const tieneAsesores = (e.asesores || []).length > 0;
return (
<Fragment key={e.estado}>
<tr>
<td className="col-name">
{tieneAsesores && (
<button
onClick={() => setExpEstado((p) => ({ ...p, [e.estado]: !p[e.estado] }))}
style={{ border: "none", background: "transparent", cursor: "pointer",
fontWeight: 700, marginRight: 6, color: "#2563eb", fontSize: 14 }}>
{abierto ? "−" : "+"}
</button>
)}
{e.estado}
</td>
<td>{num(e.cantidad)}</td>
</tr>
{abierto && (e.asesores || []).map((a) => {
const keyA = e.estado + "|" + a.asesor;
const abiertoA = !!expEstAse[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={() => setExpEstAse((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 }}>{num(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>
);
})}
{(data.matriz_etiq || []).length > 0 && (
<tr className="total-row">
<td className="col-name" style={{ position: "sticky", bottom: 0, background: "#eff6ff" }}>Total</td>
<td style={{ position: "sticky", bottom: 0, background: "#eff6ff" }}>{num((data.matriz_etiq || []).reduce((s, e) => s + (e.cantidad || 0), 0))}</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
{/* Leads Sin Responder + Matrículas por Día, lado a lado */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14, marginTop: 16 }}>
{/* Leads Sin Responder por Día */}
<div className="card" style={{ padding: "16px 18px 10px", minWidth: 0 }}>
<div className="card-title" style={{ margin: 0, marginBottom: 8, display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ width: 11, height: 11, borderRadius: "50%", background: "#f43f5e" }} />
Leads Sin Responder por Día
</div>
{(data.serie_sin_resp || []).length === 0 ? (
<div style={{ padding: 30, textAlign: "center", color: "#94a3b8", fontSize: 13 }}>
Sin leads pendientes de responder.
</div>
) : (
<div style={{ width: "100%", overflowX: "auto", overflowY: "hidden", paddingBottom: 6 }}>
<div style={{ width: (data.serie_sin_resp.length * 34) + "px", minWidth: "100%", height: 320 }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.serie_sin_resp} margin={{ top: 14, right: 20, left: 20, bottom: 30 }}>
<CartesianGrid strokeDasharray="4 4" stroke="#f0f3f8" vertical={false} />
{data.serie_sin_resp.map((p, i) =>
(i > 0 && p.mes !== data.serie_sin_resp[i - 1].mes) ? (
<ReferenceLine key={"rs" + i} x={p.fecha} stroke="#cbd5e1" strokeDasharray="4 3"
strokeWidth={1.3} position="start" />
) : null)}
<XAxis dataKey="fecha" interval={0} tick={{ fontSize: 9, fill: "#94a3b8" }}
tickFormatter={(v) => (v || "").slice(0, 2)}
axisLine={{ stroke: "#e2e8f0" }} tickLine={false} height={18} />
<XAxis dataKey="mes" xAxisId="mes2" axisLine={false} tickLine={false}
tick={<TickMes serie={data.serie_sin_resp} />} interval={0} height={16} />
<YAxis tick={{ fontSize: 11, fill: "#94a3b8" }} allowDecimals={false}
axisLine={false} tickLine={false} width={34} />
<Tooltip contentStyle={{ borderRadius: 12, border: "1px solid #e2e8f0",
boxShadow: "0 8px 24px rgba(0,0,0,0.12)", fontSize: 12.5 }}
labelFormatter={(v, p) => (p && p[0] ? p[0].payload.fecha : v)} />
<Bar dataKey="cantidad" name="Sin Responder" fill="#f43f5e" radius={[4, 4, 0, 0]} maxBarSize={28} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
</div>
{/* Matrículas por Día */}
<div className="card" style={{ padding: "16px 18px 10px", minWidth: 0 }}>
<div className="card-title" style={{ margin: 0, marginBottom: 8, display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ width: 11, height: 11, borderRadius: "50%", background: "#0f766e" }} />
Matrículas por Día
</div>
{(data.serie_mat || []).length === 0 ? (
<div style={{ padding: 30, textAlign: "center", color: "#94a3b8", fontSize: 13 }}>
Sin matrículas.
</div>
) : (
<div style={{ width: "100%", overflowX: "auto", overflowY: "hidden", paddingBottom: 6 }}>
<div style={{ width: (data.serie_mat.length * 34) + "px", minWidth: "100%", height: 320 }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.serie_mat} margin={{ top: 14, right: 20, left: 20, bottom: 30 }}>
<CartesianGrid strokeDasharray="4 4" stroke="#f0f3f8" vertical={false} />
{data.serie_mat.map((p, i) =>
(i > 0 && p.mes !== data.serie_mat[i - 1].mes) ? (
<ReferenceLine key={"rm" + i} x={p.fecha} stroke="#cbd5e1" strokeDasharray="4 3"
strokeWidth={1.3} position="start" />
) : null)}
<XAxis dataKey="fecha" interval={0} tick={{ fontSize: 9, fill: "#94a3b8" }}
tickFormatter={(v) => (v || "").slice(0, 2)}
axisLine={{ stroke: "#e2e8f0" }} tickLine={false} height={18} />
<XAxis dataKey="mes" xAxisId="mesM" axisLine={false} tickLine={false}
tick={<TickMes serie={data.serie_mat} />} interval={0} height={16} />
<YAxis tick={{ fontSize: 11, fill: "#94a3b8" }} allowDecimals={false}
axisLine={false} tickLine={false} width={34} />
<Tooltip contentStyle={{ borderRadius: 12, border: "1px solid #e2e8f0",
boxShadow: "0 8px 24px rgba(0,0,0,0.12)", fontSize: 12.5 }}
labelFormatter={(v, p) => (p && p[0] ? p[0].payload.fecha : v)} />
<Bar dataKey="cantidad" name="Matrículas" fill="#0f766e" radius={[4, 4, 0, 0]} maxBarSize={28} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
</div>
</div>
{/* Resumen de Matrículas: 4 tarjetitas al final */}
<div style={{ marginTop: 16, marginBottom: 6, fontSize: 14, fontWeight: 700, color: "#1e293b" }}>
Resumen de Matrículas
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12 }}>
{[
{ ico: "⏱️", label: "Tiempo Promedio", valor: `${num(data.tiempo_mat_prom)} días`, sub: "lead → matrícula", color: "#2563eb", bg: "#eff6ff" },
{ ico: "❓", label: "No Identificadas", valor: num(data.mat_no_iden), sub: "sin lead en cartera", color: "#b45309", bg: "#fff7ed" },
{ ico: "🟢", label: "Matr. Leads Nuevos", valor: num(data.mat_nuevos), sub: `≤ 45 días · prom. ${num(data.prom_nuevos)} días`, color: "#0f766e", bg: "#f0fdfa" },
{ ico: "🕰️", label: "Matr. Leads Antiguos", valor: num(data.mat_antiguos), sub: `> 45 días · prom. ${num(data.prom_antiguos)} días`, color: "#64748b", bg: "#f1f5f9" },
].map((c) => (
<div key={c.label} style={{ background: "#fff", border: "1px solid #eaeef4",
borderRadius: 12, padding: "14px 16px", boxShadow: "0 1px 4px rgba(16,24,40,0.06)" }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
<div style={{ width: 30, height: 30, borderRadius: 9, background: c.bg,
display: "flex", alignItems: "center", justifyContent: "center", fontSize: 15 }}>{c.ico}</div>
<span style={{ fontSize: 12, fontWeight: 600, color: "#64748b" }}>{c.label}</span>
</div>
<div style={{ fontSize: 22, fontWeight: 800, color: "#0f172a" }}>{c.valor}</div>
<div style={{ fontSize: 11, color: "#94a3b8", marginTop: 2 }}>{c.sub}</div>
</div>
))}
</div>
</div>}
</div>
);
}
// Tick del eje inferior: muestra el nombre del mes UNA sola vez, en el punto
// central de cada grupo de dias de ese mes.
function TickMes({ x, y, payload, serie }) {
const mes = payload.value;
const idxs = serie.map((p, i) => (p.mes === mes ? i : -1)).filter((i) => i >= 0);
const centro = idxs[Math.floor(idxs.length / 2)];
// Solo dibuja en el indice central del grupo (evita repetir el mes)
if (serie[payload.index] && payload.index !== centro) return null;
return (
<text x={x} y={y + 12} textAnchor="middle" fontSize={11} fontWeight={700} fill="#475569">
{mes}
</text>
);
}
function Tarjetas({ r, num }) {
const pctProc = Number(r.pct_procesados || 0);
const pctCont = Number(r.pct_contactados || 0);
const soles = (v) => `$${Number(v || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const grupos = [
...(r.tiene_pauta ? [{
titulo: "Pauta", ico: "📣", acento: "#ea580c", suave: "#fff7ed",
filas: [
{ par: [ { label: "Importe Gastado", valor: soles(r.importe) },
{ label: "V. Venta Inicial", valor: `S/ ${num(r.venta_inicial)}` } ] },
{ par: [ { label: "Resultados", valor: num(r.resultados) },
{ label: "ROAS", valor: `${Number(r.roas || 0).toLocaleString("es-PE")}x` } ] },
{ par: [ { label: "CPL", valor: soles(r.cpl) },
{ label: "", valor: "" } ] },
],
}] : []),
{
titulo: "Cartera", ico: "📇", acento: "#2563eb", suave: "#eff6ff",
filas: [
{ label: "Total", valor: num(r.cartera_total) },
{ label: "Copito", valor: num(r.cartera_copito) },
{ label: "Otros", valor: num(r.cartera_otros) },
],
},
{
titulo: "Leads", ico: "🎯", acento: "#0f766e", suave: "#f0fdfa",
filas: [
{ label: "Recibidos", valor: num(r.leads_recibidos) },
{ label: "Procesados", valor: num(r.leads_procesados) },
{ label: "% Procesados", valor: `${pctProc.toLocaleString("es-PE")}%`, pct: true },
],
},
{
titulo: "Contacto", ico: "💬", acento: "#7c3aed", suave: "#f5f3ff",
filas: [
{ label: "Procesados", valor: num(r.leads_procesados) },
{ label: "Contactados", valor: num(r.leads_contactados) },
{ label: "% Contactados", valor: `${pctCont.toLocaleString("es-PE")}%`, pct: true },
],
},
];
return (
<div style={{ display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: 14, marginBottom: 14 }}>
{grupos.map((g) => (
<div key={g.titulo} style={{
background: "#fff", border: "1px solid #eaeef4", borderRadius: 14,
overflow: "hidden", boxShadow: "0 1px 4px rgba(16,24,40,0.06)" }}>
{/* Barra de acento superior */}
<div style={{ height: 4, background: g.acento }} />
<div style={{ padding: "14px 16px 6px" }}>
{/* Título con ícono */}
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
<div style={{ width: 30, height: 30, borderRadius: 9, background: g.suave,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: 15 }}>{g.ico}</div>
<div style={{ fontSize: 12, fontWeight: 800, color: g.acento,
textTransform: "uppercase", letterSpacing: 0.6 }}>{g.titulo}</div>
</div>
{g.filas.map((f, i) => (
f.par ? (
// Dos valores lado a lado (ej. CPL | ROAS)
<div key={"par" + i} style={{ display: "grid", gridTemplateColumns: "1fr 1fr",
gap: 10, padding: "9px 0",
borderBottom: i < g.filas.length - 1 ? "1px solid #f4f6fa" : "none" }}>
{f.par.map((c, j) => (
<div key={c.label || ("v" + j)}>
{c.label ? <div style={{ fontSize: 11.5, color: "#64748b", fontWeight: 500 }}>{c.label}</div> : null}
{c.valor ? <div style={{ fontSize: 17, fontWeight: 800, color: "#0f172a" }}>{c.valor}</div> : null}
</div>
))}
</div>
) : (
<div key={f.label} style={{
display: "flex", justifyContent: "space-between", alignItems: "center",
padding: "9px 0",
borderBottom: i < g.filas.length - 1 ? "1px solid #f4f6fa" : "none" }}>
<span style={{ fontSize: 12.5, color: "#64748b", fontWeight: 500 }}>{f.label}</span>
{f.pct ? (
<span style={{ fontSize: 13, fontWeight: 800, color: g.acento,
background: g.suave, borderRadius: 999,
padding: "3px 10px" }}>{f.valor}</span>
) : (
<span style={{ fontSize: 17, fontWeight: 800, color: "#0f172a" }}>{f.valor}</span>
)}
</div>
)
))}
</div>
</div>
))}
</div>
);
}