feat(CC-085): add Trust Social page (network composition + 12-month growth chart)
This commit is contained in:
parent
25bcc0ebd8
commit
827eb3bb30
1 changed files with 117 additions and 0 deletions
117
src/app/dashboard/trust/social/page.tsx
Normal file
117
src/app/dashboard/trust/social/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue