'use client'; import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { apiFetch } from '../../../lib/api'; import { useSession } from '../../../components/session-provider'; interface Contact { id: string; fullName: string; role: string | null; } interface Observation { id: string; subjectType: string; subjectId: string; metric: string; value: string; unit: string | null; source: string; confidence: number | null; observedAt: string | null; createdAt: string; } interface ResearchBrief { id: string; title: string; organizationName: string | null; summary: string | null; briefType: string | null; createdAt: string; } interface Decision { id: string; title: string; status: string; decidedAt: string | null; createdAt: string; } interface OutcomeReview { id: string; title: string; wasSuccessful: boolean | null; rating: number | null; createdAt: string; decisionId: string | null; } type TimelineItem = { id: string; type: 'observation' | 'brief' | 'decision' | 'outcome'; date: Date; title: string; subtitle?: string; meta?: string; icon: string; }; const TYPE_CLS: Record = { observation: { bg: 'bg-blue-50 dark:bg-blue-900/20', text: 'text-blue-600 dark:text-blue-400', border: 'border-l-blue-400' }, brief: { bg: 'bg-violet-50 dark:bg-violet-900/20', text: 'text-violet-600 dark:text-violet-400', border: 'border-l-violet-400' }, decision: { bg: 'bg-amber-50 dark:bg-amber-900/20', text: 'text-amber-600 dark:text-amber-400', border: 'border-l-amber-400' }, outcome: { bg: 'bg-emerald-50 dark:bg-emerald-900/20', text: 'text-emerald-600 dark:text-emerald-400', border: 'border-l-emerald-400' }, }; function relTime(date: Date) { const days = Math.floor((Date.now() - date.getTime()) / 86400_000); if (days === 0) return 'azi'; if (days === 1) return 'ieri'; if (days < 7) return `${days}z în urmă`; if (days < 30) return `${Math.floor(days/7)}s în urmă`; if (days < 365) return `${Math.floor(days/30)}l în urmă`; return `${Math.floor(days/365)}a în urmă`; } export default function InteractionTimelinePage() { const { activeTenant } = useSession(); const tenantId = activeTenant?.tenantId ?? ''; const [filter, setFilter] = useState<'all' | 'observation' | 'brief' | 'decision' | 'outcome'>('all'); const [search, setSearch] = useState(''); const { data: contacts = [] } = useQuery({ queryKey: ['contacts-tl', tenantId], queryFn: () => apiFetch('/v1/contacts?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000, }); const { data: observations = [], isLoading: obsLoad } = useQuery({ queryKey: ['observations-tl', tenantId], queryFn: () => apiFetch('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 30_000, }); const { data: briefs = [], isLoading: briefLoad } = useQuery({ queryKey: ['briefs-tl', tenantId], queryFn: () => apiFetch('/v1/research-briefs?limit=100', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000, }); const { data: decisions = [], isLoading: decLoad } = useQuery({ queryKey: ['decisions-tl', tenantId], queryFn: () => apiFetch('/v1/decisions?limit=100', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000, }); const { data: outcomes = [], isLoading: outLoad } = useQuery({ queryKey: ['outcomes-tl', tenantId], queryFn: () => apiFetch('/v1/outcome-reviews?limit=100', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000, }); const contactMap = useMemo(() => { const m: Record = {}; for (const c of contacts) m[c.id] = c.fullName; return m; }, [contacts]); const items: TimelineItem[] = useMemo(() => { const all: TimelineItem[] = []; for (const o of observations) { const name = o.subjectType === 'contact' ? contactMap[o.subjectId] : o.subjectId.slice(0, 12) + '…'; all.push({ id: o.id, type: 'observation', date: new Date(o.observedAt ?? o.createdAt), title: `${o.metric}: ${o.value}${o.unit ? ` ${o.unit}` : ''}`, subtitle: name, meta: o.source, icon: '📊' }); } for (const b of briefs) { all.push({ id: b.id, type: 'brief', date: new Date(b.createdAt), title: b.title, subtitle: b.organizationName ?? undefined, meta: b.briefType ?? undefined, icon: '📋' }); } for (const d of decisions) { all.push({ id: d.id, type: 'decision', date: new Date(d.decidedAt ?? d.createdAt), title: d.title, meta: d.status, icon: '🧠' }); } for (const o of outcomes) { all.push({ id: o.id, type: 'outcome', date: new Date(o.createdAt), title: o.title, meta: o.wasSuccessful === true ? '✅ succes' : o.wasSuccessful === false ? '❌ eșec' : '—', icon: '🔍' }); } return all.sort((a, b) => b.date.getTime() - a.date.getTime()); }, [observations, briefs, decisions, outcomes, contactMap]); const filtered = items.filter((i) => { if (filter !== 'all' && i.type !== filter) return false; if (search && !i.title.toLowerCase().includes(search.toLowerCase()) && !(i.subtitle ?? '').toLowerCase().includes(search.toLowerCase())) return false; return true; }); const isLoading = obsLoad || briefLoad || decLoad || outLoad; const counts: Record = { all: items.length }; for (const i of items) counts[i.type] = (counts[i.type] ?? 0) + 1; return (

Timeline Interacțiuni

{isLoading ? 'Se încarcă…' : `${items.length} evenimente înregistrate`}

{/* Search */} setSearch(e.target.value)} className="w-full rounded-lg border bg-card px-4 py-2.5 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> {/* Filter chips */}
{(['all', 'observation', 'brief', 'decision', 'outcome'] as const).map((f) => ( ))}
{/* Timeline */} {isLoading ? (
Se încarcă…
) : filtered.length === 0 ? (

📅

Niciun eveniment{search ? ' pentru această căutare' : ''}.

) : (
{/* Vertical timeline line */}
{filtered.slice(0, 100).map((item) => { const cls = TYPE_CLS[item.type]; return (
{/* Dot */}
{item.icon}

{item.title}

{item.subtitle && (

{item.subtitle}

)}

{relTime(item.date)}

{item.meta && ( {item.meta} )}
); })} {filtered.length > 100 && (

și alte {filtered.length - 100} evenimente…

)}
)}
); }