feat(ui): dashboard ana grafiği ve rapor sayfaları
- dashboard: periyot seçici ve kümülatif/dönemsel görünüm, tablo; günlük plan kartı sadeleşti - /raporlar özet ve kategori, trend, kişi/hesap, bütçe sayfaları; ay seçici ve CSV indirme Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { ChartPie, ChevronRight, Target, TrendingUp, Users } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { monthLabel, formatPct } from "@/features/reports/components/report-frame";
|
||||
import { getCategoryReport, getTrendReport } from "@/features/reports/service";
|
||||
import { tr } from "@/i18n/tr";
|
||||
import { requireActiveHousehold } from "@/lib/dal";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatMoney } from "@/shared/format";
|
||||
|
||||
const t = tr.reports;
|
||||
export const metadata: Metadata = { title: t.title };
|
||||
|
||||
const REPORTS = [
|
||||
{ href: "/raporlar/kategoriler", key: "categories", icon: ChartPie },
|
||||
{ href: "/raporlar/trend", key: "trend", icon: TrendingUp },
|
||||
{ href: "/raporlar/kisi-hesap", key: "people", icon: Users },
|
||||
{ href: "/raporlar/butce", key: "budget", icon: Target },
|
||||
] as const;
|
||||
|
||||
/** /raporlar — bu bütçe ayının özeti ve raporlar. */
|
||||
export default async function ReportsPage() {
|
||||
const { user, household } = await requireActiveHousehold("viewer");
|
||||
const [trend, cats] = await Promise.all([
|
||||
getTrendReport(user.id, household.id, 1),
|
||||
getCategoryReport(user.id, household.id),
|
||||
]);
|
||||
if (!trend || !cats) notFound();
|
||||
const m = trend.months[0]!;
|
||||
const top = cats.breakdown.rows.filter((r) => r.amountMinor > 0).slice(0, 3);
|
||||
const stats = [
|
||||
{ label: t.hub.income, value: formatMoney(m.incomeMinor), tone: "text-income" },
|
||||
{ label: t.hub.expense, value: formatMoney(m.expenseMinor), tone: "text-expense" },
|
||||
{
|
||||
label: t.hub.net,
|
||||
value: `${m.netMinor < 0 ? "−" : ""}${formatMoney(Math.abs(m.netMinor))}`,
|
||||
tone: m.netMinor < 0 ? "text-expense" : undefined,
|
||||
},
|
||||
{ label: t.hub.savings, value: formatPct(m.savingsPct) },
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={t.title} description={t.description} />
|
||||
<section aria-labelledby="hub-month" className="rounded-xl border bg-card p-4">
|
||||
<h2 id="hub-month" className="text-sm font-medium">
|
||||
{t.hub.thisMonth}: <span className="capitalize">{monthLabel(m.month)}</span>{" "}
|
||||
<span className="font-normal text-muted-foreground">({t.trend.ongoing})</span>
|
||||
</h2>
|
||||
<dl className="mt-3 grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
{stats.map((s) => (
|
||||
<div key={s.label}>
|
||||
<dt className="text-xs text-muted-foreground">{s.label}</dt>
|
||||
<dd className={cn("text-lg font-semibold tabular-nums", s.tone)}>{s.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
{top.length ? (
|
||||
<div className="mt-4">
|
||||
<h3 className="text-xs text-muted-foreground">{t.hub.topCategories}</h3>
|
||||
<ul className="mt-1 flex flex-col gap-1 text-sm">
|
||||
{top.map((r) => (
|
||||
<li key={r.id} className="flex justify-between gap-3">
|
||||
<span className="truncate">{r.name}</span>
|
||||
<span className="tabular-nums">
|
||||
{formatMoney(r.amountMinor)}{" "}
|
||||
<span className="text-muted-foreground">· {formatPct(r.sharePct)}</span>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
<ul className="grid gap-3 md:grid-cols-2">
|
||||
{REPORTS.map((r) => {
|
||||
const c = t.hub.cards[r.key];
|
||||
const Icon = r.icon;
|
||||
return (
|
||||
<li key={r.href}>
|
||||
<Link
|
||||
href={r.href}
|
||||
className="flex h-full items-start gap-3 rounded-xl border bg-card p-4 transition-colors hover:bg-accent/40 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
>
|
||||
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted">
|
||||
<Icon className="size-4.5 text-muted-foreground" aria-hidden />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block font-medium">{c.title}</span>
|
||||
<span className="block text-sm text-muted-foreground">{c.description}</span>
|
||||
</span>
|
||||
<ChevronRight className="mt-2 size-4 text-muted-foreground" aria-hidden />
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user