feat: al conectar Google, auto-emparejar Drive/GA4/GSC/Ads en fichas

Bootstrap unificado post-OAuth + botón Emparejar todo. Meta discover
también intenta FB/IG pages con el token system user (API 4V2030).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 12:41:51 +02:00
parent 99895fe918
commit 7ed6e641cb
8 changed files with 374 additions and 75 deletions

View File

@@ -86,7 +86,11 @@ export default function AdsDiscoverPanel() {
const parts = [];
if (data.meta) {
parts.push(
`Meta: ${data.meta.linked}/${data.meta.accounts} enlazadas` +
`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})` : '')
);
}
@@ -179,9 +183,9 @@ export default function AdsDiscoverPanel() {
Meta Ads + Google Ads (agencia)
</h3>
<p className="text-xs text-gray-400 mt-0.5 max-w-xl">
Lista cuentas accesibles y las enlaza a cada cliente por nombre.
Google Ads ya puede emparejarse; Meta cuando tengas el token de
App Review.
Lista cuentas accesibles y las enlaza a cada ficha por nombre.
Meta Ads + páginas FB/IG usan el token system user (API 4V2030 /
App Review). Google Ads con el MCC de agencia.
</p>
</div>
</div>

View File

@@ -37,11 +37,14 @@ export default function DriveConnectPanel() {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get('drive') === 'ok') {
const boot = params.get('boot');
setMsg(
`Google conectado. Enlazadas ${params.get('linked') || 0} carpetas Drive` +
(params.get('ambiguous') && params.get('ambiguous') !== '0'
? ` · ${params.get('ambiguous')} ambiguas (revisa abajo)`
: '')
boot
? `Google conectado · ${boot}`
: `Google conectado. Enlazadas ${params.get('linked') || 0} carpetas Drive` +
(params.get('ambiguous') && params.get('ambiguous') !== '0'
? ` · ${params.get('ambiguous')} ambiguas`
: '')
);
}
if (params.get('drive') === 'error') {
@@ -101,7 +104,8 @@ export default function DriveConnectPanel() {
Una sola conexión con{' '}
<strong className="text-gray-800">marketing@4ventures.es</strong>.
En el consentimiento acepta Drive, Analytics y Search Console.
Sirve para carpetas Drive y para Orgánico (panel de abajo).
Sirve para carpetas Drive, GA4, GSC y (con MCC) Google Ads.
Tras el consentimiento se emparejan solos a las fichas.
</p>
</div>
</div>

View File

@@ -54,40 +54,36 @@ export default function GoogleOrganicPanel() {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get('drive') === 'ok') {
const boot = params.get('boot');
setMsg(
'Google reconectado. Pulsa «Descubrir y emparejar» para GA4/GSC.'
boot
? `Google reconectado · ${boot}`
: 'Google reconectado. Emparejando fichas…'
);
}
void loadStatus();
}, []);
async function discover(apply: boolean) {
async function bootstrapAll() {
setBusy('match');
setErr(null);
const res = await fetch('/api/admin/google/organic', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'match', apply }),
body: JSON.stringify({ action: 'bootstrap', days: 28 }),
});
const data = await res.json();
if (!res.ok) {
setBusy(null);
setErr(data.error || 'Discover falló');
setBusy(null);
if (!res.ok && !data.message) {
setErr(data.error || 'Emparejado falló');
return;
}
setMatches(data.matches || []);
setGa4Count(data.ga4Count || 0);
setGscCount(data.gscCount || 0);
setMsg(
apply
? `Emparejado · GA4 ${data.applied?.ga4 || 0} · GSC ${data.applied?.gsc || 0}. Actualizando datos…`
: `Vistas ${data.ga4Count} props GA4 · ${data.gscCount} sites GSC (sin escribir)`
);
if (apply) {
await syncAll();
} else {
setBusy(null);
setMsg(data.message || 'Emparejado listo');
if (data.organic) {
setGa4Count(data.organic.ga4Visible || 0);
setGscCount(data.organic.gscVisible || 0);
}
await loadStatus();
}
async function syncAll() {
@@ -109,7 +105,7 @@ export default function GoogleOrganicPanel() {
0
);
setMsg(
`Datos al día · ${data.synced || 0} clientes · ${wrote} días en Orgánico (se actualiza solo al abrir la vista y cada noche)`
`Datos al día · ${data.synced || 0} clientes · ${wrote} días en Orgánico`
);
}
@@ -128,12 +124,12 @@ export default function GoogleOrganicPanel() {
<Leaf className="w-5 h-5 text-lime-600 mt-0.5 shrink-0" />
<div>
<h3 className="text-sm font-semibold text-gray-900">
2 · Orgánico (GA4 + Search Console)
2 · Orgánico + emparejado Google
</h3>
<p className="text-xs text-gray-600 mt-0.5 max-w-xl">
No es otra cuenta: usa la conexión del panel 1. Aquí solo
emparejas propiedades/sites a cada cliente y actualizas datos.
Luego se mantienen al día solos.
Al conectar Google (panel 1) se rellenan solos Drive, GA4, GSC y
Google Ads en cada ficha. Aquí puedes repetir el emparejado y
actualizar Orgánico.
</p>
</div>
</div>
@@ -141,7 +137,7 @@ export default function GoogleOrganicPanel() {
<button
type="button"
disabled={!connected || busy !== null}
onClick={() => void discover(true)}
onClick={() => void bootstrapAll()}
className="inline-flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-lg border border-gray-200 text-gray-800 hover:bg-gray-50 disabled:opacity-40"
>
{busy === 'match' ? (
@@ -149,7 +145,7 @@ export default function GoogleOrganicPanel() {
) : (
<Search className="w-3.5 h-3.5" />
)}
Descubrir y emparejar
Emparejar todo Google
</button>
<button
type="button"