'use client'; import React, { useEffect, useState } from 'react'; import { Loader2, Megaphone, RefreshCw } from 'lucide-react'; type MatchRow = { tenantId: string; tenantName: string; tenantSlug: string; accountId: string | null; accountName: string | null; score: number; status: string; candidates?: { id: string; name: string; score: number }[]; }; type ProviderResult = { accounts: number; linked: number; matches: MatchRow[]; error?: string; }; export default function AdsDiscoverPanel() { const [metaOk, setMetaOk] = useState(false); const [metaPending, setMetaPending] = useState(true); const [gadsOk, setGadsOk] = useState(false); const [loading, setLoading] = useState(true); const [running, setRunning] = useState(false); const [syncing, setSyncing] = useState(false); const [savingToken, setSavingToken] = useState(false); const [metaToken, setMetaToken] = useState(''); const [meta, setMeta] = useState(null); const [google, setGoogle] = useState(null); const [msg, setMsg] = useState(null); const [err, setErr] = useState(null); async function loadStatus() { setLoading(true); const res = await fetch('/api/admin/ads/status'); const data = await res.json(); setMetaOk(Boolean(data.meta?.configured)); setMetaPending(Boolean(data.meta?.pendingReview)); setGadsOk(Boolean(data.googleAds?.configured)); setLoading(false); } useEffect(() => { void loadStatus(); }, []); async function saveToken() { setSavingToken(true); setErr(null); const res = await fetch('/api/admin/ads/meta-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: metaToken }), }); const data = await res.json(); if (!res.ok) { setErr(data.error || 'No se pudo guardar el token'); } else { setMsg(`Token Meta guardado${data.name ? ` · ${data.name}` : ''}`); setMetaToken(''); await loadStatus(); } setSavingToken(false); } async function discover(providers?: Array<'meta_ads' | 'google_ads'>) { setRunning(true); setErr(null); setMsg(null); const res = await fetch('/api/admin/ads/discover', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apply: true, providers }), }); const data = await res.json(); if (!res.ok) { setErr(data.error || 'Discover falló'); } else { setMeta(data.meta || null); setGoogle(data.google || null); const parts = []; if (data.meta) { parts.push( `Meta Ads: ${data.meta.linked}/${data.meta.accounts}` + (data.meta.pagesLinked ? ` · FB ${data.meta.pagesLinked}` : '') + (data.meta.igLinked ? ` · IG ${data.meta.igLinked}` : '') + (data.meta.error ? ` (${data.meta.error})` : '') ); } if (data.google) { parts.push( `Google Ads: ${data.google.linked}/${data.google.accounts} enlazadas` + (data.google.error ? ` (${data.google.error})` : '') ); } setMsg(parts.join(' · ')); } setRunning(false); } async function syncMetrics() { setSyncing(true); setErr(null); setMsg(null); const res = await fetch('/api/admin/ads/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ days: 30 }), }); const data = await res.json(); if (!res.ok) { setErr(data.error || 'Sync falló'); } else { const parts = (data.results || []) .filter( (r: { metaRows?: number; googleRows?: number }) => (r.metaRows || 0) + (r.googleRows || 0) > 0 ) .map( (r: { tenantName: string; metaRows: number; googleRows: number; }) => `${r.tenantName}: M${r.metaRows}/G${r.googleRows}` ); setMsg( `Métricas sincronizadas (${data.synced || 0} clientes, ${data.days || 30}d)` + (parts.length ? ` · ${parts.slice(0, 6).join(' · ')}` : '') + (data.withErrors ? ` · ${data.withErrors} con avisos` : '') ); } setSyncing(false); } async function assign( provider: 'meta_ads' | 'google_ads', tenantId: string, accountId: string, accountName: string ) { await fetch('/api/admin/ads/discover', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ assign: { provider, tenantId, accountId, accountName }, }), }); await discover([provider]); } if (loading) { return (

Ads…

); } const ambiguous = [ ...(meta?.matches || []) .filter((m) => m.status === 'ambiguous') .map((m) => ({ ...m, provider: 'meta_ads' as const })), ...(google?.matches || []) .filter((m) => m.status === 'ambiguous') .map((m) => ({ ...m, provider: 'google_ads' as const })), ]; return (

Meta Ads + Google Ads (agencia)

Meta (FB/IG/Ads) usa el system user token de la app{' '} 4V2030 ( META_ACCESS_TOKEN en Dokploy o pegado en Agencia). Google Ads/Drive/GA4 usan el OAuth de{' '} marketing@ — son tokens distintos.

Meta {metaOk ? 'listo' : 'pendiente App Review / token'} Google Ads {gadsOk ? 'listo' : 'falta GOOGLE_ADS_*'}
{metaPending && (

Cuando Meta apruebe la solicitud, genera el token del usuario del sistema y pégalo aquí (se guarda sin redeploy).

setMetaToken(e.target.value)} />
)} {msg &&

{msg}

} {err &&

{err}

} {ambiguous.length > 0 && (
    {ambiguous.map((m) => (
  • {m.tenantName}{' '} · {m.provider === 'meta_ads' ? 'Meta' : 'GAds'}

    Ambiguo — elige cuenta

    {(m.candidates || []).map((c) => ( ))}
  • ))}
)}
); }