feat(CC-087): add Trust Credibility Profile (6 factors with source/date/contestare)

This commit is contained in:
admin-valentin 2026-08-02 17:41:47 +00:00
parent 0153cdfbb3
commit 18cf60f3d3

View file

@ -0,0 +1,171 @@
'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; source: string | null; confidence: number | null; createdAt: string; observedAt: string | null; }
interface Goal { id: string; status: string; }
interface Task { id: string; status: string; tags: string[]; }
interface Contact { id: string; tags: string[]; consentStatus: string | null; }
interface CredibilityFactor {
id: string; label: string; icon: string;
value: number; source: string; date: string | null; detail: string;
}
const CRED_METRICS = ['certificare', 'certification', 'certificate', 'diploma', 'curs', 'course', 'badge'];
const SKILL_METRICS = ['skill', 'competenta', 'abilitate', 'limbaj', 'framework', 'tool'];
const REF_TAGS = ['reference', 'referinta', 'testimonial', 'recomandare'];
const COMMIT_TAGS = ['commitment', 'promise'];
export default function TrustCredibilityPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: observations = [] } = useQuery({ queryKey: ['cred-obs', tenantId], queryFn: () => apiFetch<Observation[]>('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: goals = [] } = useQuery({ queryKey: ['cred-goals', tenantId], queryFn: () => apiFetch<Goal[]>('/v1/goals?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: tasks = [] } = useQuery({ queryKey: ['cred-tasks', tenantId], queryFn: () => apiFetch<Task[]>('/v1/tasks?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: contacts = [] } = useQuery({ queryKey: ['cred-contacts', tenantId], queryFn: () => apiFetch<Contact[]>('/v1/contacts?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const factors = useMemo((): CredibilityFactor[] => {
const creds = observations.filter((o) => CRED_METRICS.some((k) => o.metric.toLowerCase().includes(k)));
const skills = observations.filter((o) => SKILL_METRICS.some((k) => o.metric.toLowerCase().includes(k)));
const refs = contacts.filter((c) => c.tags.some((t) => REF_TAGS.includes(t.toLowerCase())));
const commitTasks = tasks.filter((t) => t.tags.some((tag) => COMMIT_TAGS.includes(tag)));
const keptCommits = commitTasks.filter((t) => t.status === 'completed');
const completedGoals = goals.filter((g) => g.status === 'completed');
const avgConf = observations.length > 0 ? observations.reduce((s, o) => s + (o.confidence ?? 1), 0) / observations.length : 0;
return [
{
id: 'credentials',
label: 'Credențiale verificabile',
icon: '🎓',
value: Math.min(100, creds.length * 10),
source: 'auto (observations)',
date: creds[0]?.observedAt ?? creds[0]?.createdAt ?? null,
detail: `${creds.length} certificări/diplome logate`,
},
{
id: 'skills',
label: 'Competențe declarate',
icon: '🛠️',
value: Math.min(100, skills.length * 8),
source: 'auto (observations)',
date: skills[0]?.observedAt ?? null,
detail: `${skills.length} competențe logate`,
},
{
id: 'references',
label: 'Referințe disponibile',
icon: '📋',
value: Math.min(100, refs.length * 20),
source: 'auto (contacts)',
date: null,
detail: `${refs.length} contacte cu tag referință`,
},
{
id: 'commitments',
label: 'Angajamente onorate',
icon: '🤝',
value: commitTasks.length > 0 ? Math.round((keptCommits.length / commitTasks.length) * 100) : 100,
source: 'auto (tasks)',
date: null,
detail: `${keptCommits.length}/${commitTasks.length} finalizate`,
},
{
id: 'track_record',
label: 'Track record obiective',
icon: '🎯',
value: goals.length > 0 ? Math.min(100, Math.round((completedGoals.length / goals.length) * 100) + completedGoals.length * 2) : 0,
source: 'auto (goals)',
date: null,
detail: `${completedGoals.length}/${goals.length} obiective finalizate`,
},
{
id: 'data_quality',
label: 'Calitate date auto-raportate',
icon: '📊',
value: Math.round(avgConf * 100),
source: 'auto (confidence)',
date: null,
detail: `Confidență medie: ${Math.round(avgConf * 100)}%`,
},
];
}, [observations, goals, tasks, contacts]);
const overall = useMemo(() =>
factors.length > 0 ? Math.round(factors.reduce((s, f) => s + f.value, 0) / factors.length) : 0,
[factors]);
function scoreCls(s: number) { return s >= 70 ? 'text-signal-ok' : s >= 40 ? 'text-warn' : 'text-signal-danger'; }
function barCls(s: number) { return s >= 70 ? 'bg-signal-ok' : s >= 40 ? 'bg-warn' : 'bg-signal-danger'; }
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> / Profil Credibilitate
</nav>
<h1 className="font-display text-2xl font-semibold text-ink">Profil Credibilitate</h1>
<p className="text-sm text-ink-faint mt-1">Fiecare factor cu valoare, sursă, dată și posibilitate de contestare.</p>
</div>
<div className="card p-4 bg-warn/5 border-warn/30">
<p className="text-[10px] text-ink-faint">
Factorii de credibilitate sunt derivați <strong>exclusiv din date auto-raportate</strong> în CEO OS.
Nu reprezintă un scor de credit sau o evaluare externă. Utilizatorul poate contesta sau corecta orice factor.
</p>
</div>
{/* Overall */}
<div className="card p-5 flex items-center gap-6">
<div className="text-center w-24 shrink-0">
<p className={`font-display text-5xl font-bold ${scoreCls(overall)}`}>{overall}</p>
<p className="text-[10px] text-ink-faint mt-1">credibilitate</p>
</div>
<div className="flex-1">
<div className="h-3 rounded-full bg-muted overflow-hidden">
<div className={`h-full rounded-full ${barCls(overall)}`} style={{ width: `${overall}%` }} />
</div>
<p className="text-xs text-ink-faint mt-1">Medie aritmetică a {factors.length} factori</p>
</div>
</div>
{/* Factor details */}
<div className="space-y-3">
{factors.map((f) => (
<div key={f.id} className="card p-4 space-y-2">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2">
<span className="text-xl">{f.icon}</span>
<div>
<p className="text-sm font-medium text-ink">{f.label}</p>
<p className="text-[10px] text-ink-faint">{f.detail}</p>
</div>
</div>
<div className="text-right shrink-0">
<p className={`text-lg font-bold ${scoreCls(f.value)}`}>{f.value}%</p>
<p className="text-[9px] text-ink-faint">{f.source}</p>
</div>
</div>
<div className="h-1.5 rounded-full bg-muted overflow-hidden">
<div className={`h-full rounded-full ${barCls(f.value)}`} style={{ width: `${f.value}%` }} />
</div>
<div className="flex items-center justify-between">
{f.date ? (
<p className="text-[9px] text-ink-faint">Ultima actualizare: {new Date(f.date).toLocaleDateString('ro-RO', { dateStyle: 'medium' })}</p>
) : <span />}
<button className="text-[9px] text-ink-faint hover:text-warn underline">Contestă</button>
</div>
</div>
))}
</div>
<Link href="/dashboard/trust" className="text-xs text-primary hover:underline"> Trust Dashboard</Link>
</div>
);
}