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:
4Ventures
2026-09-23 10:59:15 +02:00
parent 0d286dc66c
commit a1a00840d5
13 changed files with 1392 additions and 154 deletions

View File

@@ -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'])}