feat(CC-083): add Trust Dashboard (weighted score from credentials/commitments/privacy/consistency)

This commit is contained in:
admin-valentin 2026-08-02 17:22:12 +00:00
parent d63eed0948
commit 4288839b06

View file

@ -0,0 +1,149 @@
'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 Observation { id: string; metric: string; value: string; subjectType: string; confidence: number | null; source: string | null; createdAt: string; observedAt: string | null; }
interface Goal { id: string; status: string; progress: number | null; }
interface Task { id: string; status: string; tags: string[]; priority: string | null; }
interface Contact { id: string; consentStatus: string | null; }
const TRUST_FACTORS = [
{ id: 'credentials', label: 'Credențiale & Certificări', href: '/dashboard/trust/credentials', icon: '🎓', weight: 25 },
{ id: 'professional', label: 'Reputație Profesională', href: '/dashboard/trust/professional', icon: '💼', weight: 25 },
{ id: 'commitments', label: 'Angajamente Onorate', href: '/dashboard/promises', icon: '🤝', weight: 20 },
{ id: 'consistency', label: 'Consistență Date', href: '/dashboard/data/quality', icon: '📊', weight: 15 },
{ id: 'privacy', label: 'Conformitate Privacy', href: '/dashboard/privacy/consents', icon: '🛡️', weight: 15 },
];
const TRUST_LINKS = [
{ label: 'Credențiale', href: '/dashboard/trust/credentials', icon: '🎓' },
{ label: 'Reputație Profesională', href: '/dashboard/trust/professional', icon: '💼' },
{ label: 'Reputație Socială', href: '/dashboard/trust/social', icon: '🌐' },
{ label: 'Proiecte & Rezultate', href: '/dashboard/trust/outcomes', icon: '🎯' },
{ label: 'Referințe', href: '/dashboard/trust/references', icon: '📋' },
{ label: 'Contribuții', href: '/dashboard/trust/contributions', icon: '🤲' },
{ label: 'Dispute & Incidente', href: '/dashboard/trust/disputes', icon: '⚖️' },
{ label: 'Trust Passport', href: '/dashboard/trust/passport', icon: '🗝️' },
];
export default function TrustDashboardPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: observations = [] } = useQuery({ queryKey: ['trust-obs', tenantId], queryFn: () => apiFetch<Observation[]>('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: goals = [] } = useQuery({ queryKey: ['trust-goals', tenantId], queryFn: () => apiFetch<Goal[]>('/v1/goals?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: tasks = [] } = useQuery({ queryKey: ['trust-tasks', tenantId], queryFn: () => apiFetch<Task[]>('/v1/tasks?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: contacts = [] } = useQuery({ queryKey: ['trust-contacts', tenantId], queryFn: () => apiFetch<Contact[]>('/v1/contacts?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const scores = useMemo(() => {
const eduObs = observations.filter((o) => ['course','certification','skill','education'].some((k) => o.metric.toLowerCase().includes(k)));
const credScore = Math.min(100, eduObs.length * 10);
const completedGoals = goals.filter((g) => g.status === 'completed').length;
const totalGoals = goals.length;
const profScore = totalGoals > 0 ? Math.min(100, Math.round((completedGoals / totalGoals) * 100) + completedGoals * 5) : 0;
const commitTasks = tasks.filter((t) => t.tags.some((tag) => ['commitment','promise'].includes(tag)));
const keptTasks = commitTasks.filter((t) => t.status === 'completed');
const commitScore = commitTasks.length > 0 ? Math.round((keptTasks.length / commitTasks.length) * 100) : 100;
const validObs = observations.filter((o) => (o.confidence ?? 1) >= 0.5).length;
const consistScore = observations.length > 0 ? Math.round((validObs / observations.length) * 100) : 50;
const grantedContacts = contacts.filter((c) => c.consentStatus === 'granted').length;
const privacyScore = contacts.length > 0 ? Math.round((grantedContacts / contacts.length) * 100) : 100;
const factorScores: Record<string, number> = {
credentials: credScore, professional: profScore, commitments: commitScore,
consistency: consistScore, privacy: privacyScore,
};
const overall = Math.round(
TRUST_FACTORS.reduce((sum, f) => sum + (factorScores[f.id] ?? 0) * f.weight / 100, 0)
);
return { factorScores, overall };
}, [observations, goals, tasks, contacts]);
function scoreCls(s: number) {
if (s >= 70) return 'text-signal-ok';
if (s >= 40) return 'text-warn';
return 'text-signal-danger';
}
function barCls(s: number) {
if (s >= 70) return 'bg-signal-ok';
if (s >= 40) return 'bg-warn';
return 'bg-signal-danger';
}
return (
<div className="max-w-4xl space-y-6 p-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Trust Dashboard</h1>
<p className="text-sm text-ink-faint mt-1">Completitudinea profilului de încredere date auto-raportate cu surse explicite.</p>
</div>
<div className="card p-4 bg-warn/5 border-warn/30">
<p className="text-xs text-ink-faint">
<strong>Important:</strong> Scorul afișat nu este un scor social universal sau de credit.
Reflectă completitudinea datelor auto-raportate în CEO OS. Nu poate fi folosit ca acuzație sau clasificare externă.
Utilizatorul controlează ce se partajează prin <Link href="/dashboard/trust/passport" className="text-primary hover:underline">Trust Passport</Link>.
</p>
</div>
{/* Overall score */}
<div className="card p-6 flex items-center gap-6">
<div className="text-center w-28 shrink-0">
<p className={`font-display text-5xl font-bold ${scoreCls(scores.overall)}`}>{scores.overall}</p>
<p className="text-[10px] text-ink-faint mt-1">Scor completitudine</p>
</div>
<div className="flex-1 space-y-2">
<div className="h-3 rounded-full bg-muted overflow-hidden">
<div className={`h-full rounded-full transition-all ${barCls(scores.overall)}`} style={{ width: `${scores.overall}%` }} />
</div>
<p className="text-xs text-ink-faint">
{scores.overall >= 70 ? '✅ Profil bine documentat' : scores.overall >= 40 ? '⚠ Lipsesc dovezi în unele arii' : '❌ Profil incomplet — adaugă dovezi'}
</p>
</div>
</div>
{/* Factor breakdown */}
<div className="card p-5 space-y-4">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Factori de încredere</p>
{TRUST_FACTORS.map((f) => {
const s = scores.factorScores[f.id] ?? 0;
return (
<Link key={f.id} href={f.href} className="block group">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<span>{f.icon}</span>
<span className="text-sm text-ink group-hover:text-primary transition-colors">{f.label}</span>
<span className="text-[10px] text-ink-faint">({f.weight}%)</span>
</div>
<span className={`text-sm font-bold ${scoreCls(s)}`}>{s}%</span>
</div>
<div className="h-1.5 rounded-full bg-muted overflow-hidden">
<div className={`h-full rounded-full ${barCls(s)}`} style={{ width: `${s}%` }} />
</div>
</Link>
);
})}
</div>
{/* Module links */}
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{TRUST_LINKS.map((link) => (
<Link key={link.href} href={link.href}
className="card p-3 hover:border-primary/40 transition-colors text-center group">
<span className="text-xl block mb-1">{link.icon}</span>
<p className="text-xs font-medium text-ink group-hover:text-primary transition-colors">{link.label}</p>
</Link>
))}
</div>
</div>
);
}