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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user