'use no memo'; import { Form, Head, Link } from '@inertiajs/react'; import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2, } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; import { ConfirmDialog } from '@/components/dialogs'; import { FileUploadMultiple } from '@/components/inputs'; import { ImagePreviewModal } from '@/components/dialogs'; import { InputError } from '@/components/ui'; import { RupiahInput } 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 { Input } from '@/components/ui/input'; 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 purchaseIndex, update, } from '@/routes/admin/manage/purchases'; import type { PurchaseCreateData, PurchaseForEdit, RawMaterialVariant } from './columns'; type CartLine = { key: string; photoUrl: string | null; title: string; subtitle: string; price: number; quantity: string; onAdjust: (delta: number) => void; onSet: (value: string) => void; onRemove: () => void; }; type Props = { purchase: PurchaseForEdit; suppliers: PurchaseCreateData['suppliers']; rawMaterials: { id: number; name: string; unit: string }[]; }; export default function PurchaseEdit({ purchase, suppliers, rawMaterials, }: Props) { const [supplierId, setSupplierId] = useState(String(purchase.supplier_id)); const selectedSupplier = suppliers.find((s) => String(s.id) === supplierId) ?? null; const [discount, setDiscount] = useState(purchase.discount); const [shippingCost, setShippingCost] = useState(purchase.shipping_cost); const [notes, setNotes] = useState(purchase.notes ?? ''); const [photos, setPhotos] = useState<{ key: string; url: string | null }[]>(() => { if (purchase.photo_keys && purchase.photo_keys.length > 0) { return purchase.photo_keys.map((key: string, index: number) => ({ key, url: purchase.photo_urls?.[index] ?? getTemporaryUrl(key), })); } return []; }); const [uploading, setUploading] = useState(false); const [quantities, setQuantities] = useState>(() => Object.fromEntries( Object.entries(purchase.existing_quantities).map(([id, qty]) => [ Number(id), String(qty), ]), ), ); const [cartOpen, setCartOpen] = useState(false); const [previewKey, setPreviewKey] = useState(null); const [variantPreview, setVariantPreview] = useState<{ src: string; title?: string; description?: string } | null>(null); const [cartRemoveKey, setCartRemoveKey] = useState(null); const [fetchedVariants, setFetchedVariants] = useState>(new Map()); const [loadingVariants, setLoadingVariants] = useState(false); useEffect(() => { const materialIds = purchase.existing_material_ids ?? []; if (materialIds.length === 0) return; const toFetch = materialIds.filter((id) => !fetchedVariants.has(id)); if (toFetch.length === 0) return; setLoadingVariants(true); Promise.all( toFetch.map((id) => fetch(`/admin/master/raw-materials/${id}/variants`) .then((res) => res.json()) .then((data) => [id, data.variants] as const) ), ) .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 bahan baku.'); }) .finally(() => setLoadingVariants(false)); }, [purchase.existing_material_ids]); const priceMap = useMemo(() => { const map = new Map(); for (const [materialId, variants] of fetchedVariants) { const material = rawMaterials.find((m) => m.id === materialId); if (!material) continue; for (const p of variants) { map.set(p.id, { ...p, materialName: material.name, materialUnit: material.unit }); } } return map; }, [fetchedVariants, rawMaterials]); const materialByPriceId = useMemo(() => { const map = new Map(); for (const [materialId, variants] of fetchedVariants) { const material = rawMaterials.find((m) => m.id === materialId); if (!material) continue; for (const p of variants) { map.set(p.id, { name: material.name, unit: material.unit }); } } return map; }, [fetchedVariants, rawMaterials]); const subtotal = Object.entries(quantities).reduce( (sum, [priceId, quantity]) => { const price = priceMap.get(Number(priceId)); return sum + (price ? price.price * (parseFloat(quantity) || 0) : 0); }, 0, ); const total = subtotal - discount + shippingCost; const purchasedVariantsByMaterial = useMemo(() => { const map = new Map(); for (const [priceId, quantity] of Object.entries(quantities)) { const id = Number(priceId); const price = priceMap.get(id); const material = materialByPriceId.get(id); if (!price || !material) continue; if (!map.has(material.name)) { map.set(material.name, { name: material.name, unit: material.unit, items: [] }); } map.get(material.name)!.items.push({ id: price.id, variant: price.variant, price: price.price, stock: price.stock, photo_url: price.photo_url, photo_conversion_url: price.photo_conversion_url, }); } return map; }, [quantities, priceMap, materialByPriceId]); const isMultiMaterial = purchasedVariantsByMaterial.size > 1; const updateQuantity = useCallback((priceId: number, value: string) => { setQuantities((prev) => ({ ...prev, [priceId]: value, })); }, []); const incrementQuantity = useCallback((priceId: number, amount: number) => { setQuantities((prev) => ({ ...prev, [priceId]: String(Math.max(0, (parseFloat(prev[priceId]) || 0) + amount)), })); }, []); const cartItems: CartLine[] = (() => { const lines: CartLine[] = []; for (const [priceId, quantity] of Object.entries(quantities)) { if ((parseFloat(quantity) || 0) <= 0) { continue; } const id = Number(priceId); const price = priceMap.get(id); const material = materialByPriceId.get(id); if (price && material) { lines.push({ key: `existing-${id}`, photoUrl: price.photo_conversion_url ?? price.photo_url, title: `${material.name} — ${price.variant}`, subtitle: `${formatCurrency(price.price)} / ${material.unit}`, price: price.price, 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 { mode: 'existing', supplier_id: supplierId ? Number(supplierId) : null, discount, shipping_cost: shippingCost, notes: notes || null, photo_keys: photos.map((p) => p.key), existing_items: Object.entries(quantities) .map(([priceId, quantity]) => ({ raw_material_price_id: Number(priceId), quantity: parseFloat(quantity) || 0, unit_price: priceMap.get(Number(priceId))?.price ?? 0, })) .filter((item) => item.quantity > 0), }; } return ( <>

Edit Belanja

({ ...data, ...getPayload(), })} onError={() => { toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.'); }} > {({ errors, processing }) => (
Pilih Bahan Baku {Array.from(purchasedVariantsByMaterial.entries()).map(([materialName, group]) => (
{isMultiMaterial && (

{group.name} ({group.unit})

)} {group.items.map((price) => (
{price.photo_conversion_url ?? price.photo_url ? ( {price.variant} setVariantPreview({ src: price.photo_url ?? null, title: price.variant, description: group.name })} /> ) : (
N/A
)}

{price.variant}

Stok:{' '} {formatQuantity(Number(price.stock))}{' '} {group.unit} · {formatCurrency(price.price)}

updateQuantity(price.id, e.target.value) } />
))}
))}
Ringkasan
s.name } value={selectedSupplier} onValueChange={(value) => setSupplierId( value ? String(value.id) : '', ) } > Tidak ada supplier ditemukan. {(s) => ( {s.name} )}
Subtotal {formatCurrency(subtotal)}
Diskon
Ongkir
Total {formatCurrency(total)}