feat(cc-049): Business Overview — aggregate view across all modules

This commit is contained in:
admin-valentin 2026-07-31 19:26:43 +00:00
parent e5604f2696
commit 2c3408e968

View file

@ -0,0 +1,234 @@
'use client';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { apiFetch, type Goal, type Transaction, type Decision } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
interface ExecDash {
status: 'ready' | 'building';
counters: {
openTasks: number;
overdueTasks: number;
deadlinesNext7Days: number;
organizations: number;
segments: number;
researchBriefs: number;
} | null;
generatedAt: string | null;
}
export default function BusinessOverviewPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: exec } = useQuery({
queryKey: ['exec-dash-biz', tenantId],
queryFn: () => apiFetch<ExecDash>('/v1/dashboard/executive', { tenantId }),
enabled: Boolean(tenantId),
staleTime: 60_000,
});
const { data: goals = [] } = useQuery({
queryKey: ['goals-biz', tenantId],
queryFn: () => apiFetch<Goal[]>('/v1/goals', { tenantId }),
enabled: Boolean(tenantId),
});
const { data: transactions = [] } = useQuery({
queryKey: ['transactions-biz', tenantId],
queryFn: () => apiFetch<Transaction[]>('/v1/transactions', { tenantId }),
enabled: Boolean(tenantId),
});
const { data: decisions = [] } = useQuery({
queryKey: ['decisions-biz', tenantId],
queryFn: () => apiFetch<Decision[]>('/v1/decisions', { tenantId }),
enabled: Boolean(tenantId),
});
const c = exec?.counters;
const goalsAtRisk = goals.filter((g) => g.status === 'at_risk').length;
const txMissing = transactions.filter((t) => t.evidenceStatus === 'missing').length;
const decisionsPending = decisions.filter((d) => !d.selectedOption).length;
const modules = [
{
label: 'Organizații',
value: c?.organizations ?? '—',
href: '/dashboard/organizations',
badge: null,
badgeColor: '',
},
{
label: 'Sarcini deschise',
value: c?.openTasks ?? '—',
href: '/dashboard/tasks',
badge: c?.overdueTasks ? `${c.overdueTasks} restante` : null,
badgeColor: 'bg-signal-danger text-white',
},
{
label: 'Obiective active',
value: goals.length,
href: '/dashboard/goals',
badge: goalsAtRisk > 0 ? `${goalsAtRisk} la risc` : null,
badgeColor: 'bg-signal-warn text-white',
},
{
label: 'Tranzacții',
value: transactions.length,
href: '/dashboard/transactions',
badge: txMissing > 0 ? `${txMissing} fără dovadă` : null,
badgeColor: 'bg-signal-warn text-white',
},
{
label: 'Decizii pending',
value: decisionsPending,
href: '/dashboard/decisions',
badge: null,
badgeColor: '',
},
{
label: 'Segmente',
value: c?.segments ?? '—',
href: '/dashboard/segments',
badge: null,
badgeColor: '',
},
{
label: 'Research briefs',
value: c?.researchBriefs ?? '—',
href: '/dashboard/research',
badge: null,
badgeColor: '',
},
{
label: 'Scadențe 7 zile',
value: c?.deadlinesNext7Days ?? '—',
href: '/dashboard/tasks',
badge: null,
badgeColor: '',
},
];
return (
<div className="max-w-5xl space-y-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Vedere Generală Business</h1>
<p className="mt-1 text-sm text-ink-faint">
Sumar executiv al tuturor modulelor CEO-OS.
{exec?.generatedAt && (
<> Actualizat: {new Date(exec.generatedAt).toLocaleString('ro-RO')}.</>
)}
</p>
</div>
{exec?.status === 'building' && (
<div className="card border-l-2 border-signal-warn p-4">
<p className="text-sm text-signal-warn">
Proiecția executive se construiește. Contoarele vor apărea în câteva secunde.
</p>
</div>
)}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{modules.map(({ label, value, href, badge, badgeColor }) => (
<Link
key={label}
href={href}
className="card group flex flex-col gap-1 p-5 hover:border-bronze-deep transition-colors"
>
<p className="text-xs font-medium uppercase tracking-wider text-ink-faint group-hover:text-bronze-deep">
{label}
</p>
<p className="font-display text-3xl font-semibold text-ink">{value}</p>
{badge && (
<span className={`mt-1 inline-block rounded-full px-2 py-0.5 text-[10px] font-medium ${badgeColor}`}>
{badge}
</span>
)}
</Link>
))}
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<section className="card p-5">
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-ink-faint">
Obiective
</h2>
{goals.length === 0 ? (
<p className="text-sm text-ink-faint">Niciun obiectiv definit.</p>
) : (
<ul className="space-y-2">
{goals.slice(0, 5).map((g) => {
const dot =
g.status === 'on_track'
? 'bg-signal-ok'
: g.status === 'at_risk'
? 'bg-signal-warn'
: g.status === 'achieved'
? 'bg-bronze-deep'
: g.status === 'abandoned'
? 'bg-signal-danger'
: 'bg-paper-sunken';
return (
<li key={g.id} className="flex items-center gap-3">
<span className={`h-2 w-2 shrink-0 rounded-full ${dot}`} />
<span className="flex-1 truncate text-sm text-ink">{g.metric}</span>
<span className="shrink-0 text-xs text-ink-faint">{g.target}</span>
</li>
);
})}
</ul>
)}
<Link
href="/dashboard/goals"
className="mt-3 block text-right text-xs font-medium text-bronze-deep hover:underline"
>
Toate obiectivele
</Link>
</section>
<section className="card p-5">
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-ink-faint">
Tranzacții recente
</h2>
{transactions.length === 0 ? (
<p className="text-sm text-ink-faint">Nicio tranzacție înregistrată.</p>
) : (
<ul className="space-y-2">
{transactions.slice(0, 5).map((t) => (
<li key={t.id} className="flex items-center justify-between gap-3">
<span className="flex-1 truncate text-sm text-ink">{t.type}</span>
<span className="shrink-0 font-mono text-xs text-ink-faint">
{new Intl.NumberFormat('ro-RO', {
style: 'currency',
currency: t.currency,
}).format(Number(t.amountMinorUnits) / 100)}
</span>
<span
className={`shrink-0 rounded-full px-1.5 py-0.5 text-[9px] font-medium ${
t.evidenceStatus === 'complete'
? 'bg-signal-ok text-white'
: t.evidenceStatus === 'partial'
? 'bg-signal-warn text-white'
: 'bg-signal-danger text-white'
}`}
>
{t.evidenceStatus}
</span>
</li>
))}
</ul>
)}
<Link
href="/dashboard/transactions"
className="mt-3 block text-right text-xs font-medium text-bronze-deep hover:underline"
>
Toate tranzacțiile
</Link>
</section>
</div>
</div>
);
}