feat(cc-051): Executive Reports summary page

This commit is contained in:
admin-valentin 2026-08-01 10:09:29 +00:00
parent 006a4f0562
commit 12727faa30

View file

@ -0,0 +1,155 @@
'use client';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '../../../lib/api';
import type { Goal, Task, Decision, Transaction } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
const B = '/dashboard';
interface BriefingToday {
date: string;
sections: { title: string; items: string[] }[];
}
export default function ReportsPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const opts = { enabled: Boolean(tenantId), staleTime: 60_000 };
const { data: goals = [] } = useQuery({
queryKey: ['reports-goals', tenantId],
queryFn: () => apiFetch<Goal[]>('/v1/goals', { tenantId }),
...opts,
});
const { data: tasks = [] } = useQuery({
queryKey: ['reports-tasks', tenantId],
queryFn: () => apiFetch<Task[]>('/v1/tasks', { tenantId }),
...opts,
});
const { data: decisions = [] } = useQuery({
queryKey: ['reports-decisions', tenantId],
queryFn: () => apiFetch<Decision[]>('/v1/decisions', { tenantId }),
...opts,
});
const { data: transactions = [] } = useQuery({
queryKey: ['reports-transactions', tenantId],
queryFn: () => apiFetch<Transaction[]>('/v1/transactions', { tenantId }),
...opts,
});
const { data: briefing } = useQuery({
queryKey: ['reports-briefing', tenantId],
queryFn: () => apiFetch<BriefingToday>('/v1/briefing/today', { tenantId }),
...opts,
});
const now = new Date();
const activeGoals = goals.filter((g) => g.status === 'active' || g.status === 'on_track');
const overdueTasks = tasks.filter((t) => t.dueDate && new Date(t.dueDate) < now && t.status !== 'done');
const pendingDecisions = decisions.filter((d) => !d.selectedOption);
const thisMonthTx = transactions.filter((t) => t.transactionDate.startsWith(now.toISOString().slice(0, 7)));
const thisMonthRevenue = thisMonthTx
.filter((t) => t.type === 'income' || t.type === 'revenue')
.reduce((s, t) => s + Number(t.amountMinorUnits), 0);
const REPORT_LINKS = [
{ label: 'Business Reports', href: `${B}/reports/business`, desc: 'Proiecte, pipeline, venituri, clienți.' },
{ label: 'Financial Reports', href: `${B}/reports/financial`, desc: 'Tranzacții, cash flow, conturi.' },
{ label: 'Accountant Reports', href: `${B}/reports/accountant`, desc: 'Pachet contabil cu dovezi și gaps.' },
{ label: 'Decision Reports', href: `${B}/reports/decisions`, desc: 'Decizii, scenarii, estimări.' },
];
return (
<div className="max-w-5xl space-y-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Rapoarte Executive</h1>
<p className="mt-1 text-sm text-ink-faint">
Sumar executiv generat automat din datele platformei.{' '}
{briefing?.date && (
<span>Briefing: {new Date(briefing.date).toLocaleDateString('ro-RO', { weekday: 'long', day: 'numeric', month: 'long' })}</span>
)}
</p>
</div>
{/* KPI snapshot */}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{[
{ label: 'Obiective active', value: activeGoals.length, color: 'text-ink', href: `${B}/goals` },
{ label: 'Task-uri restante', value: overdueTasks.length, color: overdueTasks.length > 0 ? 'text-signal-danger' : 'text-ink', href: `${B}/tasks` },
{ label: 'Decizii în așteptare', value: pendingDecisions.length, color: pendingDecisions.length > 0 ? 'text-signal-warn' : 'text-ink', href: `${B}/decisions` },
{ label: 'Venituri luna curentă', value: thisMonthRevenue > 0 ? `${(thisMonthRevenue / 100).toLocaleString('ro-RO')}` : '—', color: 'text-signal-ok', href: `${B}/finance` },
].map((kpi) => (
<Link key={kpi.label} href={kpi.href} className="card p-5 hover:border-bronze-deep transition-colors">
<p className="text-xs font-medium uppercase tracking-wider text-ink-faint">{kpi.label}</p>
<p className={`mt-1 font-display text-3xl font-semibold ${kpi.color}`}>{kpi.value}</p>
</Link>
))}
</div>
{/* Briefing sections */}
{briefing?.sections && briefing.sections.length > 0 && (
<section className="card p-5">
<h2 className="mb-4 text-xs font-semibold uppercase tracking-wider text-ink-faint">
Briefing zilnic
</h2>
<div className="space-y-4">
{briefing.sections.map((sec, i) => (
<div key={i}>
<p className="text-xs font-semibold text-ink">{sec.title}</p>
<ul className="mt-1 space-y-0.5">
{sec.items.map((item, j) => (
<li key={j} className="text-sm text-ink-faint">· {item}</li>
))}
</ul>
</div>
))}
</div>
</section>
)}
{/* Goals status */}
{goals.length > 0 && (
<section className="card p-5">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">
Obiective ({goals.length})
</h2>
<Link href={`${B}/goals`} className="text-xs text-bronze-deep hover:underline">Vezi toate </Link>
</div>
<ul className="space-y-2">
{goals.slice(0, 5).map((g) => (
<li key={g.id} className="flex items-center justify-between gap-4">
<p className="min-w-0 flex-1 truncate text-sm text-ink">{g.title}</p>
<span className={`shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium capitalize ${
g.status === 'achieved' ? 'bg-signal-ok text-white' :
g.status === 'at_risk' ? 'bg-signal-warn text-white' :
'bg-paper-sunken text-ink-faint'
}`}>{g.status.replace(/_/g, ' ')}</span>
</li>
))}
</ul>
</section>
)}
{/* Report type links */}
<section>
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-ink-faint">
Tipuri de rapoarte
</h2>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{REPORT_LINKS.map((r) => (
<Link key={r.label} href={r.href} className="card flex items-start gap-3 px-4 py-3 hover:border-bronze-deep transition-colors">
<div>
<p className="text-sm font-medium text-ink">{r.label}</p>
<p className="mt-0.5 text-xs text-ink-faint">{r.desc}</p>
</div>
<span className="ml-auto shrink-0 text-ink-line"></span>
</Link>
))}
</div>
</section>
</div>
);
}