'use no memo'; import { Form, Head, Link } 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 { 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 { formatNumber } from '@/lib/format'; import { getTemporaryUrl } from '@/lib/upload'; import { formatCurrency } from '@/lib/utils'; import { index as restockIndex, update } from '@/routes/admin/manage/restocks'; import type { RestockCreateData, RestockForEdit, ProductVariantForRestock } 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 = { restock: RestockForEdit; products: RestockCreateData['products']; }; export default function RestockEdit({ restock, products }: Props) { const [stockType, setStockType] = useState<'good' | 'reject'>( restock.stock_type === 'reject' ? 'reject' : 'good', ); const [selectedProductId, setSelectedProductId] = useState(() => { const items = restock.items ?? []; const product = products.find((p) => restock.existing_product_ids?.includes(p.id), ); return product ? String(product.id) : ''; }); const [quantities, setQuantities] = useState>(() => Object.fromEntries( (restock.items ?? []).map((item) => [ item.product_variant_id, item.quantity, ]), ), ); const [notes, setNotes] = useState(restock.notes ?? ''); const [photo, setPhoto] = useState(restock.photo_key); const [photoUrl, setPhotoUrl] = useState(restock.photo_url); 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 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]); useEffect(() => { if (!restock.existing_product_ids?.length) return; const toFetch = restock.existing_product_ids.filter( (id) => !fetchedVariants.has(id), ); if (toFetch.length === 0) return; setLoadingVariants(true); Promise.all( toFetch.map((id) => fetch(`/admin/master/products/${id}/variants`) .then((res) => res.json()) .then((data) => ({ id, variants: data.variants })), ), ) .then((results) => { setFetchedVariants((prev) => { const next = new Map(prev); for (const { id, variants } of results) { next.set(id, variants); } return next; }); }) .catch(() => { toast.error('Gagal memuat varian produk.'); }) .finally(() => setLoadingVariants(false)); }, [restock.existing_product_ids]); 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 ( <>

Edit Restock

({ ...formData, ...getPayload(), })} onError={() => { toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.'); }} > {({ errors, processing }) => (
Item Restock {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, ) } />
); }, ) )}
) : (

Tidak ada item.

)}
Ringkasan
setStockType( value as 'good' | 'reject', ) } className="flex flex-wrap gap-4" >
Subtotal {formatCurrency(subtotal)}
Total {formatCurrency( subtotal, )}