Align Ads and Métricas on the same paginated ad_daily warehouse.

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 <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-06 15:33:06 +02:00
parent fac9867202
commit a5bfec698b
6 changed files with 181 additions and 74 deletions

View File

@@ -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,
});
}

View File

@@ -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<AdDailyRow[]>(
`/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 (

View File

@@ -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<AdDailyRow[]> {
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);