"use client"; import { ArrowLeft, Plus, Trash2 } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useMemo, useState } from "react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { AttachmentPicker } from "@/features/attachments/components/attachment-picker"; import type { AttachmentInfo } from "@/features/attachments/service"; import { StoreDot } from "@/features/prices/components/offers-list"; import { tr } from "@/i18n/tr"; import { cn } from "@/lib/utils"; import { formatMoney } from "@/shared/format"; import { formatMoneyInput, parseMoney } from "@/shared/money"; import { parseQuantity } from "@/shared/quantity"; import type { LeftoverAction } from "@/shared/shopping"; import { quantityInput } from "@/shared/stock"; import { completeShoppingAction } from "../actions"; import type { CompletionSheet } from "../completion"; import type { ShoppingItemView } from "../service"; const t = tr.shopping.complete; /** Ürün tipi kaleminde marka belirtmeden alım (Radix Select boş değer kabul etmez). */ const GENERIC = "__generic__"; type Line = { productId: string; storeId: string; packages: string; amount: string; /** Kullanıcı tutarı elle değiştirdi: fiyattan yeniden hesaplanmaz. */ touched: boolean; }; type ItemLine = Line & { bought: boolean }; type Extra = Line & { key: number }; /** Paket fiyatı biliniyorsa tutar = fiyat × paket. */ function priceAmount(sheet: CompletionSheet, l: Pick) { const price = sheet.prices[`${l.productId}:${l.storeId}`]; const q = parseQuantity(l.packages); return price && q ? formatMoneyInput(Math.round((price * q) / 1000)) : ""; } function withPrice(sheet: CompletionSheet, l: Line, patch: Partial): Line { const next = { ...l, ...patch }; if ("amount" in patch) return { ...next, touched: true }; if (next.touched) return next; return { ...next, amount: priceAmount(sheet, next) || next.amount }; } function initialLine(sheet: CompletionSheet, item: ShoppingItemView): Line { const plan = sheet.planned[item.id]; const typeOf = (id: string | null) => sheet.products.find((p) => p.id === id)?.productTypeId; const typeId = item.kind === "product" ? typeOf(item.productId) : item.productTypeId; const productId = item.kind === "text" ? "" : (plan?.productId ?? item.productId ?? sheet.products.find((p) => p.productTypeId === typeId)?.id ?? GENERIC); const packages = plan ? String(plan.packages) : item.quantity && (item.kind === "product" || productId === GENERIC) ? quantityInput(item.quantity) : "1"; const base = { productId, storeId: plan?.storeId ?? defaultStore(sheet), packages, amount: "", touched: false, }; return { ...base, amount: priceAmount(sheet, base) }; } /** Planda en çok kalemin gittiği mağaza; plan yoksa ilk mağaza. */ function defaultStore(sheet: CompletionSheet) { const count = new Map(); for (const p of Object.values(sheet.planned)) count.set(p.storeId, (count.get(p.storeId) ?? 0) + 1); return [...count.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? sheet.stores[0]?.id ?? ""; } /** Mağazaya uygun varsayılan hesap: geçerli yemek kartı, yoksa banka/nakit, yoksa ilk hesap. */ function defaultAccount(sheet: CompletionSheet, storeId: string) { const store = sheet.stores.find((s) => s.id === storeId); const meal = sheet.accounts.find( (a) => a.type === "meal_card" && a.mealCardProvider && store?.providers.includes(a.mealCardProvider), ); return ( meal?.id ?? sheet.accounts.find((a) => a.type === "bank" || a.type === "cash")?.id ?? sheet.accounts[0]?.id ?? "" ); } function LineFields({ sheet, line, products, text, onChange, errors, idPrefix, genericUnit, }: { sheet: CompletionSheet; line: Line; products: CompletionSheet["products"]; text: boolean; onChange: (patch: Partial) => void; errors: Record; idPrefix: string; /** Ürün tipi kalemi: markasız seçeneği ve tipin birimi. */ genericUnit?: string; }) { const generic = line.productId === GENERIC; return (
{text ? (

{tr.shopping.complete.textItem}

) : ( {t.product} {errors.productId ? {errors.productId} : null} )} {t.store} {text ? ( ) : ( {generic && genericUnit ? `${t.quantity} (${genericUnit})` : t.packages} onChange({ packages: e.target.value })} className="text-right tabular-nums" aria-invalid={Boolean(errors.packages)} /> )} {t.amount} onChange({ amount: e.target.value })} className="text-right tabular-nums" aria-invalid={Boolean(errors.amount)} /> {errors.amount ? {errors.amount} : null}
); } /** /alisveris/:id/tamamla — alınanları gir, mağaza başına hesap ve fiş, alınmayanlar. */ export function CompletionForm({ sheet }: { sheet: CompletionSheet }) { const router = useRouter(); const anyChecked = sheet.items.some((i) => i.checked); const [date, setDate] = useState(sheet.today); const [rows, setRows] = useState>(() => Object.fromEntries( sheet.items.map((i) => [ i.id, { ...initialLine(sheet, i), bought: anyChecked ? i.checked : !i.archived }, ]), ), ); const [extras, setExtras] = useState([]); const [accounts, setAccounts] = useState>({}); const [receipts, setReceipts] = useState>({}); const [uploading, setUploading] = useState(0); const [fallback, setFallback] = useState(sheet.defaultFallbackCategoryId ?? ""); const [leftover, setLeftover] = useState<{ action: LeftoverAction; target: string; name: string; }>({ action: sheet.otherLists.length ? "move" : "new", target: sheet.otherLists[0]?.id ?? "", name: t.newListDefault(sheet.list.name), }); const [errors, setErrors] = useState>({}); const [busy, setBusy] = useState(false); const typeProducts = (item: ShoppingItemView) => { const typeId = item.kind === "product" ? sheet.products.find((p) => p.id === item.productId)?.productTypeId : item.productTypeId; return sheet.products.filter((p) => p.productTypeId === typeId); }; const bought = sheet.items.filter((i) => rows[i.id]!.bought); const unbought = sheet.items.filter((i) => !rows[i.id]!.bought); const allLines: (Line & { itemId: string; text: boolean })[] = [ ...bought.map((i) => ({ ...rows[i.id]!, itemId: i.id, text: i.kind === "text" })), ...extras.map((e) => ({ ...e, itemId: "", text: false })), ]; const storeIds = [...new Set(allLines.map((l) => l.storeId).filter(Boolean))]; const unmapped = new Set(sheet.unmappedTypeIds); const needsFallback = allLines.some( (l) => l.text || unmapped.has(sheet.products.find((p) => p.id === l.productId)?.productTypeId ?? ""), ); const total = allLines.reduce((a, l) => a + (parseMoney(l.amount) ?? 0), 0); const plannedTotal = useMemo( () => Object.values(sheet.planned).reduce((a, p) => a + p.priceMinor * p.packages, 0), [sheet.planned], ); function setRow(id: string, patch: Partial) { setRows((s) => { const cur = s[id]!; const { bought: b, ...rest } = patch; const next = Object.keys(rest).length ? { ...cur, ...withPrice(sheet, cur, rest) } : cur; return { ...s, [id]: { ...next, bought: b ?? cur.bought } }; }); } async function submit() { setBusy(true); const lineKeys = allLines.map((l, i) => l.itemId ? `item:${l.itemId}` : `extra:${extras[i - bought.length]?.key}`, ); const r = await completeShoppingAction(sheet.list.id, { date, lines: allLines.map((l) => ({ itemId: l.itemId, productId: l.text || l.productId === GENERIC ? "" : l.productId, storeId: l.storeId, packages: l.text ? "1" : l.packages, amount: l.amount, })), stores: storeIds.map((id) => ({ storeId: id, accountId: accounts[id] ?? defaultAccount(sheet, id), attachmentIds: (receipts[id] ?? []).map((a) => a.id), })), fallbackCategoryId: needsFallback ? fallback : "", leftover: unbought.length ? { action: leftover.action, targetListId: leftover.target, newListName: leftover.name } : { action: "drop" }, }); setBusy(false); if (r.ok) { toast.success(t.saved); router.push(`/alisveris/${sheet.list.id}`); return; } // Satır hatalarını satır anahtarına çevir. const mapped: Record = {}; for (const [k, msg] of Object.entries(r.fieldErrors ?? {})) { const m = /^lines\.(\d+)\.(\w+)$/.exec(k); if (m) mapped[`${lineKeys[Number(m[1])]}.${m[2]}`] = msg; else { const s = /^stores\.(\d+)\.(\w+)$/.exec(k); mapped[s ? `store:${storeIds[Number(s[1])]}` : k] = msg; } } setErrors({ ...mapped, root: r.error }); toast.error(r.error); } const errs = (key: string) => ({ productId: errors[`${key}.productId`], storeId: errors[`${key}.storeId`], packages: errors[`${key}.packages`], amount: errors[`${key}.amount`], }); return (

{t.title}: {sheet.list.name}

{t.description}

{sheet.stores.length === 0 ? (

{t.noStores}

) : null} {t.date} setDate(e.target.value)} /> {errors.date ? {errors.date} : null}

{t.itemsTitle}

    {sheet.items.map((item) => { const row = rows[item.id]!; const plan = sheet.planned[item.id]; const planProduct = plan ? sheet.products.find((p) => p.id === plan.productId) : null; const planStore = plan ? sheet.stores.find((s) => s.id === plan.storeId) : null; const substitute = row.bought && item.kind !== "text" && (item.kind === "product" ? row.productId !== item.productId : plan && row.productId !== plan.productId); const key = `item:${item.id}`; return (
  • setRow(item.id, { bought: x === true })} className="mt-0.5 size-5" /> {substitute ? {t.substitute} : null}
    {row.bought ? ( setRow(item.id, patch)} errors={errs(key)} idPrefix={`l-${item.id}`} /> ) : null}
  • ); })}

