feat(CC-086): add Professional Network page (contacts with pro tags, search, tag filter, stats)

This commit is contained in:
admin-valentin 2026-08-02 17:38:05 +00:00
parent ad43e8d831
commit 21c5aa05ec

View file

@ -0,0 +1,155 @@
'use client';
import { useMemo, useState } 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; role: string | null; organization: string | null; tags: string[]; email: string | null; linkedinUrl: string | null; country: string | null; createdAt: string; }
const PRO_TAGS = ['mentor', 'investor', 'investitor', 'partner', 'partener', 'advisor', 'board', 'client', 'collaborator', 'coleg', 'colleague', 'recruiter', 'founder', 'ceo', 'cto'];
const INDUSTRIES = ['Tech', 'Finance', 'Legal', 'Healthcare', 'Education', 'Consulting', 'AI/ML', 'SaaS', 'Altele'];
export default function ProfessionalNetworkPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const [search, setSearch] = useState('');
const [tagFilter, setTagFilter] = useState('');
const { data: contacts = [], isLoading } = useQuery({
queryKey: ['pro-network', tenantId],
queryFn: () => apiFetch<Contact[]>('/v1/contacts?limit=500', { tenantId }),
enabled: Boolean(tenantId), staleTime: 60_000,
});
const network = useMemo(() =>
contacts.filter((c) => c.tags.some((t) => PRO_TAGS.includes(t.toLowerCase()))),
[contacts]);
const tagCounts = useMemo(() => {
const counts: Record<string, number> = {};
for (const c of network) {
for (const t of c.tags) {
if (PRO_TAGS.includes(t.toLowerCase())) {
counts[t.toLowerCase()] = (counts[t.toLowerCase()] ?? 0) + 1;
}
}
}
return Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 8);
}, [network]);
const filtered = useMemo(() => {
let list = network;
if (tagFilter) list = list.filter((c) => c.tags.some((t) => t.toLowerCase() === tagFilter));
if (search.trim()) {
const q = search.toLowerCase();
list = list.filter((c) =>
`${c.firstName} ${c.lastName ?? ''} ${c.organization ?? ''} ${c.role ?? ''}`.toLowerCase().includes(q)
);
}
return list.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
}, [network, tagFilter, search]);
const stats = useMemo(() => {
const countries = new Set(network.map((c) => c.country).filter(Boolean)).size;
const orgs = new Set(network.map((c) => c.organization).filter(Boolean)).size;
const withLinkedIn = network.filter((c) => c.linkedinUrl).length;
return { countries, orgs, withLinkedIn };
}, [network]);
return (
<div className="max-w-4xl space-y-6 p-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Rețea Profesională</h1>
<p className="text-sm text-ink-faint mt-1">
{isLoading ? 'Se încarcă…' : `${network.length} contacte profesionale cheie din ${contacts.length} total`}
</p>
</div>
{/* Stats */}
<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">{network.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.orgs}</p>
<p className="text-[10px] text-ink-faint">organizații</p>
</div>
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-ink">{stats.countries}</p>
<p className="text-[10px] text-ink-faint">țări</p>
</div>
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-ink">{stats.withLinkedIn}</p>
<p className="text-[10px] text-ink-faint">LinkedIn</p>
</div>
</div>
{/* Tag breakdown */}
<div className="flex flex-wrap gap-2">
<button onClick={() => setTagFilter('')}
className={`rounded-full px-3 py-1 text-xs font-medium border transition-colors ${tagFilter === '' ? 'bg-primary text-white border-primary' : 'bg-background text-ink-faint border-border hover:border-primary/40'}`}>
Toate ({network.length})
</button>
{tagCounts.map(([tag, count]) => (
<button key={tag} onClick={() => setTagFilter(tagFilter === tag ? '' : tag)}
className={`rounded-full px-3 py-1 text-xs font-medium border capitalize transition-colors ${tagFilter === tag ? 'bg-primary text-white border-primary' : 'bg-background text-ink-faint border-border hover:border-primary/40'}`}>
{tag} ({count})
</button>
))}
</div>
{/* Search */}
<input value={search} onChange={(e) => setSearch(e.target.value)}
placeholder="Caută după nume, companie, rol…"
className="w-full rounded-lg border bg-background px-4 py-2.5 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
{isLoading ? (
<div className="text-center text-sm text-ink-faint py-8">Se încarcă</div>
) : filtered.length === 0 ? (
<div className="card p-8 text-center space-y-2">
<p className="text-2xl">💼</p>
<p className="text-sm text-ink-faint">
{network.length === 0
? 'Niciun contact profesional. Adaugă contacte cu tag mentor/investor/partner din Contacte.'
: 'Niciun rezultat pentru filtrul curent.'}
</p>
{network.length === 0 && (
<Link href="/dashboard/relationships/contacts" className="text-sm text-primary hover:underline">Gestionează contacte </Link>
)}
</div>
) : (
<div className="grid gap-3 sm:grid-cols-2">
{filtered.map((c) => {
const proTags = c.tags.filter((t) => PRO_TAGS.includes(t.toLowerCase()));
return (
<div key={c.id} className="card p-4 space-y-2">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className="text-sm font-semibold text-ink truncate">{c.firstName} {c.lastName ?? ''}</p>
{c.role && <p className="text-[10px] text-ink-faint truncate">{c.role}</p>}
{c.organization && <p className="text-[10px] text-primary truncate">{c.organization}</p>}
</div>
<div className="flex flex-wrap gap-1 shrink-0 justify-end">
{proTags.slice(0, 2).map((t) => (
<span key={t} className="rounded-full bg-primary/10 px-1.5 py-0.5 text-[8px] font-bold text-primary capitalize">{t}</span>
))}
</div>
</div>
<div className="flex items-center gap-3 text-[10px] text-ink-faint">
{c.email && <a href={`mailto:${c.email}`} className="text-primary hover:underline truncate">{c.email}</a>}
{c.linkedinUrl && <a href={c.linkedinUrl} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline shrink-0">LinkedIn </a>}
{c.country && !c.email && !c.linkedinUrl && <span>{c.country}</span>}
</div>
</div>
);
})}
</div>
)}
<Link href="/dashboard/relationships" className="text-xs text-primary hover:underline"> Toate relațiile</Link>
</div>
);
}