feat(CC-084): add Trust Professional page (goal/task/decision/contract indicators)
This commit is contained in:
parent
08de6217cb
commit
d9686fb70d
1 changed files with 146 additions and 0 deletions
146
src/app/dashboard/trust/professional/page.tsx
Normal file
146
src/app/dashboard/trust/professional/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue