feat: menú Ads + Social; Drive por defecto en activos

Renombra Ad Tracker a Ads, añade módulo Social (perfiles RRSS)
y abre Biblioteca en pestaña Drive. Aclara histórico GSC en Orgánico.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 13:44:56 +02:00
parent 7ed6e641cb
commit ba05626fed
9 changed files with 280 additions and 10 deletions

View File

@@ -0,0 +1,169 @@
'use client';
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import {
Facebook,
Instagram,
Linkedin,
Loader2,
Share2,
ExternalLink,
} from 'lucide-react';
import PageHeader from '@/components/PageHeader';
type Integ = {
provider: string;
status: string;
external_account_id?: string | null;
label?: string | null;
last_sync_at?: string | null;
meta?: { public_url?: string; account_name?: string; warning?: string } | null;
};
const CHANNELS: {
id: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
}[] = [
{ id: 'instagram', label: 'Instagram', icon: Instagram },
{ id: 'facebook', label: 'Facebook', icon: Facebook },
{ id: 'linkedin', label: 'LinkedIn', icon: Linkedin },
{ id: 'tiktok', label: 'TikTok', icon: Share2 },
];
export default function SocialDashboard({
tenantLabel,
isAgency,
}: {
tenantLabel: string;
isAgency: boolean;
}) {
const [loading, setLoading] = useState(true);
const [integs, setIntegs] = useState<Integ[]>([]);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
(async () => {
setLoading(true);
const res = await fetch('/api/social');
const data = await res.json().catch(() => ({}));
if (cancelled) return;
if (!res.ok) setErr(data.error || 'No se pudo cargar Social');
else setIntegs(data.integrations || []);
setLoading(false);
})();
return () => {
cancelled = true;
};
}, []);
const by = new Map(integs.map((i) => [i.provider, i]));
return (
<div className="space-y-6">
<PageHeader
eyebrow={`Social · ${tenantLabel}`}
title="Redes sociales"
description="Perfiles orgánicos (FB, IG, LinkedIn, TikTok). Distinto de Ads pagados. Publicación y métricas live: siguiente fase."
/>
{err && (
<p className="text-xs text-red-700 bg-red-50 border border-red-200 rounded-lg px-3 py-2">
{err}
</p>
)}
{loading ? (
<p className="text-sm text-gray-500 flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" /> Cargando…
</p>
) : (
<ul className="grid gap-3 sm:grid-cols-2">
{CHANNELS.map((ch) => {
const row = by.get(ch.id);
const connected =
row?.status === 'connected' && Boolean(row.external_account_id);
const Icon = ch.icon;
const url =
row?.meta?.public_url ||
(row?.external_account_id?.startsWith('http')
? row.external_account_id
: null);
return (
<li
key={ch.id}
className="rounded-xl border border-gray-200 bg-white p-4 space-y-2"
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Icon className="w-4 h-4 text-gray-700" />
<p className="text-sm font-semibold text-gray-900">
{ch.label}
</p>
</div>
<span
className={`text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded ${
connected
? 'bg-emerald-50 text-emerald-700'
: 'bg-gray-100 text-gray-500'
}`}
>
{connected ? 'Conectado' : 'Sin enlazar'}
</span>
</div>
<p className="text-xs text-gray-600 truncate">
{row?.meta?.account_name ||
row?.label ||
row?.external_account_id ||
'—'}
</p>
{row?.meta?.warning && (
<p className="text-[11px] text-amber-800 bg-amber-50 border border-amber-200 rounded-lg px-2 py-1">
{row.meta.warning}
</p>
)}
{url && (
<a
href={url}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-[11px] text-brand-600 hover:underline"
>
Abrir perfil <ExternalLink className="w-3 h-3" />
</a>
)}
</li>
);
})}
</ul>
)}
<div className="rounded-xl border border-gray-200 bg-white p-4 space-y-2">
<p className="text-sm font-semibold text-gray-900">Próximo en Social</p>
<ul className="text-xs text-gray-600 space-y-1 list-disc pl-4">
<li>Métricas orgánicas (alcance, engagement) vía Graph API</li>
<li>Últimas publicaciones y rendimiento</li>
<li>Puente con Calendario editorial</li>
</ul>
<div className="flex flex-wrap gap-2 pt-1">
<Link
href="/dashboard/calendario"
className="text-xs font-semibold px-3 py-1.5 rounded-lg border border-gray-200 text-gray-800 hover:bg-gray-50"
>
Calendario editorial
</Link>
{isAgency && (
<Link
href="/dashboard/agencia"
className="text-xs font-semibold px-3 py-1.5 rounded-lg border border-gray-200 text-gray-800 hover:bg-gray-50"
>
Emparejar en Agencia / ficha
</Link>
)}
</div>
</div>
</div>
);
}