From a5bfec698b60ad264773ab6496900d5477f7a738 Mon Sep 17 00:00:00 2001 From: 4Ventures <4ventures@MacBook-Air-de-4Ventures.local> Date: Tue, 6 Oct 2026 15:33:06 +0200 Subject: [PATCH] =?UTF-8?q?Align=20Ads=20and=20M=C3=A9tricas=20on=20the=20?= =?UTF-8?q?same=20paginated=20ad=5Fdaily=20warehouse.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ad Tracker only loaded 2000 newest rows, so Brava showed 0 € while Métricas summed the full month. Both now page the same table for the selected range. Co-authored-by: Cursor --- src/app/api/ads/daily/route.ts | 46 +++++++++++++++++ src/app/dashboard/ad-tracker/page.tsx | 22 +++----- src/app/dashboard/kpis/page.tsx | 72 +++++++-------------------- src/components/AdTrackerClient.tsx | 44 ++++++++++++++-- src/components/MetricsDashboard.tsx | 8 +-- src/lib/fetch-ad-daily.ts | 63 +++++++++++++++++++++++ 6 files changed, 181 insertions(+), 74 deletions(-) create mode 100644 src/app/api/ads/daily/route.ts create mode 100644 src/lib/fetch-ad-daily.ts diff --git a/src/app/api/ads/daily/route.ts b/src/app/api/ads/daily/route.ts new file mode 100644 index 0000000..e569c8d --- /dev/null +++ b/src/app/api/ads/daily/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@/lib/supabase/server'; +import { resolveActiveTenant } from '@/lib/tenant-context'; +import { isAgencyRole, userRole } from '@/lib/agency'; +import { fetchAdDailyPaged } from '@/lib/fetch-ad-daily'; + +export const dynamic = 'force-dynamic'; + +export async function GET(req: NextRequest) { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const ctx = await resolveActiveTenant(user); + if (!ctx.tenantId || ctx.portfolioWide) { + return NextResponse.json({ + rows: [], + warning: 'Selecciona un cliente en Contexto', + }); + } + + const since = req.nextUrl.searchParams.get('since') || ''; + const until = req.nextUrl.searchParams.get('until') || ''; + if (!/^\d{4}-\d{2}-\d{2}$/.test(since) || !/^\d{4}-\d{2}-\d{2}$/.test(until)) { + return NextResponse.json({ error: 'since y until (YYYY-MM-DD)' }, { status: 400 }); + } + + const agency = isAgencyRole(userRole(user)); + const rows = await fetchAdDailyPaged({ + tenantId: ctx.tenantId, + useServiceRole: agency && ctx.viaView, + since, + until, + }); + + return NextResponse.json({ + rows, + since, + until, + tenant_label: ctx.label, + }); +} diff --git a/src/app/dashboard/ad-tracker/page.tsx b/src/app/dashboard/ad-tracker/page.tsx index 77847f6..78eeae5 100644 --- a/src/app/dashboard/ad-tracker/page.tsx +++ b/src/app/dashboard/ad-tracker/page.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { createClient } from '@/lib/supabase/server'; import { redirect } from 'next/navigation'; -import { resolveActiveTenant, restGet } from '@/lib/tenant-context'; +import { resolveActiveTenant } from '@/lib/tenant-context'; import { isAgencyRole, userRole } from '@/lib/agency'; import { AdDailyRow } from '@/lib/metrics'; import AdTrackerClient from '@/components/AdTrackerClient'; +import { fetchAdDailyPaged, adDailyLookbackSince } from '@/lib/fetch-ad-daily'; export const dynamic = 'force-dynamic'; @@ -41,19 +42,12 @@ export default async function AdTrackerPage() { } let daily: AdDailyRow[] = []; - if (agency && ctx.viaView && ctx.tenantId) { - const tid = encodeURIComponent(ctx.tenantId); - const d = await restGet( - `/rest/v1/ad_daily?tenant_id=eq.${tid}&select=*&order=date.desc&limit=2000` - ); - daily = d.data || []; - } else { - const { data } = await supabase - .from('ad_daily') - .select('*') - .order('date', { ascending: false }) - .limit(2000); - daily = (data as AdDailyRow[]) || []; + if (ctx.tenantId) { + daily = await fetchAdDailyPaged({ + tenantId: ctx.tenantId, + useServiceRole: agency && ctx.viaView, + since: adDailyLookbackSince(), + }); } return ( diff --git a/src/app/dashboard/kpis/page.tsx b/src/app/dashboard/kpis/page.tsx index ae90625..1465984 100644 --- a/src/app/dashboard/kpis/page.tsx +++ b/src/app/dashboard/kpis/page.tsx @@ -18,59 +18,13 @@ import { } from '@/lib/metrics'; import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency'; import { OrganicDailyRow } from '@/lib/organic'; +import { + adDailyLookbackSince, + fetchAdDailyPaged, +} from '@/lib/fetch-ad-daily'; 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, @@ -344,7 +298,11 @@ export default async function KpisPage() { '/rest/v1/kpi_weekly?select=*&order=week.desc' ); weekly = w.data || []; - daily = await fetchAdDaily(null, true); + daily = await fetchAdDailyPaged({ + tenantId: null, + useServiceRole: true, + since: adDailyLookbackSince(), + }); crmLeads = []; wonDeals = await fetchWonDeals(null, true); organic = await fetchOrganicDaily(null, true); @@ -355,7 +313,11 @@ export default async function KpisPage() { `/rest/v1/kpi_weekly?tenant_id=eq.${tid}&select=*&order=week.desc` ); weekly = w.data || []; - daily = await fetchAdDaily(ctx.tenantId, true); + daily = await fetchAdDailyPaged({ + tenantId: ctx.tenantId, + useServiceRole: true, + since: adDailyLookbackSince(), + }); crmLeads = await fetchCrmLeads(ctx.tenantId, true); wonDeals = await fetchWonDeals(ctx.tenantId, true); organic = await fetchOrganicDaily(ctx.tenantId, true); @@ -367,7 +329,11 @@ export default async function KpisPage() { .select('*') .order('week', { ascending: false }); weekly = (w as KpiWeeklyRow[]) || []; - daily = await fetchAdDaily(ctx.tenantId, false); + daily = await fetchAdDailyPaged({ + tenantId: ctx.tenantId, + useServiceRole: false, + since: adDailyLookbackSince(), + }); crmLeads = await fetchCrmLeads(ctx.tenantId, false); wonDeals = await fetchWonDeals(ctx.tenantId, false); organic = await fetchOrganicDaily(ctx.tenantId, false); diff --git a/src/components/AdTrackerClient.tsx b/src/components/AdTrackerClient.tsx index e7cf638..bbc9227 100644 --- a/src/components/AdTrackerClient.tsx +++ b/src/components/AdTrackerClient.tsx @@ -68,7 +68,9 @@ function rates(input: Omit): PlatformKpis { } function convOf(r: AdDailyRow): number { - return Number(r.conversions ?? r.platform_leads ?? 0) || 0; + const x = r.platform_leads ?? r.conversions; + const n = Number(x); + return Number.isFinite(n) ? n : 0; } function campaignName(r: AdDailyRow): string { @@ -541,12 +543,44 @@ export default function AdTrackerClient({ } | null>(null); const [zoomAd, setZoomAd] = useState(null); + const [rowsLive, setRowsLive] = useState(rows); + const [rowsLoading, setRowsLoading] = useState(false); + const range = useMemo(() => ({ since, until, label: rangeLabel }), [ since, until, rangeLabel, ]); + useEffect(() => { + setRowsLive(rows); + }, [rows]); + + useEffect(() => { + let cancelled = false; + async function loadRange() { + setRowsLoading(true); + try { + const qs = new URLSearchParams({ + since: range.since, + until: range.until, + }); + const r = await fetch(`/api/ads/daily?${qs}`, { cache: 'no-store' }); + const data = await r.json().catch(() => ({})); + if (cancelled) return; + if (r.ok && Array.isArray(data.rows)) { + setRowsLive(data.rows as AdDailyRow[]); + } + } finally { + if (!cancelled) setRowsLoading(false); + } + } + void loadRange(); + return () => { + cancelled = true; + }; + }, [range.since, range.until]); + useEffect(() => { if (tab === 'resumen') { setCreatives([]); @@ -636,8 +670,8 @@ export default function AdTrackerClient({ }, [range.since, range.until, tab]); const filtered = useMemo( - () => rows.filter((r) => inRange(r.date, range.since, range.until)), - [rows, range.since, range.until] + () => rowsLive.filter((r) => inRange(r.date, range.since, range.until)), + [rowsLive, range.since, range.until] ); const metaRows = filtered.filter((r) => normalizePlatform(r.platform) === 'meta'); @@ -694,7 +728,9 @@ export default function AdTrackerClient({ } /> diff --git a/src/components/MetricsDashboard.tsx b/src/components/MetricsDashboard.tsx index 038b901..a951395 100644 --- a/src/components/MetricsDashboard.tsx +++ b/src/components/MetricsDashboard.tsx @@ -361,8 +361,10 @@ export default function MetricsDashboard({ const periodTotals = useMemo(() => { let base: MetricTotals; - if (rangedDaily.length) { - base = metricTotalsFromDaily(rangedDaily); + if (dailyRows.length) { + base = rangedDaily.length + ? metricTotalsFromDaily(rangedDaily) + : metricTotalsFromDaily([]); } else { const ranged = weeklyRows.filter((r) => { if (!r.week) return false; @@ -372,7 +374,7 @@ export default function MetricsDashboard({ base = ranged.length ? sumWeekly(ranged) : month; } return withCohortFromDeals(base, rangedDeals); - }, [rangedDaily, rangedDeals, weeklyRows, since, until, month]); + }, [dailyRows.length, rangedDaily, rangedDeals, weeklyRows, since, until, month]); const rangeDeltas = useMemo( () => buildRangeDeltas(dailyRows, wonDeals, since, until), diff --git a/src/lib/fetch-ad-daily.ts b/src/lib/fetch-ad-daily.ts new file mode 100644 index 0000000..dec2bfd --- /dev/null +++ b/src/lib/fetch-ad-daily.ts @@ -0,0 +1,63 @@ +import { createClient } from '@/lib/supabase/server'; +import { serviceHeaders, supabaseUrl } from '@/lib/agency'; +import type { AdDailyRow } from '@/lib/metrics'; + +const PAGE = 1000; +const MAX_ROWS = 200000; + +export async function fetchAdDailyPaged(opts: { + tenantId: string | null; + useServiceRole: boolean; + since: string; + until?: string; +}): Promise { + const { tenantId, useServiceRole, since, until } = opts; + const out: AdDailyRow[] = []; + + if (useServiceRole) { + const headers = serviceHeaders(); + const base = supabaseUrl(); + if (!headers || !base) return []; + const tid = tenantId + ? `&tenant_id=eq.${encodeURIComponent(tenantId)}` + : ''; + const untilF = until ? `&date=lte.${until}` : ''; + for (let offset = 0; offset < MAX_ROWS; offset += PAGE) { + const r = await fetch( + `${base}/rest/v1/ad_daily?select=*&date=gte.${since}${untilF}${tid}&order=date.desc&limit=${PAGE}&offset=${offset}`, + { headers, cache: 'no-store' } + ); + if (!r.ok) break; + const chunk = (await r.json()) as AdDailyRow[]; + out.push(...chunk); + if (chunk.length < PAGE) break; + } + return out; + } + + const supabase = await createClient(); + for (let offset = 0; offset < MAX_ROWS; offset += PAGE) { + let q = supabase + .from('ad_daily') + .select('*') + .gte('date', since) + .order('date', { ascending: false }) + .range(offset, offset + PAGE - 1); + if (until) q = q.lte('date', until); + const { data, error } = await q; + if (error) break; + const chunk = (data as AdDailyRow[]) || []; + out.push(...chunk); + if (chunk.length < PAGE) break; + } + return out; +} + +export function adDailyLookbackSince(): string { + const since = new Date(); + since.setUTCDate( + since.getUTCDate() - + (Math.ceil((Date.now() - Date.UTC(2025, 0, 1)) / 86400000) + 14) + ); + return since.toISOString().slice(0, 10); +}