From f05c880d7774a7c9aa4b3420836e87c98b219cd5 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:27:02 +0000 Subject: [PATCH] feat(CC-070): add Insights page (observations log + research briefs with tabs) --- .../dashboard/intelligence/insights/page.tsx | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 src/app/dashboard/intelligence/insights/page.tsx diff --git a/src/app/dashboard/intelligence/insights/page.tsx b/src/app/dashboard/intelligence/insights/page.tsx new file mode 100644 index 0000000..2687636 --- /dev/null +++ b/src/app/dashboard/intelligence/insights/page.tsx @@ -0,0 +1,216 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +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; +} + +const SUBJECT_TYPES = ['all','organization','goal','decision','contact','project','market','custom']; + +export default function InsightsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + + const [tab, setTab] = useState<'observations' | 'briefs'>('observations'); + const [subjectFilter, setSubjectFilter] = useState('all'); + const [showCreate, setShowCreate] = useState(false); + const [form, setForm] = useState({ subjectType: 'organization', subjectId: '', metric: '', value: '', unit: '', source: 'analyst', confidence: '0.8' }); + + const { data: observations = [], isLoading: obsLoading } = useQuery({ + queryKey: ['observations', tenantId, subjectFilter], + queryFn: () => { + const params = subjectFilter !== 'all' ? `?subjectType=${subjectFilter}` : ''; + return apiFetch(`/v1/observations${params}`, { tenantId }); + }, + enabled: Boolean(tenantId), staleTime: 30_000, + }); + + const { data: briefs = [], isLoading: briefsLoading } = useQuery({ + queryKey: ['briefs', tenantId], + queryFn: () => apiFetch('/v1/research-briefs?limit=100', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const createMut = useMutation({ + mutationFn: (body: Record) => + apiFetch('/v1/observations', { tenantId, method: 'POST', body }), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['observations', tenantId] }); setShowCreate(false); setForm({ subjectType: 'organization', subjectId: '', metric: '', value: '', unit: '', source: 'analyst', confidence: '0.8' }); }, + }); + + // Group observations by metric + const byMetric: Record = {}; + for (const o of observations) { + byMetric[o.metric] = [...(byMetric[o.metric] ?? []), o]; + } + + const topMetrics = Object.entries(byMetric) + .sort(([, a], [, b]) => b.length - a.length) + .slice(0, 6); + + return ( +
+
+
+

Insights

+

+ {observations.length} observații · {briefs.length} research briefs +

+
+ {tab === 'observations' && ( + + )} +
+ + {/* Tabs */} +
+ {[['observations', 'Observații'], ['briefs', 'Research Briefs']].map(([key, label]) => ( + + ))} +
+ + {tab === 'observations' && ( + <> + {/* Top metrics overview */} + {topMetrics.length > 0 && ( +
+ {topMetrics.map(([metric, items]) => ( + + ))} +
+ )} + + {/* Subject type filter */} +
+ {SUBJECT_TYPES.map((t) => ( + + ))} +
+ + {/* Create form */} + {showCreate && ( +
+

Observație nouă

+
+ + setForm({ ...form, subjectId: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm({ ...form, metric: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm({ ...form, value: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm({ ...form, unit: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ + setForm({ ...form, confidence: e.target.value })} + className="flex-1" /> + {Math.round(parseFloat(form.confidence)*100)}% +
+
+
+ + +
+
+ )} + + {/* Observations list */} + {obsLoading ? ( +
Se încarcă…
+ ) : observations.length === 0 ? ( +
+

📊

+

Nicio observație înregistrată.

+
+ ) : ( +
+ {observations.slice(0, 50).map((o) => ( +
+
+
+ {o.metric} + {o.value}{o.unit ? ` ${o.unit}` : ''} + {o.confidence != null && ( + {Math.round(o.confidence*100)}% conf + )} +
+

+ {o.subjectType}:{o.subjectId.slice(0, 12)}… · {o.source} +

+
+ + {new Date(o.observedAt ?? o.createdAt).toLocaleDateString('ro-RO')} + +
+ ))} + {observations.length > 50 && ( +

și alte {observations.length - 50} observații…

+ )} +
+ )} + + )} + + {tab === 'briefs' && ( + <> + {briefsLoading ? ( +
Se încarcă…
+ ) : briefs.length === 0 ? ( +
+

📋

+

Niciun research brief.

+
+ ) : ( +
+ {briefs.map((b) => ( +
+

{b.title}

+ {b.organizationName &&

🏢 {b.organizationName}

} + {b.summary &&

{b.summary}

} +

{new Date(b.createdAt).toLocaleDateString('ro-RO')}

+
+ ))} +
+ )} + + )} +
+ ); +}