feat: conectar GA4/GSC/Drive desde la ficha (verificar + sync real)

Al guardar ID o URL en el cliente se valida con OAuth de agencia;
Drive acepta URL o folder id; Orgánico se rellena si hay acceso.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 12:27:15 +02:00
parent 81ba5df37d
commit 99895fe918
3 changed files with 387 additions and 21 deletions

View File

@@ -27,7 +27,7 @@ const PLACEHOLDERS: Record<string, string> = {
clientify: 'ID / workspace Clientify',
klaviyo: 'Account / list ID',
mailchimp: 'Audience ID',
google_drive: 'Folder ID (auto-match o pegar)',
google_drive: 'URL de carpeta Drive o folder id',
wordpress: 'https://sitio.com + Application Password',
prestashop: 'https://tienda.com + API key',
shopify: 'mitienda.myshopify.com + Admin API token',
@@ -37,6 +37,8 @@ const PLACEHOLDERS: Record<string, string> = {
linkedin: 'Company Page ID / linkedin.com/company/…',
};
const LIVE_FROM_FICHA = new Set(['ga4', 'search_console', 'google_drive']);
export default function ClientIntegrationsPanel({
tenantId,
tenantName,
@@ -52,6 +54,7 @@ export default function ClientIntegrationsPanel({
const [integrations, setIntegrations] = useState<Integ[]>([]);
const [providers, setProviders] = useState<Provider[]>([]);
const [warning, setWarning] = useState<string | null>(null);
const [flash, setFlash] = useState<string | null>(null);
const [saving, setSaving] = useState<string | null>(null);
const [accountDraft, setAccountDraft] = useState<Record<string, string>>({});
const [followed, setFollowed] = useState(true);
@@ -87,11 +90,18 @@ export default function ClientIntegrationsPanel({
async function patch(provider: string, body: Record<string, unknown>) {
setSaving(provider);
await fetch('/api/admin/integrations', {
setFlash(null);
const res = await fetch('/api/admin/integrations', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tenantId, provider, ...body }),
});
const data = await res.json().catch(() => ({}));
if (data.activation?.message) setFlash(data.activation.message);
else if (data.error) setFlash(String(data.error));
else if (LIVE_FROM_FICHA.has(provider) && body.status === 'connected') {
setFlash(res.ok ? 'Conexión verificada' : 'No se pudo activar');
}
await load();
onChanged?.();
setSaving(null);
@@ -168,6 +178,16 @@ export default function ClientIntegrationsPanel({
{warning}
</p>
)}
{flash && (
<p className="text-xs text-gray-800 border border-gray-200 bg-gray-50 rounded-lg px-3 py-2">
{flash}
</p>
)}
<p className="text-[11px] text-gray-600">
GA4, Search Console y Drive: al guardar/conectar se verifica con el
Google de agencia (marketing@) y se sincronizan datos. URL de carpeta =
mismo que folder id. Primero conecta Google en Agencia si aún no.
</p>
{groups.map((g) => {
const list = providers.filter((p) => p.group === g.id);
@@ -275,8 +295,11 @@ export default function ClientIntegrationsPanel({
disabled={saving === p.id || !enabled}
onClick={() =>
void patch(p.id, {
status:
status === 'disconnected' ? 'pending' : status,
status: LIVE_FROM_FICHA.has(p.id)
? 'connected'
: status === 'disconnected'
? 'pending'
: status,
externalAccountId: accountDraft[p.id] || '',
})
}
@@ -284,6 +307,8 @@ export default function ClientIntegrationsPanel({
>
{saving === p.id ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : LIVE_FROM_FICHA.has(p.id) ? (
'Guardar y conectar'
) : (
'Guardar ID'
)}
@@ -291,9 +316,15 @@ export default function ClientIntegrationsPanel({
</div>
{p.id === 'google_drive' && (
<p className="text-[10px] text-gray-500">
Preferible: Agencia → Conectar Drive → Re-emparejar
(asigna carpeta por nombre). Aquí puedes pegar un Folder
ID manual.
Pega la URL completa o solo el id (
…/folders/<strong>ID</strong>). Da igual: se normaliza y
se comprueba acceso con marketing@.
</p>
)}
{(p.id === 'ga4' || p.id === 'search_console') && (
<p className="text-[10px] text-gray-500">
Al conectar se verifica que marketing@ vea la propiedad /
site y se cargan datos en Orgánico.
</p>
)}
{(p.id === 'wordpress' ||