'use client'; import React, { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; import { Facebook, Instagram, Linkedin, Loader2, Share2, ExternalLink, Eye, Heart, MessageCircle, Repeat2, BarChart3, } from 'lucide-react'; import { PageHeader } from '@/components/ui'; import SyncNowButton from '@/components/SyncNowButton'; import { useDateRange } from '@/components/DateRangeContext'; import { HelpDetails, HeroBand, HeroStat, SectionHead, SparkBars, StatMini, } from '@/components/dash-ui'; type Integ = { provider: string; status: string; external_account_id?: string | null; label?: string | null; meta?: { public_url?: string; account_name?: string; warning?: string } | null; }; type DayPoint = { day: string; value: number }; type SeriesBag = Partial>; type MetricsFacebook = { name?: string; fan_count?: number; followers_count?: number; link?: string; error?: string; postsError?: string; insightsError?: string; pageInsights?: { impressions?: number | null; reach?: number | null; postEngagements?: number | null; fans?: number | null; pageViews?: number | null; follows?: number | null; reactions?: number | null; }; series?: SeriesBag; }; type MetricsIg = { username?: string; followers_count?: number; media_count?: number; follows_count?: number; error?: string; postsError?: string; insightsError?: string; igInsights?: { reach?: number | null; impressions?: number | null; profileViews?: number | null; websiteClicks?: number | null; accountsEngaged?: number | null; followerDelta?: number | null; interactions?: number | null; }; series?: SeriesBag; }; type SocialPost = { id: string; platform: 'facebook' | 'instagram'; message?: string; mediaUrl?: string | null; thumbnailUrl?: string | null; permalink?: string | null; createdAt?: string | null; likes: number; comments: number; shares: number; reach?: number | null; impressions?: number | null; mediaType?: string | null; mediaProductType?: string | null; children?: { id?: string; mediaType?: string | null; mediaUrl?: string | null; thumbnailUrl?: string | null; }[]; }; function formatLabel(p: SocialPost): string { const t = (p.mediaType || '').toUpperCase(); const prod = (p.mediaProductType || '').toUpperCase(); if (prod === 'REELS' || t === 'REELS' || t === 'REEL') return 'Reel'; if (t.includes('CAROUSEL') || t.includes('ALBUM')) return 'Carrusel'; if (t.includes('VIDEO')) return 'Vídeo'; if (t.includes('IMAGE') || t === 'PHOTO') return 'Imagen'; if (t === 'STORY' || prod === 'STORY') return 'Story'; return t || 'Post'; } function isVideoSlide(mediaType?: string | null, url?: string | null): boolean { const t = (mediaType || '').toUpperCase(); if (t.includes('VIDEO') || t === 'REELS' || t === 'REEL') return true; return /\.mp4($|\?)/i.test(url || '') || /video/i.test(url || ''); } function proxiedSocialUrl(raw?: string | null): string { if (!raw) return ''; if (raw.startsWith('/api/')) return raw; return `/api/ad-tracker/media?url=${encodeURIComponent(raw)}`; } function PostMedia({ post }: { post: SocialPost }) { const slides = post.children && post.children.length > 0 ? post.children : [ { id: post.id, mediaType: post.mediaType, mediaUrl: post.mediaUrl, thumbnailUrl: post.thumbnailUrl, }, ]; const [idx, setIdx] = useState(0); const [dims, setDims] = useState<{ w: number; h: number } | null>(null); const slide = slides[Math.min(idx, slides.length - 1)] || slides[0]; const video = isVideoSlide(slide?.mediaType, slide?.mediaUrl); const src = proxiedSocialUrl(slide?.mediaUrl || slide?.thumbnailUrl); const poster = proxiedSocialUrl(slide?.thumbnailUrl); const aspect = dims && dims.w > 0 && dims.h > 0 ? `${dims.w} / ${dims.h}` : video ? '9 / 16' : '1 / 1'; return (
{src && video ? (
{dims ? (

Tamaño original: {dims.w} × {dims.h}px {video ? ' (vídeo)' : slides.length > 1 ? ` · slide ${idx + 1}` : ''}

) : null}
); } const CHANNELS: { id: string; label: string; icon: React.ComponentType<{ className?: string }>; /** Live Graph analytics in this panel (Meta only today). */ metrics: boolean; }[] = [ { id: 'instagram', label: 'Instagram', icon: Instagram, metrics: true }, { id: 'facebook', label: 'Facebook', icon: Facebook, metrics: true }, { id: 'linkedin', label: 'LinkedIn', icon: Linkedin, metrics: false }, { id: 'tiktok', label: 'TikTok', icon: Share2, metrics: false }, ]; function n(v?: number | null) { if (v == null || !Number.isFinite(v)) return '—'; return new Intl.NumberFormat('es-ES').format(v); } function fmtDate(iso?: string | null) { if (!iso) return '—'; try { return new Date(iso).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric', }); } catch { return iso.slice(0, 10); } } function seriesDelta(series?: DayPoint[]): { abs: number; pct: number | null; up: boolean; } | null { if (!series || series.length < 2) return null; const first = series[0].value; const last = series[series.length - 1].value; const abs = last - first; const pct = first !== 0 ? (100 * abs) / Math.abs(first) : null; return { abs, pct, up: abs >= 0 }; } function ChartCard({ title, hint, series, total, color, }: { title: string; hint?: string; series?: DayPoint[]; total?: number | null; color: string; }) { const delta = seriesDelta(series); return (

{title}

{hint ?

{hint}

: null}

{n(total)}

{delta ? (

{delta.up ? '↑' : '↓'} {n(Math.abs(delta.abs))} {delta.pct != null ? ` (${delta.pct > 0 ? '+' : ''}${delta.pct.toFixed(0)}%)` : ''}

) : null}
({ date: p.day, value: p.value }))} color={color} formatValue={(v) => n(v)} empty="Sin serie en el periodo" />
); } export default function SocialDashboard({ tenantLabel, isAgency, }: { tenantLabel: string; isAgency: boolean; }) { const { since, until, label: rangeLabel } = useDateRange(); const [loading, setLoading] = useState(true); const [integs, setIntegs] = useState([]); const [metrics, setMetrics] = useState<{ facebook?: MetricsFacebook; instagram?: MetricsIg; warning?: string; }>({}); const [posts, setPosts] = useState([]); const [err, setErr] = useState(null); useEffect(() => { let cancelled = false; (async () => { setLoading(true); setErr(null); const qs = new URLSearchParams({ since, until }); const res = await fetch(`/api/social?${qs}`); const data = await res.json().catch(() => ({})); if (cancelled) return; if (!res.ok) setErr(data.error || 'No se pudo cargar Social'); else { setIntegs(data.integrations || []); setMetrics(data.metrics || {}); setPosts(data.posts || []); } setLoading(false); })(); return () => { cancelled = true; }; }, [since, until]); const by = useMemo(() => new Map(integs.map((i) => [i.provider, i])), [integs]); const ig = metrics.instagram; const fb = metrics.facebook; const visiblePosts = useMemo( () => posts.filter((p) => p.platform === 'instagram'), [posts] ); const kpiIg = [ { label: 'Publicaciones IG', value: n(ig?.media_count), hint: 'Stock del perfil', }, { label: 'Visualizaciones IG', value: n(ig?.igInsights?.impressions), hint: 'Periodo', }, { label: 'Visitas perfil', value: n(ig?.igInsights?.profileViews), hint: 'Instagram', }, { label: 'Clics web IG', value: n(ig?.igInsights?.websiteClicks), hint: 'Desde el perfil', }, { label: 'Δ seguidores', value: n(ig?.igInsights?.followerDelta), hint: 'Cambio en el periodo', }, ]; const kpiFb = [ { label: 'Visualizaciones FB', value: n(fb?.pageInsights?.impressions), hint: 'Periodo', }, { label: 'Engagement posts', value: n(fb?.pageInsights?.postEngagements), hint: 'Facebook', }, ]; const chartBlocks: { title: string; hint: string; series?: DayPoint[]; total?: number | null; color: string; }[] = [ { title: 'IG · Alcance / día', hint: 'Serie diaria en el periodo', series: ig?.series?.reach, total: ig?.igInsights?.reach, color: 'bg-pink-400/80', }, { title: 'IG · Visualizaciones', hint: 'views (metric_type=total_value)', series: ig?.series?.impressions, total: ig?.igInsights?.impressions, color: 'bg-fuchsia-400/80', }, { title: 'IG · Visitas al perfil', hint: 'profile_views', series: ig?.series?.profileViews, total: ig?.igInsights?.profileViews, color: 'bg-rose-400/80', }, { title: 'IG · Clics web', hint: 'website_clicks', series: ig?.series?.websiteClicks, total: ig?.igInsights?.websiteClicks, color: 'bg-orange-400/80', }, { title: 'IG · Cuentas engaged', hint: 'accounts_engaged', series: ig?.series?.accountsEngaged, total: ig?.igInsights?.accountsEngaged, color: 'bg-amber-400/80', }, { title: 'IG · Δ seguidores / día', hint: 'follower_count (delta)', series: ig?.series?.followerDelta, total: ig?.igInsights?.followerDelta, color: 'bg-yellow-500/70', }, { title: 'FB · Alcance / día', hint: 'page_total_media_view_unique', series: fb?.series?.reach, total: fb?.pageInsights?.reach, color: 'bg-sky-400/80', }, { title: 'FB · Visualizaciones', hint: 'page_media_view', series: fb?.series?.impressions, total: fb?.pageInsights?.impressions, color: 'bg-blue-500/70', }, { title: 'FB · Engagement posts', hint: 'page_post_engagements', series: fb?.series?.postEngagements, total: fb?.pageInsights?.postEngagements, color: 'bg-indigo-400/80', }, { title: 'FB · Fans (stock)', hint: 'page_fans diario', series: fb?.series?.fans, total: fb?.pageInsights?.fans, color: 'bg-violet-400/80', }, { title: 'FB · Vistas página', hint: 'page_views_total', series: fb?.series?.pageViews, total: fb?.pageInsights?.pageViews, color: 'bg-cyan-400/80', }, { title: 'FB · Nuevos follows', hint: 'page_follows / daily_follows', series: fb?.series?.follows, total: fb?.pageInsights?.follows, color: 'bg-teal-400/80', }, ]; return (
} /> {err && (

{err}

)} {metrics.warning && (

{metrics.warning}

)} {loading ? (

Cargando analytics…

) : ( <>

Los totales de alcance e interacciones son del periodo del header. Seguidores y fans son stock actual del perfil.

En los gráficos, la flecha es el cambio del primer al último día (no es un MoM).

Instagram y Facebook sí tienen métricas (Graph API). LinkedIn y TikTok, si aparecen, son solo el perfil guardado en la ficha: aún no hay evolución de impresiones ni seguidores.

{kpiIg.map((k) => ( ))}
{kpiFb.map((k) => ( ))}
{(ig?.error || fb?.error || ig?.postsError || fb?.postsError || ig?.insightsError || fb?.insightsError) && (

{[ ig?.error && `IG: ${ig.error}`, fb?.error && `FB: ${fb.error}`, ig?.insightsError && `IG insights: ${ig.insightsError}`, fb?.insightsError && `FB insights: ${fb.insightsError}`, ig?.postsError && `IG posts: ${ig.postsError}`, fb?.postsError && `FB posts: ${fb.postsError}`, ] .filter(Boolean) .join(' · ')}

)}
} title="Evolución diaria" hint={`${rangeLabel} · barras = valor diario · flecha = primer→último día`} />
{chartBlocks.map((c) => ( ))}
} title="Publicaciones Instagram" hint="Feed, reels y carruseles. Sin posts de Facebook." /> {visiblePosts.length === 0 ? (

Sin publicaciones de Instagram en este periodo.

) : (
    {visiblePosts.map((p) => (
  • {fmtDate(p.createdAt)} {p.mediaProductType ? ` · ${p.mediaProductType}` : ''}

    {p.message ? (

    {p.message}

    ) : null}
    {n(p.likes)} {n(p.comments)} {n(p.shares)} {p.reach != null && ( {n(p.reach)} alc. )} {p.impressions != null && ( {n(p.impressions)} views )}
    {p.permalink && ( Ver en red )}
  • ))}
)}
    {CHANNELS.map((ch) => { const row = by.get(ch.id); const linked = Boolean(row?.external_account_id) && (row?.status === 'connected' || row?.status === 'pending'); const live = linked && ch.metrics; const Icon = ch.icon; const url = row?.meta?.public_url || (row?.external_account_id?.startsWith('http') ? row.external_account_id : null); const badge = !linked ? { text: 'Sin enlazar', cls: 'bg-gray-100 text-gray-500' } : live ? { text: 'Métricas en vivo', cls: 'bg-emerald-50 text-emerald-700' } : { text: 'Perfil guardado', cls: 'bg-amber-50 text-amber-800', }; return (
  • {ch.label}

    {badge.text}

    {row?.meta?.account_name || row?.label || row?.external_account_id || '—'}

    {linked && !ch.metrics ? (

    Hay un enlace de página en la ficha. No se leen impresiones, seguidores ni interacciones todavía.

    ) : null} {url && ( Abrir perfil )}
  • ); })}
)}
Calendario editorial {isAgency && ( Emparejar en Agencia )}
); }