From 6bb902cb40d7d35a25b533e839b383dd43abf551 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:27:01 +0000 Subject: [PATCH] feat(CC-070): add People Intelligence page (contacts + observations + research briefs) --- .../dashboard/intelligence/people/page.tsx | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 src/app/dashboard/intelligence/people/page.tsx diff --git a/src/app/dashboard/intelligence/people/page.tsx b/src/app/dashboard/intelligence/people/page.tsx new file mode 100644 index 0000000..0b9141f --- /dev/null +++ b/src/app/dashboard/intelligence/people/page.tsx @@ -0,0 +1,209 @@ +'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 Contact { + id: string; fullName: string; email: string | null; phone: string | null; + role: string | null; tags: string[]; consentStatus: string; + notes: string | null; createdAt: string; +} +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; createdAt: string; +} + +const CONSENT_CLS: Record = { + granted: 'text-signal-ok', revoked: 'text-signal-danger', unknown: 'text-ink-faint', +}; + +export default function PeopleIntelligencePage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + + const [search, setSearch] = useState(''); + const [selected, setSelected] = useState(null); + const [obsForm, setObsForm] = useState({ metric: '', value: '', unit: '', source: 'manual' }); + + const { data: contacts = [], isLoading } = useQuery({ + queryKey: ['contacts-intel', tenantId], + queryFn: () => apiFetch('/v1/contacts?limit=200', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const { data: observations = [] } = useQuery({ + queryKey: ['obs-contact', tenantId, selected?.id], + queryFn: () => apiFetch(`/v1/observations?subjectType=contact&subjectId=${selected!.id}`, { tenantId }), + enabled: Boolean(tenantId) && Boolean(selected), + staleTime: 30_000, + }); + + const { data: briefs = [] } = useQuery({ + queryKey: ['briefs-intel', tenantId], + queryFn: () => apiFetch('/v1/research-briefs?limit=50', { tenantId }), + enabled: Boolean(tenantId), staleTime: 120_000, + }); + + const createObsMut = useMutation({ + mutationFn: (body: Record) => + apiFetch('/v1/observations', { tenantId, method: 'POST', body }), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['obs-contact'] }); setObsForm({ metric: '', value: '', unit: '', source: 'manual' }); }, + }); + + const filtered = contacts.filter((c) => + !search || c.fullName.toLowerCase().includes(search.toLowerCase()) + || c.email?.toLowerCase().includes(search.toLowerCase()) + || c.role?.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+
+

Intelligence Persoane

+

+ {isLoading ? 'Se încarcă…' : `${contacts.length} contacte · ${briefs.length} research briefs disponibile`} +

+
+ +
+ {/* Contact list */} +
+ setSearch(e.target.value)} + className="w-full rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + + {isLoading ? ( +
Se încarcă…
+ ) : filtered.length === 0 ? ( +
Niciun contact.
+ ) : ( +
+ {filtered.map((c) => ( + + ))} +
+ )} +
+ + {/* Contact detail + observations */} +
+ {!selected ? ( +
+

👤

+

Selectează un contact pentru a vedea intelligence-ul.

+
+ ) : ( + <> + {/* Contact header */} +
+
+
+

{selected.fullName}

+
+ {selected.role && {selected.role}} + {selected.email && {selected.email}} + {selected.phone && {selected.phone}} +
+
+ + {selected.consentStatus} + +
+ {selected.tags.length > 0 && ( +
+ {selected.tags.map((tag) => ( + {tag} + ))} +
+ )} + {selected.notes &&

{selected.notes}

} +
+ + {/* Linked research briefs */} + {briefs.length > 0 && ( +
+

Research briefs relevante

+ {briefs + .filter((b) => b.title.toLowerCase().includes(selected.fullName.toLowerCase().split(' ')[0]) || (b.organizationName ?? '').toLowerCase().includes(selected.fullName.toLowerCase().split(' ')[0])) + .slice(0, 3) + .map((b) => ( +
+

{b.title}

+ {b.summary &&

{b.summary}

} +
+ )) + } + {briefs.filter((b) => b.title.toLowerCase().includes(selected.fullName.toLowerCase().split(' ')[0])).length === 0 && ( +

Niciun brief direct.

+ )} +
+ )} + + {/* Observations */} +
+

Observații ({observations.length})

+ + {/* Add observation form */} +
+
+ setObsForm({ ...obsForm, metric: e.target.value })} + className="rounded-lg border bg-card px-2 py-1.5 text-xs text-ink focus:outline-none focus:ring-1 focus:ring-ring" /> + setObsForm({ ...obsForm, value: e.target.value })} + className="rounded-lg border bg-card px-2 py-1.5 text-xs text-ink focus:outline-none focus:ring-1 focus:ring-ring" /> + setObsForm({ ...obsForm, unit: e.target.value })} + className="rounded-lg border bg-card px-2 py-1.5 text-xs text-ink focus:outline-none focus:ring-1 focus:ring-ring" /> +
+ +
+ + {observations.length === 0 ? ( +

Nicio observație.

+ ) : ( +
+ {observations.map((o) => ( +
+
+ {o.metric}: + {o.value}{o.unit ? ` ${o.unit}` : ''} + {o.confidence != null && ( + {Math.round(o.confidence * 100)}% + )} +
+ + {new Date(o.observedAt ?? o.createdAt).toLocaleDateString('ro-RO')} + +
+ ))} +
+ )} +
+ + )} +
+
+
+ ); +}