'use client'; import React, { useEffect, useMemo, useState } from 'react'; import { ExternalLink, LineChart as LineIcon, Loader2, MessageSquareText, Radio, } from 'lucide-react'; import { ChannelWeekPoint, formatEur, formatNum, } from '@/lib/metrics'; type Entry = { id: string; entry_type: string; title: string; summary?: string | null; body_md?: string | null; browser_link?: string | null; metrics?: { ventas?: number; cpl?: number; cac?: number; periodo?: string; }; occurred_at?: string | null; }; type Payload = { label: string; portfolioWide?: boolean; hint?: string; entries: Entry[]; channelSeries: { weeks: string[]; channels: string[]; points: ChannelWeekPoint[]; }; dailySummary?: { days: number; spend: number; leads: number }; }; type MetricKey = 'leads' | 'spend' | 'cpl'; const CHANNEL_COLORS = [ '#E58A3C', '#38BDF8', '#34D399', '#A78BFA', '#F472B6', '#FBBF24', '#94A3B8', ]; function typeLabel(t: string) { if (t === 'meeting') return 'Reunión'; if (t === 'kpis') return 'KPIs'; if (t === 'topic') return 'Tema'; return 'Nota'; } function typeTone(t: string) { if (t === 'meeting') return 'bg-amber-500/15 text-amber-300 border-amber-500/30'; if (t === 'kpis') return 'bg-sky-500/15 text-sky-300 border-sky-500/30'; if (t === 'topic') return 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30'; return 'bg-gray-500/15 text-gray-300 border-gray-500/30'; } function ChannelEvolutionChart({ weeks, channels, points, metric, activeChannels, }: { weeks: string[]; channels: string[]; points: ChannelWeekPoint[]; metric: MetricKey; activeChannels: Set; }) { const [hover, setHover] = useState<{ week: string; channel: string; value: number; x: number; y: number; } | null>(null); const width = 720; const height = 260; const pad = { t: 16, r: 16, b: 36, l: 44 }; const innerW = width - pad.l - pad.r; const innerH = height - pad.t - pad.b; const series = useMemo(() => { return channels .filter((c) => activeChannels.has(c)) .map((channel, i) => { const vals = weeks.map((week) => { const p = points.find((x) => x.week === week && x.channel === channel); return p ? Number(p[metric] || 0) : 0; }); return { channel, color: CHANNEL_COLORS[i % CHANNEL_COLORS.length], vals, }; }); }, [channels, activeChannels, weeks, points, metric]); const maxY = Math.max(1, ...series.flatMap((s) => s.vals)); const xAt = (i: number) => pad.l + (weeks.length <= 1 ? innerW / 2 : (i / (weeks.length - 1)) * innerW); const yAt = (v: number) => pad.t + innerH - (v / maxY) * innerH; if (!weeks.length || !series.length) { return (
Aún no hay series de canal en kpi_weekly para este cliente.
); } return (
{[0, 0.25, 0.5, 0.75, 1].map((t) => { const y = pad.t + innerH * (1 - t); const val = maxY * t; return ( {metric === 'spend' || metric === 'cpl' ? formatNum(val) : formatNum(val)} ); })} {series.map((s) => { const d = s.vals .map((v, i) => `${i === 0 ? 'M' : 'L'} ${xAt(i)} ${yAt(v)}`) .join(' '); return ( {s.vals.map((v, i) => ( setHover({ week: weeks[i], channel: s.channel, value: v, x: xAt(i), y: yAt(v), }) } onMouseLeave={() => setHover(null)} /> ))} ); })} {weeks.map((w, i) => ( {w.slice(5)} ))} {hover && (

{hover.channel}

{hover.week}

{metric === 'spend' ? formatEur(hover.value) : metric === 'cpl' ? `${formatNum(hover.value)} € CPL` : `${formatNum(hover.value)} leads`}

)}
); } export default function SeguimientoBoard() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [metric, setMetric] = useState('leads'); const [activeChannels, setActiveChannels] = useState>(new Set()); const [openId, setOpenId] = useState(null); const [query, setQuery] = useState(''); useEffect(() => { let cancelled = false; (async () => { setLoading(true); setError(null); try { const r = await fetch('/api/seguimiento', { cache: 'no-store' }); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'Error cargando seguimiento'); if (!cancelled) { setData(j); setActiveChannels(new Set(j.channelSeries?.channels || [])); } } catch (e) { if (!cancelled) { setError(e instanceof Error ? e.message : 'Error'); } } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, []); const filteredEntries = useMemo(() => { const list = data?.entries || []; const q = query.trim().toLowerCase(); if (!q) return list; return list.filter( (e) => e.title.toLowerCase().includes(q) || (e.summary || '').toLowerCase().includes(q) || (e.body_md || '').toLowerCase().includes(q) ); }, [data, query]); function toggleChannel(ch: string) { setActiveChannels((prev) => { const next = new Set(prev); if (next.has(ch)) next.delete(ch); else next.add(ch); return next; }); } if (loading) { return (
Cargando seguimiento…
); } if (error) { return (

{error}

); } if (!data) return null; if (data.portfolioWide) { return (

Seguimiento por cliente

{data.hint || 'Selecciona un cliente en el encabezado para ver la evolución reunión a reunión y por canales.'}

); } const { channelSeries, dailySummary } = data; return (

Seguimiento & evolución

{data.label} · timeline Superhuman + series de canal (kpi_weekly)

{dailySummary && dailySummary.days > 0 && (
Ads recientes · {formatEur(dailySummary.spend)} ·{' '} {formatNum(dailySummary.leads)} leads · {dailySummary.days} días
)}
{/* Channel evolution */}
Evolución por canal
{( [ ['leads', 'Leads'], ['spend', 'Inversión'], ['cpl', 'CPL'], ] as [MetricKey, string][] ).map(([k, label]) => ( ))}
{channelSeries.channels.map((ch, i) => { const on = activeChannels.has(ch); const color = CHANNEL_COLORS[i % CHANNEL_COLORS.length]; return ( ); })}
{/* Timeline */}
Timeline reunión a reunión ({filteredEntries.length})
setQuery(e.target.value)} placeholder="Filtrar temas, CPL, Meta…" className="w-full sm:w-64 rounded-lg bg-black/30 border border-gray-800 px-3 py-1.5 text-sm text-white placeholder:text-gray-600 focus:outline-none focus:border-brand-500/50" />
{filteredEntries.length === 0 ? (
Sin entradas aún. Desde Agencia → sincroniza Superhuman Docs para este cliente (páginas de Seguimiento; nunca contraseñas).
) : (
    {filteredEntries.map((e) => { const open = openId === e.id; const when = e.occurred_at ? new Date(e.occurred_at).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric', }) : '—'; return (
  1. ); })}
)}
); }