feat(CC-087): add Trust Credibility Profile (6 factors with source/date/contestare)
This commit is contained in:
parent
0153cdfbb3
commit
18cf60f3d3
1 changed files with 171 additions and 0 deletions
171
src/app/dashboard/trust/credibility/page.tsx
Normal file
171
src/app/dashboard/trust/credibility/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue