fix: cohort CAC/ROAS desde deals Clientify y más contraste en dashboards
Los totales de Métricas usaban solo ad_daily (cohort a 0). Ahora suman deals won; selectores y tipografía ganan contraste sobre fondo blanco. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
AttributionChannelRow,
|
||||
ChannelWeekPoint,
|
||||
CrmLeadRow,
|
||||
DealWonRow,
|
||||
Delta,
|
||||
MetricTotals,
|
||||
formatEur,
|
||||
@@ -26,14 +27,16 @@ import {
|
||||
formatPct,
|
||||
attributionCross,
|
||||
filterDailyByDayRange,
|
||||
filterDealsForRange,
|
||||
filterLeadsByDayRange,
|
||||
metricTotalsFromDaily,
|
||||
resolveKpiDayRange,
|
||||
sumWeekly,
|
||||
withCohortFromDeals,
|
||||
KpiWeeklyRow,
|
||||
KpiRangeKey,
|
||||
} from '@/lib/metrics';
|
||||
import { PageHeader } from '@/components/ui';
|
||||
import { PageHeader, SegmentedControl } from '@/components/ui';
|
||||
|
||||
type Card = {
|
||||
key: keyof MetricTotals;
|
||||
@@ -42,11 +45,28 @@ type Card = {
|
||||
format: (v: number) => string;
|
||||
icon: React.ReactNode;
|
||||
invert?: boolean;
|
||||
/** When true and value is 0 with no deals, show em dash + empty state. */
|
||||
cohort?: boolean;
|
||||
};
|
||||
|
||||
type RangeKey = KpiRangeKey;
|
||||
|
||||
function DeltaTriple({ delta, invert }: { delta: Delta; invert?: boolean }) {
|
||||
function DeltaTriple({
|
||||
delta,
|
||||
invert,
|
||||
empty,
|
||||
}: {
|
||||
delta: Delta;
|
||||
invert?: boolean;
|
||||
empty?: boolean;
|
||||
}) {
|
||||
if (empty) {
|
||||
return (
|
||||
<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 el periodo — sincroniza deals Clientify
|
||||
</p>
|
||||
);
|
||||
}
|
||||
const cells: { label: string; v: number | null }[] = [
|
||||
{ label: 'WoW', v: delta.wow },
|
||||
{ label: 'MoM', v: delta.mom },
|
||||
@@ -55,16 +75,15 @@ function DeltaTriple({ delta, invert }: { delta: Delta; invert?: boolean }) {
|
||||
return (
|
||||
<div className="mt-3 grid grid-cols-3 gap-1.5">
|
||||
{cells.map((c) => {
|
||||
const good =
|
||||
c.v === null ? null : invert ? c.v < 0 : c.v > 0;
|
||||
const good = c.v === null ? null : invert ? c.v < 0 : c.v > 0;
|
||||
const color =
|
||||
c.v === null
|
||||
? 'text-gray-500'
|
||||
: good
|
||||
? 'text-emerald-400'
|
||||
? 'text-emerald-700'
|
||||
: c.v === 0
|
||||
? 'text-gray-400'
|
||||
: 'text-red-400';
|
||||
? 'text-gray-600'
|
||||
: 'text-red-600';
|
||||
const Icon =
|
||||
c.v === null || c.v === 0
|
||||
? Minus
|
||||
@@ -74,15 +93,15 @@ function DeltaTriple({ delta, invert }: { delta: Delta; invert?: boolean }) {
|
||||
return (
|
||||
<div
|
||||
key={c.label}
|
||||
className="rounded-md bg-gray-50 border border-gray-200 px-1.5 py-1 text-center"
|
||||
className="rounded-md bg-gray-50 border border-gray-200 px-1.5 py-1.5 text-center"
|
||||
>
|
||||
<p className="text-[9px] uppercase tracking-wider text-gray-500">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wider text-gray-600">
|
||||
{c.label}
|
||||
</p>
|
||||
<p
|
||||
className={`text-[11px] font-semibold inline-flex items-center justify-center gap-0.5 ${color}`}
|
||||
className={`text-xs font-bold inline-flex items-center justify-center gap-0.5 ${color}`}
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
{formatPct(c.v)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -93,11 +112,11 @@ function DeltaTriple({ delta, invert }: { delta: Delta; invert?: boolean }) {
|
||||
}
|
||||
|
||||
function gapClass(gap: number): string {
|
||||
if (gap === 0) return 'text-gray-400';
|
||||
if (gap === 0) return 'text-gray-600';
|
||||
const abs = Math.abs(gap);
|
||||
if (abs <= 5) return 'text-emerald-400';
|
||||
if (abs <= 25) return 'text-amber-400';
|
||||
return 'text-red-400';
|
||||
if (abs <= 5) return 'text-emerald-700';
|
||||
if (abs <= 25) return 'text-amber-700';
|
||||
return 'text-red-600';
|
||||
}
|
||||
|
||||
export default function MetricsDashboard({
|
||||
@@ -112,6 +131,7 @@ export default function MetricsDashboard({
|
||||
channelPoints = [],
|
||||
dailyRows = [],
|
||||
crmLeads = [],
|
||||
wonDeals = [],
|
||||
}: {
|
||||
label: string;
|
||||
portfolioWide: boolean;
|
||||
@@ -127,10 +147,9 @@ export default function MetricsDashboard({
|
||||
sparse: boolean;
|
||||
weeklyRows?: KpiWeeklyRow[];
|
||||
channelPoints?: ChannelWeekPoint[];
|
||||
/** Raw ad_daily — period totals/matrix use calendar days (same as Ad Tracker). */
|
||||
dailyRows?: AdDailyRow[];
|
||||
/** Clientify leads for CRM vs platform attribution. */
|
||||
crmLeads?: CrmLeadRow[];
|
||||
wonDeals?: DealWonRow[];
|
||||
}) {
|
||||
const [range, setRange] = useState<RangeKey>('7d');
|
||||
|
||||
@@ -148,25 +167,39 @@ export default function MetricsDashboard({
|
||||
return filterLeadsByDayRange(crmLeads, dayWindow.since, dayWindow.until);
|
||||
}, [crmLeads, dayWindow]);
|
||||
|
||||
const rangedDeals = useMemo(
|
||||
() =>
|
||||
filterDealsForRange(
|
||||
wonDeals,
|
||||
dayWindow?.since ?? null,
|
||||
dayWindow?.until ?? null
|
||||
),
|
||||
[wonDeals, dayWindow]
|
||||
);
|
||||
|
||||
const periodTotals = useMemo(() => {
|
||||
if (rangedDaily.length) return metricTotalsFromDaily(rangedDaily);
|
||||
// Fallback when no ad_daily: approximate from weekly (legacy)
|
||||
if (!dayWindow) return sumWeekly(weeklyRows);
|
||||
const from = dayWindow.since;
|
||||
const until = dayWindow.until;
|
||||
const ranged = weeklyRows.filter((r) => {
|
||||
if (!r.week) return false;
|
||||
const w = r.week.slice(0, 10);
|
||||
return w <= until && w >= from;
|
||||
});
|
||||
return ranged.length ? sumWeekly(ranged) : month;
|
||||
}, [rangedDaily, weeklyRows, dayWindow, month]);
|
||||
let base: MetricTotals;
|
||||
if (rangedDaily.length) {
|
||||
base = metricTotalsFromDaily(rangedDaily);
|
||||
} else if (!dayWindow) {
|
||||
base = sumWeekly(weeklyRows);
|
||||
} else {
|
||||
const from = dayWindow.since;
|
||||
const until = dayWindow.until;
|
||||
const ranged = weeklyRows.filter((r) => {
|
||||
if (!r.week) return false;
|
||||
const w = r.week.slice(0, 10);
|
||||
return w <= until && w >= from;
|
||||
});
|
||||
base = ranged.length ? sumWeekly(ranged) : month;
|
||||
}
|
||||
return withCohortFromDeals(base, rangedDeals);
|
||||
}, [rangedDaily, rangedDeals, weeklyRows, dayWindow, month]);
|
||||
|
||||
const attributionRows: AttributionChannelRow[] = useMemo(() => {
|
||||
if (rangedDaily.length || rangedLeads.length) {
|
||||
return attributionCross(rangedDaily, rangedLeads);
|
||||
}
|
||||
// Fallback: weekly series without CRM cross
|
||||
const map = new Map<string, { spend: number; leads: number }>();
|
||||
const src =
|
||||
range === 'all'
|
||||
@@ -197,6 +230,7 @@ export default function MetricsDashboard({
|
||||
|
||||
const crmPeriodTotal = rangedLeads.length;
|
||||
const hasCrm = crmLeads.length > 0;
|
||||
const hasCohort = rangedDeals.length > 0;
|
||||
|
||||
const rangeHint = dayWindow
|
||||
? `${dayWindow.since} → ${dayWindow.until}`
|
||||
@@ -208,44 +242,51 @@ export default function MetricsDashboard({
|
||||
label: 'Inversión ads',
|
||||
hint: 'Paid del periodo',
|
||||
format: formatEur,
|
||||
icon: <DollarSign className="w-4 h-4 text-brand-500" />,
|
||||
icon: <DollarSign className="w-4 h-4 text-brand-600" />,
|
||||
},
|
||||
{
|
||||
key: 'leads',
|
||||
label: 'Leads',
|
||||
hint: 'Captación periodo',
|
||||
format: formatNum,
|
||||
icon: <Users className="w-4 h-4 text-blue-400" />,
|
||||
icon: <Users className="w-4 h-4 text-blue-600" />,
|
||||
},
|
||||
{
|
||||
key: 'cpl',
|
||||
label: 'CPL',
|
||||
hint: 'Coste por lead',
|
||||
format: (v) => `${formatNum(v)} €`,
|
||||
icon: <MousePointerClick className="w-4 h-4 text-violet-400" />,
|
||||
icon: <MousePointerClick className="w-4 h-4 text-violet-600" />,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
key: 'cac',
|
||||
label: 'CAC cohorte',
|
||||
hint: 'Paid / cierres (parcial v1)',
|
||||
hint: hasCohort
|
||||
? `Paid ÷ ${formatNum(periodTotals.deals)} cierres`
|
||||
: 'Paid ÷ deals ganados Clientify',
|
||||
format: (v) => `${formatNum(v)} €`,
|
||||
icon: <Target className="w-4 h-4 text-amber-400" />,
|
||||
icon: <Target className="w-4 h-4 text-amber-600" />,
|
||||
invert: true,
|
||||
cohort: true,
|
||||
},
|
||||
{
|
||||
key: 'revenue',
|
||||
label: 'Revenue cohorte',
|
||||
hint: 'Deals won atribuidos',
|
||||
hint: hasCohort
|
||||
? `${formatNum(periodTotals.deals)} deals won`
|
||||
: 'Suma amount deals won',
|
||||
format: formatEur,
|
||||
icon: <TrendingUp className="w-4 h-4 text-emerald-400" />,
|
||||
icon: <TrendingUp className="w-4 h-4 text-emerald-600" />,
|
||||
cohort: true,
|
||||
},
|
||||
{
|
||||
key: 'roas',
|
||||
label: 'ROAS cohorte',
|
||||
hint: 'Revenue / spend',
|
||||
hint: 'Revenue ÷ spend',
|
||||
format: (v) => `${formatNum(v)}x`,
|
||||
icon: <TrendingUp className="w-4 h-4 text-emerald-300" />,
|
||||
icon: <TrendingUp className="w-4 h-4 text-emerald-700" />,
|
||||
cohort: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -257,42 +298,38 @@ export default function MetricsDashboard({
|
||||
description={`${label}${portfolioWide ? ' · portafolio' : ''}. Periodo ${rangeHint} (mismos días calendario que Ad Tracker).`}
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarRange className="w-4 h-4 text-gray-500" />
|
||||
<div className="flex gap-1 rounded-lg border border-white/[0.08] p-0.5 bg-gray-50">
|
||||
{(
|
||||
[
|
||||
['7d', '7d'],
|
||||
['30d', '30d'],
|
||||
['month', 'Mes'],
|
||||
['all', 'Todo'],
|
||||
] as [RangeKey, string][]
|
||||
).map(([k, lbl]) => (
|
||||
<button
|
||||
key={k}
|
||||
type="button"
|
||||
onClick={() => setRange(k)}
|
||||
className={`px-2.5 py-1 text-xs rounded-md ${
|
||||
range === k
|
||||
? 'bg-brand-500/20 text-brand-300'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{lbl}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<CalendarRange className="w-4 h-4 text-gray-600" />
|
||||
<SegmentedControl
|
||||
value={range}
|
||||
onChange={setRange}
|
||||
size="sm"
|
||||
options={[
|
||||
{ id: '7d', label: '7d' },
|
||||
{ id: '30d', label: '30d' },
|
||||
{ id: 'month', label: 'Mes' },
|
||||
{ id: 'all', label: 'Todo' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{sparse && (
|
||||
<p className="text-xs text-amber-400/90 border border-amber-500/20 rounded-lg px-3 py-2">
|
||||
<p className="text-xs text-amber-800 border border-amber-300 bg-amber-50 rounded-lg px-3 py-2">
|
||||
Histórico limitado: si faltan deltas o canales, el warehouse aún no
|
||||
tiene semanas suficientes.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* North Star strip */}
|
||||
{!hasCohort && (
|
||||
<p className="text-xs text-gray-700 border border-gray-300 bg-gray-50 rounded-lg px-3 py-2">
|
||||
CAC / Revenue / ROAS a cero: no hay deals ganados con fecha de cierre
|
||||
en este periodo. Se calculan con{' '}
|
||||
<span className="font-semibold">deals Clientify (stage=won)</span>, no
|
||||
solo con leads de ads.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ label: 'Inversión periodo', value: formatEur(periodTotals.spend) },
|
||||
@@ -316,45 +353,54 @@ export default function MetricsDashboard({
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="rounded-xl border border-brand-500/20 bg-brand-500/5 px-4 py-3"
|
||||
className="rounded-xl border border-brand-200 bg-brand-50 px-4 py-3 shadow-sm"
|
||||
>
|
||||
<p className="text-[10px] uppercase tracking-wider text-brand-400/80">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-brand-700">
|
||||
{s.label}
|
||||
</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1">{s.value}</p>
|
||||
<p className="text-xl font-bold text-gray-900 mt-1 tabular-nums">
|
||||
{s.value}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{cards.map((c) => (
|
||||
<div
|
||||
key={c.key}
|
||||
className="bg-white border border-gray-200 rounded-xl p-5"
|
||||
>
|
||||
<div className="flex items-center justify-between text-gray-400 mb-2">
|
||||
<span className="text-xs font-medium uppercase tracking-wider">
|
||||
{c.label}
|
||||
</span>
|
||||
{c.icon}
|
||||
{cards.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={deltas[c.key]}
|
||||
invert={c.invert}
|
||||
empty={emptyCohort}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
{c.format(periodTotals[c.key])}
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-500 mt-1">{c.hint}</p>
|
||||
<DeltaTriple delta={deltas[c.key]} invert={c.invert} />
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Channel attribution cross */}
|
||||
<section className="bg-white border border-gray-200 rounded-xl p-5 space-y-3">
|
||||
<section className="bg-white border border-gray-200 rounded-xl p-5 space-y-3 shadow-sm">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900">
|
||||
Cruce por canal · ads vs CRM
|
||||
</h3>
|
||||
<p className="text-[11px] text-gray-500 mt-0.5">
|
||||
<p className="text-xs text-gray-600 mt-0.5">
|
||||
Plataforma = leads reportados por Meta/Google · CRM = atribución
|
||||
Clientify (origen/UTM)
|
||||
{!hasCrm && portfolioWide
|
||||
@@ -362,61 +408,61 @@ export default function MetricsDashboard({
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500 shrink-0">
|
||||
<p className="text-xs text-gray-600 shrink-0 font-medium">
|
||||
Gap = CRM − plat
|
||||
</p>
|
||||
</div>
|
||||
{attributionRows.length === 0 ? (
|
||||
<p className="text-xs text-gray-500 py-6 text-center">
|
||||
<p className="text-sm text-gray-600 py-6 text-center">
|
||||
Sin series de canal en el periodo. Los canales Meta/Google aparecen
|
||||
cuando hay filas en ad_daily para este cliente (elige uno en
|
||||
Contexto).
|
||||
</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs">
|
||||
<thead className="text-gray-500 border-b border-gray-200">
|
||||
<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-medium">Canal</th>
|
||||
<th className="py-2 pr-3 font-medium">Inversión</th>
|
||||
<th className="py-2 pr-3 font-medium">Leads plat</th>
|
||||
<th className="py-2 pr-3 font-medium">Leads CRM</th>
|
||||
<th className="py-2 pr-3 font-medium">Gap</th>
|
||||
<th className="py-2 pr-3 font-medium">CPL plat</th>
|
||||
<th className="py-2 font-medium">CPL CRM</th>
|
||||
<th className="py-2 pr-3 font-semibold">Canal</th>
|
||||
<th className="py-2 pr-3 font-semibold">Inversión</th>
|
||||
<th className="py-2 pr-3 font-semibold">Leads plat</th>
|
||||
<th className="py-2 pr-3 font-semibold">Leads CRM</th>
|
||||
<th className="py-2 pr-3 font-semibold">Gap</th>
|
||||
<th className="py-2 pr-3 font-semibold">CPL plat</th>
|
||||
<th className="py-2 font-semibold">CPL CRM</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{attributionRows.map((row) => (
|
||||
<tr
|
||||
key={row.channel}
|
||||
className="border-b border-gray-900 hover:bg-white/[0.02]"
|
||||
className="border-b border-gray-100 hover:bg-gray-50"
|
||||
>
|
||||
<td className="py-2.5 pr-3">
|
||||
{row.spend > 0 ? (
|
||||
<Link
|
||||
href={`/dashboard/kpis/canal/${encodeURIComponent(row.channel)}`}
|
||||
className="text-brand-400 hover:text-brand-300 font-semibold capitalize"
|
||||
className="text-brand-700 hover:text-brand-600 font-semibold capitalize"
|
||||
>
|
||||
{row.channel}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-gray-600 font-semibold capitalize">
|
||||
<span className="text-gray-700 font-semibold capitalize">
|
||||
{row.channel}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-800">
|
||||
<td className="py-2.5 pr-3 text-gray-800 tabular-nums">
|
||||
{row.spend ? formatEur(row.spend) : '—'}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-800">
|
||||
<td className="py-2.5 pr-3 text-gray-800 tabular-nums">
|
||||
{formatNum(row.platformLeads)}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-800">
|
||||
<td className="py-2.5 pr-3 text-gray-800 tabular-nums">
|
||||
{hasCrm ? formatNum(row.crmLeads) : '—'}
|
||||
</td>
|
||||
<td
|
||||
className={`py-2.5 pr-3 font-medium ${
|
||||
className={`py-2.5 pr-3 font-semibold tabular-nums ${
|
||||
hasCrm ? gapClass(row.gap) : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
@@ -424,12 +470,12 @@ export default function MetricsDashboard({
|
||||
? `${row.gap > 0 ? '+' : ''}${formatNum(row.gap)}`
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-800">
|
||||
<td className="py-2.5 pr-3 text-gray-800 tabular-nums">
|
||||
{row.cplPlatform
|
||||
? `${formatNum(row.cplPlatform)} €`
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="py-2.5 text-gray-800">
|
||||
<td className="py-2.5 text-gray-800 tabular-nums">
|
||||
{hasCrm && row.cplCrm
|
||||
? `${formatNum(row.cplCrm)} €`
|
||||
: '—'}
|
||||
@@ -440,52 +486,45 @@ export default function MetricsDashboard({
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 pt-2">
|
||||
{['GEO', 'CRM close %', 'LTV'].map((soon) => (
|
||||
<div
|
||||
key={soon}
|
||||
className="rounded-lg border border-dashed border-gray-200 px-3 py-2 text-[11px] text-gray-500"
|
||||
>
|
||||
{soon} · fase 2 (stub)
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<div className="flex items-center justify-between text-gray-400 mb-2">
|
||||
<span className="text-xs uppercase tracking-wider">
|
||||
<div 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">
|
||||
Ads · ventana diaria
|
||||
</span>
|
||||
<Eye className="w-4 h-4 text-sky-400" />
|
||||
<Eye className="w-4 h-4 text-sky-600" />
|
||||
</div>
|
||||
<p className="text-lg font-bold text-gray-900">{formatEur(daily.spend)}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
<p className="text-lg font-bold text-gray-900 tabular-nums">
|
||||
{formatEur(daily.spend)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{formatNum(daily.leads)} leads · {formatNum(daily.impressions)} impr.
|
||||
· {formatNum(daily.clicks)} clicks
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-600 mt-2">
|
||||
<p className="text-xs text-gray-700 mt-2 font-medium">
|
||||
Semana actual: {formatEur(week.spend)} · {formatNum(week.leads)}{' '}
|
||||
leads
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5 md:col-span-2">
|
||||
<p className="text-xs uppercase tracking-wider text-gray-500 mb-2">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5 md:col-span-2 shadow-sm">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-gray-700 mb-2">
|
||||
Cómo leer
|
||||
</p>
|
||||
<ul className="text-xs text-gray-400 space-y-1 list-disc list-inside">
|
||||
<ul className="text-sm text-gray-700 space-y-1.5 list-disc list-inside">
|
||||
<li>
|
||||
El selector filtra el mismo calendario que Ad Tracker; el cruce
|
||||
compara leads de la plataforma ads vs atribución Clientify.
|
||||
CAC / Revenue / ROAS usan deals ganados de Clientify (
|
||||
<span className="font-semibold">won_at</span> en el periodo), no
|
||||
leads de Meta/Google.
|
||||
</li>
|
||||
<li>
|
||||
Gap cercano a 0 en Meta/Google = buena atribución. Canales sin
|
||||
spend (whatsapp, manual…) son orgánicos/CRM.
|
||||
spend son orgánicos/CRM.
|
||||
</li>
|
||||
<li>
|
||||
CPL CRM usa inversión paid ÷ leads CRM del canal (más realista
|
||||
que el CPL de la plataforma cuando hay desfase).
|
||||
CPL CRM = inversión paid ÷ leads CRM del canal (más realista
|
||||
cuando hay desfase con la plataforma).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user