feat: KPIs con totales absolutos, backfill ads y ventas en cruce canal

Comparativas WoW/MoM/YoY muestran prev/Δ además del %; sync ads admite
histórico desde 2025; tabla ads↔CRM añade cierres, ventas y ROAS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-28 10:23:36 +02:00
parent dd50251de2
commit cf27eba74d
30 changed files with 2367 additions and 514 deletions

View File

@@ -12,6 +12,7 @@ import {
buildComparisons,
mergeDealsIntoWeekly,
resolveWeeklyRows,
resolveWonCohort,
sumDaily,
sumWeekly,
} from '@/lib/metrics';
@@ -19,12 +20,13 @@ import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agenc
export const dynamic = 'force-dynamic';
/** Paginate CRM leads (source + created_at) for attribution cross. */
/** Paginate CRM leads (source + created_at + status) for attribution + cohort. */
async function fetchCrmLeads(
tenantId: string | null,
useServiceRole: boolean
): Promise<CrmLeadRow[]> {
const select = 'source,created_at';
const select = 'source,created_at,status,tenant_id,updated_at,status_changed_at';
const selectFallback = 'source,created_at,status,tenant_id';
const pageSize = 1000;
const out: CrmLeadRow[] = [];
@@ -35,12 +37,20 @@ async function fetchCrmLeads(
const tidFilter = tenantId
? `&tenant_id=eq.${encodeURIComponent(tenantId)}`
: '';
let useSelect = select;
for (let offset = 0; offset < 50000; offset += pageSize) {
const r = await fetch(
`${base}/rest/v1/leads?select=${select}${tidFilter}&crm=eq.clientify&order=created_at.desc&limit=${pageSize}&offset=${offset}`,
`${base}/rest/v1/leads?select=${useSelect}${tidFilter}&crm=eq.clientify&order=created_at.desc&limit=${pageSize}&offset=${offset}`,
{ headers, cache: 'no-store' }
);
if (!r.ok) break;
if (!r.ok) {
if (useSelect !== selectFallback && offset === 0) {
useSelect = selectFallback;
offset = -pageSize;
continue;
}
break;
}
const chunk = (await r.json()) as CrmLeadRow[];
out.push(...chunk);
if (chunk.length < pageSize) break;
@@ -50,12 +60,23 @@ async function fetchCrmLeads(
const supabase = await createClient();
for (let offset = 0; offset < 50000; offset += pageSize) {
const { data } = await supabase
let { data, error } = await supabase
.from('leads')
.select(select)
.eq('crm', 'clientify')
.order('created_at', { ascending: false })
.range(offset, offset + pageSize - 1);
if (error && offset === 0) {
const retry = await supabase
.from('leads')
.select(selectFallback)
.eq('crm', 'clientify')
.order('created_at', { ascending: false })
.range(offset, offset + pageSize - 1);
data = retry.data;
error = retry.error;
}
if (error) break;
const chunk = (data as CrmLeadRow[]) || [];
out.push(...chunk);
if (chunk.length < pageSize) break;
@@ -163,6 +184,9 @@ export default async function KpisPage() {
wonDeals = await fetchWonDeals(ctx.tenantId, false);
}
const cohort = resolveWonCohort(wonDeals, crmLeads);
wonDeals = cohort.deals;
weekly = mergeDealsIntoWeekly(resolveWeeklyRows(weekly, daily), wonDeals);
const comparisons = buildComparisons(weekly);
@@ -188,6 +212,7 @@ export default async function KpisPage() {
dailyRows={daily}
crmLeads={crmLeads}
wonDeals={wonDeals}
cohortSource={cohort.source}
/>
);
}