'use no memo'; import { Form, Head, Link, usePage } from '@inertiajs/react'; import { ArrowLeft, Check, ClipboardPaste, Copy, Minus, Plus, ShoppingCart, Trash2, } from 'lucide-react'; import { useCallback, useMemo, useRef, useState } from 'react'; import { toast } from 'sonner'; import { ConfirmDialog } from '@/components/dialogs'; import { FileUpload, FileUploadMultiple } from '@/components/inputs'; import { ImagePreviewModal } from '@/components/dialogs'; import { InputError } from '@/components/ui'; import { NumberInput } from '@/components/inputs'; 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 { Input } from '@/components/ui/input'; 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Textarea } from '@/components/ui/textarea'; import { usePurchaseDraftSave } from '@/hooks/use-purchase-draft'; import { UNITS } from '@/lib/constants'; import { formatNumber } from '@/lib/format'; import { loadPurchaseDraft } from '@/lib/purchase-draft'; import { getTemporaryUrl } from '@/lib/upload'; import { formatCurrency } from '@/lib/utils'; import { index as purchaseIndex, store } from '@/routes/admin/manage/purchases'; import type { PurchaseCreateData } from './columns'; type VariantState = { variant: string; price: number; stock: number; photo: string | null; photoUrl: string | null; uploading: boolean; }; 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 = { suppliers: PurchaseCreateData['suppliers']; rawMaterials: PurchaseCreateData['rawMaterials']; }; export default function PurchaseCreate({ suppliers, rawMaterials }: Props) { const { auth } = usePage().props as { auth: { user?: { id?: number } } }; const userId = auth.user?.id; const draft = loadPurchaseDraft('create', userId); const [name, setName] = useState(draft?.name ?? ''); const [unit, setUnit] = useState(draft?.unit ?? 'kg'); const [variants, setVariants] = useState(() => { if (draft?.variants && draft.variants.length > 0) { return draft.variants.map((v) => ({ variant: v.variant, price: v.price, stock: v.stock, photo: v.photo ?? null, photoUrl: v.photo ? getTemporaryUrl(v.photo) : null, uploading: false, })); } return [ { variant: '', price: 0, stock: 0, photo: null, photoUrl: null, uploading: false, }, ]; }); const [supplierId, setSupplierId] = useState(draft?.supplierId ?? ''); const selectedSupplier = suppliers.find((s) => String(s.id) === supplierId) ?? null; const [discount, setDiscount] = useState(draft?.discount ?? 0); const [shippingCost, setShippingCost] = useState(draft?.shippingCost ?? 0); const [notes, setNotes] = useState(draft?.notes ?? ''); const [photos, setPhotos] = useState<{ key: string; url: string | null }[]>(() => { if (draft?.photo_keys && draft.photo_keys.length > 0) { return draft.photo_keys.map((key: string) => ({ key, url: getTemporaryUrl(key), })); } return []; }); const [uploading, setUploading] = useState(false); const [mode, setMode] = useState<'new' | 'existing'>(draft?.mode ?? 'new'); const [selectedMaterialName, setSelectedMaterialName] = useState( draft?.selectedMaterialName ?? '', ); const [quantities, setQuantities] = useState>(() => Object.fromEntries( Object.entries(draft?.quantities ?? {}).map(([id, qty]) => [ Number(id), qty, ]), ), ); const [cartOpen, setCartOpen] = useState(false); const [previewKey, setPreviewKey] = useState(null); const [cartRemoveKey, setCartRemoveKey] = useState(null); const draftData = useMemo( () => ({ name, unit, supplierId, discount, shippingCost, notes, variants: variants.map((v) => ({ variant: v.variant, price: v.price, stock: v.stock, photo: v.photo ?? undefined, })), mode, selectedMaterialName, quantities: Object.fromEntries( Object.entries(quantities).map(([id, qty]) => [ String(id), qty, ]), ), photo_keys: photos.map((p) => p.key), }), [ name, unit, supplierId, discount, shippingCost, notes, variants, mode, selectedMaterialName, quantities, photos, ], ); usePurchaseDraftSave('create', draftData, userId); const variantsRef = useRef(variants); variantsRef.current = variants; const priceMap = useMemo( () => new Map( rawMaterials.flatMap((m) => m.raw_material_prices.map((p) => [p.id, p]), ), ), [rawMaterials], ); const materialByPriceId = useMemo( () => new Map( rawMaterials.flatMap((m) => m.raw_material_prices.map((p) => [ p.id, { name: m.name, unit: m.unit }, ]), ), ), [rawMaterials], ); const newSubtotal = variants.reduce( (sum, v) => sum + Number(v.price) * Number(v.stock), 0, ); const existingSubtotal = Object.entries(quantities).reduce( (sum, [priceId, quantity]) => { const price = priceMap.get(Number(priceId)); return sum + (price ? price.price * quantity : 0); }, 0, ); const subtotal = mode === 'existing' ? existingSubtotal : newSubtotal; const total = subtotal - discount + shippingCost; const addVariant = useCallback(() => { setVariants((prev) => [ ...prev, { variant: '', price: 0, stock: 0, photo: null, photoUrl: null, uploading: false, }, ]); }, []); const removeVariant = useCallback((index: number) => { setVariants((prev) => prev.filter((_, i) => i !== index)); }, []); const updateVariant = useCallback( (index: number, field: keyof VariantState, value: unknown) => { setVariants((prev) => { const updated = [...prev]; (updated[index] as Record)[field] = value; return updated; }); }, [], ); const [copiedIndex, setCopiedIndex] = useState(null); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [deleteVariantIndex, setDeleteVariantIndex] = useState( null, ); const confirmRemoveVariant = useCallback((index: number) => { setDeleteVariantIndex(index); setDeleteConfirmOpen(true); }, []); const copyPrice = useCallback((variantIndex: number) => { setVariants((prev) => { const price = prev[variantIndex].price; navigator.clipboard.writeText(String(price)); setCopiedIndex(variantIndex); setTimeout(() => setCopiedIndex(null), 1500); return prev; }); }, []); const pastePrice = useCallback((variantIndex: number) => { navigator.clipboard.readText().then((text) => { try { const price = Number(text); if (!isNaN(price)) { setVariants((prev) => { const updated = [...prev]; updated[variantIndex] = { ...updated[variantIndex], price, }; return updated; }); } } catch { // invalid clipboard data } }); }, []); const applyToAll = useCallback((variantIndex: number) => { setVariants((prev) => { const sourcePrice = prev[variantIndex].price; return prev.map((v, i) => i === variantIndex ? v : { ...v, price: sourcePrice }, ); }); }, []); const selectedMaterial = useMemo( () => rawMaterials.find((m) => m.name === selectedMaterialName) ?? null, [rawMaterials, selectedMaterialName], ); const updateQuantity = useCallback((priceId: number, value: number) => { setQuantities((prev) => ({ ...prev, [priceId]: Math.max(0, value), })); }, []); const incrementQuantity = useCallback((priceId: number, amount: number) => { setQuantities((prev) => ({ ...prev, [priceId]: Math.max(0, (prev[priceId] ?? 0) + amount), })); }, []); const adjustVariantStock = useCallback((index: number, amount: number) => { setVariants((prev) => { const updated = [...prev]; updated[index] = { ...updated[index], stock: Math.max(0, Number(updated[index].stock) + amount), }; return updated; }); }, []); const cartItems: CartLine[] = (() => { if (mode === 'existing') { const lines: CartLine[] = []; for (const [priceId, quantity] of Object.entries(quantities)) { if (quantity <= 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)); } return variants .map((v, index): CartLine => ({ key: `new-${index}`, photoUrl: v.photoUrl, title: `${name || 'Bahan Baku Baru'} — ${v.variant || `Varian ${index + 1}`}`, subtitle: `${formatCurrency(v.price)} / ${unit}`, price: v.price, quantity: v.stock, onAdjust: (delta) => adjustVariantStock(index, delta), onSet: (value) => updateVariant(index, 'stock', value), onRemove: () => removeVariant(index), })) .filter((line) => line.quantity > 0); })(); function formatQuantity(value: number): string { return formatNumber(value, { maximumFractionDigits: 4 }); } function getPayload() { const base = { mode, supplier_id: supplierId ? Number(supplierId) : null, discount, shipping_cost: shippingCost, notes: notes || null, photo_keys: photos.map((p) => p.key), }; if (mode === 'existing') { return { ...base, existing_items: Object.entries(quantities) .map(([priceId, quantity]) => ({ raw_material_price_id: Number(priceId), quantity: Number(quantity), unit_price: priceMap.get(Number(priceId))?.price ?? 0, })) .filter((item) => item.quantity > 0), }; } return { ...base, name, unit, variants: variantsRef.current.map((v) => ({ variant: v.variant, price: Number(v.price), stock: Number(v.stock), photo_key: v.photo, })), }; } return ( <>

Tambah Belanja

({ ...data, ...getPayload(), })} onError={() => { toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.'); }} > {({ errors, processing }) => (
setMode(value as 'new' | 'existing') } > Baru Lama Informasi Bahan Baku
setName( e.target.value, ) } placeholder="Masukkan nama bahan baku" />
{UNITS.map((u) => (
))}
Varian Bahan Baku {variants.map( (variant, variantIndex) => (

Varian{' '} {variantIndex + 1}

{variantIndex > 0 && ( )}
updateVariant( variantIndex, 'variant', e .target .value, ) } placeholder="Contoh: Ukuran L, Warna Merah" />
updateVariant( variantIndex, 'price', val, ) } />
updateVariant( variantIndex, 'stock', val, ) } />
{ updateVariant( variantIndex, 'photo', key, ); updateVariant( variantIndex, 'photoUrl', key ? getTemporaryUrl( key, ) : null, ); }} folder="raw-material-variant" existingUrl={ variant.photoUrl } onUploadingChange={( uploading, ) => updateVariant( variantIndex, 'uploading', uploading, ) } />
), )}
Pilih Bahan Baku
m.name} value={selectedMaterial} onValueChange={( value, ) => setSelectedMaterialName( value?.name ?? '', ) } > Tidak ada bahan baku ditemukan. {(m) => ( {m.name}{' '} ( {m.unit} ) )}
{selectedMaterial && (
{selectedMaterial.raw_material_prices.map( (price) => (
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' } >
{price.photo_conversion_url ?? price.photo_url ? ( { ) : (
N/A
)}

{ price.variant }

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

updateQuantity( price.id, val, ) } />
), )}
)}
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)}