'use client'; import React, { useEffect, useState } from 'react'; import Link from 'next/link'; import { ArrowLeft, Loader2, Power } from 'lucide-react'; type Integ = { id?: string; provider: string; status: string; label?: string | null; external_account_id?: string | null; error_message?: string | null; last_sync_at?: string | null; enabled?: boolean; meta?: { folder_name?: string; match_score?: number } | null; }; type Provider = { id: string; label: string; group: string; live?: boolean }; const PLACEHOLDERS: Record = { meta_ads: 'act_… (Ad Account ID)', google_ads: 'Customer ID MCC / cuenta', ga4: 'G-… o Property ID', search_console: 'sc-domain:ejemplo.com', tag_manager: 'GTM-…', clientify: 'ID / workspace Clientify', klaviyo: 'Account / list ID', mailchimp: 'Audience ID', 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', instagram: '@handle o Instagram Business Account ID', facebook: 'Page ID / facebook.com/…', tiktok: '@handle o Advertiser / Business ID', linkedin: 'Company Page ID / linkedin.com/company/…', }; const LIVE_FROM_FICHA = new Set(['ga4', 'search_console', 'google_drive']); export default function ClientIntegrationsPanel({ tenantId, tenantName, embedded = false, onChanged, }: { tenantId: string; tenantName: string; /** Hide page chrome when rendered inside ClientFicha. */ embedded?: boolean; onChanged?: () => void; }) { const [integrations, setIntegrations] = useState([]); const [providers, setProviders] = useState([]); const [warning, setWarning] = useState(null); const [flash, setFlash] = useState(null); const [saving, setSaving] = useState(null); const [accountDraft, setAccountDraft] = useState>({}); const [followed, setFollowed] = useState(true); const [followBusy, setFollowBusy] = useState(false); async function load() { const res = await fetch( `/api/admin/integrations?tenantId=${encodeURIComponent(tenantId)}` ); const data = await res.json(); setIntegrations(data.integrations || []); setProviders(data.providers || []); setWarning(data.warning || null); const drafts: Record = {}; for (const i of data.integrations || []) { drafts[i.provider] = i.external_account_id || ''; } setAccountDraft(drafts); } useEffect(() => { void load(); fetch('/api/admin/portfolio') .then((r) => r.json()) .then((d) => { const row = (d.portfolio || []).find( (t: { id: string }) => t.id === tenantId ); if (row) setFollowed(row.agency_followed !== false); }) .catch(() => {}); }, [tenantId]); async function patch(provider: string, body: Record) { setSaving(provider); 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); } async function toggleFollow() { setFollowBusy(true); const next = !followed; await fetch('/api/admin/tenants', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenantId, followed: next }), }); setFollowed(next); setFollowBusy(false); } const byProvider = new Map(integrations.map((i) => [i.provider, i])); const groups = [ { id: 'paid', title: 'Paid media' }, { id: 'social', title: 'Redes sociales (orgánico)' }, { id: 'analytics', title: 'Analítica y medición' }, { id: 'crm', title: 'CRM' }, { id: 'email', title: 'Email / lifecycle' }, { id: 'cms', title: 'Web / e‑commerce' }, { id: 'files', title: 'Archivos' }, { id: 'ops', title: 'Operaciones' }, ]; return (
{!embedded ? (

{tenantName}

Hub de conexiones · asocia cuentas y decide qué entra a la plataforma

) : null} {warning && (

{warning}

)} {flash && (

{flash}

)}

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.

{groups.map((g) => { const list = providers.filter((p) => p.group === g.id); if (!list.length) return null; return (

{g.title}

    {list.map((p) => { const row = byProvider.get(p.id); const status = row?.status || 'disconnected'; const enabled = row?.enabled !== false; const folderHint = p.id === 'google_drive' ? row?.meta?.folder_name || row?.label : null; return (
  • {p.label} {p.live === false && ( (sin conector API aún) )}

    {folderHint && (

    Carpeta: {folderHint}

    )} {row?.last_sync_at ? (

    Último sync real{' '} {new Date(row.last_sync_at).toLocaleString('es-ES')}

    ) : status === 'connected' ? (

    Referencia guardada (aún sin sync de datos)

    ) : null} {row?.error_message && (

    {row.error_message}

    )}
    setAccountDraft((d) => ({ ...d, [p.id]: e.target.value, })) } />
    {p.id === 'google_drive' && (

    Pega la URL completa o solo el id ( …/folders/ID). Da igual: se normaliza y se comprueba acceso con marketing@.

    )} {(p.id === 'ga4' || p.id === 'search_console') && (

    Al conectar se verifica que marketing@ vea la propiedad / site y se cargan datos en Orgánico.

    )} {(p.id === 'wordpress' || p.id === 'prestashop' || p.id === 'shopify') && (

    Guarda la URL del site (y key/token si aplica) en el campo. Sync automático de pedidos/contenidos: siguiente fase.

    )} {(p.id === 'instagram' || p.id === 'facebook' || p.id === 'tiktok' || p.id === 'linkedin') && (

    Orgánico (páginas/perfiles). Distinto de Meta Ads / TikTok Ads. OAuth live y publicación: siguiente fase.

    )}
  • ); })}
); })}
); }