Align CRM, Ads, Social and Organic dashboards with the metrics visual system.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-06 13:12:27 +02:00
parent 4d662430c0
commit dfb44d240e
5 changed files with 743 additions and 552 deletions

View File

@@ -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"