'use client'; /** * Seguimiento = notas + histórico (Superhuman + manual). * Ads/KPIs viven en Métricas. */ import React, { useEffect, useMemo, useState } from 'react'; import { ExternalLink, Loader2, MessageSquareText, Radio, Send, } from 'lucide-react'; import { PageHeader } from '@/components/ui'; import NoteRichEditor, { richTextHasContent } from '@/components/NoteRichEditor'; type Entry = { id: string; entry_type: string; title: string; summary?: string | null; body_md?: string | null; browser_link?: string | null; occurred_at?: string | null; source?: string; }; type Payload = { label: string; portfolioWide?: boolean; tenantId?: string; hint?: string; entries: Entry[]; }; 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-50 text-amber-800 border-amber-200'; if (t === 'kpis') return 'bg-sky-50 text-sky-800 border-sky-200'; if (t === 'topic') return 'bg-emerald-50 text-emerald-800 border-emerald-200'; return 'bg-violet-50 text-violet-800 border-violet-200'; } function looksLikeHtml(s: string) { return /<\/?(?:ul|ol|li|p|a|strong|b|em|i|u|br|span|font|div|h[1-6])\b/i.test( s ); } function escapeHtml(s: string) { return s .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } export default function SeguimientoBoard() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [query, setQuery] = useState(''); const [openId, setOpenId] = useState(null); const [noteTitle, setNoteTitle] = useState(''); const [noteBody, setNoteBody] = useState(''); const [noteBusy, setNoteBusy] = useState(false); const [noteMsg, setNoteMsg] = useState(null); const [syncBusy, setSyncBusy] = useState(false); async function load() { setLoading(true); setErr(null); try { const res = await fetch('/api/seguimiento', { cache: 'no-store' }); const json = (await res.json()) as Payload & { error?: string }; if (!res.ok) throw new Error(json.error || 'Error'); setData(json); } catch (e) { setErr(e instanceof Error ? e.message : 'Error'); } finally { setLoading(false); } } useEffect(() => { void load(); }, []); const filteredEntries = useMemo(() => { const entries = data?.entries || []; const q = query.trim().toLowerCase(); // Histórico completo (más reciente → más antiguo). El periodo del header // no oculta notas: el doc Superhuman/Drive acumula todos los días. return entries.filter((e) => { if (!q) return true; const blob = `${e.title} ${e.summary || ''} ${e.body_md || ''}`.toLowerCase(); return blob.includes(q); }); }, [data, query]); async function saveNote() { if (!richTextHasContent(noteBody)) return; setNoteBusy(true); setNoteMsg(null); try { const res = await fetch('/api/seguimiento', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: noteTitle.trim() || 'Nota de seguimiento', body: noteBody, occurredAt: new Date().toISOString(), }), }); const json = await res.json(); if (!res.ok) throw new Error(json.error || 'No se pudo guardar'); setNoteTitle(''); setNoteBody(''); setNoteMsg('Nota guardada'); await load(); } catch (e) { setNoteMsg(e instanceof Error ? e.message : 'Error'); } finally { setNoteBusy(false); } } async function syncSuperhuman() { if (!data?.tenantId) return; setSyncBusy(true); setNoteMsg(null); try { const res = await fetch('/api/admin/superhuman/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenantId: data.tenantId, exportBodies: true, maxEntries: 120, }), }); const json = await res.json().catch(() => ({})); if (!res.ok) throw new Error(json.error || 'Sync falló'); const first = Array.isArray(json.results) ? json.results[0] : null; const n = typeof first?.upserted === 'number' ? first.upserted : typeof json.upserted === 'number' ? json.upserted : null; setNoteMsg( n != null ? `Superhuman: ${n} entradas sincronizadas` : first?.status === 'unmatched' ? 'Sin doc Superhuman emparejado por nombre' : 'Superhuman sincronizado' ); await load(); } catch (e) { setNoteMsg(e instanceof Error ? e.message : 'Error sync'); } finally { setSyncBusy(false); } } if (loading && !data) { return (

Cargando seguimiento…

); } if (err) { return (

{err}

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

{data.hint || 'Elige un cliente en Contexto para ver su seguimiento.'}

); } return (
Nueva nota

Formato enriquecido (negrita, color, resaltado, listas, enlaces). El histórico también incluye entradas de Superhuman / Google Doc.

setNoteTitle(e.target.value)} placeholder="Título (opcional)" className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-900" />
{noteMsg ? (

{noteMsg}

) : null}
Histórico ({filteredEntries.length})
setQuery(e.target.value)} placeholder="Filtrar…" className="w-full sm:w-64 rounded-lg border border-gray-200 px-3 py-1.5 text-sm" />
{filteredEntries.length === 0 ? (
Sin notas en este periodo. Añade una arriba o sincroniza Superhuman desde Agencia (docs «seguimiento»).
) : (
    {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. ); })}
)}
); }