feat(CC-085): add Trust Social page (network composition + 12-month growth chart)

This commit is contained in:
admin-valentin 2026-08-02 17:34:53 +00:00
parent 25bcc0ebd8
commit 827eb3bb30

View file

@ -0,0 +1,117 @@
'use client';
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import Link from 'next/link';
import { apiFetch } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
interface Contact { id: string; firstName: string; lastName: string | null; tags: string[]; consentStatus: string | null; createdAt: string; }
interface Observation { id: string; metric: string; value: string; subjectType: string; confidence: number | null; createdAt: string; }
const NETWORK_TAGS = ['mentor', 'investor', 'partner', 'advisors', 'advisor', 'board', 'collaborator', 'client', 'client-key'];
const MONTHS_12 = Array.from({ length: 12 }, (_, i) => {
const d = new Date();
d.setMonth(d.getMonth() - (11 - i));
return d.toISOString().slice(0, 7);
});
export default function TrustSocialPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: contacts = [] } = useQuery({ queryKey: ['ts-contacts', tenantId], queryFn: () => apiFetch<Contact[]>('/v1/contacts?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: observations = [] } = useQuery({ queryKey: ['ts-obs', tenantId], queryFn: () => apiFetch<Observation[]>('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const stats = useMemo(() => {
const networkContacts = contacts.filter((c) => c.tags.some((t) => NETWORK_TAGS.includes(t.toLowerCase())));
const consentGranted = contacts.filter((c) => c.consentStatus === 'granted');
const mentors = contacts.filter((c) => c.tags.some((t) => t.toLowerCase() === 'mentor'));
const investors = contacts.filter((c) => c.tags.some((t) => ['investor','investitor'].includes(t.toLowerCase())));
const partners = contacts.filter((c) => c.tags.some((t) => ['partner','partener'].includes(t.toLowerCase())));
const clients = contacts.filter((c) => c.tags.some((t) => t.toLowerCase().startsWith('client')));
const socialObs = observations.filter((o) => o.subjectType === 'relationship' || o.metric.toLowerCase().includes('relat') || o.metric.toLowerCase().includes('network'));
const growthByMonth = MONTHS_12.map((m) => ({
month: m,
count: contacts.filter((c) => c.createdAt.startsWith(m)).length,
}));
const maxCount = Math.max(...growthByMonth.map((g) => g.count), 1);
return { networkContacts, consentGranted, mentors, investors, partners, clients, socialObs, growthByMonth, maxCount };
}, [contacts, observations]);
return (
<div className="max-w-4xl space-y-6 p-6">
<div>
<nav className="text-xs text-ink-faint mb-1">
<Link href="/dashboard/trust" className="hover:underline">Trust Dashboard</Link> / Reputație Socială
</nav>
<h1 className="font-display text-2xl font-semibold text-ink">Reputație Socială & Rețea</h1>
<p className="text-sm text-ink-faint mt-1">Calitatea și diversitatea rețelei de relații profesionale.</p>
</div>
{/* KPIs */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-ink">{contacts.length}</p>
<p className="text-[10px] text-ink-faint">contacte totale</p>
</div>
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-ink">{stats.networkContacts.length}</p>
<p className="text-[10px] text-ink-faint">rețea profesională</p>
</div>
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-ink">{stats.consentGranted.length}</p>
<p className="text-[10px] text-ink-faint">consimțite GDPR</p>
</div>
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-ink">{stats.socialObs.length}</p>
<p className="text-[10px] text-ink-faint">obs. relații</p>
</div>
</div>
{/* Network breakdown */}
<div className="card p-5 space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Compoziția rețelei</p>
{[
{ label: 'Mentori', count: stats.mentors.length, icon: '🧭' },
{ label: 'Investitori', count: stats.investors.length, icon: '💰' },
{ label: 'Parteneri', count: stats.partners.length, icon: '🤝' },
{ label: 'Clienți', count: stats.clients.length, icon: '🎯' },
].map((cat) => (
<div key={cat.label} className="flex items-center gap-3">
<span className="text-xl shrink-0">{cat.icon}</span>
<div className="flex-1">
<div className="flex items-center justify-between mb-0.5">
<p className="text-sm text-ink">{cat.label}</p>
<span className="text-sm font-bold text-ink">{cat.count}</span>
</div>
<div className="h-1.5 rounded-full bg-muted overflow-hidden">
<div className="h-full rounded-full bg-primary/50"
style={{ width: contacts.length > 0 ? `${(cat.count / contacts.length) * 100}%` : '0%' }} />
</div>
</div>
</div>
))}
</div>
{/* Growth chart */}
<div className="card p-5 space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Creștere rețea (12 luni)</p>
<div className="flex items-end gap-1 h-24">
{stats.growthByMonth.map((g) => (
<div key={g.month} className="flex-1 flex flex-col items-center gap-1">
<div className="w-full bg-primary/40 rounded-sm"
style={{ height: `${(g.count / stats.maxCount) * 80}px`, minHeight: g.count > 0 ? '4px' : '0' }} />
<p className="text-[8px] text-ink-faint rotate-45 origin-left">{g.month.slice(5)}</p>
</div>
))}
</div>
</div>
<Link href="/dashboard/relationships" className="text-xs text-primary hover:underline"> Toate relațiile</Link>
</div>
);
}