fix: KPIs — ocultar WoW >7d, YoY con histórico completo, ventas en nº
Pagina ad_daily (~420d) para que YoY no quede vacío; Ventas muestra conteo de deals (importe Clientify en hint/columna Importe). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
filterDealsForRange,
|
||||
filterLeadsByDayRange,
|
||||
metricTotalsFromDaily,
|
||||
rangeDaySpan,
|
||||
sumWeekly,
|
||||
withCohortFromDeals,
|
||||
KpiWeeklyRow,
|
||||
@@ -53,12 +54,15 @@ function DeltaTriple({
|
||||
empty,
|
||||
format,
|
||||
current,
|
||||
showWow,
|
||||
}: {
|
||||
delta: Delta;
|
||||
invert?: boolean;
|
||||
empty?: boolean;
|
||||
format: (v: number) => string;
|
||||
current: number;
|
||||
/** Hide WoW when selected range is longer than a week. */
|
||||
showWow: boolean;
|
||||
}) {
|
||||
if (empty) {
|
||||
return (
|
||||
@@ -73,12 +77,18 @@ function DeltaTriple({
|
||||
v: number | null;
|
||||
base: number | null;
|
||||
}[] = [
|
||||
{ label: 'WoW', v: delta.wow, base: delta.wowBase },
|
||||
...(showWow
|
||||
? [{ label: 'WoW', v: delta.wow, base: delta.wowBase }]
|
||||
: []),
|
||||
{ label: 'MoM', v: delta.mom, base: delta.momBase },
|
||||
{ label: 'YoY', v: delta.yoy, base: delta.yoyBase },
|
||||
];
|
||||
return (
|
||||
<div className="mt-3 grid grid-cols-3 gap-1.5">
|
||||
<div
|
||||
className={`mt-3 grid gap-1.5 ${
|
||||
showWow ? 'grid-cols-3' : 'grid-cols-2'
|
||||
}`}
|
||||
>
|
||||
{cells.map((c) => {
|
||||
const good = c.v === null ? null : invert ? c.v < 0 : c.v > 0;
|
||||
const color =
|
||||
@@ -95,7 +105,8 @@ function DeltaTriple({
|
||||
: good
|
||||
? ArrowUpRight
|
||||
: ArrowDownRight;
|
||||
const hasBase = c.base !== null && !(c.base === 0 && c.v === null);
|
||||
const hasBase =
|
||||
c.base !== null && Number.isFinite(c.base) && !(c.base === 0 && c.v === null);
|
||||
const absDelta =
|
||||
hasBase && c.base !== null ? current - c.base : null;
|
||||
return (
|
||||
@@ -242,8 +253,8 @@ export default function MetricsDashboard({
|
||||
gap: -v.leads,
|
||||
cplPlatform: v.leads > 0 ? v.spend / v.leads : 0,
|
||||
cplCrm: 0,
|
||||
cierres: 0,
|
||||
ventas: 0,
|
||||
importe: 0,
|
||||
roas: 0,
|
||||
cac: 0,
|
||||
}))
|
||||
@@ -257,16 +268,16 @@ export default function MetricsDashboard({
|
||||
platformLeads: a.platformLeads + r.platformLeads,
|
||||
crmLeads: a.crmLeads + r.crmLeads,
|
||||
gap: a.gap + r.gap,
|
||||
cierres: a.cierres + r.cierres,
|
||||
ventas: a.ventas + r.ventas,
|
||||
importe: a.importe + r.importe,
|
||||
}),
|
||||
{
|
||||
spend: 0,
|
||||
platformLeads: 0,
|
||||
crmLeads: 0,
|
||||
gap: 0,
|
||||
cierres: 0,
|
||||
ventas: 0,
|
||||
importe: 0,
|
||||
}
|
||||
);
|
||||
}, [attributionRows]);
|
||||
@@ -274,10 +285,11 @@ export default function MetricsDashboard({
|
||||
const crmPeriodTotal = rangedLeads.length;
|
||||
const hasCrm = crmLeads.length > 0;
|
||||
const hasCohort = rangedDeals.length > 0;
|
||||
const cierres = periodTotals.deals;
|
||||
const ventasCount = periodTotals.deals;
|
||||
const cohortFromLeads = cohortSource === 'cliente_leads';
|
||||
const revenueMissing =
|
||||
hasCohort && periodTotals.revenue <= 0 && cohortFromLeads;
|
||||
const showWow = rangeDaySpan(since, until) <= 7;
|
||||
|
||||
const cards: Card[] = [
|
||||
{
|
||||
@@ -303,14 +315,14 @@ export default function MetricsDashboard({
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
key: 'revenue',
|
||||
key: 'deals',
|
||||
label: 'Ventas (CRM)',
|
||||
hint: hasCohort
|
||||
? cohortFromLeads
|
||||
? `${formatNum(cierres)} clientes Clientify · importe no en webhook`
|
||||
: `${formatNum(cierres)} deals ganados · importe Clientify`
|
||||
: 'Deals won / status cliente Clientify en el periodo',
|
||||
format: formatEur,
|
||||
? 'Clientes Clientify en el periodo (sin importe en webhook)'
|
||||
: `Deals ganados Clientify · importe ${formatEur(periodTotals.revenue)}`
|
||||
: 'Deals won Clientify en el periodo',
|
||||
format: formatNum,
|
||||
icon: <TrendingUp className="w-4 h-4 text-emerald-600" />,
|
||||
cohort: true,
|
||||
},
|
||||
@@ -318,7 +330,7 @@ export default function MetricsDashboard({
|
||||
key: 'cac',
|
||||
label: 'CAC',
|
||||
hint: hasCohort
|
||||
? `Inversión ads ÷ ${formatNum(cierres)} ${cohortFromLeads ? 'clientes' : 'ventas'}`
|
||||
? `Inversión ads ÷ ${formatNum(ventasCount)} ventas`
|
||||
: 'Inversión ÷ deals ganados (no leads)',
|
||||
format: (v) => `${formatNum(v)} €`,
|
||||
icon: <Target className="w-4 h-4 text-amber-600" />,
|
||||
@@ -330,7 +342,7 @@ export default function MetricsDashboard({
|
||||
label: 'ROAS ventas',
|
||||
hint: revenueMissing
|
||||
? 'Falta importe en CRM (API BPS) para ROAS'
|
||||
: `Importe deals ganados Clientify (${formatEur(periodTotals.revenue)}) ÷ inversión ads (${formatEur(periodTotals.spend)})`,
|
||||
: `Importe Clientify (${formatEur(periodTotals.revenue)}) ÷ inversión ads (${formatEur(periodTotals.spend)})`,
|
||||
format: (v) => `${formatNum(v)}x`,
|
||||
icon: <TrendingUp className="w-4 h-4 text-emerald-700" />,
|
||||
cohort: true,
|
||||
@@ -440,13 +452,9 @@ export default function MetricsDashboard({
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{cards.map((c) => {
|
||||
const emptyCohort = Boolean(c.cohort) && !hasCohort;
|
||||
const showClientesCount =
|
||||
c.key === 'revenue' && revenueMissing && !emptyCohort;
|
||||
const value = emptyCohort
|
||||
? '—'
|
||||
: showClientesCount
|
||||
? `${formatNum(cierres)} clientes`
|
||||
: c.format(periodTotals[c.key]);
|
||||
: c.format(periodTotals[c.key]);
|
||||
return (
|
||||
<div
|
||||
key={c.key}
|
||||
@@ -468,6 +476,7 @@ export default function MetricsDashboard({
|
||||
empty={emptyCohort}
|
||||
format={c.format}
|
||||
current={periodTotals[c.key]}
|
||||
showWow={showWow}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -481,9 +490,9 @@ export default function MetricsDashboard({
|
||||
Cruce por canal · ads vs CRM
|
||||
</h3>
|
||||
<p className="text-xs text-gray-600 mt-0.5">
|
||||
Gasto y conversiones de ads vs altas / cierres / ventas CRM. Las
|
||||
ventas de deals sin fuente de contacto aparecen en{' '}
|
||||
<span className="font-medium">sin-atribuir</span>.
|
||||
Leads plat = conversiones ads. Leads CRM = altas Clientify por
|
||||
fuente. Ventas = nº deals ganados; Importe = amount Clientify.
|
||||
Sin fuente de contacto → canal sin-atribuir.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 shrink-0 font-medium">
|
||||
@@ -519,8 +528,8 @@ export default function MetricsDashboard({
|
||||
<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">Cierres</th>
|
||||
<th className="py-2 pr-3 font-semibold">Ventas</th>
|
||||
<th className="py-2 pr-3 font-semibold">Importe</th>
|
||||
<th className="py-2 pr-3 font-semibold">ROAS</th>
|
||||
<th className="py-2 pr-3 font-semibold">CPL plat</th>
|
||||
<th className="py-2 font-semibold">CPL CRM</th>
|
||||
@@ -564,11 +573,11 @@ export default function MetricsDashboard({
|
||||
? `${row.gap > 0 ? '+' : ''}${formatNum(row.gap)}`
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-800 tabular-nums">
|
||||
{hasCrm ? formatNum(row.cierres) : '—'}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-900 font-semibold tabular-nums">
|
||||
{row.ventas > 0 ? formatEur(row.ventas) : '—'}
|
||||
{row.ventas > 0 ? formatNum(row.ventas) : '—'}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-800 tabular-nums">
|
||||
{row.importe > 0 ? formatEur(row.importe) : '—'}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-800 tabular-nums">
|
||||
{row.roas > 0 ? `${formatNum(row.roas)}x` : '—'}
|
||||
@@ -612,21 +621,23 @@ export default function MetricsDashboard({
|
||||
? `${attributionTotals.gap > 0 ? '+' : ''}${formatNum(attributionTotals.gap)}`
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 tabular-nums">
|
||||
{hasCrm ? formatNum(attributionTotals.cierres) : '—'}
|
||||
<td className="py-2.5 pr-3 tabular-nums text-gray-900">
|
||||
{formatNum(
|
||||
attributionTotals.ventas || ventasCount
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 tabular-nums text-gray-900">
|
||||
{attributionTotals.ventas > 0 || periodTotals.revenue > 0
|
||||
{attributionTotals.importe > 0 || periodTotals.revenue > 0
|
||||
? formatEur(
|
||||
attributionTotals.ventas || periodTotals.revenue
|
||||
attributionTotals.importe || periodTotals.revenue
|
||||
)
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 tabular-nums">
|
||||
{attributionTotals.spend > 0 &&
|
||||
(attributionTotals.ventas || periodTotals.revenue) > 0
|
||||
(attributionTotals.importe || periodTotals.revenue) > 0
|
||||
? `${formatNum(
|
||||
(attributionTotals.ventas ||
|
||||
(attributionTotals.importe ||
|
||||
periodTotals.revenue) / attributionTotals.spend
|
||||
)}x`
|
||||
: '—'}
|
||||
|
||||
Reference in New Issue
Block a user