feat(CC-085): add Trust Outcomes page (portfolio: goals + delivered contracts)

This commit is contained in:
admin-valentin 2026-08-02 17:34:53 +00:00
parent 827eb3bb30
commit 7d782ddf0e

View file

@ -0,0 +1,133 @@
'use client';
import { useMemo, useState } 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; description: string | null; status: string; progress: number | null; tags: string[]; targetDate: string | null; updatedAt: string; createdAt: string; }
interface Contract { id: string; title: string; status: string; value: number | null; currency: string | null; counterpartyName: string | null; endDate: string | null; }
const PORTFOLIO_TAGS = ['project', 'proiect', 'product', 'produs', 'launch', 'lansare', 'client', 'outcome'];
export default function TrustOutcomesPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const [view, setView] = useState<'goals' | 'contracts'>('goals');
const { data: goals = [] } = useQuery({ queryKey: ['to-goals', tenantId], queryFn: () => apiFetch<Goal[]>('/v1/goals?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: contracts = [] } = useQuery({ queryKey: ['to-contracts', tenantId], queryFn: () => apiFetch<Contract[]>('/v1/contracts?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const portfolio = useMemo(() => {
const projectGoals = goals.filter((g) =>
g.status === 'completed' || g.tags.some((t) => PORTFOLIO_TAGS.includes(t.toLowerCase()))
).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
const deliveredContracts = contracts.filter((c) => ['signed', 'active', 'completed', 'delivered'].includes(c.status))
.sort((a, b) => (b.value ?? 0) - (a.value ?? 0));
const totalContractValue = deliveredContracts.reduce((s, c) => s + (c.value ?? 0), 0);
const currencies = [...new Set(deliveredContracts.map((c) => c.currency).filter(Boolean))];
return { projectGoals, deliveredContracts, totalContractValue, currencies };
}, [goals, contracts]);
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> / Proiecte & Rezultate
</nav>
<h1 className="font-display text-2xl font-semibold text-ink">Proiecte & Rezultate</h1>
<p className="text-sm text-ink-faint mt-1">Portofoliu de obiective finalizate și contracte livrate.</p>
</div>
{/* Summary */}
<div className="grid grid-cols-3 gap-3">
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-ink">{portfolio.projectGoals.length}</p>
<p className="text-[10px] text-ink-faint">proiecte / obiective</p>
</div>
<div className="card p-4 text-center">
<p className="text-2xl font-bold text-ink">{portfolio.deliveredContracts.length}</p>
<p className="text-[10px] text-ink-faint">contracte livrate</p>
</div>
<div className="card p-4 text-center">
<p className="text-lg font-bold text-ink">
{portfolio.totalContractValue > 0
? `${portfolio.totalContractValue.toLocaleString()} ${portfolio.currencies[0] ?? ''}`
: '—'}
</p>
<p className="text-[10px] text-ink-faint">valoare contracte</p>
</div>
</div>
{/* Toggle */}
<div className="flex gap-2">
{(['goals', 'contracts'] as const).map((v) => (
<button key={v} onClick={() => setView(v)}
className={`rounded-full px-4 py-1.5 text-xs font-medium border transition-colors ${view === v ? 'bg-primary text-white border-primary' : 'bg-background text-ink-faint border-border hover:border-primary/40'}`}>
{v === 'goals' ? `Obiective (${portfolio.projectGoals.length})` : `Contracte (${portfolio.deliveredContracts.length})`}
</button>
))}
</div>
{view === 'goals' ? (
portfolio.projectGoals.length === 0 ? (
<div className="card p-8 text-center">
<p className="text-2xl mb-2">🎯</p>
<p className="text-sm text-ink-faint">Niciun obiectiv finalizat sau cu tag de proiect.</p>
</div>
) : (
<div className="grid gap-3 sm:grid-cols-2">
{portfolio.projectGoals.map((g) => (
<div key={g.id} className="card p-4 space-y-2">
<div className="flex items-start justify-between gap-2">
<p className="text-sm font-medium text-ink leading-snug">{g.title}</p>
<span className={`rounded-full px-2 py-0.5 text-[9px] font-bold shrink-0 ${g.status === 'completed' ? 'bg-signal-ok/10 text-signal-ok' : 'bg-primary/10 text-primary'}`}>
{g.status}
</span>
</div>
{g.description && <p className="text-[10px] text-ink-faint line-clamp-2">{g.description}</p>}
{g.progress !== null && (
<div className="h-1 rounded-full bg-muted overflow-hidden">
<div className="h-full bg-primary/60 rounded-full" style={{ width: `${g.progress}%` }} />
</div>
)}
<p className="text-[9px] text-ink-faint">
{new Date(g.updatedAt).toLocaleDateString('ro-RO', { month: 'short', year: 'numeric' })}
</p>
</div>
))}
</div>
)
) : (
portfolio.deliveredContracts.length === 0 ? (
<div className="card p-8 text-center">
<p className="text-2xl mb-2">📄</p>
<p className="text-sm text-ink-faint">Niciun contract în portofoliu.</p>
</div>
) : (
<div className="card divide-y divide-border/50">
{portfolio.deliveredContracts.map((c) => (
<div key={c.id} className="p-4 flex items-center gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-ink truncate">{c.title}</p>
<div className="flex gap-3 text-[10px] text-ink-faint mt-0.5">
{c.counterpartyName && <span>{c.counterpartyName}</span>}
{c.endDate && <span>{new Date(c.endDate).toLocaleDateString('ro-RO', { month: 'short', year: 'numeric' })}</span>}
</div>
</div>
<div className="shrink-0 text-right">
{c.value ? <p className="text-sm font-bold text-ink">{c.value.toLocaleString()} {c.currency}</p> : null}
<span className="text-[9px] text-signal-ok font-bold">{c.status}</span>
</div>
</div>
))}
</div>
)
)}
</div>
);
}