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'; import { OrganicDailyRow } from '@/lib/organic'; export const dynamic = 'force-dynamic'; /** Enough history for YoY from Jan 2025 (inclusive). No artificial 700d floor. */ const AD_DAILY_LOOKBACK_DAYS = Math.ceil((Date.now() - Date.UTC(2025, 0, 1)) / 86400000) + 14; 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,source,name,lead_id'; const selectFallback = 'amount,won_at,stage,tenant_id,pipeline_id,pipeline_name,lead_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)}` : ''; let useSelect = select; for (let offset = 0; offset < 50000; offset += pageSize) { const r = await fetch( `${base}/rest/v1/deals?select=${useSelect}${tidFilter}&stage=eq.won&order=won_at.desc.nullslast&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 DealWonRow[]; out.push(...chunk); if (chunk.length < pageSize) break; } } else { const supabase = await createClient(); let useSelect = select; for (let offset = 0; offset < 50000; offset += pageSize) { const { data, error } = await supabase .from('deals') .select(useSelect) .eq('stage', 'won') .order('won_at', { ascending: false, nullsFirst: false }) .range(offset, offset + pageSize - 1); if (error && useSelect !== selectFallback && offset === 0) { useSelect = selectFallback; offset = -pageSize; continue; } if (error) break; const chunk = (data as DealWonRow[]) || []; out.push(...chunk); if (chunk.length < pageSize) break; } } return enrichDealSources(out, tenantId, useServiceRole); } /** Fill deal.source from linked lead when missing. */ async function enrichDealSources( deals: DealWonRow[], tenantId: string | null, useServiceRole: boolean ): Promise { const need = deals.filter((d) => !d.source && d.lead_id); if (!need.length) { return deals.map((d) => ({ ...d, source: d.source || (/^order\s+\d+/i.test(d.name || '') ? 'ecommerce' : d.source) || null, })); } const leadIds = [...new Set(need.map((d) => d.lead_id!).filter(Boolean))]; const leadSource = new Map(); if (useServiceRole) { const headers = serviceHeaders(); const base = supabaseUrl(); if (headers && base) { for (let i = 0; i < leadIds.length; i += 200) { const chunk = leadIds.slice(i, i + 200); const tidFilter = tenantId ? `&tenant_id=eq.${encodeURIComponent(tenantId)}` : ''; const r = await fetch( `${base}/rest/v1/leads?select=id,source&id=in.(${chunk.map(encodeURIComponent).join(',')})${tidFilter}`, { headers, cache: 'no-store' } ); if (!r.ok) continue; const rows = (await r.json()) as { id: string; source?: string }[]; for (const row of rows) { if (row.source) leadSource.set(row.id, row.source); } } } } else { const supabase = await createClient(); for (let i = 0; i < leadIds.length; i += 200) { const chunk = leadIds.slice(i, i + 200); const { data } = await supabase .from('leads') .select('id,source') .in('id', chunk); for (const row of (data as { id: string; source?: string }[]) || []) { if (row.source) leadSource.set(row.id, row.source); } } } return deals.map((d) => { let source = d.source || (d.lead_id ? leadSource.get(d.lead_id) : null) || null; if (!source && /^order\s+\d+/i.test(d.name || '')) source = 'ecommerce'; return { ...d, source }; }); } /** Paginate organic & web daily metrics (GSC + GA4) for active tenant / portfolio. */ async function fetchOrganicDaily( 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); if (useServiceRole) { const headers = serviceHeaders(); const base = supabaseUrl(); if (!headers || !base) return []; const tidFilter = tenantId ? `&tenant_id=eq.${encodeURIComponent(tenantId)}` : ''; const r = await fetch( `${base}/rest/v1/organic_daily?select=*&day=gte.${sinceStr}${tidFilter}&order=day.desc&limit=1000`, { headers, cache: 'no-store' } ); if (!r.ok) return []; return (await r.json()) as OrganicDailyRow[]; } const supabase = await createClient(); let query = supabase .from('organic_daily') .select('*') .gte('day', sinceStr) .order('day', { ascending: false }) .limit(1000); if (tenantId) { query = query.eq('tenant_id', tenantId); } const { data, error } = await query; if (error) return []; return (data as OrganicDailyRow[]) || []; } /** Active integrations for connector status pills. */ async function fetchIntegrations( tenantId: string | null, useServiceRole: boolean ) { if (!tenantId) return []; if (useServiceRole) { const headers = serviceHeaders(); const base = supabaseUrl(); if (!headers || !base) return []; const r = await fetch( `${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tenantId)}&select=provider,status,external_account_id,last_sync_at,label`, { headers, cache: 'no-store' } ); if (!r.ok) return []; return await r.json(); } const supabase = await createClient(); const { data } = await supabase .from('integrations') .select('provider,status,external_account_id,last_sync_at,label') .eq('tenant_id', tenantId); return data || []; } 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[] = []; let organic: OrganicDailyRow[] = []; let integrations: any[] = []; 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); organic = await fetchOrganicDaily(null, true); integrations = []; } 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); organic = await fetchOrganicDaily(ctx.tenantId, true); integrations = await fetchIntegrations(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); organic = await fetchOrganicDaily(ctx.tenantId, false); integrations = await fetchIntegrations(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 ( ); }