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:
4Ventures
2026-09-24 10:40:12 +02:00
parent 8fda2996b6
commit 21051afda9
20 changed files with 383 additions and 184 deletions

View File

@@ -6,9 +6,11 @@ import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
import {
AdDailyRow,
CrmLeadRow,
DealWonRow,
KpiWeeklyRow,
buildChannelSeries,
buildComparisons,
mergeDealsIntoWeekly,
resolveWeeklyRows,
sumDaily,
sumWeekly,
@@ -46,7 +48,6 @@ async function fetchCrmLeads(
return out;
}
// Client path: RLS already scopes tenant
const supabase = await createClient();
for (let offset = 0; offset < 50000; offset += pageSize) {
const { data } = await supabase
@@ -62,6 +63,50 @@ async function fetchCrmLeads(
return out;
}
/** Won deals for cohort CAC / revenue / ROAS. */
async function fetchWonDeals(
tenantId: string | null,
useServiceRole: boolean
): Promise<DealWonRow[]> {
const select = 'amount,won_at,stage,tenant_id';
const pageSize = 1000;
const out: DealWonRow[] = [];
if (useServiceRole) {
const headers = serviceHeaders();
const base = supabaseUrl();
if (!headers || !base) return [];
const tidFilter = tenantId
? `&tenant_id=eq.${encodeURIComponent(tenantId)}`
: '';
for (let offset = 0; offset < 50000; offset += pageSize) {
const r = await fetch(
`${base}/rest/v1/deals?select=${select}${tidFilter}&stage=eq.won&order=won_at.desc.nullslast&limit=${pageSize}&offset=${offset}`,
{ headers, cache: 'no-store' }
);
if (!r.ok) break;
const chunk = (await r.json()) as DealWonRow[];
out.push(...chunk);
if (chunk.length < pageSize) break;
}
return out;
}
const supabase = await createClient();
for (let offset = 0; offset < 50000; offset += pageSize) {
const { data } = await supabase
.from('deals')
.select(select)
.eq('stage', 'won')
.order('won_at', { ascending: false, nullsFirst: false })
.range(offset, offset + pageSize - 1);
const chunk = (data as DealWonRow[]) || [];
out.push(...chunk);
if (chunk.length < pageSize) break;
}
return out;
}
export default async function KpisPage() {
const supabase = await createClient();
const {
@@ -75,9 +120,9 @@ export default async function KpisPage() {
let weekly: KpiWeeklyRow[] = [];
let daily: AdDailyRow[] = [];
let crmLeads: CrmLeadRow[] = [];
let wonDeals: DealWonRow[] = [];
if (agency && (ctx.portfolioWide || ctx.viaView)) {
// Service role for portfolio / zoom (bypass single-tenant RLS)
if (ctx.portfolioWide) {
const w = await restGet<KpiWeeklyRow[]>(
'/rest/v1/kpi_weekly?select=*&order=week.desc'
@@ -87,8 +132,8 @@ export default async function KpisPage() {
);
weekly = w.data || [];
daily = d.data || [];
// Attribution cross is clearest per-tenant; skip portfolio dump.
crmLeads = [];
wonDeals = await fetchWonDeals(null, true);
} else if (ctx.tenantId) {
const tid = encodeURIComponent(ctx.tenantId);
const w = await restGet<KpiWeeklyRow[]>(
@@ -100,6 +145,7 @@ export default async function KpisPage() {
weekly = w.data || [];
daily = d.data || [];
crmLeads = await fetchCrmLeads(ctx.tenantId, true);
wonDeals = await fetchWonDeals(ctx.tenantId, true);
}
} else {
const { data: w } = await supabase
@@ -114,15 +160,16 @@ export default async function KpisPage() {
weekly = (w as KpiWeeklyRow[]) || [];
daily = (d as AdDailyRow[]) || [];
crmLeads = await fetchCrmLeads(ctx.tenantId, false);
wonDeals = await fetchWonDeals(ctx.tenantId, false);
}
weekly = resolveWeeklyRows(weekly, daily);
weekly = mergeDealsIntoWeekly(resolveWeeklyRows(weekly, daily), wonDeals);
const comparisons = buildComparisons(weekly);
const month =
comparisons.currentMonth.spend > 0 || comparisons.currentMonth.leads > 0
? comparisons.currentMonth
: sumWeekly(weekly); // fallback: all available weeks if current month empty
: sumWeekly(weekly);
const dailyTotals = sumDaily(daily);
const sparse = weekly.length < 4;
const channelSeries = buildChannelSeries(weekly);
@@ -140,6 +187,7 @@ export default async function KpisPage() {
channelPoints={channelSeries.points}
dailyRows={daily}
crmLeads={crmLeads}
wonDeals={wonDeals}
/>
);
}