feat: sync Meta metrics, objetivos ppto/consecución y simulador por tenant
Rellena ad_daily desde Meta, KPIs con fallback a daily, objetivos de presupuesto vs consecución, y simulador sin TikTok anclado a métricas del cliente. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -27,6 +27,7 @@ export default function AdsDiscoverPanel() {
|
||||
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<ProviderResult | null>(null);
|
||||
@@ -100,6 +101,41 @@ export default function AdsDiscoverPanel() {
|
||||
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,
|
||||
@@ -150,6 +186,19 @@ export default function AdsDiscoverPanel() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void syncMetrics()}
|
||||
disabled={syncing || (!metaOk && !gadsOk)}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-lg border border-emerald-700/60 text-emerald-200 hover:border-emerald-500 disabled:opacity-40"
|
||||
>
|
||||
{syncing ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
)}
|
||||
Sincronizar métricas
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void discover(['google_ads'])}
|
||||
|
||||
Reference in New Issue
Block a user