feat: periodo global, métricas negocio, seguimiento notas, Drive/Social/Orgánico
Selector de fechas en header (con Personalizado) compartido en el dashboard; Métricas con Ventas/CAC/ROAS CRM, cruce canal y gráficas diarias; Seguimiento solo notas + sync Superhuman; Activos con navegación Drive; Social con métricas IG/FB; Orgánico con prioridades SEO/GEO expertas. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,10 +2,49 @@ import { NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
|
||||
import { isAgencyRole, userRole } from '@/lib/agency';
|
||||
import { resolveMetaAccessToken } from '@/lib/ads-discovery';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/** GET — social channel integrations for active tenant. */
|
||||
const META = 'https://graph.facebook.com/v21.0';
|
||||
|
||||
type Integ = {
|
||||
provider: string;
|
||||
status: string;
|
||||
external_account_id?: string | null;
|
||||
label?: string | null;
|
||||
last_sync_at?: string | null;
|
||||
meta?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
async function fetchPageMetrics(token: string, pageId: string) {
|
||||
const fields =
|
||||
'name,fan_count,followers_count,link,picture{url},engagement';
|
||||
const r = await fetch(
|
||||
`${META}/${encodeURIComponent(pageId)}?fields=${fields}&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (!r.ok) return { error: (await r.text()).slice(0, 160) };
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async function fetchIgMetrics(token: string, igId: string) {
|
||||
const r = await fetch(
|
||||
`${META}/${encodeURIComponent(igId)}?fields=username,name,followers_count,follows_count,media_count,profile_picture_url&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (!r.ok) return { error: (await r.text()).slice(0, 160) };
|
||||
const profile = await r.json();
|
||||
const ins = await fetch(
|
||||
`${META}/${encodeURIComponent(igId)}/insights?metric=reach,impressions,profile_views&period=day&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
let insights: unknown = null;
|
||||
if (ins.ok) insights = await ins.json();
|
||||
return { ...profile, insights };
|
||||
}
|
||||
|
||||
/** GET — social integrations + live Meta metrics when token allows. */
|
||||
export async function GET() {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
@@ -19,33 +58,59 @@ export async function GET() {
|
||||
if (!ctx.tenantId || ctx.portfolioWide) {
|
||||
return NextResponse.json({
|
||||
integrations: [],
|
||||
metrics: {},
|
||||
warning: 'Selecciona un cliente en Contexto',
|
||||
});
|
||||
}
|
||||
|
||||
const agency = isAgencyRole(userRole(user));
|
||||
const providers = 'instagram,facebook,tiktok,linkedin';
|
||||
let integrations: Integ[] = [];
|
||||
|
||||
if (agency && ctx.viaView) {
|
||||
const tid = encodeURIComponent(ctx.tenantId);
|
||||
const r = await restGet(
|
||||
`/rest/v1/integrations?tenant_id=eq.${tid}&provider=in.(${providers})&select=provider,status,external_account_id,label,last_sync_at,meta,error_message`
|
||||
);
|
||||
return NextResponse.json({
|
||||
integrations: r.data || [],
|
||||
warning: r.error || null,
|
||||
});
|
||||
integrations = (r.data as Integ[]) || [];
|
||||
} else {
|
||||
const { data } = await supabase
|
||||
.from('integrations')
|
||||
.select(
|
||||
'provider,status,external_account_id,label,last_sync_at,meta,error_message'
|
||||
)
|
||||
.in('provider', ['instagram', 'facebook', 'tiktok', 'linkedin']);
|
||||
integrations = (data as Integ[]) || [];
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('integrations')
|
||||
.select(
|
||||
'provider,status,external_account_id,label,last_sync_at,meta,error_message'
|
||||
)
|
||||
.in('provider', ['instagram', 'facebook', 'tiktok', 'linkedin']);
|
||||
const metrics: Record<string, unknown> = {};
|
||||
const token = await resolveMetaAccessToken();
|
||||
if (token) {
|
||||
for (const i of integrations) {
|
||||
if (i.status !== 'connected' || !i.external_account_id) continue;
|
||||
const id = i.external_account_id;
|
||||
if (id.startsWith('http')) continue;
|
||||
try {
|
||||
if (i.provider === 'facebook') {
|
||||
metrics.facebook = await fetchPageMetrics(token, id);
|
||||
}
|
||||
if (i.provider === 'instagram') {
|
||||
metrics.instagram = await fetchIgMetrics(token, id);
|
||||
}
|
||||
} catch (e) {
|
||||
metrics[i.provider] = {
|
||||
error: e instanceof Error ? e.message : 'metrics error',
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
metrics.warning =
|
||||
'Sin token Meta de agencia: perfiles enlazados pero sin métricas live.';
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
integrations: data || [],
|
||||
warning: error?.message || null,
|
||||
integrations,
|
||||
metrics,
|
||||
label: ctx.label,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user