29d7b29cfa
- 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>
197 lines
7.1 KiB
TypeScript
197 lines
7.1 KiB
TypeScript
"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>
|
||
);
|
||
}
|