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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user