import React from 'react'; import { createClient } from '@/lib/supabase/server'; import { redirect } from 'next/navigation'; import MetricsDashboard from '@/components/MetricsDashboard'; import { resolveActiveTenant, restGet } from '@/lib/tenant-context'; import { AdDailyRow, CrmLeadRow, DealWonRow, KpiWeeklyRow, buildChannelSeries, buildComparisons, mergeDealsIntoWeekly, resolveWeeklyRows, resolveWonCohort, sumDaily, sumWeekly, } from '@/lib/metrics'; import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency'; export const dynamic = 'force-dynamic'; /** Enough history for YoY on ad-level Meta rows (can be dense). */ const AD_DAILY_LOOKBACK_DAYS = 420; const AD_DAILY_PAGE = 1000; async function fetchAdDaily( tenantId: string | null, useServiceRole: boolean ): Promise { const since = new Date(); since.setUTCDate(since.getUTCDate() - AD_DAILY_LOOKBACK_DAYS); const sinceStr = since.toISOString().slice(0, 10); const out: AdDailyRow[] = []; 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 < 200000; offset += AD_DAILY_PAGE) { const r = await fetch( `${base}/rest/v1/ad_daily?select=*&date=gte.${sinceStr}${tidFilter}&order=date.desc&limit=${AD_DAILY_PAGE}&offset=${offset}`, { headers, cache: 'no-store' } ); if (!r.ok) break; const chunk = (await r.json()) as AdDailyRow[]; out.push(...chunk); if (chunk.length < AD_DAILY_PAGE) break; } return out; } const supabase = await createClient(); for (let offset = 0; offset < 200000; offset += AD_DAILY_PAGE) { const { data, error } = await supabase .from('ad_daily') .select('*') .gte('date', sinceStr) .order('date', { ascending: false }) .range(offset, offset + AD_DAILY_PAGE - 1); if (error) break; const chunk = (data as AdDailyRow[]) || []; out.push(...chunk); if (chunk.length < AD_DAILY_PAGE) break; } return out; } /** Paginate CRM leads (source + created_at + status) for attribution + cohort. */ async function fetchCrmLeads( tenantId: string | null, useServiceRole: boolean ): Promise { 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[] = []; if (useServiceRole) { const headers = serviceHeaders(); const base = supabaseUrl(); if (!headers || !base) return []; 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=${useSelect}${tidFilter}&crm=eq.clientify&order=created_at.desc&limit=${pageSize}&offset=${offset}`, { headers, cache: 'no-store' } ); 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; } return out; } const supabase = await createClient(); for (let offset = 0; offset < 50000; offset += pageSize) { 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 as typeof data; error = retry.error; } if (error) break; const chunk = (data as CrmLeadRow[]) || []; out.push(...chunk); if (chunk.length < pageSize) break; } return out; } /** Won deals for cohort CAC / revenue / ROAS. */ async function fetchWonDeals( tenantId: string | null, useServiceRole: boolean ): Promise { const select = 'amount,won_at,stage,tenant_id,pipeline_id,pipeline_name'; 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 { data: { user }, } = await supabase.auth.getUser(); if (!user) redirect('/login'); const ctx = await resolveActiveTenant(user); const agency = isAgencyRole(userRole(user)); let weekly: KpiWeeklyRow[] = []; let daily: AdDailyRow[] = []; let crmLeads: CrmLeadRow[] = []; let wonDeals: DealWonRow[] = []; if (agency && (ctx.portfolioWide || ctx.viaView)) { if (ctx.portfolioWide) { const w = await restGet( '/rest/v1/kpi_weekly?select=*&order=week.desc' ); weekly = w.data || []; daily = await fetchAdDaily(null, true); crmLeads = []; wonDeals = await fetchWonDeals(null, true); } else if (ctx.tenantId) { const tid = encodeURIComponent(ctx.tenantId); const w = await restGet( `/rest/v1/kpi_weekly?tenant_id=eq.${tid}&select=*&order=week.desc` ); weekly = w.data || []; daily = await fetchAdDaily(ctx.tenantId, true); crmLeads = await fetchCrmLeads(ctx.tenantId, true); wonDeals = await fetchWonDeals(ctx.tenantId, true); } } else { const { data: w } = await supabase .from('kpi_weekly') .select('*') .order('week', { ascending: false }); weekly = (w as KpiWeeklyRow[]) || []; daily = await fetchAdDaily(ctx.tenantId, false); crmLeads = await fetchCrmLeads(ctx.tenantId, false); wonDeals = await fetchWonDeals(ctx.tenantId, false); } const cohort = resolveWonCohort(wonDeals, crmLeads); wonDeals = cohort.deals; weekly = mergeDealsIntoWeekly(resolveWeeklyRows(weekly, daily), wonDeals); const comparisons = buildComparisons(weekly); const month = comparisons.currentMonth.spend > 0 || comparisons.currentMonth.leads > 0 ? comparisons.currentMonth : sumWeekly(weekly); const dailyTotals = sumDaily(daily); const sparse = weekly.length < 4; const channelSeries = buildChannelSeries(weekly); return ( ); }