diff --git a/src/components/ClientIntegrationsPanel.tsx b/src/components/ClientIntegrationsPanel.tsx
index 553f83a..7fea54c 100644
--- a/src/components/ClientIntegrationsPanel.tsx
+++ b/src/components/ClientIntegrationsPanel.tsx
@@ -13,7 +13,15 @@ type Integ = {
error_message?: string | null;
last_sync_at?: string | null;
enabled?: boolean;
- meta?: { folder_name?: string; match_score?: number } | null;
+ meta?: {
+ folder_name?: string;
+ match_score?: number;
+ extra_ad_accounts?: {
+ account_id?: string;
+ external_account_id?: string;
+ name?: string;
+ }[];
+ } | null;
};
type Provider = { id: string; label: string; group: string; live?: boolean };
@@ -354,6 +362,18 @@ export default function ClientIntegrationsPanel({
TikTok Ads. OAuth live y publicación: siguiente fase.
)}
+ {p.id === 'meta_ads' &&
+ (row?.meta?.extra_ad_accounts || []).length ? (
+
+ Cuentas extra:{' '}
+ {(row?.meta?.extra_ad_accounts || [])
+ .map(
+ (x) =>
+ `${x.name || 'cuenta'} (${x.account_id || x.external_account_id})`
+ )
+ .join(' · ')}
+
+ ) : null}
);
})}
diff --git a/src/components/WindsorAiSection.tsx b/src/components/WindsorAiSection.tsx
index 1a926ab..4b21504 100644
--- a/src/components/WindsorAiSection.tsx
+++ b/src/components/WindsorAiSection.tsx
@@ -394,7 +394,18 @@ export default function WindsorAiSection({
},
{
name: 'Meta Ads',
- id: meta?.external_account_id || meta?.label || '—',
+ id: (() => {
+ const primary = meta?.external_account_id || '';
+ const extras = (
+ (meta?.meta as { extra_ad_accounts?: { account_id?: string; external_account_id?: string; name?: string }[] } | null)
+ ?.extra_ad_accounts || []
+ )
+ .map((x) => x.account_id || x.external_account_id)
+ .filter(Boolean);
+ const ids = [primary, ...extras].filter(Boolean);
+ if (ids.length) return ids.join(' · ');
+ return meta?.label || '—';
+ })(),
status: meta?.status === 'connected' ? 'connected' : 'disconnected',
type: 'Instagram & Facebook Ads',
},
diff --git a/src/lib/ad-creatives-meta.ts b/src/lib/ad-creatives-meta.ts
index f0b5673..b4619ed 100644
--- a/src/lib/ad-creatives-meta.ts
+++ b/src/lib/ad-creatives-meta.ts
@@ -411,18 +411,42 @@ function metaCardsFromDaily(
});
}
-async function resolveMetaAccountId(tenantId: string): Promise {
+async function resolveMetaAccountIds(tenantId: string): Promise {
const headers = serviceHeaders();
const base = supabaseUrl();
- if (!headers || !base) return null;
+ if (!headers || !base) return [];
const r = await fetch(
- `${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tenantId)}&provider=eq.meta_ads&select=external_account_id,status&limit=1`,
+ `${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(
+ tenantId
+ )}&provider=eq.meta_ads&select=external_account_id,status,meta&limit=1`,
{ headers, cache: 'no-store' }
);
- if (!r.ok) return null;
- const rows = (await r.json()) as { external_account_id?: string | null }[];
- const id = rows[0]?.external_account_id?.trim();
- return id || null;
+ if (!r.ok) return [];
+ const rows = (await r.json()) as {
+ external_account_id?: string | null;
+ meta?: {
+ extra_ad_accounts?: { account_id?: string; external_account_id?: string }[];
+ alternate_accounts?: { external_account_id?: string }[];
+ } | null;
+ }[];
+ const row = rows[0];
+ const out: string[] = [];
+ const primary = row?.external_account_id?.trim();
+ if (primary) out.push(primary);
+ for (const x of row?.meta?.extra_ad_accounts || []) {
+ const id = (x.account_id || x.external_account_id || '').trim();
+ if (id) out.push(id);
+ }
+ for (const x of row?.meta?.alternate_accounts || []) {
+ const id = (x.external_account_id || '').trim();
+ if (id) out.push(id);
+ }
+ return [...new Set(out)];
+}
+
+async function resolveMetaAccountId(tenantId: string): Promise {
+ const ids = await resolveMetaAccountIds(tenantId);
+ return ids[0] || null;
}
async function resolveImageHashes(
@@ -771,13 +795,14 @@ async function fetchMetaCreativeCardsInner(
: { ads: [], warning: 'Sin token Meta (META_ACCESS_TOKEN / agency_oauth)' };
}
- const account = await resolveMetaAccountId(opts.tenantId);
- if (!account) {
+ const accounts = await resolveMetaAccountIds(opts.tenantId);
+ if (!accounts.length) {
const ads = warehouse();
return ads.length
? { ads }
: { ads: [], warning: 'Cliente sin cuenta Meta Ads enlazada' };
}
+ const account = accounts[0];
const limit = opts.limit ?? 16;
let targets: {
@@ -790,16 +815,20 @@ async function fetchMetaCreativeCardsInner(
conversions: number;
}[] = [];
- try {
- targets = await fetchMetaAdLevelInsights(
- account,
- token,
- opts.since,
- opts.until,
- limit
- );
- } catch {
- /* fall through — never surface ad-level Graph errors */
+ for (const actId of accounts) {
+ if (targets.length >= limit) break;
+ try {
+ const part = await fetchMetaAdLevelInsights(
+ actId,
+ token,
+ opts.since,
+ opts.until,
+ limit - targets.length
+ );
+ targets.push(...part);
+ } catch {
+ /* fall through — never surface ad-level Graph errors */
+ }
}
// Fallback: expand campaigns from ad_daily → ads under each campaign
@@ -914,37 +943,48 @@ export async function fetchMetaAccountClickSplit(opts: {
}): Promise<{ clicks: number; linkClicks: number; spend: number } | null> {
const token = await resolveMetaAccessToken();
if (!token) return null;
- const account = await resolveMetaAccountId(opts.tenantId);
- if (!account) return null;
- const act = account.startsWith('act_') ? account : `act_${account}`;
+ const accounts = await resolveMetaAccountIds(opts.tenantId);
+ if (!accounts.length) return null;
const timeRange = JSON.stringify({ since: opts.since, until: opts.until });
- try {
- const raw = await graphGet<{
- data?: {
- spend?: string;
- clicks?: string;
- actions?: { action_type?: string; value?: string }[];
- }[];
- }>(
- `${act}/insights`,
- {
- fields: 'spend,clicks,actions',
- level: 'account',
- time_increment: 'all_days',
- time_range: timeRange,
- limit: '1',
- },
- token
- );
- const row = raw.data?.[0];
- if (!row) return null;
- const clicks = n(row.clicks);
- return {
- clicks,
- linkClicks: linkClicksFromMetaActions(row.actions, clicks),
- spend: Math.round(n(row.spend) * 100) / 100,
- };
- } catch {
- return null;
+ let clicks = 0;
+ let linkClicks = 0;
+ let spend = 0;
+ let any = false;
+ for (const account of accounts) {
+ const act = account.startsWith('act_') ? account : `act_${account}`;
+ try {
+ const raw = await graphGet<{
+ data?: {
+ spend?: string;
+ clicks?: string;
+ actions?: { action_type?: string; value?: string }[];
+ }[];
+ }>(
+ `${act}/insights`,
+ {
+ fields: 'spend,clicks,actions',
+ level: 'account',
+ time_increment: 'all_days',
+ time_range: timeRange,
+ limit: '1',
+ },
+ token
+ );
+ const row = raw.data?.[0];
+ if (!row) continue;
+ any = true;
+ const c = n(row.clicks);
+ clicks += c;
+ linkClicks += linkClicksFromMetaActions(row.actions, c);
+ spend += n(row.spend);
+ } catch {
+ /* skip inaccessible account */
+ }
}
+ if (!any) return null;
+ return {
+ clicks,
+ linkClicks,
+ spend: Math.round(spend * 100) / 100,
+ };
}