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>
179 lines
6.4 KiB
TypeScript
179 lines
6.4 KiB
TypeScript
"use client";
|
||
|
||
import { useRouter } from "next/navigation";
|
||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||
import {
|
||
ChartContainer,
|
||
ChartLegend,
|
||
ChartLegendContent,
|
||
ChartTooltip,
|
||
ChartTooltipContent,
|
||
type ChartConfig,
|
||
} from "@/components/ui/chart";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { tr } from "@/i18n/tr";
|
||
import { formatUnitPrice, unitPriceBase } from "@/shared/prices";
|
||
import type { UnitPriceReport } from "../extra";
|
||
import { dayLabel, monthLabel, monthShort } from "./report-frame";
|
||
|
||
const t = tr.reports.unitPrice;
|
||
const config = {
|
||
min: { label: t.min, color: "var(--income)" },
|
||
avg: { label: t.avg, color: "var(--budget)" },
|
||
} satisfies ChartConfig;
|
||
|
||
/** Ürün tipi seçimi (URL'de) ve aylık en düşük / ortalama birim fiyat. */
|
||
export function UnitPriceView({ data }: { data: UnitPriceReport }) {
|
||
const router = useRouter();
|
||
const sel = data.selected;
|
||
if (!sel)
|
||
return (
|
||
<p className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||
{t.noTypes}
|
||
</p>
|
||
);
|
||
const base = unitPriceBase(sel.unit);
|
||
// Gösterim biriminde TL (g → kg, ml → lt).
|
||
const tl = (minor: number | null) => (minor === null ? null : (minor * base.factor) / 100);
|
||
const chart = sel.points.map((p) => ({
|
||
month: p.month,
|
||
label: monthShort(p.month),
|
||
min: tl(p.minMinor),
|
||
avg: tl(p.avgMinor),
|
||
}));
|
||
const fmt = (minor: number | null) => (minor === null ? "—" : formatUnitPrice(minor, sel.unit));
|
||
|
||
return (
|
||
<div className="flex flex-col gap-4">
|
||
<div className="flex items-center gap-2">
|
||
<label htmlFor="up-type" className="text-sm text-muted-foreground">
|
||
{t.type}
|
||
</label>
|
||
<Select
|
||
value={sel.id}
|
||
onValueChange={(id) => router.replace(`/raporlar/birim-fiyat?tip=${id}`)}
|
||
>
|
||
<SelectTrigger id="up-type" className="w-64">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent className="max-h-72">
|
||
{data.types.map((x) => (
|
||
<SelectItem key={x.id} value={x.id}>
|
||
{x.name}{" "}
|
||
<span className="text-xs text-muted-foreground">({t.records(x.records)})</span>
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="rounded-xl border bg-card p-4">
|
||
<ChartContainer
|
||
config={config}
|
||
className="aspect-auto h-60 w-full"
|
||
role="img"
|
||
aria-label={t.chartLabel(sel.name)}
|
||
>
|
||
<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={56}
|
||
tickFormatter={(v: number) =>
|
||
`₺${v.toLocaleString("tr-TR", { maximumFractionDigits: 2 })}`
|
||
}
|
||
/>
|
||
<ChartTooltip
|
||
content={
|
||
<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">
|
||
{config[name as keyof typeof config]?.label}
|
||
</span>
|
||
<span className="font-medium text-foreground tabular-nums">
|
||
{formatUnitPrice((Number(value) * 100) / base.factor, sel.unit)}
|
||
</span>
|
||
</div>
|
||
)}
|
||
/>
|
||
}
|
||
/>
|
||
<ChartLegend content={<ChartLegendContent />} />
|
||
{(["min", "avg"] as const).map((k) => (
|
||
<Line
|
||
key={k}
|
||
dataKey={k}
|
||
stroke={`var(--color-${k})`}
|
||
strokeWidth={2}
|
||
strokeDasharray={k === "avg" ? "6 3" : undefined}
|
||
dot={{ r: 3 }}
|
||
connectNulls
|
||
isAnimationActive={false}
|
||
/>
|
||
))}
|
||
</LineChart>
|
||
</ChartContainer>
|
||
<table className="mt-3 w-full text-sm tabular-nums">
|
||
<thead className="text-xs text-muted-foreground">
|
||
<tr>
|
||
<th className="py-1 text-left font-normal">{t.month}</th>
|
||
<th className="py-1 text-right font-normal">{t.min}</th>
|
||
<th className="py-1 text-right font-normal">{t.avg}</th>
|
||
<th className="py-1 text-right font-normal">{t.count}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y">
|
||
{[...sel.points].reverse().map((p) => (
|
||
<tr key={p.month}>
|
||
<td className="py-1 capitalize">{monthLabel(p.month)}</td>
|
||
<td className="py-1 text-right">{fmt(p.minMinor)}</td>
|
||
<td className="py-1 text-right">{fmt(p.avgMinor)}</td>
|
||
<td className="py-1 text-right">{p.count}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{sel.stores.length ? (
|
||
<section className="rounded-xl border bg-card p-4" aria-labelledby="up-stores">
|
||
<h2 id="up-stores" className="mb-2 text-sm font-medium">
|
||
{t.storesTitle}
|
||
</h2>
|
||
<table className="w-full text-sm tabular-nums">
|
||
<thead className="text-xs text-muted-foreground">
|
||
<tr>
|
||
<th className="py-1 text-left font-normal">{t.store}</th>
|
||
<th className="py-1 text-right font-normal">{t.min}</th>
|
||
<th className="py-1 text-right font-normal">{t.last}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y">
|
||
{sel.stores.map((s) => (
|
||
<tr key={s.id}>
|
||
<td className="py-1">{s.label}</td>
|
||
<td className="py-1 text-right">{fmt(s.minMinor)}</td>
|
||
<td className="py-1 text-right">{dayLabel(s.lastDate)}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|