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,196 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ReferenceLine,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/components/ui/chart";
|
||||
import { tr } from "@/i18n/tr";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatMoney } from "@/shared/format";
|
||||
import type { TrendReport } from "../service";
|
||||
import { monthLabel, monthShort, formatPct } from "./report-frame";
|
||||
|
||||
const t = tr.reports.trend;
|
||||
const compact = new Intl.NumberFormat("tr-TR", { notation: "compact", maximumFractionDigits: 1 });
|
||||
|
||||
const barConfig = {
|
||||
income: { label: t.income, color: "var(--income)" },
|
||||
expense: { label: t.expense, color: "var(--expense)" },
|
||||
} satisfies ChartConfig;
|
||||
const rateConfig = { rate: { label: t.savings, color: "var(--budget)" } } satisfies ChartConfig;
|
||||
|
||||
export function TrendView({ data }: { data: TrendReport }) {
|
||||
const chart = data.months.map((m) => ({
|
||||
month: m.month,
|
||||
label: monthShort(m.month),
|
||||
income: m.incomeMinor / 100,
|
||||
expense: m.expenseMinor / 100,
|
||||
// Devam eden ayın oranı (maaş henüz yatmamış olabilir) ölçeği bozmasın: grafikte yok.
|
||||
rate: m.month === data.currentMonth ? null : m.savingsPct,
|
||||
}));
|
||||
const moneyTip = (
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, payload) => {
|
||||
const m = payload?.[0]?.payload?.month as string | undefined;
|
||||
return m ? monthLabel(m) : "";
|
||||
}}
|
||||
formatter={(value, name) => (
|
||||
<div className="flex w-full items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">
|
||||
{barConfig[name as keyof typeof barConfig]?.label}
|
||||
</span>
|
||||
<span className="font-medium text-foreground tabular-nums">
|
||||
{formatMoney(Math.round(Number(value) * 100))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-xl border bg-card p-4">
|
||||
<ChartContainer
|
||||
config={barConfig}
|
||||
className="aspect-auto h-64 w-full"
|
||||
role="img"
|
||||
aria-label={t.chartLabel}
|
||||
>
|
||||
<BarChart data={chart} margin={{ left: 4, right: 8, top: 8 }} barGap={2}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis dataKey="label" tickLine={false} axisLine={false} tickMargin={8} />
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={48}
|
||||
tickFormatter={(v: number) => compact.format(v)}
|
||||
/>
|
||||
<ChartTooltip content={moneyTip} />
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<Bar
|
||||
dataKey="income"
|
||||
fill="var(--color-income)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="expense"
|
||||
fill="var(--color-expense)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-4">
|
||||
<h2 className="text-sm font-medium">{t.rateTitle}</h2>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
{t.rateHint} {t.partialHint}
|
||||
</p>
|
||||
<ChartContainer
|
||||
config={rateConfig}
|
||||
className="aspect-auto h-40 w-full"
|
||||
role="img"
|
||||
aria-label={t.rateChartLabel}
|
||||
>
|
||||
<LineChart data={chart} margin={{ left: 4, right: 8, top: 8 }}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis dataKey="label" tickLine={false} axisLine={false} tickMargin={8} />
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={40}
|
||||
tickFormatter={(v: number) => `%${v}`}
|
||||
/>
|
||||
<ReferenceLine y={0} stroke="var(--border)" />
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, payload) => {
|
||||
const m = payload?.[0]?.payload?.month as string | undefined;
|
||||
return m ? monthLabel(m) : "";
|
||||
}}
|
||||
formatter={(value) => (
|
||||
<span className="font-medium text-foreground tabular-nums">
|
||||
{formatPct(Number(value))}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
dataKey="rate"
|
||||
stroke="var(--color-rate)"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3 }}
|
||||
connectNulls={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border bg-card">
|
||||
<table className="w-full min-w-[32rem] text-sm tabular-nums">
|
||||
<thead className="border-b text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-normal">{t.month}</th>
|
||||
<th className="px-3 py-2 text-right font-normal">{t.income}</th>
|
||||
<th className="px-3 py-2 text-right font-normal">{t.expense}</th>
|
||||
<th className="px-3 py-2 text-right font-normal">{t.net}</th>
|
||||
<th className="px-3 py-2 text-right font-normal">{t.savings}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{[...data.months].reverse().map((m) => (
|
||||
<tr key={m.month} className={cn(m.month === data.currentMonth && "bg-muted/40")}>
|
||||
<td className="px-3 py-1.5">
|
||||
<span className="capitalize">{monthLabel(m.month)}</span>
|
||||
{m.month === data.currentMonth ? (
|
||||
<span className="ml-1 text-xs text-muted-foreground">({t.ongoing})</span>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="px-3 py-1.5 text-right">{formatMoney(m.incomeMinor)}</td>
|
||||
<td className="px-3 py-1.5 text-right">{formatMoney(m.expenseMinor)}</td>
|
||||
<td className={cn("px-3 py-1.5 text-right", m.netMinor < 0 && "text-expense")}>
|
||||
{m.netMinor < 0 ? "−" : ""}
|
||||
{formatMoney(Math.abs(m.netMinor))}
|
||||
</td>
|
||||
<td className="px-3 py-1.5 text-right">{formatPct(m.savingsPct)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot className="border-t font-medium">
|
||||
<tr>
|
||||
<td className="px-3 py-2">{t.total}</td>
|
||||
<td className="px-3 py-2 text-right">{formatMoney(data.totals.incomeMinor)}</td>
|
||||
<td className="px-3 py-2 text-right">{formatMoney(data.totals.expenseMinor)}</td>
|
||||
<td
|
||||
className={cn("px-3 py-2 text-right", data.totals.netMinor < 0 && "text-expense")}
|
||||
>
|
||||
{data.totals.netMinor < 0 ? "−" : ""}
|
||||
{formatMoney(Math.abs(data.totals.netMinor))}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">{formatPct(data.totals.savingsPct)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user