"use client"; import { ArrowLeft, ClipboardCheck, Eraser, ListChecks } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useMemo, useState, useTransition } from "react"; import { toast } from "sonner"; import { PageHeader } from "@/components/page-header"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { tr } from "@/i18n/tr"; import { cn } from "@/lib/utils"; import { parseQuantity } from "@/shared/quantity"; import { formatStock, quantityInput, WASTE_REASONS, type WasteReason } from "@/shared/stock"; import { submitCountAction } from "../actions"; import type { CountSheet as Data, CountSheetRow } from "../service"; import { ymdLabel } from "./stock-view"; const t = tr.stock; const ALL = "__all__"; type Entry = { actual: string; waste: string; reason: WasteReason | "" }; const EMPTY: Entry = { actual: "", waste: "", reason: "" }; function signed(q: number, unit: string) { if (q === 0) return t.count.same; return `${q > 0 ? "+" : "−"}${formatStock(Math.abs(q), unit)}`; } /** /stok/sayim — liste hâlinde beklenen → sayılan; fark sayım düzeltmesi, bir kısmı fire. */ export function CountSheet({ data }: { data: Data }) { const router = useRouter(); const [entries, setEntries] = useState>({}); const [errors, setErrors] = useState>({}); const [onlyDue, setOnlyDue] = useState(data.rows.some((r) => r.count?.due)); const [note, setNote] = useState(""); const [pending, start] = useTransition(); const rows = useMemo( () => (onlyDue ? data.rows.filter((r) => r.count?.due) : data.rows), [data.rows, onlyDue], ); const groups = useMemo(() => { const by = new Map(); for (const r of rows) { const g = by.get(r.categoryId) ?? { path: r.categoryPath, rows: [] }; g.rows.push(r); by.set(r.categoryId, g); } return [...by.values()]; }, [rows]); const dueTotal = data.rows.filter((r) => r.count?.due).length; const filled = data.rows.filter((r) => entries[r.id]?.actual.trim()).length; const set = (id: string, patch: Partial) => setEntries((e) => ({ ...e, [id]: { ...(e[id] ?? EMPTY), ...patch } })); const fillExpected = () => setEntries((e) => { const next = { ...e }; for (const r of rows) if (!next[r.id]?.actual.trim()) next[r.id] = { ...(next[r.id] ?? EMPTY), actual: quantityInput(Math.max(0, r.expected)) }; return next; }); const submit = () => start(async () => { const sent = data.rows.filter((r) => entries[r.id]?.actual.trim()); const r = await submitCountAction({ date: data.today, categoryId: data.category?.id ?? "", note, rows: sent.map((row) => { const e = entries[row.id]!; // Fire alanı yalnızca sayımda eksik çıkınca geçerlidir. const actual = parseQuantity(e.actual); const short = actual !== null && actual < row.expected; return { productTypeId: row.id, actualQuantity: e.actual, wasteQuantity: short ? e.waste : "", wasteReason: short ? e.reason : "", }; }), }); if (r.ok) { const d = r.data as { counted: number; adjusted: number }; toast.success(t.count.done(d.counted, d.adjusted)); setEntries({}); setErrors({}); setNote(""); router.refresh(); return; } const next: Record = {}; for (const [key, msg] of Object.entries(r.fieldErrors ?? {})) { const m = /^rows\.(\d+)\.(\w+)$/.exec(key); const row = m ? sent[Number(m[1])] : undefined; if (row && msg) next[`${row.id}.${m![2]}`] = msg; } setErrors(next); toast.error(r.fieldErrors?.rows ?? r.error); }); return (
{dueTotal > 0 ? ( ) : null} {data.canWrite && rows.length ? (
{filled ? ( ) : null}
) : null}
{rows.length === 0 ? (

{t.count.empty}

) : null} {groups.map((g) => (

{g.path}

    {g.rows.map((r) => ( set(r.id, patch)} /> ))}
))} {data.canWrite && data.rows.length ? (
setNote(e.target.value)} placeholder={t.count.note} aria-label={t.count.note} maxLength={300} className="min-w-40 flex-1" /> {t.count.counted(filled)}
) : null}
); } function CountRow({ row, entry, errors, disabled, onChange, }: { row: CountSheetRow; entry: Entry; errors: Record; disabled: boolean; onChange: (patch: Partial) => void; }) { const actual = entry.actual.trim() ? parseQuantity(entry.actual) : null; const diff = actual === null ? null : actual - row.expected; const shortage = diff !== null && diff < 0 ? -diff : 0; const actualError = errors[`${row.id}.actualQuantity`]; const wasteError = errors[`${row.id}.wasteQuantity`] ?? errors[`${row.id}.wasteReason`]; const inputId = `count-${row.id}`; return (
  • {[ row.location, row.count ? row.count.lastDate ? t.count.lastCount(ymdLabel(row.count.lastDate)) : t.count.neverCounted : null, ] .filter(Boolean) .join(" · ")}

    {t.count.expected}

    {formatStock(Math.max(0, row.expected), row.unit)}
    {t.count.actual} ({row.unit}) onChange({ actual: e.target.value })} disabled={disabled} aria-invalid={Boolean(actualError) || (entry.actual.trim() !== "" && actual === null)} className="h-8 tabular-nums" />

    {t.count.diff}

    0 ? "text-income" : diff < 0 ? "text-expense" : "text-muted-foreground", )} > {diff === null ? "—" : signed(diff, row.unit)}
    {actualError ?

    {actualError}

    : null} {shortage > 0 ? (
    {t.count.markWaste}: onChange({ waste: e.target.value })} disabled={disabled} placeholder={t.count.wasteMax(formatStock(shortage, row.unit))} aria-label={`${row.name}: ${t.count.wasteQuantity}`} aria-invalid={Boolean(errors[`${row.id}.wasteQuantity`])} className="h-8 w-36 tabular-nums" /> {wasteError ?

    {wasteError}

    : null}
    ) : null}
  • ); }