feat(cc-049): Decision Dashboard — aggregate stats over decisions register
This commit is contained in:
parent
db8eb1787b
commit
e5604f2696
1 changed files with 177 additions and 0 deletions
177
src/app/dashboard/decisions/page.tsx
Normal file
177
src/app/dashboard/decisions/page.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiFetch, type Decision } from '../../../lib/api';
|
||||
import { useSession } from '../../../components/session-provider';
|
||||
|
||||
export default function DecisionsDashboardPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? '';
|
||||
|
||||
const { data: decisions = [], isLoading } = useQuery({
|
||||
queryKey: ['decisions-dashboard', tenantId],
|
||||
queryFn: () => apiFetch<Decision[]>('/v1/decisions', { tenantId }),
|
||||
enabled: Boolean(tenantId),
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const pending = decisions.filter((d) => !d.selectedOption);
|
||||
const decided = decisions.filter((d) => d.selectedOption);
|
||||
const overdueReviews = decisions.filter(
|
||||
(d) => d.selectedOption && d.reviewDueAt && new Date(d.reviewDueAt) < now,
|
||||
);
|
||||
|
||||
const stats = [
|
||||
{ label: 'Total', value: decisions.length, color: 'text-ink' },
|
||||
{
|
||||
label: 'În așteptare',
|
||||
value: pending.length,
|
||||
color: pending.length > 0 ? 'text-signal-warn' : 'text-ink',
|
||||
},
|
||||
{
|
||||
label: 'Decise',
|
||||
value: decided.length,
|
||||
color: decided.length > 0 ? 'text-signal-ok' : 'text-ink',
|
||||
},
|
||||
{
|
||||
label: 'Review întârziat',
|
||||
value: overdueReviews.length,
|
||||
color: overdueReviews.length > 0 ? 'text-signal-danger' : 'text-ink',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-semibold text-ink">Decizii</h1>
|
||||
<p className="mt-1 text-sm text-ink-faint">
|
||||
Vizualizare agregată a registrului de decizii.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard/decisions/register"
|
||||
className="rounded-lg bg-bronze-deep px-4 py-2 text-sm font-medium text-white hover:opacity-90"
|
||||
>
|
||||
Deschide registrul →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
{stats.map(({ label, value, color }) => (
|
||||
<div key={label} className="card p-5">
|
||||
<p className="text-xs font-medium uppercase tracking-wider text-ink-faint">{label}</p>
|
||||
<p className={`mt-1 font-display text-3xl font-semibold ${color}`}>{value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-ink-faint">Se încarcă…</p>
|
||||
) : decisions.length === 0 ? (
|
||||
<div className="card p-10 text-center">
|
||||
<p className="text-sm font-medium text-ink">Nicio decizie înregistrată</p>
|
||||
<p className="mt-1 text-xs text-ink-faint">
|
||||
Documentează deciziile importante pentru a le putea revizui ulterior.
|
||||
</p>
|
||||
<Link
|
||||
href="/dashboard/decisions/register"
|
||||
className="mt-4 inline-block rounded-lg bg-bronze-deep px-4 py-2 text-sm font-medium text-white hover:opacity-90"
|
||||
>
|
||||
Adaugă prima decizie
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{overdueReviews.length > 0 && (
|
||||
<section>
|
||||
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-signal-danger">
|
||||
Review întârziat ({overdueReviews.length})
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{overdueReviews.map((d) => (
|
||||
<li
|
||||
key={d.id}
|
||||
className="card flex items-start gap-4 border-l-2 border-signal-danger px-4 py-3"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-ink">{d.context}</p>
|
||||
<p className="mt-0.5 text-[11px] text-signal-danger">
|
||||
Review due: {new Date(d.reviewDueAt!).toLocaleDateString('ro-RO')}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 rounded-full bg-bronze-wash px-2 py-0.5 text-[10px] font-medium text-bronze-deep">
|
||||
{d.selectedOption}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{pending.length > 0 && (
|
||||
<section>
|
||||
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-signal-warn">
|
||||
În așteptare ({pending.length})
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{pending.slice(0, 6).map((d) => (
|
||||
<li key={d.id} className="card flex items-center gap-4 px-4 py-3">
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-signal-warn" />
|
||||
<p className="flex-1 truncate text-sm text-ink">{d.context}</p>
|
||||
<span className="shrink-0 text-[11px] text-ink-faint">
|
||||
{new Date(d.createdAt).toLocaleDateString('ro-RO')}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-ink-faint">
|
||||
Toate deciziile ({decisions.length})
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{decisions.slice(0, 10).map((d) => (
|
||||
<li key={d.id} className="card flex items-center gap-4 px-4 py-3">
|
||||
<span
|
||||
className={`h-2 w-2 shrink-0 rounded-full ${
|
||||
d.selectedOption ? 'bg-signal-ok' : 'bg-paper-sunken border border-ink-line'
|
||||
}`}
|
||||
/>
|
||||
<p className="flex-1 truncate text-sm text-ink">{d.context}</p>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{d.selectedOption ? (
|
||||
<span className="rounded-full bg-bronze-wash px-2 py-0.5 text-[10px] font-medium text-bronze-deep">
|
||||
Decis
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded-full bg-paper-sunken px-2 py-0.5 text-[10px] font-medium text-ink-faint">
|
||||
Pending
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[11px] text-ink-line">
|
||||
{new Date(d.createdAt).toLocaleDateString('ro-RO')}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{decisions.length > 10 && (
|
||||
<div className="mt-3 text-right">
|
||||
<Link
|
||||
href="/dashboard/decisions/register"
|
||||
className="text-xs font-medium text-bronze-deep hover:underline"
|
||||
>
|
||||
Toate {decisions.length} deciziile în registru →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue