@@ -69,6 +74,23 @@ export async function LowStockCard({
))}
+ {data.countDue.slice(0, 3).map((g) => (
+
+
+ {g.path}
+
+
+
+ {t.count.dueBadge(g.overdueDays)}
+
+
+ ))}
{data.suggestions.length > low.length ? (
diff --git a/src/features/stock/components/product-detail-view.tsx b/src/features/stock/components/product-detail-view.tsx
index ce9a43e..a957398 100644
--- a/src/features/stock/components/product-detail-view.tsx
+++ b/src/features/stock/components/product-detail-view.tsx
@@ -37,7 +37,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { tr } from "@/i18n/tr";
import { cn } from "@/lib/utils";
-import { formatStock } from "@/shared/stock";
+import { formatStock, roundForDisplay } from "@/shared/stock";
import { deleteStockMovementAction, setProductTypeArchivedAction } from "../actions";
import type { ProductTypeDetail, StockMovementRow, StockOption } from "../service";
import {
@@ -46,7 +46,14 @@ import {
StockMovementDialog,
type MovementKindInput,
} from "./stock-dialogs";
-import { ExpiryBadge, LevelBadge, QuickConsumeButton, StockBar, ymdLabel } from "./stock-view";
+import {
+ ExpiryBadge,
+ LevelBadge,
+ QuickConsumeButton,
+ StockBar,
+ suggestionReasons,
+ ymdLabel,
+} from "./stock-view";
const t = tr.stock;
@@ -156,6 +163,53 @@ export function ProductDetailView({
{q(data.suggestedQuantity)}
+ {suggestionReasons(data).length ? (
+
+ {suggestionReasons(data).join(" ")}
+
+ ) : null}
+
+
+ {data.suggestion.usage ? (
+ t.detail.perWeek(q(roundForDisplay(data.suggestion.usage.perDay * 7)))
+ ) : (
+ {t.detail.noUsage}
+ )}
+
+
+ {data.count ? (
+
+ {t.detail.countEvery(data.count.everyDays)}
+
+ {data.count.due
+ ? tr.stock.count.dueBadge(data.count.overdueDays)
+ : t.detail.nextCount(ymdLabel(data.count.nextDate))}
+ {data.count.lastDate
+ ? ` · ${tr.stock.count.lastCount(ymdLabel(data.count.lastDate))}`
+ : ""}
+
+
+ ) : (
+
+ {t.detail.noCountSchedule}
+
+ )}
+
+
{data.location ? (
{t.detail.location}: {data.location}
diff --git a/src/features/stock/components/stock-analysis-view.tsx b/src/features/stock/components/stock-analysis-view.tsx
new file mode 100644
index 0000000..448ef5d
--- /dev/null
+++ b/src/features/stock/components/stock-analysis-view.tsx
@@ -0,0 +1,181 @@
+import { ArrowLeft } from "lucide-react";
+import Link from "next/link";
+import { PageHeader } from "@/components/page-header";
+import { Button } from "@/components/ui/button";
+import { tr } from "@/i18n/tr";
+import { cn } from "@/lib/utils";
+import { formatStock } from "@/shared/stock";
+import type { StockAnalysis, StockAnalysisRow } from "../service";
+
+const t = tr.stock.analysis;
+/** Bu oranın (%) üstündeki fire vurgulanır. */
+const HIGH_WASTE_PCT = 15;
+
+const monthShort = (month: string) =>
+ new Intl.DateTimeFormat("tr-TR", { month: "short" }).format(new Date(`${month}-15T12:00:00Z`));
+const monthLong = (month: string) =>
+ new Intl.DateTimeFormat("tr-TR", { month: "long", year: "numeric" }).format(
+ new Date(`${month}-15T12:00:00Z`),
+ );
+
+/** Ürünün kendi ölçeğinde son aylar: tüketim (alt) + fire (üst), taban çizgisine oturur. */
+function Trend({ row }: { row: StockAnalysisRow }) {
+ const max = Math.max(1, ...row.months.map((m) => m.used + m.wasted));
+ return (
+
+ {row.months.map((m) => {
+ const total = m.used + m.wasted;
+ const label = t.monthValue(
+ monthLong(m.month),
+ formatStock(m.used, row.unit),
+ formatStock(m.wasted, row.unit),
+ );
+ return (
+
+ {m.wasted > 0 ? (
+
+ ) : null}
+ {m.used > 0 ? (
+
0 ? "" : "rounded-t-[3px]")}
+ style={{ height: `${(m.used / max) * 100}%`, minHeight: 2 }}
+ />
+ ) : null}
+ {total === 0 ? {label} : null}
+
+ );
+ })}
+
+ );
+}
+
+function Num({ value, unit, tone }: { value: number | null; unit: string; tone?: string }) {
+ return (
+
+ {value === null ? t.noData : formatStock(value, unit)}
+
+ );
+}
+
+/** /stok/analiz: ürün tipi başına aylık ortalama tüketim, fire ve son ayların eğilimi. */
+export function StockAnalysisView({ data }: { data: StockAnalysis }) {
+ const groups = new Map
();
+ for (const r of data.rows) groups.set(r.categoryPath, [...(groups.get(r.categoryPath) ?? []), r]);
+ const first = data.months[0]!;
+ const last = data.months.at(-1)!;
+
+ return (
+
+
+
+
+ {t.back}
+
+
+
+
+ {data.rows.length === 0 ? (
+
+ {t.empty}
+
+ ) : (
+ <>
+
+
+ {t.trend(data.months.length)}: {monthShort(first)} – {monthShort(last)}
+
+
+
+ {tr.stock.kinds.consume}
+
+
+
+ {tr.stock.kinds.waste}
+
+
+
+
+
+ {t.product}
+ {t.trend(data.months.length)}
+ {t.avgUsed}
+ {t.avgWasted}
+ {t.wastePct}
+ {t.thisMonth}
+
+ {[...groups.entries()].map(([path, rows]) => (
+
+ ))}
+
+ >
+ )}
+
+ );
+}
diff --git a/src/features/stock/components/stock-view.tsx b/src/features/stock/components/stock-view.tsx
index 52a9640..915f4ff 100644
--- a/src/features/stock/components/stock-view.tsx
+++ b/src/features/stock/components/stock-view.tsx
@@ -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 (
@@ -144,6 +164,18 @@ export function StockView({ data, options }: { data: Data; options: StockOption[
return (
+
+
+
+ {t.analysisLink}
+
+
+
+
+
+ {t.countLink}
+
+
@@ -182,26 +214,76 @@ export function StockView({ data, options }: { data: Data; options: StockOption[
{t.suggestions.empty}
) : (
- {data.suggestions.map((i) => (
-
-
- {i.name}
-
- {t.list.of(
- formatStock(Math.max(0, i.current), i.unit),
- formatStock(i.targetQuantity, i.unit),
- )}
-
-
-
- {t.suggestions.suggested(formatStock(i.suggestedQuantity, i.unit))}
-
-
- ))}
+ {data.suggestions.map((i) => {
+ const reasons = suggestionReasons(i);
+ return (
+
+
+
+ {i.name}
+
+ {t.list.of(
+ formatStock(Math.max(0, i.current), i.unit),
+ formatStock(i.targetQuantity, i.unit),
+ )}
+
+
+
+ {t.suggestions.suggested(formatStock(i.suggestedQuantity, i.unit))}
+
+
+ {reasons.length ? (
+
+ {reasons.join(" ")}
+
+ ) : null}
+
+ );
+ })}
)}
+ {data.countDue.length ? (
+
+
+ {t.countDue.title}
+ {t.countDue.description}
+
+
+
+
+
+ ) : null}
{data.expiring.length ? (