feat(CC-083): add Trust Dashboard (weighted score from credentials/commitments/privacy/consistency)
This commit is contained in:
parent
d63eed0948
commit
4288839b06
1 changed files with 149 additions and 0 deletions
149
src/app/dashboard/trust/page.tsx
Normal file
149
src/app/dashboard/trust/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue