Present metrics as one guided scorecard, not two dashboards.
Split ads, CRM and web into numbered blocks, drop the Windsor second panel, and keep Google campaigns as a single extra table. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -39,7 +39,6 @@ import {
|
|||||||
import { PageHeader } from '@/components/ui';
|
import { PageHeader } from '@/components/ui';
|
||||||
import SyncNowButton from '@/components/SyncNowButton';
|
import SyncNowButton from '@/components/SyncNowButton';
|
||||||
import { useDateRange } from '@/components/DateRangeContext';
|
import { useDateRange } from '@/components/DateRangeContext';
|
||||||
import WindsorAiSection from '@/components/WindsorAiSection';
|
|
||||||
import { OrganicDailyRow } from '@/lib/organic';
|
import { OrganicDailyRow } from '@/lib/organic';
|
||||||
|
|
||||||
type Card = {
|
type Card = {
|
||||||
@@ -72,8 +71,8 @@ function DeltaTriple({
|
|||||||
if (empty) {
|
if (empty) {
|
||||||
return (
|
return (
|
||||||
<p className="mt-3 text-[11px] text-gray-500 border border-dashed border-gray-200 rounded-md px-2 py-2 text-center">
|
<p className="mt-3 text-[11px] text-gray-500 border border-dashed border-gray-200 rounded-md px-2 py-2 text-center">
|
||||||
Sin cierres en este rango — amplía el periodo o revisa deals ganados en
|
Sin cierres en este rango — amplía el periodo o revisa oportunidades
|
||||||
Clientify
|
ganadas en el CRM
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -308,6 +307,10 @@ export default function MetricsDashboard({
|
|||||||
return d >= since && d <= until;
|
return d >= since && d <= until;
|
||||||
});
|
});
|
||||||
const gscClicks = rows.reduce((a, r) => a + (Number(r.gsc_clicks) || 0), 0);
|
const gscClicks = rows.reduce((a, r) => a + (Number(r.gsc_clicks) || 0), 0);
|
||||||
|
const gscImpressions = rows.reduce(
|
||||||
|
(a, r) => a + (Number(r.gsc_impressions) || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
const ga4Sessions = rows.reduce(
|
const ga4Sessions = rows.reduce(
|
||||||
(a, r) => a + (Number(r.ga4_sessions) || 0),
|
(a, r) => a + (Number(r.ga4_sessions) || 0),
|
||||||
0
|
0
|
||||||
@@ -319,6 +322,7 @@ export default function MetricsDashboard({
|
|||||||
const denom = periodTotals.leads + ga4Conversions;
|
const denom = periodTotals.leads + ga4Conversions;
|
||||||
return {
|
return {
|
||||||
gscClicks,
|
gscClicks,
|
||||||
|
gscImpressions,
|
||||||
ga4Sessions,
|
ga4Sessions,
|
||||||
ga4Conversions,
|
ga4Conversions,
|
||||||
organicShare: denom > 0 ? (ga4Conversions / denom) * 100 : 0,
|
organicShare: denom > 0 ? (ga4Conversions / denom) * 100 : 0,
|
||||||
@@ -353,9 +357,9 @@ export default function MetricsDashboard({
|
|||||||
label: 'Ventas (CRM)',
|
label: 'Ventas (CRM)',
|
||||||
hint: hasCohort
|
hint: hasCohort
|
||||||
? cohortFromLeads
|
? cohortFromLeads
|
||||||
? 'Clientes Clientify en el periodo (sin importe en webhook)'
|
? 'Contactos marcados cliente en el CRM (sin importe)'
|
||||||
: `Deals ganados Clientify · importe ${formatEur(periodTotals.revenue)}`
|
: `Oportunidades ganadas · importe ${formatEur(periodTotals.revenue)}`
|
||||||
: 'Deals won Clientify en el periodo',
|
: 'Oportunidades ganadas en el CRM en el periodo',
|
||||||
format: formatNum,
|
format: formatNum,
|
||||||
icon: <TrendingUp className="w-4 h-4 text-emerald-600" />,
|
icon: <TrendingUp className="w-4 h-4 text-emerald-600" />,
|
||||||
cohort: true,
|
cohort: true,
|
||||||
@@ -375,8 +379,8 @@ export default function MetricsDashboard({
|
|||||||
key: 'roas',
|
key: 'roas',
|
||||||
label: 'ROAS ventas',
|
label: 'ROAS ventas',
|
||||||
hint: revenueMissing
|
hint: revenueMissing
|
||||||
? 'Falta importe en CRM (API BPS) para ROAS'
|
? 'Falta importe en el CRM para calcular ROAS'
|
||||||
: `Importe Clientify (${formatEur(periodTotals.revenue)}) ÷ inversión ads (${formatEur(periodTotals.spend)})`,
|
: `Importe CRM (${formatEur(periodTotals.revenue)}) ÷ inversión ads (${formatEur(periodTotals.spend)})`,
|
||||||
format: (v) => `${formatNum(v)}x`,
|
format: (v) => `${formatNum(v)}x`,
|
||||||
icon: <TrendingUp className="w-4 h-4 text-emerald-700" />,
|
icon: <TrendingUp className="w-4 h-4 text-emerald-700" />,
|
||||||
cohort: true,
|
cohort: true,
|
||||||
@@ -414,12 +418,68 @@ export default function MetricsDashboard({
|
|||||||
return { top, worstGap, platTotal, crmTotal, crmPaid, crmOther };
|
return { top, worstGap, platTotal, crmTotal, crmPaid, crmOther };
|
||||||
}, [attributionRows]);
|
}, [attributionRows]);
|
||||||
|
|
||||||
|
const crmName = useMemo(() => {
|
||||||
|
const rows = (integrations || []) as { provider?: string; status?: string }[];
|
||||||
|
const ghl = rows.find(
|
||||||
|
(i) => i.provider === 'gohighlevel' && i.status === 'connected'
|
||||||
|
);
|
||||||
|
const cfy = rows.find(
|
||||||
|
(i) => i.provider === 'clientify' && i.status === 'connected'
|
||||||
|
);
|
||||||
|
if (ghl) return 'GoHighLevel';
|
||||||
|
if (cfy) return 'Clientify';
|
||||||
|
return 'CRM';
|
||||||
|
}, [integrations]);
|
||||||
|
|
||||||
|
const googleCampaigns = useMemo(() => {
|
||||||
|
const byCampaign = new Map<
|
||||||
|
string,
|
||||||
|
{ spend: number; impressions: number; clicks: number; leads: number }
|
||||||
|
>();
|
||||||
|
for (const r of rangedDaily) {
|
||||||
|
if (r.platform !== 'google') continue;
|
||||||
|
const name = r.campaign_name || r.campaign || 'Campaña sin nombre';
|
||||||
|
const cur = byCampaign.get(name) || {
|
||||||
|
spend: 0,
|
||||||
|
impressions: 0,
|
||||||
|
clicks: 0,
|
||||||
|
leads: 0,
|
||||||
|
};
|
||||||
|
cur.spend += Number(r.spend) || 0;
|
||||||
|
cur.impressions += Number(r.impressions) || 0;
|
||||||
|
cur.clicks += Number(r.clicks) || 0;
|
||||||
|
cur.leads += Number(r.platform_leads ?? r.conversions) || 0;
|
||||||
|
byCampaign.set(name, cur);
|
||||||
|
}
|
||||||
|
const totalSpend = [...byCampaign.values()].reduce((a, b) => a + b.spend, 0);
|
||||||
|
return [...byCampaign.entries()]
|
||||||
|
.map(([name, v]) => ({
|
||||||
|
name,
|
||||||
|
...v,
|
||||||
|
cpl: v.leads > 0 ? v.spend / v.leads : 0,
|
||||||
|
share: totalSpend > 0 ? (v.spend / totalSpend) * 100 : 0,
|
||||||
|
kind: /pmax|performance/i.test(name)
|
||||||
|
? 'Performance Max'
|
||||||
|
: /search|brand/i.test(name)
|
||||||
|
? 'Búsqueda'
|
||||||
|
: 'Otra red',
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.spend - a.spend);
|
||||||
|
}, [rangedDaily]);
|
||||||
|
|
||||||
|
const adsCards = cards.filter((c) =>
|
||||||
|
['spend', 'leads', 'cpl'].includes(c.key)
|
||||||
|
);
|
||||||
|
const outcomeCards = cards.filter((c) =>
|
||||||
|
['deals', 'cac', 'roas'].includes(c.key)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
eyebrow="Rendimiento"
|
eyebrow="Rendimiento"
|
||||||
title="Métricas"
|
title="Métricas"
|
||||||
description={`${label}${portfolioWide ? ' · portafolio' : ''}. Periodo ${rangeHint} (selector global del header).`}
|
description={`${label}${portfolioWide ? ' · portafolio' : ''}. Un solo panel para el periodo ${rangeHint}.`}
|
||||||
actions={<SyncNowButton forceModule="metrics" />}
|
actions={<SyncNowButton forceModule="metrics" />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -442,19 +502,43 @@ export default function MetricsDashboard({
|
|||||||
)}
|
)}
|
||||||
{cohortFromLeads && hasCohort && (
|
{cohortFromLeads && hasCohort && (
|
||||||
<p className="text-xs text-amber-900 border border-amber-300 bg-amber-50 rounded-lg px-3 py-2">
|
<p className="text-xs text-amber-900 border border-amber-300 bg-amber-50 rounded-lg px-3 py-2">
|
||||||
Cohorte desde contactos Clientify status «cliente» (no hay deals
|
Cohorte desde contactos del CRM con status «cliente» (no hay oportunidades
|
||||||
ganados sincronizados). Importe € incompleto hasta conectar la API
|
ganadas sincronizadas). El importe puede ir incompleto.
|
||||||
Clientify de este tenant. Meta pixel reporta compras en el periodo;
|
|
||||||
el CRM marca clientes sin amount en el webhook.
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-xs text-gray-700 space-y-1.5">
|
||||||
|
<p className="font-semibold text-gray-900 text-[11px] uppercase tracking-wider">
|
||||||
|
Cómo leer este panel
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Publicidad:</strong> lo que Meta y Google dicen (gasto,
|
||||||
|
conversiones, CPL). No es el listado de clientes.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>{crmName}:</strong> personas y ventas reales en el CRM. Un
|
||||||
|
lead de ads y un alta CRM casi nunca coinciden 1 a 1 (el gap lo
|
||||||
|
explica).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Web y SEO:</strong> visitas y clics orgánicos (Search Console
|
||||||
|
y Analytics). No se suman a los leads de anuncios.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold text-gray-900">
|
||||||
|
1. Publicidad
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-gray-600 mt-0.5">
|
||||||
|
Inversión y conversiones de las plataformas de ads · {rangeHint}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
{cards.map((c) => {
|
{adsCards.map((c) => {
|
||||||
const emptyCohort = Boolean(c.cohort) && !hasCohort;
|
const emptyCohort = Boolean(c.cohort) && !hasCohort;
|
||||||
const value = emptyCohort
|
const value = emptyCohort ? '—' : c.format(periodTotals[c.key]);
|
||||||
? '—'
|
|
||||||
: c.format(periodTotals[c.key]);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={c.key}
|
key={c.key}
|
||||||
@@ -482,7 +566,18 @@ export default function MetricsDashboard({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold text-gray-900">
|
||||||
|
2. Negocio ({crmName})
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-gray-600 mt-0.5">
|
||||||
|
Altas y ventas del CRM · no uses estos números como si fueran los de
|
||||||
|
ads
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3">
|
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600">
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600">
|
||||||
@@ -492,7 +587,7 @@ export default function MetricsDashboard({
|
|||||||
{hasCrm ? formatNum(crmPeriodTotal) : '—'}
|
{hasCrm ? formatNum(crmPeriodTotal) : '—'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
Altas Clientify en el periodo (no es el mismo número que ads)
|
Altas en {crmName} en el periodo
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||||
@@ -505,12 +600,12 @@ export default function MetricsDashboard({
|
|||||||
: '—'}
|
: '—'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
Inversión ads ÷ leads CRM (distinto del CPL ads)
|
Inversión ads ÷ leads CRM
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600">
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600">
|
||||||
Gap plat vs CRM
|
Gap ads vs CRM
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={`text-xl font-bold mt-1 tabular-nums ${
|
className={`text-xl font-bold mt-1 tabular-nums ${
|
||||||
@@ -522,9 +617,51 @@ export default function MetricsDashboard({
|
|||||||
: '—'}
|
: '—'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
CRM − leads plataforma · detalle por canal abajo
|
CRM − conversiones ads · detalle por canal abajo
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
|
{outcomeCards.map((c) => {
|
||||||
|
const emptyCohort = Boolean(c.cohort) && !hasCohort;
|
||||||
|
const value = emptyCohort ? '—' : c.format(periodTotals[c.key]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={c.key}
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-5 shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between text-gray-700 mb-2">
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wider">
|
||||||
|
{c.label}
|
||||||
|
</span>
|
||||||
|
{c.icon}
|
||||||
|
</div>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 tabular-nums">
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-600 mt-1">{c.hint}</p>
|
||||||
|
<DeltaTriple
|
||||||
|
delta={rangeDeltas[c.key] ?? deltas[c.key]}
|
||||||
|
invert={c.invert}
|
||||||
|
empty={emptyCohort}
|
||||||
|
format={c.format}
|
||||||
|
current={periodTotals[c.key]}
|
||||||
|
showWow={showWow}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold text-gray-900">3. Web y SEO</h2>
|
||||||
|
<p className="text-xs text-gray-600 mt-0.5">
|
||||||
|
Tráfico del sitio · mismo periodo · no es publicidad
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600 inline-flex items-center gap-1">
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600 inline-flex items-center gap-1">
|
||||||
<Search className="w-3.5 h-3.5" /> Clics SEO
|
<Search className="w-3.5 h-3.5" /> Clics SEO
|
||||||
@@ -532,7 +669,16 @@ export default function MetricsDashboard({
|
|||||||
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
||||||
{formatNum(organicPeriod.gscClicks)}
|
{formatNum(organicPeriod.gscClicks)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">Search Console · mismo periodo</p>
|
<p className="text-xs text-gray-500 mt-1">Search Console</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600">
|
||||||
|
Impresiones SEO
|
||||||
|
</p>
|
||||||
|
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
||||||
|
{formatNum(organicPeriod.gscImpressions)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Apariciones en Google</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600 inline-flex items-center gap-1">
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600 inline-flex items-center gap-1">
|
||||||
@@ -541,35 +687,32 @@ export default function MetricsDashboard({
|
|||||||
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
||||||
{formatNum(organicPeriod.ga4Sessions)}
|
{formatNum(organicPeriod.ga4Sessions)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">Analytics (GA4)</p>
|
||||||
GA4 · {formatNum(organicPeriod.ga4Conversions)} conv. web
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600">
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-600">
|
||||||
Cuota orgánica
|
Conv. web
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
||||||
{formatPct(organicPeriod.organicShare)}
|
{formatNum(organicPeriod.ga4Conversions)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
Conv. GA4 / (leads ads + conv. web)
|
Eventos GA4 · cuota {formatPct(organicPeriod.organicShare)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="bg-white border border-gray-200 rounded-xl p-5 space-y-4 shadow-sm">
|
<section className="bg-white border border-gray-200 rounded-xl p-5 space-y-4 shadow-sm">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-2">
|
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-gray-900">
|
<h3 className="text-sm font-semibold text-gray-900">
|
||||||
Cruce por canal · ads vs CRM
|
4. Cruce por canal · ads vs {crmName}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-600 mt-0.5">
|
<p className="text-xs text-gray-600 mt-0.5">
|
||||||
Leads plat = conversiones ads. Leads CRM = altas Clientify por
|
Leads plat = conversiones que reporta el anuncio. Leads CRM =
|
||||||
fuente. Ventas = nº deals ganados; Importe = amount Clientify.
|
altas en {crmName} según la fuente del contacto. Ventas =
|
||||||
Ventas = deals ganados Online (importe {'>'} 0), como el board
|
oportunidades ganadas (importe {'>'} 0). Gap = CRM − plat.
|
||||||
Clientify. Fuente del contacto → canal (meta / google /
|
|
||||||
ecommerce…). Sin fuente → sin-atribuir.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-600 shrink-0 font-medium">
|
<p className="text-xs text-gray-600 shrink-0 font-medium">
|
||||||
@@ -800,21 +943,62 @@ export default function MetricsDashboard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-gray-500">
|
<p className="text-[10px] text-gray-500">
|
||||||
Barras compactas por día · periodo del header · detalle en Ads
|
Barras por día · mismo periodo del header
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<WindsorAiSection
|
{googleCampaigns.length > 0 && (
|
||||||
embedded
|
<section className="bg-white border border-gray-200 rounded-xl p-5 space-y-3 shadow-sm">
|
||||||
tenantId={tenantId}
|
<div>
|
||||||
label={label}
|
<h3 className="text-sm font-semibold text-gray-900">
|
||||||
portfolioWide={portfolioWide}
|
5. Campañas Google Ads
|
||||||
dailyRows={dailyRows}
|
</h3>
|
||||||
organicRows={organicRows}
|
<p className="text-xs text-gray-600 mt-0.5">
|
||||||
crmLeads={crmLeads}
|
Desglose de la inversión Google del periodo. Meta va en Ads /
|
||||||
wonDeals={wonDeals}
|
creatividades. No vuelve a contar el scorecard de arriba.
|
||||||
integrations={integrations}
|
</p>
|
||||||
/>
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-left text-sm">
|
||||||
|
<thead className="text-gray-600 border-b border-gray-200">
|
||||||
|
<tr>
|
||||||
|
<th className="py-2 pr-3 font-semibold">Campaña</th>
|
||||||
|
<th className="py-2 pr-3 font-semibold">Red</th>
|
||||||
|
<th className="py-2 pr-3 font-semibold">Inversión</th>
|
||||||
|
<th className="py-2 pr-3 font-semibold">Clics</th>
|
||||||
|
<th className="py-2 pr-3 font-semibold">Conv.</th>
|
||||||
|
<th className="py-2 font-semibold">CPL</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{googleCampaigns.slice(0, 12).map((c) => (
|
||||||
|
<tr
|
||||||
|
key={c.name}
|
||||||
|
className="border-b border-gray-100 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<td className="py-2 pr-3 font-medium text-gray-900 max-w-[280px] truncate" title={c.name}>
|
||||||
|
{c.name}
|
||||||
|
</td>
|
||||||
|
<td className="py-2 pr-3 text-gray-600">{c.kind}</td>
|
||||||
|
<td className="py-2 pr-3 tabular-nums">{formatEur(c.spend)}</td>
|
||||||
|
<td className="py-2 pr-3 tabular-nums">{formatNum(c.clicks)}</td>
|
||||||
|
<td className="py-2 pr-3 tabular-nums">{formatNum(c.leads)}</td>
|
||||||
|
<td className="py-2 tabular-nums">
|
||||||
|
{c.cpl > 0 ? `${formatNum(c.cpl)} €` : '—'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{googleCampaigns.length > 12 && (
|
||||||
|
<p className="text-[11px] text-gray-500">
|
||||||
|
Mostrando 12 de {googleCampaigns.length} campañas (las de más
|
||||||
|
gasto). El resto está en Ads.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user