{t.extrasTitle}

{extras.length ? (
    {extras.map((e) => (
  • setExtras((s) => s.map((x) => x.key === e.key ? { ...x, ...withPrice(sheet, x, patch) } : x, ), ) } errors={errs(`extra:${e.key}`)} idPrefix={`e-${e.key}`} />
  • ))}
) : null}
{storeIds.length ? (

{t.storesTitle}

{t.storesDescription}

    {storeIds.map((id) => { const store = sheet.stores.find((s) => s.id === id); const lines = allLines.filter((l) => l.storeId === id); const sum = lines.reduce((a, l) => a + (parseMoney(l.amount) ?? 0), 0); return (
  • {store?.label} {t.subtotal(lines.length)} · {formatMoney(sum)}
    {t.account} {errors[`store:${id}`] ? ( {errors[`store:${id}`]} ) : null} {t.receipts} setReceipts((s) => ({ ...s, [id]: items }))} allowedKinds={["image"]} onBusyChange={(b) => setUploading((n) => n + (b ? 1 : -1))} addLabel={t.addReceipt} hint={t.receiptHint} />
  • ); })}
) : null} {needsFallback ? ( {t.fallbackCategory} {t.fallbackHint} {errors.fallbackCategoryId ? {errors.fallbackCategoryId} : null} ) : null} {unbought.length ? (

{t.leftoverTitle}

{t.leftoverDescription(unbought.length)} {unbought.map((i) => i.label).join(", ")}

{(["move", "new", "drop"] as const) .filter((a) => a !== "move" || sheet.otherLists.length) .map((a) => ( ))}
{leftover.action === "move" ? ( ) : leftover.action === "new" ? ( setLeftover((s) => ({ ...s, name: e.target.value }))} className="max-w-md" /> ) : null} {errors["leftover.targetListId"] || errors["leftover.newListName"] ? ( {errors["leftover.targetListId"] ?? errors["leftover.newListName"]} ) : null}
) : null}
{t.total}: {formatMoney(total)} {plannedTotal ? ( {" · "} {t.plannedTotal}: {formatMoney(plannedTotal)} ) : null} {errors.root ? ( {errors.root} ) : null}
); }