feat: Ads KPIs completos, Social feed+analytics, Orgánico por sección

Meta/Google con las mismas métricas que Resumen y solo campañas/ads con actividad en el periodo; Social con insights FB/IG y preview de posts (likes/comentarios/shares); Orgánico con paneles distintos por ítem del menú y prioridades/notas solo en visión general.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 16:30:17 +02:00
parent f4dfc50cc9
commit 4b6f7f1896
7 changed files with 568 additions and 109 deletions

View File

@@ -112,12 +112,21 @@ function buildSeries(rows: AdDailyRow[]): DailyPoint[] {
}
function aggregate(rows: AdDailyRow[]): PlatformKpis {
const spend = rows.reduce((a, r) => a + Number(r.spend || 0), 0);
const impressions = rows.reduce((a, r) => a + Number(r.impressions || 0), 0);
const clicks = rows.reduce((a, r) => a + Number(r.clicks || 0), 0);
const conversions = rows.reduce((a, r) => a + convOf(r), 0);
const active = rows.filter(
(r) =>
Number(r.spend || 0) > 0 ||
Number(r.impressions || 0) > 0 ||
Number(r.clicks || 0) > 0 ||
convOf(r) > 0
);
const spend = active.reduce((a, r) => a + Number(r.spend || 0), 0);
const impressions = active.reduce((a, r) => a + Number(r.impressions || 0), 0);
const clicks = active.reduce((a, r) => a + Number(r.clicks || 0), 0);
const conversions = active.reduce((a, r) => a + convOf(r), 0);
const ads = new Set(
rows.map((r) => campaignName(r)).filter((n) => n && n !== 'Sin nombre')
active
.map((r) => String(r.ad_id || campaignName(r) || '').trim())
.filter((n) => n && n !== 'Sin nombre')
).size;
return rates({ spend, impressions, clicks, conversions, ads });
}
@@ -125,6 +134,13 @@ function aggregate(rows: AdDailyRow[]): PlatformKpis {
function byCampaign(rows: AdDailyRow[]): CampaignRow[] {
const map = new Map<string, CampaignRow>();
for (const r of rows) {
const spend = Number(r.spend || 0);
const impressions = Number(r.impressions || 0);
const clicks = Number(r.clicks || 0);
const conversions = convOf(r);
if (spend <= 0 && impressions <= 0 && clicks <= 0 && conversions <= 0) {
continue;
}
const name = campaignName(r);
const cur =
map.get(name) ||
@@ -136,10 +152,10 @@ function byCampaign(rows: AdDailyRow[]): CampaignRow[] {
conversions: 0,
cpl: null,
} satisfies CampaignRow);
cur.spend += Number(r.spend || 0);
cur.impressions += Number(r.impressions || 0);
cur.clicks += Number(r.clicks || 0);
cur.conversions += convOf(r);
cur.spend += spend;
cur.impressions += impressions;
cur.clicks += clicks;
cur.conversions += conversions;
map.set(name, cur);
}
return [...map.values()]
@@ -622,16 +638,36 @@ export default function AdTrackerClient({
<div className="space-y-6">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<KpiTile label="Gasto" value={formatEur(meta.spend)} />
<KpiTile
label="Impresiones"
value={formatNum(meta.impressions)}
/>
<KpiTile label="Clics" value={formatNum(meta.clicks)} />
<KpiTile label="Leads" value={formatNum(meta.conversions)} />
<KpiTile
label="Conversiones"
value={formatNum(meta.conversions)}
/>
<KpiTile
label="CTR"
value={meta.ctr != null ? `${meta.ctr.toFixed(2)}%` : '—'}
/>
<KpiTile
label="CPC"
value={meta.cpc != null ? formatEur(meta.cpc) : '—'}
/>
<KpiTile
label="CPL"
value={meta.cpl != null ? formatEur(meta.cpl) : '—'}
/>
<KpiTile label="Ads activos" value={formatNum(meta.ads)} />
</div>
<CreativesGallery
platform="meta"
ads={creatives}
ads={creatives.filter(
(c) =>
c.platform === 'meta' &&
(c.spend > 0 || c.impressions > 0 || c.clicks > 0 || c.conversions > 0)
)}
loading={creativesLoading}
warning={creativesWarning}
onZoom={setZoomAd}
@@ -649,9 +685,9 @@ export default function AdTrackerClient({
<SparkBars data={daily} mode="meta" metric="spend" />
</section>
<CampaignTable
title="Meta Ads"
title="Campañas Meta con rendimiento"
rows={metaCampaigns}
empty="Sin campañas Meta activas en este periodo"
empty="Sin campañas Meta con actividad en este periodo"
/>
</div>
)}
@@ -660,16 +696,36 @@ export default function AdTrackerClient({
<div className="space-y-6">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<KpiTile label="Gasto" value={formatEur(google.spend)} />
<KpiTile
label="Impresiones"
value={formatNum(google.impressions)}
/>
<KpiTile label="Clics" value={formatNum(google.clicks)} />
<KpiTile label="Conv." value={formatNum(google.conversions)} />
<KpiTile
label="Conversiones"
value={formatNum(google.conversions)}
/>
<KpiTile
label="CTR"
value={google.ctr != null ? `${google.ctr.toFixed(2)}%` : '—'}
/>
<KpiTile
label="CPC"
value={google.cpc != null ? formatEur(google.cpc) : '—'}
/>
<KpiTile
label="CPL"
value={google.cpl != null ? formatEur(google.cpl) : '—'}
/>
<KpiTile label="Ads activos" value={formatNum(google.ads)} />
</div>
<CreativesGallery
platform="google"
ads={creatives}
ads={creatives.filter(
(c) =>
c.platform === 'google' &&
(c.spend > 0 || c.impressions > 0 || c.clicks > 0 || c.conversions > 0)
)}
loading={creativesLoading}
warning={creativesWarning}
onZoom={setZoomAd}
@@ -687,9 +743,9 @@ export default function AdTrackerClient({
<SparkBars data={daily} mode="google" metric="spend" />
</section>
<CampaignTable
title="Google Ads"
title="Campañas Google con rendimiento"
rows={googleCampaigns}
empty="Sin campañas Google activas en este periodo"
empty="Sin campañas Google con actividad en este periodo"
/>
</div>
)}