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:
4Ventures
2026-09-24 15:45:30 +02:00
parent eb1622c88c
commit 3425364b07
15 changed files with 1067 additions and 1011 deletions

View File

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