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:
@@ -1,17 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { CartesianGrid, Line, LineChart, ReferenceLine, XAxis, YAxis } from "recharts";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/components/ui/chart";
|
||||
import { tr } from "@/i18n/tr";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatDate, formatMoney } from "@/shared/format";
|
||||
@@ -19,24 +10,13 @@ import type { DailyPlanView as Data } from "../plan";
|
||||
|
||||
const t = tr.budget.daily;
|
||||
const d = (ymd: string) => formatDate(`${ymd}T12:00:00+03:00`);
|
||||
const dayFmt = new Intl.DateTimeFormat("tr-TR", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
timeZone: "Europe/Istanbul",
|
||||
});
|
||||
const monthFmt = new Intl.DateTimeFormat("tr-TR", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
timeZone: "Europe/Istanbul",
|
||||
});
|
||||
const compact = new Intl.NumberFormat("tr-TR", { notation: "compact", maximumFractionDigits: 1 });
|
||||
const signed = (minor: number) => `${minor < 0 ? "−" : ""}${formatMoney(Math.abs(minor))}`;
|
||||
|
||||
const config = {
|
||||
planned: { label: t.cumPlanned, color: "var(--budget)" },
|
||||
actual: { label: t.cumActual, color: "var(--expense)" },
|
||||
} satisfies ChartConfig;
|
||||
|
||||
function Stat({
|
||||
label,
|
||||
value,
|
||||
@@ -57,7 +37,7 @@ function Stat({
|
||||
);
|
||||
}
|
||||
|
||||
/** Dashboard: ayın günlük planı, kümülatif grafik ve gelirle karşılaştırma. */
|
||||
/** Dashboard: ayın günlük planı ve gelirle karşılaştırma (grafik ana grafikte: Aylık). */
|
||||
export function DailyPlanView({ data }: { data: Data }) {
|
||||
const p = data.plan;
|
||||
if (!data.hasBudget) {
|
||||
@@ -75,14 +55,6 @@ export function DailyPlanView({ data }: { data: Data }) {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
const chart = p.days.map((day, i) => ({
|
||||
date: day.date,
|
||||
label: dayFmt.format(new Date(`${day.date}T12:00:00+03:00`)),
|
||||
planned: day.cumPlannedMinor / 100,
|
||||
// Gerçekleşen yalnızca bugüne kadar çizilir.
|
||||
actual: p.todayIndex === -1 || i <= p.todayIndex ? day.cumActualMinor / 100 : null,
|
||||
}));
|
||||
const income = data.income.expectedMinor / 100;
|
||||
const nextDiff =
|
||||
data.next.budgetMinor === null ? null : data.next.expectedIncomeMinor - data.next.budgetMinor;
|
||||
|
||||
@@ -116,82 +88,6 @@ export function DailyPlanView({ data }: { data: Data }) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ChartContainer
|
||||
config={config}
|
||||
className="aspect-auto h-56 w-full"
|
||||
aria-label={t.chartLabel}
|
||||
role="img"
|
||||
>
|
||||
<LineChart data={chart} margin={{ left: 4, right: 12, top: 8 }}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={24}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={48}
|
||||
tickFormatter={(v: number) => compact.format(v)}
|
||||
/>
|
||||
{income > 0 ? (
|
||||
<ReferenceLine
|
||||
y={income}
|
||||
stroke="var(--income)"
|
||||
strokeDasharray="4 4"
|
||||
label={{
|
||||
value: `${t.income} ${compact.format(income)}`,
|
||||
position: "insideTopLeft",
|
||||
fill: "var(--muted-foreground)",
|
||||
fontSize: 11,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, payload) => {
|
||||
const date = payload?.[0]?.payload?.date as string | undefined;
|
||||
return date ? d(date) : "";
|
||||
}}
|
||||
formatter={(value, name) => (
|
||||
<div className="flex w-full items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">
|
||||
{config[name as keyof typeof config]?.label}
|
||||
</span>
|
||||
<span className="font-medium text-foreground tabular-nums">
|
||||
{formatMoney(Math.round(Number(value) * 100))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<Line
|
||||
dataKey="planned"
|
||||
type="linear"
|
||||
stroke="var(--color-planned)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Line
|
||||
dataKey="actual"
|
||||
type="linear"
|
||||
stroke="var(--color-actual)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 4 }}
|
||||
connectNulls={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t.modes[data.mode]} {p.fixedMinor ? t.fixedNote(formatMoney(p.fixedMinor)) : ""}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user