fix: mostrar Google Ads desde ad_daily cuando kpi_weekly está vacío
kpi_weekly es una MV de solo lectura sin canales paid; canal/seguimiento ahora hacen fallback a ad_daily. Bump Google Ads API a v21 en sync. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
AdDailyRow,
|
||||
KpiWeeklyRow,
|
||||
buildChannelSeries,
|
||||
resolveWeeklyRows,
|
||||
sumDaily,
|
||||
sumWeekly,
|
||||
} from '@/lib/metrics';
|
||||
@@ -76,7 +77,7 @@ export async function GET() {
|
||||
`/rest/v1/kpi_weekly?tenant_id=eq.${tid}&select=*&order=week.asc`
|
||||
),
|
||||
restGet<AdDailyRow[]>(
|
||||
`/rest/v1/ad_daily?tenant_id=eq.${tid}&select=*&order=date.asc&limit=800`
|
||||
`/rest/v1/ad_daily?tenant_id=eq.${tid}&select=*&order=date.desc&limit=5000`
|
||||
),
|
||||
]);
|
||||
entries = e.data || [];
|
||||
@@ -93,14 +94,15 @@ export async function GET() {
|
||||
supabase
|
||||
.from('ad_daily')
|
||||
.select('*')
|
||||
.order('date', { ascending: true })
|
||||
.limit(800),
|
||||
.order('date', { ascending: false })
|
||||
.limit(5000),
|
||||
]);
|
||||
entries = (e as SeguimientoEntry[]) || [];
|
||||
weekly = (w as KpiWeeklyRow[]) || [];
|
||||
daily = (d as AdDailyRow[]) || [];
|
||||
}
|
||||
|
||||
weekly = resolveWeeklyRows(weekly, daily);
|
||||
const channelSeries = buildChannelSeries(weekly);
|
||||
const totalsWeekly = sumWeekly(weekly);
|
||||
const totalsDaily = sumDaily(daily);
|
||||
|
||||
@@ -5,10 +5,12 @@ import { redirect } from 'next/navigation';
|
||||
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
|
||||
import { isAgencyRole, userRole } from '@/lib/agency';
|
||||
import {
|
||||
AdDailyRow,
|
||||
KpiWeeklyRow,
|
||||
buildChannelSeries,
|
||||
formatEur,
|
||||
formatNum,
|
||||
resolveWeeklyRows,
|
||||
} from '@/lib/metrics';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
@@ -40,20 +42,33 @@ export default async function CanalDashPage({
|
||||
}
|
||||
|
||||
let weekly: KpiWeeklyRow[] = [];
|
||||
let daily: AdDailyRow[] = [];
|
||||
if (agency && ctx.viaView && ctx.tenantId) {
|
||||
const tid = encodeURIComponent(ctx.tenantId);
|
||||
const w = await restGet<KpiWeeklyRow[]>(
|
||||
`/rest/v1/kpi_weekly?tenant_id=eq.${tid}&select=*&order=week.asc`
|
||||
);
|
||||
const [w, d] = await Promise.all([
|
||||
restGet<KpiWeeklyRow[]>(
|
||||
`/rest/v1/kpi_weekly?tenant_id=eq.${tid}&select=*&order=week.asc`
|
||||
),
|
||||
restGet<AdDailyRow[]>(
|
||||
`/rest/v1/ad_daily?tenant_id=eq.${tid}&select=*&order=date.desc&limit=5000`
|
||||
),
|
||||
]);
|
||||
weekly = w.data || [];
|
||||
daily = d.data || [];
|
||||
} else {
|
||||
const { data } = await supabase
|
||||
.from('kpi_weekly')
|
||||
.select('*')
|
||||
.order('week', { ascending: true });
|
||||
weekly = (data as KpiWeeklyRow[]) || [];
|
||||
const [{ data: w }, { data: d }] = await Promise.all([
|
||||
supabase.from('kpi_weekly').select('*').order('week', { ascending: true }),
|
||||
supabase
|
||||
.from('ad_daily')
|
||||
.select('*')
|
||||
.order('date', { ascending: false })
|
||||
.limit(5000),
|
||||
]);
|
||||
weekly = (w as KpiWeeklyRow[]) || [];
|
||||
daily = (d as AdDailyRow[]) || [];
|
||||
}
|
||||
|
||||
weekly = resolveWeeklyRows(weekly, daily);
|
||||
const series = buildChannelSeries(weekly);
|
||||
const points = series.points.filter((p) => p.channel === channel);
|
||||
const spend = points.reduce((a, p) => a + p.spend, 0);
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
KpiWeeklyRow,
|
||||
buildChannelSeries,
|
||||
buildComparisons,
|
||||
resolveWeeklyRows,
|
||||
sumDaily,
|
||||
sumWeekly,
|
||||
weeklyFromDaily,
|
||||
} from '@/lib/metrics';
|
||||
import { isAgencyRole, userRole } from '@/lib/agency';
|
||||
|
||||
@@ -65,13 +65,7 @@ export default async function KpisPage() {
|
||||
daily = (d as AdDailyRow[]) || [];
|
||||
}
|
||||
|
||||
// kpi_weekly is often a stale MV — prefer ad_daily when it has more signal
|
||||
const fromDaily = weeklyFromDaily(daily);
|
||||
const weeklySpend = weekly.reduce((a, r) => a + Number(r.total_spend || 0), 0);
|
||||
const dailySpend = daily.reduce((a, r) => a + Number(r.spend || 0), 0);
|
||||
if (fromDaily.length && dailySpend >= weeklySpend) {
|
||||
weekly = fromDaily;
|
||||
}
|
||||
weekly = resolveWeeklyRows(weekly, daily);
|
||||
|
||||
const comparisons = buildComparisons(weekly);
|
||||
const month =
|
||||
|
||||
@@ -349,7 +349,8 @@ export default function MetricsDashboard({
|
||||
{channelRows.length === 0 ? (
|
||||
<p className="text-xs text-gray-500 py-6 text-center">
|
||||
Sin series de canal en el periodo. Cuando Windsor cargue
|
||||
`kpi_weekly.channel`, aparecerán aquí.
|
||||
Los canales con inversión en ads (Meta, Google) aparecen cuando hay
|
||||
filas en ad_daily o kpi_weekly para este cliente.
|
||||
</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { serviceHeaders, supabaseUrl } from '@/lib/agency';
|
||||
import { resolveMetaAccessToken } from '@/lib/ads-discovery';
|
||||
|
||||
const META_GRAPH = 'https://graph.facebook.com/v21.0';
|
||||
const GADS_API = 'https://googleads.googleapis.com/v18';
|
||||
const GADS_API = 'https://googleads.googleapis.com/v21';
|
||||
|
||||
export type SyncTenantResult = {
|
||||
tenantId: string;
|
||||
@@ -285,14 +285,16 @@ async function fetchGoogleCampaignDaily(
|
||||
const date = row.segments?.date;
|
||||
if (!date) continue;
|
||||
const spend = Math.round((n(row.metrics?.costMicros) / 1_000_000) * 100) / 100;
|
||||
const campaignId = row.campaign?.id ? String(row.campaign.id) : null;
|
||||
out.push({
|
||||
tenant_id: tenantId,
|
||||
date,
|
||||
platform: 'google',
|
||||
campaign_id: row.campaign?.id ? String(row.campaign.id) : null,
|
||||
campaign_id: campaignId,
|
||||
campaign_name: row.campaign?.name || null,
|
||||
ad_id: null,
|
||||
ad_name: null,
|
||||
// ad_daily.ad_id is NOT NULL — campaign-level rows reuse campaign id
|
||||
ad_id: campaignId || '',
|
||||
ad_name: '',
|
||||
impressions: n(row.metrics?.impressions),
|
||||
clicks: n(row.metrics?.clicks),
|
||||
spend,
|
||||
|
||||
@@ -284,4 +284,31 @@ export function weeklyFromDaily(rows: AdDailyRow[]): KpiWeeklyRow[] {
|
||||
});
|
||||
}
|
||||
|
||||
const ADS_CHANNELS = new Set(['meta', 'google']);
|
||||
|
||||
/**
|
||||
* kpi_weekly is often a non-writable MV without paid channels.
|
||||
* Prefer ad_daily rollups when they carry meta/google (or more spend).
|
||||
*/
|
||||
export function resolveWeeklyRows(
|
||||
weekly: KpiWeeklyRow[],
|
||||
daily: AdDailyRow[]
|
||||
): KpiWeeklyRow[] {
|
||||
const fromDaily = weeklyFromDaily(daily);
|
||||
if (!fromDaily.length) return weekly;
|
||||
|
||||
const weeklySpend = weekly.reduce((a, r) => a + n(r.total_spend), 0);
|
||||
const dailySpend = daily.reduce((a, r) => a + n(r.spend), 0);
|
||||
const weeklyAds = weekly.some((r) =>
|
||||
ADS_CHANNELS.has((r.channel || '').toLowerCase())
|
||||
);
|
||||
const dailyAds = daily.some((r) =>
|
||||
ADS_CHANNELS.has((r.platform || '').toLowerCase())
|
||||
);
|
||||
|
||||
if (dailyAds && !weeklyAds) return fromDaily;
|
||||
if (dailySpend >= weeklySpend) return fromDaily;
|
||||
return weekly;
|
||||
}
|
||||
|
||||
export { isoDate, weekStart };
|
||||
|
||||
Reference in New Issue
Block a user