'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 { 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 { FieldDescription } from '@/components/ui/field'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; import { useTransactionDraftSave } from '@/hooks/use-transaction-draft'; import { formatNumber } from '@/lib/format'; import { loadTransactionDraft } from '@/lib/transaction-draft'; import { getTemporaryUrl } from '@/lib/upload'; import { formatCurrency } from '@/lib/utils'; import { store, index as transactionIndex } from '@/routes/admin/manage/transactions'; import type { ProductForTransaction, TransactionCreateData } 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: TransactionCreateData['products']; customers: TransactionCreateData['customers']; employees: TransactionCreateData['employees']; channelOptions: TransactionCreateData['channelOptions']; paymentTypeOptions: TransactionCreateData['paymentTypeOptions']; priceTypeOptions: TransactionCreateData['priceTypeOptions']; }; const SELLING_PRICE_TYPES = ['distributor', 'agent', 'sub_agent', 'wholesale', 'retail', 'tiktok', 'shopee']; export default function TransactionCreate({ products, customers, employees, channelOptions, paymentTypeOptions, priceTypeOptions, }: Props) { const { auth } = usePage().props as { auth: { user?: { id?: number } } }; const userId = auth.user?.id; const draft = loadTransactionDraft('create', userId); const [stockType, setStockType] = useState<'good' | 'reject'>( draft?.stockType === 'reject' ? 'reject' : 'good', ); const [channel, setChannel] = useState(draft?.channel ?? 'store'); const [priceType, setPriceType] = useState(draft?.priceType ?? 'retail'); const [paymentType, setPaymentType] = useState(draft?.paymentType ?? 'cash'); const [customerId, setCustomerId] = useState(draft?.customerId ?? null); const [marketingId, setMarketingId] = useState(draft?.marketingId ?? null); const [discount, setDiscount] = useState(draft?.discount ?? 0); const [negoPrice, setNegoPrice] = useState(draft?.negoPrice ?? null); const [isCompleted, setIsCompleted] = useState(draft?.isCompleted ?? false); const [isAffiliate, setIsAffiliate] = useState(draft?.isAffiliate ?? false); 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 [selectedProductId, setSelectedProductId] = useState(draft?.selectedProductId ?? ''); 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 [tiktokOrderId, setTiktokOrderId] = useState(draft?.tiktokOrderId ?? ''); const [shopeeOrderId, setShopeeOrderId] = useState(draft?.shopeeOrderId ?? ''); const quantitiesRef = useRef(quantities); useEffect(() => { quantitiesRef.current = quantities; }, [quantities]); const draftData = useMemo( () => ({ stockType, channel, priceType, paymentType, customerId, marketingId, discount, negoPrice, isCompleted, isAffiliate, tiktokOrderId: channel === 'tiktok' ? tiktokOrderId : '', shopeeOrderId: channel === 'shopee' ? shopeeOrderId : '', selectedProductId, quantities: Object.fromEntries( Object.entries(quantities).map(([id, qty]) => [ String(id), qty, ]), ), notes, photo: photo ?? undefined, }), [ stockType, channel, priceType, paymentType, customerId, marketingId, discount, negoPrice, isCompleted, isAffiliate, tiktokOrderId, shopeeOrderId, selectedProductId, quantities, notes, photo, ], ); useTransactionDraftSave('create', draftData, userId); const selectedProduct = useMemo( () => products.find((p) => String(p.id) === selectedProductId) ?? null, [products, selectedProductId], ); const variantById = useMemo( () => new Map( products.flatMap((p: ProductForTransaction) => p.product_variants.map((v) => [v.id, v]), ), ), [products], ); useEffect(() => { if (channel === 'tiktok') { setPriceType('tiktok'); setPaymentType('marketplace'); } else if (channel === 'shopee') { setPriceType('shopee'); setPaymentType('marketplace'); } }, [channel]); useEffect(() => { if (stockType === 'reject') { setPriceType('reject'); } else if (priceType === 'reject') { setPriceType('retail'); } }, [stockType]); const showPhoto = paymentType === 'transfer' || paymentType === 'qris'; const availablePriceTypes = useMemo(() => { if (stockType === 'reject') { return priceTypeOptions.filter((o) => o.value === 'reject'); } return priceTypeOptions.filter((o) => SELLING_PRICE_TYPES.includes(o.value)); }, [stockType, priceTypeOptions]); const getUnitPrice = useCallback( (variantId: number) => { const variant = variantById.get(variantId); if (!variant) { return 0; } if (stockType === 'reject') { return variant.prices?.reject ?? 0; } return variant.prices?.[priceType] ?? 0; }, [variantById, stockType, priceType], ); const subtotal = Object.entries(quantities).reduce( (sum, [variantId, quantity]) => { const unitPrice = getUnitPrice(Number(variantId)); return sum + unitPrice * quantity; }, 0, ); const total = subtotal - discount + (negoPrice ?? 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 = getUnitPrice(id); lines.push({ key: `variant-${id}`, photoUrl: variant.photo_url, title: 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; })(); function formatQuantity(value: number): string { return formatNumber(value, { maximumFractionDigits: 4 }); } function getPayload() { return { stock_type: stockType, channel, price_type: stockType === 'reject' ? 'reject' : priceType, payment_type: paymentType, customer_id: customerId, marketing_id: marketingId, discount, nego_price: negoPrice, is_completed: isCompleted, is_affiliate: isAffiliate, tiktok_order_id: channel === 'tiktok' ? tiktokOrderId || null : null, shopee_order_id: channel === 'shopee' ? shopeeOrderId || null : null, 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: showPhoto ? photo : null, }; } return ( <>

Tambah Transaksi

({ ...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 currentStock = stockType === 'good' ? variant.stock : variant.reject_stock; return (
0 ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3' } >
{variant.photo_url ? ( { ) : (
N/A
)}

{ variant.name }

Stok:{' '} {formatQuantity( Number( currentStock, ), )}{' '} pcs ยท{' '} {formatCurrency( stockType === 'reject' ? (variant.prices?.reject ?? 0) : (variant.prices?.[priceType] ?? 0), )}

updateQuantity( variant.id, val, ) } />
); }, )}
)}
Ringkasan
setStockType( value as 'good' | 'reject', ) } className="flex flex-wrap gap-4" >
{channel === 'tiktok' && (
setTiktokOrderId(e.target.value)} placeholder="Masukkan ID pesanan TikTok" className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50" />
)} {channel === 'shopee' && (
setShopeeOrderId(e.target.value)} placeholder="Masukkan ID pesanan Shopee" className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50" />
)}
{showPhoto && (
{ setPhoto(key); setPhotoUrl( key ? getTemporaryUrl( key, ) : null, ); }} folder="transaction" existingUrl={photoUrl} onUploadingChange={setUploading} />
)}
c.name} value={customers.find((c) => c.id === customerId) ?? null} onValueChange={(value) => setCustomerId(value ? value.id : null) } > Tidak ada pelanggan. {(c) => ( {c.name} )}
e.user_profile?.full_name ?? '-' } value={employees.find((e) => e.id === marketingId) ?? null} onValueChange={(value) => setMarketingId(value ? value.id : null) } > Tidak ada karyawan. {(e) => ( {e.user_profile?.full_name ?? '-'} )}
Subtotal {formatCurrency(subtotal)}
setNegoPrice(val || null) } placeholder="0" /> Jika diisi, harga nego menjadi total akhir.
Total {formatCurrency( total, )}