'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 { ConfirmDialog } from '@/components/dialogs'; import { FileUpload } from '@/components/inputs'; import { ImagePreviewModal } from '@/components/dialogs'; 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 { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import { Textarea } from '@/components/ui/textarea'; import { useRestockDraftSave } from '@/hooks/use-restock-draft'; import { formatNumber } from '@/lib/format'; import { loadRestockDraft } from '@/lib/restock-draft'; import { getTemporaryUrl } from '@/lib/upload'; import { formatCurrency } from '@/lib/utils'; import { index as restockIndex, store } from '@/routes/admin/manage/restocks'; import type { ProductForRestock, ProductVariantForRestock, RestockCreateData } from './columns'; type CartLine = { key: string; photoUrl: string | null; title: string; subtitle: string; price: number; quantity: number; onAdjust: (delta: number) => void; onSet: (value: number) => void; onRemove: () => void; }; type Props = { products: RestockCreateData['products']; }; export default function RestockCreate({ products }: Props) { const { auth } = usePage().props as { auth: { user?: { id?: number } } }; const userId = auth.user?.id; const draft = loadRestockDraft('create', userId); const [stockType, setStockType] = useState<'good' | 'reject'>( draft?.stockType === 'reject' ? 'reject' : 'good', ); const [selectedProductId, setSelectedProductId] = useState( draft?.selectedProductId ?? '', ); const [quantities, setQuantities] = useState>(() => Object.fromEntries( Object.entries(draft?.quantities ?? {}).map(([id, qty]) => [ Number(id), qty, ]), ), ); const [notes, setNotes] = useState(draft?.notes ?? ''); const [photo, setPhoto] = useState(draft?.photo ?? null); const [photoUrl, setPhotoUrl] = useState( draft?.photo ? getTemporaryUrl(draft.photo) : null, ); const [uploading, setUploading] = useState(false); const [cartOpen, setCartOpen] = useState(false); const [previewKey, setPreviewKey] = useState(null); const [cartRemoveKey, setCartRemoveKey] = useState(null); const [fetchedVariants, setFetchedVariants] = useState>(new Map()); const [loadingVariants, setLoadingVariants] = useState(false); const draftData = useMemo( () => ({ stockType, selectedProductId, quantities: Object.fromEntries( Object.entries(quantities).map(([id, qty]) => [ String(id), qty, ]), ), notes, photo: photo ?? undefined, }), [stockType, selectedProductId, quantities, notes, photo], ); useRestockDraftSave('create', draftData, userId); const quantitiesRef = useRef(quantities); useEffect(() => { quantitiesRef.current = quantities; }, [quantities]); const selectedProduct = useMemo( () => products.find((p) => String(p.id) === selectedProductId) ?? null, [products, selectedProductId], ); const selectedProductVariants = useMemo( () => (selectedProduct ? fetchedVariants.get(selectedProduct.id) ?? [] : []), [selectedProduct, fetchedVariants], ); useEffect(() => { if (!selectedProduct) return; if (fetchedVariants.has(selectedProduct.id)) return; setLoadingVariants(true); fetch(`/admin/master/products/${selectedProduct.id}/variants`) .then((res) => res.json()) .then((data) => { setFetchedVariants((prev) => { const next = new Map(prev); next.set(selectedProduct.id, data.variants); return next; }); }) .catch(() => { toast.error('Gagal memuat varian produk.'); }) .finally(() => setLoadingVariants(false)); }, [selectedProduct, fetchedVariants]); const variantById = useMemo(() => { const map = new Map(); for (const [, variants] of fetchedVariants) { for (const v of variants) { map.set(v.id, v); } } return map; }, [fetchedVariants]); const productByVariantId = useMemo(() => { const map = new Map(); for (const [productId, variants] of fetchedVariants) { const product = products.find((p) => p.id === productId); if (product) { for (const v of variants) { map.set(v.id, product.name); } } } return map; }, [fetchedVariants, products]); const getUnitPrice = useCallback( (variantId: number) => { const variant = variantById.get(variantId); if (!variant) { return 0; } return stockType === 'reject' ? variant.reject_price : variant.capital_price; }, [variantById, stockType], ); const subtotal = Object.entries(quantities).reduce( (sum, [variantId, quantity]) => { const unitPrice = getUnitPrice(Number(variantId)); return sum + unitPrice * quantity; }, 0, ); const updateQuantity = useCallback((variantId: number, value: number) => { setQuantities((prev) => ({ ...prev, [variantId]: Math.max(0, value), })); }, []); const incrementQuantity = useCallback( (variantId: number, amount: number) => { setQuantities((prev) => ({ ...prev, [variantId]: Math.max(0, (prev[variantId] ?? 0) + amount), })); }, [], ); const cartItems: CartLine[] = (() => { const lines: CartLine[] = []; for (const [variantId, quantity] of Object.entries(quantities)) { if (quantity <= 0) { continue; } const id = Number(variantId); const variant = variantById.get(id); if (variant) { const unitPrice = stockType === 'reject' ? variant.reject_price : variant.capital_price; const productName = productByVariantId.get(id) ?? ''; lines.push({ key: `variant-${id}`, photoUrl: variant.photo_url, title: `${productName} — ${variant.name}`, subtitle: `${formatCurrency(unitPrice)} / pcs`, price: unitPrice, quantity, onAdjust: (delta) => incrementQuantity(id, delta), onSet: (value) => updateQuantity(id, value), onRemove: () => updateQuantity(id, 0), }); } } return lines.sort((a, b) => a.title.localeCompare(b.title)); })(); function formatQuantity(value: number): string { return formatNumber(value, { maximumFractionDigits: 4 }); } function getPayload() { return { stock_type: stockType, items: Object.entries(quantitiesRef.current) .map(([variantId, quantity]) => ({ product_variant_id: Number(variantId), quantity: Number(quantity), })) .filter((item) => item.quantity > 0), notes: notes || null, photo_key: photo, }; } return ( <>

Tambah Restock

({ ...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 && (
{loadingVariants ? (
Memuat varian...
) : selectedProductVariants.length === 0 ? (
Tidak ada varian ditemukan.
) : ( selectedProductVariants.map( (variant) => { const currentStock = stockType === 'good' ? variant.stock : variant.reject_stock; return (
0 ? 'flex flex-wrap items-center justify-between gap-2 rounded-lg border border-primary p-3' : 'flex flex-wrap items-center justify-between gap-2 rounded-lg border p-3' } >
{variant.photo_url ? ( { ) : (
N/A
)}

{ variant.name }

Stok:{' '} {formatQuantity( Number( currentStock, ), )}{' '} pcs ·{' '} {formatCurrency( stockType === 'reject' ? variant.reject_price : variant.capital_price, )}

updateQuantity( variant.id, val, ) } />
); }, ))}
)}
Ringkasan
setStockType( value as 'good' | 'reject', ) } className="flex flex-wrap gap-4" >
Subtotal {formatCurrency(subtotal)}
Total {formatCurrency( subtotal, )}