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>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user