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:
46
src/app/api/ads/daily/route.ts
Normal file
46
src/app/api/ads/daily/route.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user