Files
butcem/src/app/(app)/raporlar/page.tsx
T
suphicakir c0b114efeb feat(ui): Kakeibo grupları — kategori, işlem formu, rapor ve bütçe özeti
- kategori formunda grup ve banka maliyeti; listede grup rozeti ve grupsuz uyarısı
- işlem formunda "Kategoriden: …" önerili grup seçimi, bölünmüş kalem başına
- /raporlar/kakeibo (yığılmış pay çubuğu, grup kartları, banka maliyeti, grupsuzlar, CSV)
- bütçe sayfasında Kakeibo özeti

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 04:51:35 +03:00

120 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
BookOpenCheck,
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/kakeibo", key: "kakeibo", icon: BookOpenCheck },
{ 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>
</>
);
}