'use no memo'; import { Form, Head, Link, usePage } from '@inertiajs/react'; import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'sonner'; import { InputError } from '@/components/ui'; import { NumberInput } from '@/components/inputs'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from '@/components/ui/combobox'; import { Label } from '@/components/ui/label'; import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import { Textarea } from '@/components/ui/textarea'; import { useStokOpnameDraftSave } from '@/hooks/use-stok-opname-draft'; import { formatNumber } from '@/lib/format'; import { loadStokOpnameDraft } from '@/lib/stok-opname-draft'; import { index as stokOpnameIndex, update } from '@/routes/admin/manage/stok-opnames'; import type { ProductForStokOpname, StokOpnameForEdit } from './columns'; type StockType = 'good' | 'reject' | 'retail'; type CartLine = { key: string; photoUrl: string | null; title: string; subtitle: string; quantity: number; onAdjust: (delta: number) => void; onSet: (value: number) => void; onRemove: () => void; }; type Props = { stokOpname: StokOpnameForEdit; products: ProductForStokOpname[]; }; export default function StokOpnameEdit({ stokOpname, products }: Props) { const { auth } = usePage().props as { auth: { user?: { id?: number } } }; const userId = auth.user?.id; const draft = loadStokOpnameDraft('edit', userId); const [selectedProductId, setSelectedProductId] = useState( draft?.selectedProductId ?? '', ); const [physicalStocks, setPhysicalStocks] = useState>>(() => { if (draft?.physicalStocks && Object.keys(draft.physicalStocks).length > 0) { return draft.physicalStocks; } const initial: Record> = {}; for (const item of stokOpname.items) { const variantId = String(item.product_variant_id); if (!initial[variantId]) { initial[variantId] = { good: 0, reject: 0, retail: 0 }; } initial[variantId][item.stock_quality] = item.physical_stock; } return initial; }); const [notes, setNotes] = useState(draft?.notes ?? stokOpname.notes ?? ''); const [cartOpen, setCartOpen] = useState(false); const draftData = useMemo( () => ({ selectedProductId, physicalStocks, notes, }), [selectedProductId, physicalStocks, notes], ); useStokOpnameDraftSave('edit', draftData, userId); const physicalStocksRef = useRef(physicalStocks); useEffect(() => { physicalStocksRef.current = physicalStocks; }, [physicalStocks]); const selectedProduct = useMemo( () => products.find((p) => String(p.id) === selectedProductId) ?? null, [products, selectedProductId], ); const variantById = useMemo( () => new Map( products.flatMap((p: ProductForStokOpname) => p.product_variants.map((v) => [v.id, v]), ), ), [products], ); const updatePhysicalStock = useCallback((variantId: number, stockType: StockType, value: number) => { setPhysicalStocks((prev) => ({ ...prev, [variantId]: { ...prev[variantId], [stockType]: Math.max(0, value), }, })); }, []); const hasAnyStock = useCallback((variantId: number) => { const stocks = physicalStocks[variantId]; if (!stocks) return false; return stocks.good > 0 || stocks.reject > 0 || stocks.retail > 0; }, [physicalStocks]); const cartItems: CartLine[] = (() => { const lines: CartLine[] = []; for (const [variantId, stocks] of Object.entries(physicalStocks)) { const id = Number(variantId); const variant = variantById.get(id); if (!variant) continue; for (const [stockType, quantity] of Object.entries(stocks)) { if (quantity <= 0) continue; const type = stockType as StockType; const typeLabel = type === 'good' ? 'Bagus' : type === 'reject' ? 'Reject' : 'Ecer'; lines.push({ key: `variant-${id}-${type}`, photoUrl: variant.photo_url, title: variant.name, subtitle: `${typeLabel}: ${formatNumber(quantity)}`, quantity, onAdjust: (delta) => updatePhysicalStock(id, type, quantity + delta), onSet: (value) => updatePhysicalStock(id, type, value), onRemove: () => updatePhysicalStock(id, type, 0), }); } } return lines.sort((a, b) => a.title.localeCompare(b.title)); })(); function formatQuantity(value: number): string { return formatNumber(value, { maximumFractionDigits: 4 }); } function getPayload() { const items: Array<{ product_variant_id: number; stock_quality: StockType; physical_stock: number }> = []; for (const [variantId, stocks] of Object.entries(physicalStocksRef.current)) { const variant = variantById.get(Number(variantId)); if (!variant) continue; for (const [stockType, quantity] of Object.entries(stocks)) { if (quantity <= 0) continue; items.push({ product_variant_id: Number(variantId), stock_quality: stockType as StockType, physical_stock: quantity, }); } } return { opname_date: stokOpname.opname_date, items, notes: notes || null, }; } return ( <>

Edit Stok Opname

({ ...formData, ...getPayload(), })} onError={() => { toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.'); }} > {({ errors, processing }) => (
Pilih Produk
p.name } value={selectedProduct} onValueChange={(value) => setSelectedProductId( value ? String(value.id) : '', ) } > Tidak ada produk ditemukan. {(p) => ( {p.name} )}
{selectedProduct && (
{selectedProduct.product_variants.map( (variant) => { const isActive = hasAnyStock(variant.id); return (
{variant.photo_url ? ( {variant.name} ) : (
N/A
)}

{variant.name}

Bagus: {formatQuantity(variant.stock)}

updatePhysicalStock(variant.id, 'good', val) } />

Reject: {formatQuantity(variant.reject_stock)}

updatePhysicalStock(variant.id, 'reject', val) } />

Ecer: {formatQuantity(variant.retail_stock)}

updatePhysicalStock(variant.id, 'retail', val) } />
); }, )}
)}
Ringkasan
Jumlah Item {cartItems.length}