'use client'; import React, { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; import { Facebook, Instagram, Linkedin, Loader2, Share2, ExternalLink, Users, Eye, Image as ImageIcon, Heart, MessageCircle, Repeat2, } from 'lucide-react'; import { PageHeader } from '@/components/ui'; import { useDateRange } from '@/components/DateRangeContext'; 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 }>; }[] = [ { id: 'instagram', label: 'Instagram', icon: Instagram }, { id: 'facebook', label: 'Facebook', icon: Facebook }, { id: 'linkedin', label: 'LinkedIn', icon: Linkedin }, { id: 'tiktok', label: 'TikTok', icon: Share2 }, ]; 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 shortDay(day: string) { const d = day.slice(5); // MM-DD return d.replace('-', '/'); } function MiniBars({ series, color = 'bg-brand-400/80', empty = 'Sin serie en el periodo', }: { series?: DayPoint[]; color?: string; empty?: string; }) { const pts = series || []; if (!pts.length) { return

{empty}

; } const max = Math.max(1, ...pts.map((p) => p.value)); return (
{pts.map((p) => { const pct = Math.max(6, Math.round((p.value / max) * 100)); return (
); })}
); } 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)}%)` : ''} inicio→fin

) : null}
{series && series.length > 1 ? (
{shortDay(series[0].day)} {shortDay(series[series.length - 1].day)}
) : null}
); } 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 kpiCards = [ { label: 'Seguidores IG (global)', value: n(ig?.followers_count), icon: Users, }, { label: 'Publicaciones IG (global)', value: n(ig?.media_count), icon: ImageIcon, }, { label: 'Alcance IG (periodo)', value: n(ig?.igInsights?.reach), icon: Eye, }, { label: 'Visualizaciones IG', value: n(ig?.igInsights?.impressions), icon: Eye, }, { label: 'Visitas perfil IG', value: n(ig?.igInsights?.profileViews), icon: Users, }, { label: 'Interacciones IG', value: n(ig?.igInsights?.interactions ?? ig?.igInsights?.accountsEngaged), icon: Heart, }, { label: 'Clics web IG', value: n(ig?.igInsights?.websiteClicks), icon: ExternalLink, }, { label: 'Δ seguidores IG', value: n(ig?.igInsights?.followerDelta), icon: Users, }, { label: 'Fans FB (global)', value: n(fb?.followers_count ?? fb?.fan_count ?? fb?.pageInsights?.fans), icon: Users, }, { label: 'Alcance FB (periodo)', value: n(fb?.pageInsights?.reach), icon: Eye, }, { label: 'Visualizaciones FB', value: n(fb?.pageInsights?.impressions), icon: Eye, }, { label: 'Engagement posts FB', value: n(fb?.pageInsights?.postEngagements), icon: Heart, }, ]; 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…

) : ( <>
{kpiCards.map((k) => (

{k.label}

{k.value}

))}
{(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(' · ')}

)}

Evolución diaria · {rangeLabel}

Barras = valor diario. Flecha = cambio del primer al último día del periodo. Totales arriba a la derecha.

{chartBlocks.map((c) => ( ))}

Publicaciones Instagram

Solo IG (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 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 (
  • {ch.label}

    {connected ? 'Conectado' : 'Sin enlazar'}

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

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