166 lines
7.2 KiB
TypeScript
166 lines
7.2 KiB
TypeScript
'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; fullName: string; email: string | null; phone: string | null; role: string | null; linkedinUrl: string | null; tags: string[]; }
|
|
interface Organization { id: string; name: string; industry: string | null; country: string | null; website: string | null; employeeCount: number | null; }
|
|
|
|
interface EnrichmentItem { id: string; name: string; type: 'contact' | 'organization'; missing: string[]; completeness: number; }
|
|
|
|
export default function EnrichmentPage() {
|
|
const { activeTenant } = useSession();
|
|
const tenantId = activeTenant?.tenantId ?? '';
|
|
|
|
const { data: contacts = [], isLoading: cL } = useQuery({
|
|
queryKey: ['enr-contacts', tenantId],
|
|
queryFn: () => apiFetch<Contact[]>('/v1/contacts?limit=500', { tenantId }),
|
|
enabled: Boolean(tenantId), staleTime: 120_000,
|
|
});
|
|
const { data: orgs = [], isLoading: oL } = useQuery({
|
|
queryKey: ['enr-orgs', tenantId],
|
|
queryFn: () => apiFetch<Organization[]>('/v1/organizations?limit=500', { tenantId }),
|
|
enabled: Boolean(tenantId), staleTime: 120_000,
|
|
});
|
|
|
|
const items = useMemo<EnrichmentItem[]>(() => {
|
|
const contactItems: EnrichmentItem[] = contacts.map((c) => {
|
|
const missing: string[] = [];
|
|
if (!c.email) missing.push('email');
|
|
if (!c.phone) missing.push('telefon');
|
|
if (!c.role) missing.push('funcție');
|
|
if (!c.linkedinUrl) missing.push('LinkedIn');
|
|
const completeness = Math.round(((4 - missing.length) / 4) * 100);
|
|
return { id: c.id, name: c.fullName, type: 'contact', missing, completeness };
|
|
}).filter((i) => i.completeness < 100);
|
|
|
|
const orgItems: EnrichmentItem[] = orgs.map((o) => {
|
|
const missing: string[] = [];
|
|
if (!o.industry) missing.push('industrie');
|
|
if (!o.country) missing.push('țară');
|
|
if (!o.website) missing.push('website');
|
|
if (!o.employeeCount) missing.push('angajați');
|
|
const completeness = Math.round(((4 - missing.length) / 4) * 100);
|
|
return { id: o.id, name: o.name, type: 'organization', missing, completeness };
|
|
}).filter((i) => i.completeness < 100);
|
|
|
|
return [...contactItems, ...orgItems].sort((a, b) => a.completeness - b.completeness);
|
|
}, [contacts, orgs]);
|
|
|
|
const avgContact = useMemo(() => {
|
|
if (!contacts.length) return 0;
|
|
const scores = contacts.map((c) => {
|
|
let filled = 0;
|
|
if (c.email) filled++;
|
|
if (c.phone) filled++;
|
|
if (c.role) filled++;
|
|
if (c.linkedinUrl) filled++;
|
|
return (filled / 4) * 100;
|
|
});
|
|
return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length);
|
|
}, [contacts]);
|
|
|
|
const avgOrg = useMemo(() => {
|
|
if (!orgs.length) return 0;
|
|
const scores = orgs.map((o) => {
|
|
let filled = 0;
|
|
if (o.industry) filled++;
|
|
if (o.country) filled++;
|
|
if (o.website) filled++;
|
|
if (o.employeeCount) filled++;
|
|
return (filled / 4) * 100;
|
|
});
|
|
return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length);
|
|
}, [orgs]);
|
|
|
|
function clsCls(pct: number) {
|
|
if (pct >= 75) return 'text-signal-ok';
|
|
if (pct >= 50) return 'text-warn';
|
|
return 'text-signal-danger';
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-4xl space-y-6 p-6">
|
|
<div>
|
|
<h1 className="font-display text-2xl font-semibold text-ink">Enrichment Date</h1>
|
|
<p className="text-sm text-ink-faint mt-1">
|
|
{cL || oL ? 'Se calculează…' : `${items.length} entități cu date incomplete`}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="card p-4 text-center">
|
|
<p className={`text-3xl font-bold ${clsCls(avgContact)}`}>{avgContact}%</p>
|
|
<p className="text-xs text-ink-faint mt-1">Completitudine medie contacte</p>
|
|
<div className="h-1.5 rounded-full bg-muted overflow-hidden mt-2">
|
|
<div className={`h-full rounded-full ${avgContact >= 75 ? 'bg-signal-ok' : avgContact >= 50 ? 'bg-warn' : 'bg-signal-danger'}`}
|
|
style={{ width: `${avgContact}%` }} />
|
|
</div>
|
|
</div>
|
|
<div className="card p-4 text-center">
|
|
<p className={`text-3xl font-bold ${clsCls(avgOrg)}`}>{avgOrg}%</p>
|
|
<p className="text-xs text-ink-faint mt-1">Completitudine medie organizații</p>
|
|
<div className="h-1.5 rounded-full bg-muted overflow-hidden mt-2">
|
|
<div className={`h-full rounded-full ${avgOrg >= 75 ? 'bg-signal-ok' : avgOrg >= 50 ? 'bg-warn' : 'bg-signal-danger'}`}
|
|
style={{ width: `${avgOrg}%` }} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card p-4 bg-primary/5 border-primary/20">
|
|
<p className="text-xs font-semibold text-ink mb-1">Surse de enrichment disponibile</p>
|
|
<div className="flex flex-wrap gap-2 text-[10px] text-ink-faint">
|
|
<span>📊 ClickHouse (147M+ rânduri date publice)</span>
|
|
<span>🔍 AnythingLLM (RAG pe documente interne)</span>
|
|
<span>🤖 Hermes (enrichment automat prin AI)</span>
|
|
</div>
|
|
<p className="text-[10px] text-ink-faint mt-2">
|
|
Enrichment-ul automat rulează periodic pe server. Această pagină afișează starea curentă.
|
|
</p>
|
|
</div>
|
|
|
|
{cL || oL ? (
|
|
<div className="text-center text-sm text-ink-faint py-8">Se calculează…</div>
|
|
) : items.length === 0 ? (
|
|
<div className="card p-8 text-center space-y-2">
|
|
<p className="text-2xl">✅</p>
|
|
<p className="text-sm text-ink-faint">Toate entitățile au date complete.</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint mb-2">
|
|
Entități de îmbogățit (sortat după completitudine crescătoare)
|
|
</p>
|
|
<div className="card divide-y divide-border/50">
|
|
{items.slice(0, 50).map((item) => (
|
|
<div key={item.id} className="flex items-center gap-4 p-3">
|
|
<span className={`w-10 text-right text-xs font-bold shrink-0 ${clsCls(item.completeness)}`}>
|
|
{item.completeness}%
|
|
</span>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-xs font-medium text-ink truncate">{item.name}</p>
|
|
<div className="flex flex-wrap gap-1 mt-0.5">
|
|
{item.missing.map((m) => (
|
|
<span key={m} className="rounded bg-signal-danger/10 text-signal-danger px-1 text-[8px]">-{m}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<Link
|
|
href={item.type === 'contact' ? '/dashboard/people' : '/dashboard/crm'}
|
|
className="text-[10px] text-primary hover:underline shrink-0">
|
|
Editează →
|
|
</Link>
|
|
</div>
|
|
))}
|
|
{items.length > 50 && (
|
|
<p className="text-xs text-center text-ink-faint p-3">și alte {items.length - 50}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|