c3c3c43bd1
- dönem seçici, tablolar, birim fiyat grafiği ve CSV indirme - rapor ana sayfasında yeni kartlar Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
4.7 KiB
TypeScript
118 lines
4.7 KiB
TypeScript
import {
|
||
ChartPie,
|
||
ChevronRight,
|
||
Coins,
|
||
HandCoins,
|
||
ShoppingCart,
|
||
Tags,
|
||
Target,
|
||
Trash2,
|
||
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 },
|
||
{ href: "/raporlar/cari", key: "contacts", icon: HandCoins },
|
||
{ href: "/raporlar/birikim", key: "assets", icon: Coins },
|
||
{ href: "/raporlar/alisveris", key: "shopping", icon: ShoppingCart },
|
||
{ href: "/raporlar/birim-fiyat", key: "unitPrice", icon: Tags },
|
||
{ href: "/raporlar/fire", key: "waste", icon: Trash2 },
|
||
] 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>
|
||
</>
|
||
);
|
||
}
|