Align CRM, Ads, Social and Organic dashboards with the metrics visual system.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,9 +8,17 @@ import {
|
||||
CreativesGallery,
|
||||
CreativeZoomModal,
|
||||
} from '@/components/AdCreativesGallery';
|
||||
import { PageHeader } from '@/components/ui';
|
||||
import { PageHeader, SegmentedControl } from '@/components/ui';
|
||||
import SyncNowButton from '@/components/SyncNowButton';
|
||||
import { useDateRange } from '@/components/DateRangeContext';
|
||||
import {
|
||||
HelpDetails,
|
||||
HeroBand,
|
||||
HeroStat,
|
||||
ShareBar,
|
||||
StatMini,
|
||||
tableHeadClass,
|
||||
} from '@/components/dash-ui';
|
||||
|
||||
type PlatformKpis = {
|
||||
spend: number;
|
||||
@@ -439,21 +447,6 @@ function SparkBars({
|
||||
);
|
||||
}
|
||||
|
||||
function KpiTile({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-200 bg-white px-3 py-3">
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500">{label}</p>
|
||||
<p className="mt-1 text-xl font-bold text-gray-900 tabular-nums">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CampaignTable({
|
||||
title,
|
||||
rows,
|
||||
@@ -463,44 +456,57 @@ function CampaignTable({
|
||||
rows: CampaignRow[];
|
||||
empty: string;
|
||||
}) {
|
||||
const spendMax = Math.max(1, ...rows.map((r) => r.spend));
|
||||
return (
|
||||
<section className="rounded-2xl border border-gray-200 bg-white overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-gray-200">
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500">
|
||||
Campañas activas
|
||||
<section className="rounded-2xl border border-gray-200 bg-white overflow-hidden shadow-sm">
|
||||
<div className="px-4 py-3 border-b border-gray-100">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500">
|
||||
Campañas
|
||||
</p>
|
||||
<h3 className="text-lg font-bold text-gray-900">{title}</h3>
|
||||
<h3 className="text-base font-semibold text-gray-900 tracking-tight">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
{rows.length === 0 ? (
|
||||
<p className="px-4 py-8 text-sm text-gray-500">{empty}</p>
|
||||
<p className="px-4 py-10 text-sm text-gray-500 text-center">{empty}</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-left text-sm">
|
||||
<thead className="text-[10px] uppercase tracking-wider text-gray-500 bg-gray-50">
|
||||
<thead className={tableHeadClass()}>
|
||||
<tr>
|
||||
<th className="px-4 py-2 font-medium">Campaña</th>
|
||||
<th className="px-3 py-2 font-medium">Gasto</th>
|
||||
<th className="px-3 py-2 font-medium">Clics</th>
|
||||
<th className="px-3 py-2 font-medium">Conv.</th>
|
||||
<th className="px-3 py-2 font-medium">CPL</th>
|
||||
<th className="px-4 py-3 font-semibold">Campaña</th>
|
||||
<th className="px-3 py-3 font-semibold min-w-[150px]">Gasto</th>
|
||||
<th className="px-3 py-3 font-semibold">Clics</th>
|
||||
<th className="px-3 py-3 font-semibold">Conv.</th>
|
||||
<th className="px-3 py-3 font-semibold">CPL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.slice(0, 25).map((r) => (
|
||||
<tr key={r.name} className="border-t border-gray-200">
|
||||
<td className="px-4 py-2.5 text-gray-800 max-w-[220px] truncate">
|
||||
<tr
|
||||
key={r.name}
|
||||
className="border-t border-gray-100 hover:bg-brand-50/40"
|
||||
>
|
||||
<td
|
||||
className="px-4 py-2.5 text-gray-800 max-w-[240px] truncate"
|
||||
title={r.name}
|
||||
>
|
||||
{r.name}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-gray-900 tabular-nums">
|
||||
{formatEur(r.spend)}
|
||||
<td className="px-3 py-2.5">
|
||||
<ShareBar
|
||||
value={r.spend}
|
||||
max={spendMax}
|
||||
label={formatEur(r.spend)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-gray-600 tabular-nums">
|
||||
<td className="px-3 py-2.5 text-gray-700 tabular-nums">
|
||||
{formatNum(r.clicks)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-gray-600 tabular-nums">
|
||||
<td className="px-3 py-2.5 text-gray-700 tabular-nums">
|
||||
{formatNum(r.conversions)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-gray-600 tabular-nums">
|
||||
<td className="px-3 py-2.5 text-gray-700 tabular-nums">
|
||||
{r.cpl != null ? formatEurCents(r.cpl) : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -684,86 +690,78 @@ export default function AdTrackerClient({
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
<PageHeader
|
||||
eyebrow={`Ads · ${label}`}
|
||||
title={tagline || 'Meta + Google Ads'}
|
||||
description={`Gasto, clics, conversiones y creatividades. Periodo del header: ${rangeLabel}.`}
|
||||
eyebrow="Publicidad"
|
||||
title={tagline || 'Ads'}
|
||||
description={`${label} · Meta + Google · ${rangeLabel}`}
|
||||
actions={<SyncNowButton forceModule="ads" />}
|
||||
/>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{(
|
||||
[
|
||||
{ id: 'resumen', label: 'Resumen' },
|
||||
{ id: 'meta', label: 'Meta Ads' },
|
||||
{ id: 'google', label: 'Google Ads' },
|
||||
] as const
|
||||
).map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
onClick={() => setTab(t.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
|
||||
tab === t.id
|
||||
? 'bg-gray-900 text-white'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<SegmentedControl
|
||||
value={tab}
|
||||
onChange={setTab}
|
||||
size="sm"
|
||||
options={[
|
||||
{ id: 'resumen', label: 'Resumen' },
|
||||
{ id: 'meta', label: 'Meta Ads' },
|
||||
{ id: 'google', label: 'Google Ads' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{tab === 'resumen' && (
|
||||
<div className="space-y-6">
|
||||
<section className="space-y-4">
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500">
|
||||
Resumen de cuenta
|
||||
</p>
|
||||
<h3 className="text-xl font-bold text-gray-900 mt-1">
|
||||
Meta + Google, juntos
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-1 max-w-3xl">
|
||||
Gasto, impresiones, clics y conversiones sumados. CTR, CPC y CPL
|
||||
se recalculan sobre el total (no se suman). Ventana:{' '}
|
||||
{range.since} → {range.until}.
|
||||
</p>
|
||||
</div>
|
||||
<HeroBand>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
|
||||
<HeroStat label="Inversión" value={formatEur(combined.spend)} />
|
||||
<HeroStat
|
||||
label="Conversiones"
|
||||
value={formatNum(combined.conversions)}
|
||||
/>
|
||||
<HeroStat
|
||||
label="CPL"
|
||||
value={
|
||||
combined.cpl != null ? formatEurCents(combined.cpl) : '—'
|
||||
}
|
||||
/>
|
||||
<HeroStat
|
||||
label="CTR"
|
||||
value={
|
||||
combined.ctr != null
|
||||
? `${combined.ctr.toFixed(2)}%`
|
||||
: '—'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</HeroBand>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<KpiTile label="Gasto" value={formatEur(combined.spend)} />
|
||||
<KpiTile
|
||||
<HelpDetails title="Cómo leer Ads">
|
||||
<p>
|
||||
Gasto, impresiones, clics y conversiones se suman entre Meta y
|
||||
Google. CTR, CPC y CPL se recalculan sobre el total (no se
|
||||
suman porcentajes).
|
||||
</p>
|
||||
</HelpDetails>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<StatMini
|
||||
label="Impresiones"
|
||||
value={formatNum(combined.impressions)}
|
||||
/>
|
||||
<KpiTile label="Clics" value={formatNum(combined.clicks)} />
|
||||
<KpiTile
|
||||
label="Conversiones"
|
||||
value={formatNum(combined.conversions)}
|
||||
/>
|
||||
<KpiTile
|
||||
label="CTR"
|
||||
<StatMini label="Clics" value={formatNum(combined.clicks)} />
|
||||
<StatMini
|
||||
label="CPC"
|
||||
value={
|
||||
combined.ctr != null ? `${combined.ctr.toFixed(2)}%` : '—'
|
||||
combined.cpc != null ? formatEurCents(combined.cpc) : '—'
|
||||
}
|
||||
/>
|
||||
<KpiTile
|
||||
label="CPC"
|
||||
value={combined.cpc != null ? formatEurCents(combined.cpc) : '—'}
|
||||
/>
|
||||
<KpiTile
|
||||
label="CPL"
|
||||
value={combined.cpl != null ? formatEurCents(combined.cpl) : '—'}
|
||||
/>
|
||||
<KpiTile label="Ads" value={formatNum(combined.ads)} />
|
||||
<StatMini label="Ads activos" value={formatNum(combined.ads)} />
|
||||
</div>
|
||||
|
||||
<div className="h-2.5 rounded-full overflow-hidden border border-gray-200 bg-gray-900 flex">
|
||||
<div className="h-2.5 rounded-full overflow-hidden border border-gray-200 bg-gray-100 flex">
|
||||
<div
|
||||
className="bg-[#7C66B8]"
|
||||
className="bg-brand-500"
|
||||
style={{ width: `${metaShare}%` }}
|
||||
title={`Meta ${metaShare.toFixed(0)}%`}
|
||||
/>
|
||||
@@ -773,17 +771,21 @@ export default function AdTrackerClient({
|
||||
title={`Google ${googleShare.toFixed(0)}%`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500 -mt-2">
|
||||
Reparto de inversión · Meta {metaShare.toFixed(0)}% · Google{' '}
|
||||
{googleShare.toFixed(0)}%
|
||||
</p>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTab('meta')}
|
||||
className="rounded-xl border border-gray-200 bg-white p-4 text-left hover:border-[#7C66B8]/50"
|
||||
className="rounded-xl border border-gray-200 bg-white p-4 text-left hover:border-brand-400 hover:bg-brand-50/40 transition-colors"
|
||||
>
|
||||
<p className="text-[11px] uppercase tracking-wider text-[#a78bfa]">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-brand-700">
|
||||
Meta Ads
|
||||
</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1">
|
||||
<p className="text-xl font-semibold text-gray-900 mt-1 tabular-nums tracking-tight">
|
||||
{formatEur(meta.spend)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
@@ -796,10 +798,10 @@ export default function AdTrackerClient({
|
||||
onClick={() => setTab('google')}
|
||||
className="rounded-xl border border-gray-200 bg-white p-4 text-left hover:border-sky-500/50"
|
||||
>
|
||||
<p className="text-[11px] uppercase tracking-wider text-sky-400">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-sky-700">
|
||||
Google Ads
|
||||
</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1">
|
||||
<p className="text-xl font-semibold text-gray-900 mt-1 tabular-nums tracking-tight">
|
||||
{formatEur(google.spend)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
@@ -895,42 +897,46 @@ export default function AdTrackerClient({
|
||||
|
||||
{tab === 'meta' && (
|
||||
<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
|
||||
<HeroBand eyebrow="Meta Ads">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
|
||||
<HeroStat label="Inversión" value={formatEur(meta.spend)} />
|
||||
<HeroStat
|
||||
label="Conversiones"
|
||||
value={formatNum(meta.conversions)}
|
||||
/>
|
||||
<HeroStat
|
||||
label="CPL"
|
||||
value={meta.cpl != null ? formatEurCents(meta.cpl) : '—'}
|
||||
/>
|
||||
<HeroStat
|
||||
label="CTR"
|
||||
value={
|
||||
meta.impressions > 0
|
||||
? `${((100 * metaClicksAll) / meta.impressions).toFixed(2)}%`
|
||||
: '—'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</HeroBand>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
<StatMini
|
||||
label="Impresiones"
|
||||
value={formatNum(meta.impressions)}
|
||||
/>
|
||||
<KpiTile label="Clics (todos)" value={formatNum(metaClicksAll)} />
|
||||
<KpiTile
|
||||
<StatMini label="Clics (todos)" value={formatNum(metaClicksAll)} />
|
||||
<StatMini
|
||||
label="Clics (enlace)"
|
||||
value={formatNum(metaClicksLink)}
|
||||
/>
|
||||
<KpiTile
|
||||
label="Conversiones"
|
||||
value={formatNum(meta.conversions)}
|
||||
/>
|
||||
<KpiTile
|
||||
label="CTR"
|
||||
value={
|
||||
meta.impressions > 0
|
||||
? `${((100 * metaClicksAll) / meta.impressions).toFixed(2)}%`
|
||||
: '—'
|
||||
}
|
||||
/>
|
||||
<KpiTile
|
||||
<StatMini
|
||||
label="CPC (todos)"
|
||||
value={metaCpcAll != null ? formatEurCents(metaCpcAll) : '—'}
|
||||
/>
|
||||
<KpiTile
|
||||
<StatMini
|
||||
label="CPC (enlace)"
|
||||
value={metaCpcLink != null ? formatEurCents(metaCpcLink) : '—'}
|
||||
/>
|
||||
<KpiTile
|
||||
label="CPL"
|
||||
value={meta.cpl != null ? formatEurCents(meta.cpl) : '—'}
|
||||
/>
|
||||
<KpiTile label="Ads activos" value={formatNum(meta.ads)} />
|
||||
<StatMini label="Ads activos" value={formatNum(meta.ads)} />
|
||||
</div>
|
||||
<CreativesGallery
|
||||
platform="meta"
|
||||
@@ -965,30 +971,36 @@ export default function AdTrackerClient({
|
||||
|
||||
{tab === 'google' && (
|
||||
<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
|
||||
<HeroBand eyebrow="Google Ads">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
|
||||
<HeroStat label="Inversión" value={formatEur(google.spend)} />
|
||||
<HeroStat
|
||||
label="Conversiones"
|
||||
value={formatNum(google.conversions)}
|
||||
/>
|
||||
<HeroStat
|
||||
label="CPL"
|
||||
value={google.cpl != null ? formatEurCents(google.cpl) : '—'}
|
||||
/>
|
||||
<HeroStat
|
||||
label="CTR"
|
||||
value={
|
||||
google.ctr != null ? `${google.ctr.toFixed(2)}%` : '—'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</HeroBand>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<StatMini
|
||||
label="Impresiones"
|
||||
value={formatNum(google.impressions)}
|
||||
/>
|
||||
<KpiTile label="Clics" value={formatNum(google.clicks)} />
|
||||
<KpiTile
|
||||
label="Conversiones"
|
||||
value={formatNum(google.conversions)}
|
||||
/>
|
||||
<KpiTile
|
||||
label="CTR"
|
||||
value={google.ctr != null ? `${google.ctr.toFixed(2)}%` : '—'}
|
||||
/>
|
||||
<KpiTile
|
||||
<StatMini label="Clics" value={formatNum(google.clicks)} />
|
||||
<StatMini
|
||||
label="CPC"
|
||||
value={google.cpc != null ? formatEurCents(google.cpc) : '—'}
|
||||
/>
|
||||
<KpiTile
|
||||
label="CPL"
|
||||
value={google.cpl != null ? formatEurCents(google.cpl) : '—'}
|
||||
/>
|
||||
<KpiTile label="Ads activos" value={formatNum(google.ads)} />
|
||||
<StatMini label="Ads activos" value={formatNum(google.ads)} />
|
||||
</div>
|
||||
<CreativesGallery
|
||||
platform="google"
|
||||
|
||||
Reference in New Issue
Block a user