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:
4Ventures
2026-09-28 11:05:21 +02:00
parent dff9c46747
commit 4c8042aa23
3 changed files with 119 additions and 64 deletions

View File

@@ -214,7 +214,7 @@ export function totalsForDayRange(
/**
* WoW / MoM / YoY relative to the selected date range (not “today”).
* - WoW: previous window of the same length
* - WoW: previous window of the same length (only meaningful if span ≤ 7d)
* - MoM: previous calendar month if the range is a full month; else prior equal-length window
* - YoY: same calendar dates one year earlier
*/
@@ -273,10 +273,11 @@ export function buildRangeDeltas(
const deltas = {} as Record<keyof MetricTotals, Delta>;
for (const k of keys) {
deltas[k] = {
wow: pctChange(cur[k], wow[k]),
// WoW only when the selected window is ≤ 7 days
wow: span <= 7 ? pctChange(cur[k], wow[k]) : null,
mom: pctChange(cur[k], mom[k]),
yoy: pctChange(cur[k], yoy[k]),
wowBase: wow[k],
wowBase: span <= 7 ? wow[k] : null,
momBase: mom[k],
yoyBase: yoy[k],
};
@@ -284,6 +285,11 @@ export function buildRangeDeltas(
return deltas;
}
/** Day count of an inclusive ISO range (for UI comparative rules). */
export function rangeDaySpan(since: string, until: string): number {
return inclusiveDaySpan(since, until);
}
export function formatEur(v: number): string {
return `${v.toLocaleString('es-ES', {
minimumFractionDigits: 0,
@@ -836,20 +842,20 @@ export type AttributionChannelRow = {
gap: number;
cplPlatform: number;
cplCrm: number;
/** Contacts marked cliente (by CRM source) in the period. */
cierres: number;
/** Won-deal revenue attributed to channel (or sin-atribuir). */
/** Won deals count (Clientify stage=won) attributed to channel. */
ventas: number;
/** Ventas ÷ inversión (0 if no spend). */
/** Won-deal revenue (€) attributed to channel (or sin-atribuir). */
importe: number;
/** Importe ÷ inversión (0 if no spend). */
roas: number;
/** Inversión ÷ cierres (0 if no cierres). */
/** Inversión ÷ ventas (0 if no ventas). */
cac: number;
};
/**
* Cross paid platform leads (ad_daily) vs CRM-attributed leads (Clientify).
* Gap = CRM − platform (positive ⇒ CRM claims more than the ad UI).
* Ventas come from won deals; without deal.source they land in `sin-atribuir`.
* Ventas/importe come from won deals; without deal.source they land in `sin-atribuir`.
*/
export function attributionCross(
daily: AdDailyRow[],
@@ -862,8 +868,8 @@ export function attributionCross(
spend: number;
platformLeads: number;
crmLeads: number;
cierres: number;
ventas: number;
importe: number;
}
>();
@@ -874,8 +880,8 @@ export function attributionCross(
spend: 0,
platformLeads: 0,
crmLeads: 0,
cierres: 0,
ventas: 0,
importe: 0,
};
map.set(channel, cur);
}
@@ -893,7 +899,6 @@ export function attributionCross(
const channel = normalizeLeadChannel(r.source);
const cur = ensure(channel);
cur.crmLeads += 1;
if (isClienteStatus(r.status)) cur.cierres += 1;
}
for (const d of deals) {
@@ -901,7 +906,8 @@ export function attributionCross(
const raw = (d.source || '').trim();
const channel = raw ? normalizeLeadChannel(raw) : 'sin-atribuir';
const cur = ensure(channel);
cur.ventas += n(d.amount);
cur.ventas += 1;
cur.importe += n(d.amount);
}
return [...map.entries()]
@@ -913,15 +919,15 @@ export function attributionCross(
gap: v.crmLeads - v.platformLeads,
cplPlatform: v.platformLeads > 0 ? v.spend / v.platformLeads : 0,
cplCrm: v.crmLeads > 0 ? v.spend / v.crmLeads : 0,
cierres: v.cierres,
ventas: v.ventas,
roas: v.spend > 0 ? v.ventas / v.spend : 0,
cac: v.cierres > 0 ? v.spend / v.cierres : 0,
importe: v.importe,
roas: v.spend > 0 ? v.importe / v.spend : 0,
cac: v.ventas > 0 ? v.spend / v.ventas : 0,
}))
.sort(
(a, b) =>
b.spend - a.spend ||
b.ventas - a.ventas ||
b.importe - a.importe ||
b.crmLeads + b.platformLeads - (a.crmLeads + a.platformLeads)
);
}