feat(cc-051): Executive Reports summary page
This commit is contained in:
parent
006a4f0562
commit
12727faa30
1 changed files with 155 additions and 0 deletions
155
src/app/dashboard/reports/page.tsx
Normal file
155
src/app/dashboard/reports/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue