feat(ui): sayım ekranı, tüketim analizi ve sayım hatırlatmaları

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 10:58:12 +03:00
parent 814bdbbc2a
commit 07ba531d31
8 changed files with 749 additions and 24 deletions
+99 -17
View File
@@ -1,6 +1,15 @@
"use client";
import { AlertTriangle, ChevronRight, Clock, FolderTree, Plus, Search } from "lucide-react";
import {
AlertTriangle,
ChartColumn,
ChevronRight,
ClipboardCheck,
Clock,
FolderTree,
Plus,
Search,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useMemo, useState, useTransition } from "react";
@@ -111,6 +120,17 @@ export function QuickConsumeButton({ item }: { item: ProductTypeView }) {
);
}
/** Önerilen miktarın gerekçesi: fire indirimi, tüketim hızı yuvarlaması, kaç gün yeter. */
export function suggestionReasons(item: ProductTypeView): string[] {
const s = item.suggestion;
if (s.quantity === 0) return [];
const out: string[] = [];
if (s.waste) out.push(t.suggestions.wasteReduced(s.waste.purchases, s.waste.pct));
if (s.roundedByUsage) out.push(t.suggestions.roundedByUsage(formatStock(s.roundTo, item.unit)));
if (s.coverDays !== null) out.push(t.suggestions.covers(s.coverDays));
return out;
}
function Stat({ label, value, tone }: { label: string; value: number; tone?: string }) {
return (
<div className="min-w-0">
@@ -144,6 +164,18 @@ export function StockView({ data, options }: { data: Data; options: StockOption[
return (
<div className="flex flex-col gap-4">
<div className="flex flex-wrap justify-end gap-2">
<Button variant="ghost" size="sm" asChild>
<Link href="/stok/analiz">
<ChartColumn />
{t.analysisLink}
</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href="/stok/sayim">
<ClipboardCheck />
{t.countLink}
</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href="/stok/kategoriler">
<FolderTree />
@@ -182,26 +214,76 @@ export function StockView({ data, options }: { data: Data; options: StockOption[
<p className="text-sm text-muted-foreground">{t.suggestions.empty}</p>
) : (
<ul className="divide-y text-sm">
{data.suggestions.map((i) => (
<li key={i.id} className="flex items-center justify-between gap-3 py-2">
<Link href={`/stok/${i.id}`} className="min-w-0 hover:underline">
<span className="block truncate font-medium">{i.name}</span>
<span className="block text-xs text-muted-foreground tabular-nums">
{t.list.of(
formatStock(Math.max(0, i.current), i.unit),
formatStock(i.targetQuantity, i.unit),
)}
</span>
</Link>
<span className="shrink-0 text-right font-medium tabular-nums">
{t.suggestions.suggested(formatStock(i.suggestedQuantity, i.unit))}
</span>
</li>
))}
{data.suggestions.map((i) => {
const reasons = suggestionReasons(i);
return (
<li key={i.id} className="flex flex-col gap-0.5 py-2">
<div className="flex items-center justify-between gap-3">
<Link href={`/stok/${i.id}`} className="min-w-0 hover:underline">
<span className="block truncate font-medium">{i.name}</span>
<span className="block text-xs text-muted-foreground tabular-nums">
{t.list.of(
formatStock(Math.max(0, i.current), i.unit),
formatStock(i.targetQuantity, i.unit),
)}
</span>
</Link>
<span className="shrink-0 text-right font-medium tabular-nums">
{t.suggestions.suggested(formatStock(i.suggestedQuantity, i.unit))}
</span>
</div>
{reasons.length ? (
<p
className={cn(
"text-xs",
i.suggestion.waste
? "text-amber-700 dark:text-amber-400"
: "text-muted-foreground",
)}
>
{reasons.join(" ")}
</p>
) : null}
</li>
);
})}
</ul>
)}
</CardContent>
</Card>
{data.countDue.length ? (
<Card size="sm">
<CardHeader>
<CardTitle className="text-base">{t.countDue.title}</CardTitle>
<CardDescription>{t.countDue.description}</CardDescription>
</CardHeader>
<CardContent>
<ul className="divide-y text-sm">
{data.countDue.map((g) => (
<li key={g.categoryId} className="flex items-center justify-between gap-3 py-2">
<div className="min-w-0">
<span className="block truncate font-medium">{g.path}</span>
<span className="block text-xs text-muted-foreground">
{t.countDue.products(g.products)} ·{" "}
<span className="text-amber-700 dark:text-amber-400">
{t.countDue.overdue(g.overdueDays)}
</span>
</span>
</div>
{data.canWrite ? (
<Button size="sm" variant="outline" asChild>
<Link href={`/stok/sayim?kategori=${g.categoryId}`}>
<ClipboardCheck />
{t.countDue.start}
</Link>
</Button>
) : null}
</li>
))}
</ul>
</CardContent>
</Card>
) : null}
{data.expiring.length ? (
<Card size="sm">
<CardHeader>