Files
butcem/src/features/reports/components/unit-price-view.tsx
T
suphicakir c3c3c43bd1 feat(ui): cari, birikim, alışveriş, birim fiyat ve fire rapor sayfaları
- 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>
2026-11-07 00:53:02 +03:00

179 lines
6.4 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.
"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>
);
}