feat(CC-084): add Trust Professional page (goal/task/decision/contract indicators)

This commit is contained in:
admin-valentin 2026-08-02 17:31:31 +00:00
parent 08de6217cb
commit d9686fb70d

View file

@ -0,0 +1,146 @@
'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 Goal { id: string; title: string; status: string; progress: number | null; tags: string[]; createdAt: string; updatedAt: string; }
interface Task { id: string; title: string; status: string; priority: string | null; tags: string[]; createdAt: string; }
interface Decision { id: string; title: string; impact: string | null; rationale: string | null; tags: string[]; createdAt: string; }
interface Contract { id: string; title: string; status: string; value: number | null; currency: string | null; counterpartyName: string | null; }
export default function TrustProfessionalPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: goals = [] } = useQuery({ queryKey: ['tp-goals', tenantId], queryFn: () => apiFetch<Goal[]>('/v1/goals?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: tasks = [] } = useQuery({ queryKey: ['tp-tasks', tenantId], queryFn: () => apiFetch<Task[]>('/v1/tasks?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: decisions = [] } = useQuery({ queryKey: ['tp-decisions', tenantId], queryFn: () => apiFetch<Decision[]>('/v1/decisions?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: contracts = [] } = useQuery({ queryKey: ['tp-contracts', tenantId], queryFn: () => apiFetch<Contract[]>('/v1/contracts?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const metrics = useMemo(() => {
const completedGoals = goals.filter((g) => g.status === 'completed');
const activeGoals = goals.filter((g) => g.status === 'active');
const completedTasks = tasks.filter((t) => t.status === 'completed');
const urgentDone = completedTasks.filter((t) => t.priority === 'urgent');
const docDecisions = decisions.filter((d) => d.rationale && d.rationale.length > 30);
const highImpact = decisions.filter((d) => ['high','critical'].includes(d.impact ?? ''));
const signedContracts = contracts.filter((c) => c.status === 'signed' || c.status === 'active');
const totalValue = signedContracts.reduce((s, c) => s + (c.value ?? 0), 0);
const goalCompletionRate = goals.length > 0 ? Math.round((completedGoals.length / goals.length) * 100) : 0;
const taskCompletionRate = tasks.length > 0 ? Math.round((completedTasks.length / tasks.length) * 100) : 0;
const decisionQuality = decisions.length > 0 ? Math.round((docDecisions.length / decisions.length) * 100) : 0;
return { completedGoals, activeGoals, completedTasks, urgentDone, docDecisions, highImpact, signedContracts, totalValue, goalCompletionRate, taskCompletionRate, decisionQuality };
}, [goals, tasks, decisions, contracts]);
function scoreColor(s: number) { return s >= 70 ? 'text-signal-ok' : s >= 40 ? 'text-warn' : 'text-signal-danger'; }
function barColor(s: number) { return s >= 70 ? 'bg-signal-ok' : s >= 40 ? 'bg-warn' : 'bg-signal-danger'; }
const INDICATORS = [
{ label: 'Rată finalizare obiective', score: metrics.goalCompletionRate, detail: `${metrics.completedGoals.length}/${goals.length} goals` },
{ label: 'Rată finalizare task-uri', score: metrics.taskCompletionRate, detail: `${metrics.completedTasks.length}/${tasks.length} tasks` },
{ label: 'Calitate decizii documentate', score: metrics.decisionQuality, detail: `${metrics.docDecisions.length}/${decisions.length} cu rațional` },
];
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> / Reputație Profesională
</nav>
<h1 className="font-display text-2xl font-semibold text-ink">Reputație Profesională</h1>
<p className="text-sm text-ink-faint mt-1">Indicatori derivați din execuție: obiective, taskuri, decizii, contracte.</p>
</div>
{/* KPI cards */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-signal-ok">{metrics.completedGoals.length}</p>
<p className="text-[10px] text-ink-faint">obiective finalizate</p>
</div>
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-ink">{metrics.activeGoals.length}</p>
<p className="text-[10px] text-ink-faint">obiective active</p>
</div>
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-ink">{metrics.highImpact.length}</p>
<p className="text-[10px] text-ink-faint">decizii high-impact</p>
</div>
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-ink">{metrics.signedContracts.length}</p>
<p className="text-[10px] text-ink-faint">contracte active</p>
</div>
</div>
{/* Indicators */}
<div className="card p-5 space-y-5">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Indicatori de performanță</p>
{INDICATORS.map((ind) => (
<div key={ind.label} className="space-y-1">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-ink">{ind.label}</p>
<p className="text-[10px] text-ink-faint">{ind.detail}</p>
</div>
<span className={`text-sm font-bold ml-4 shrink-0 ${scoreColor(ind.score)}`}>{ind.score}%</span>
</div>
<div className="h-2 rounded-full bg-muted overflow-hidden">
<div className={`h-full rounded-full ${barColor(ind.score)}`} style={{ width: `${ind.score}%` }} />
</div>
</div>
))}
</div>
{/* Recent completed goals */}
{metrics.completedGoals.length > 0 && (
<div className="card p-5 space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Obiective finalizate recente</p>
<div className="space-y-2">
{metrics.completedGoals.slice(0, 8).map((g) => (
<div key={g.id} className="flex items-center gap-3">
<span className="text-signal-ok text-sm"></span>
<p className="text-sm text-ink flex-1 truncate">{g.title}</p>
<span className="text-[10px] text-ink-faint shrink-0">
{new Date(g.updatedAt).toLocaleDateString('ro-RO', { month: 'short', year: 'numeric' })}
</span>
</div>
))}
</div>
{metrics.completedGoals.length > 8 && (
<Link href="/dashboard/goals" className="text-xs text-primary hover:underline">
+{metrics.completedGoals.length - 8} mai multe
</Link>
)}
</div>
)}
{/* Contracts */}
{metrics.signedContracts.length > 0 && (
<div className="card p-5 space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Contracte semnate / active</p>
<div className="space-y-2">
{metrics.signedContracts.slice(0, 6).map((c) => (
<div key={c.id} className="flex items-center justify-between">
<p className="text-sm text-ink truncate flex-1">{c.title}</p>
<div className="flex items-center gap-3 shrink-0 ml-3">
{c.counterpartyName && <span className="text-[10px] text-ink-faint">{c.counterpartyName}</span>}
{c.value ? <span className="text-[10px] font-mono text-ink">{c.value.toLocaleString()} {c.currency}</span> : null}
</div>
</div>
))}
</div>
</div>
)}
<div className="flex gap-3 flex-wrap text-xs">
<Link href="/dashboard/goals" className="text-primary hover:underline">Obiective </Link>
<Link href="/dashboard/decisions" className="text-primary hover:underline">Decizii </Link>
<Link href="/dashboard/contracts" className="text-primary hover:underline">Contracte </Link>
</div>
</div>
);
}