'use client'; import React, { useEffect, useState } from 'react'; import { Leaf, Loader2, RefreshCw, Search, Sparkles, } from 'lucide-react'; type MatchRow = { tenantId: string; tenantName: string; tenantSlug: string; ga4?: { propertyId: string; measurementId: string | null; displayName: string; score: number; } | null; gsc?: { siteUrl: string; score: number } | null; }; export default function GoogleOrganicPanel() { const [loading, setLoading] = useState(true); const [connected, setConnected] = useState(false); const [email, setEmail] = useState(null); const [hasAnalytics, setHasAnalytics] = useState(false); const [hasGsc, setHasGsc] = useState(false); const [ga4Count, setGa4Count] = useState(0); const [gscCount, setGscCount] = useState(0); const [matches, setMatches] = useState(null); const [busy, setBusy] = useState(null); const [msg, setMsg] = useState(null); const [err, setErr] = useState(null); async function loadStatus() { setLoading(true); setErr(null); const res = await fetch('/api/admin/google/organic'); const data = await res.json(); setConnected(Boolean(data.connected)); setEmail(data.email || null); setHasAnalytics(Boolean(data.hasAnalytics)); setHasGsc(Boolean(data.hasSearchConsole)); setGa4Count((data.ga4 || []).length); setGscCount((data.gsc || []).length); if (data.warning) setMsg(data.warning); if (!res.ok && data.error) setErr(data.error); setLoading(false); } useEffect(() => { const params = new URLSearchParams(window.location.search); if (params.get('drive') === 'ok') { setMsg( 'Google reconectado. Pulsa «Descubrir y emparejar» para GA4/GSC.' ); } void loadStatus(); }, []); async function discover(apply: boolean) { 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 }), }); const data = await res.json(); if (!res.ok) { setBusy(null); setErr(data.error || 'Discover 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); } } async function syncAll() { setBusy('sync'); setErr(null); const res = await fetch('/api/admin/google/organic', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'sync', days: 28 }), }); const data = await res.json(); setBusy(null); if (!res.ok) { setErr(data.error || 'Actualización falló'); return; } const wrote = (data.results || []).reduce( (a: number, r: { daysWritten?: number }) => a + (r.daysWritten || 0), 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)` ); } if (loading) { return (

Google orgánico…

); } return (

2 · Orgánico (GA4 + Search Console)

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.

{connected ? (

Misma conexión{email ? ` · ${email}` : ''} {hasAnalytics ? ' · Analytics ✓' : ' · Analytics ✗'} {hasGsc ? ' · GSC ✓' : ' · GSC ✗'} {ga4Count || gscCount ? ` · vistas: ${ga4Count} props / ${gscCount} sites` : ''}

) : (

Primero pulsa «Conectar Google» en el panel 1 (arriba) y acepta Analytics + Search Console además de Drive.

)} {msg && (

{msg}

)} {err && (

{err}

)} {matches && matches.length > 0 && (
{matches.map((m) => ( ))}
Cliente GA4 GSC
{m.tenantName} {m.ga4 ? ( <> {m.ga4.displayName} {' '} · {m.ga4.measurementId || m.ga4.propertyId} · score{' '} {m.ga4.score} ) : ( — )} {m.gsc ? ( <> {m.gsc.siteUrl} {' '} · score {m.gsc.score} ) : ( — )}
)}
); }