'use client' import React, { useState, useMemo } from 'react' import { Card, CardContent, Button, Box, Typography, IconButton, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, CircularProgress, Alert, Popover, Divider } from '@mui/material' import Grid from '@mui/material/Grid2' import CustomAutocomplete from '@/@core/components/mui/Autocomplete' import CustomTextField from '@/@core/components/mui/TextField' import ImageUpload from '@/components/ImageUpload' import { DropdownOption } from '@/types/apps/purchaseOrderTypes' import { useVendorActive } from '@/services/queries/vendor' import { useIngredients } from '@/services/queries/ingredients' import { useUnits } from '@/services/queries/units' import { useFilesMutation } from '@/services/mutations/files' import { usePurchaseOrdersMutation } from '@/services/mutations/purchaseOrder' import { PurchaseOrderFormData, PurchaseOrderFormItem, PurchaseOrderRequest } from '@/types/services/purchaseOrder' import { IngredientItem } from '@/types/services/ingredient' export type Unit = { id: string name: string } interface ValidationErrors { vendor?: string po_number?: string transaction_date?: string due_date?: string items?: string general?: string } interface PopoverState { isOpen: boolean anchorEl: HTMLElement | null itemIndex: number | null } // Komponen PricePopover const PricePopover: React.FC<{ anchorEl: HTMLElement | null open: boolean onClose: () => void ingredientData: any }> = ({ anchorEl, open, onClose, ingredientData }) => { if (!ingredientData) return null const lastPrice = ingredientData.originalData?.cost || 0 return ( Harga beli terakhir {new Intl.NumberFormat('id-ID').format(lastPrice)} ) } const PurchaseAddForm: React.FC = () => { const [imageUrl, setImageUrl] = useState('') const [errors, setErrors] = useState({}) const [popoverState, setPopoverState] = useState({ isOpen: false, anchorEl: null, itemIndex: null }) const [formData, setFormData] = useState({ vendor: null, po_number: '', transaction_date: '', due_date: '', reference: '', status: 'sent', showPesan: false, showAttachment: false, message: '', items: [ { id: 1, ingredient: null, description: '', quantity: 1, unit: null, amount: 0, total: 0 } ], attachment_file_ids: [] }) // API Hooks const { data: vendors, isLoading: isLoadingVendors } = useVendorActive() const { data: ingredients, isLoading: isLoadingIngredients } = useIngredients() const { data: units, isLoading: isLoadingUnits } = useUnits({ page: 1, limit: 50 }) const { mutate, isPending } = useFilesMutation().uploadFile const { createPurchaseOrder } = usePurchaseOrdersMutation() // Transform vendors data to dropdown options const vendorOptions: DropdownOption[] = useMemo(() => { return ( vendors?.map(vendor => ({ label: vendor.name, value: vendor.id })) || [] ) }, [vendors]) // Transform ingredients data to autocomplete options format const ingredientOptions = useMemo(() => { if (!ingredients || isLoadingIngredients) { return [] } return ingredients?.data.map((ingredient: IngredientItem) => ({ label: ingredient.name, value: ingredient.id, id: ingredient.id, originalData: ingredient })) }, [ingredients, isLoadingIngredients]) // Transform units data to dropdown options const unitOptions = useMemo(() => { if (!units || isLoadingUnits) { return [] } return ( units?.data?.map((unit: any) => ({ label: unit.name || unit.nama || unit.unit_name, value: unit.id || unit.code || unit.value })) || [] ) }, [units, isLoadingUnits]) // Handle price field click untuk menampilkan popover const handlePriceFieldClick = (event: React.MouseEvent, itemIndex: number) => { const item = formData.items[itemIndex] if (item.ingredient) { setPopoverState({ isOpen: true, anchorEl: event.currentTarget, itemIndex: itemIndex }) } } // Close popover const handleClosePopover = () => { setPopoverState({ isOpen: false, anchorEl: null, itemIndex: null }) } // Fungsi validasi const validateForm = (): boolean => { const newErrors: ValidationErrors = {} if (!formData.vendor || !formData.vendor.value) { newErrors.vendor = 'Vendor wajib dipilih' } if (!formData.po_number.trim()) { newErrors.po_number = 'Nomor PO wajib diisi' } if (!formData.transaction_date) { newErrors.transaction_date = 'Tanggal transaksi wajib diisi' } if (!formData.due_date) { newErrors.due_date = 'Tanggal jatuh tempo wajib diisi' } if (formData.transaction_date && formData.due_date) { if (new Date(formData.due_date) < new Date(formData.transaction_date)) { newErrors.due_date = 'Tanggal jatuh tempo tidak boleh sebelum tanggal transaksi' } } const validItems = formData.items.filter( item => item.ingredient && item.unit && item.quantity > 0 && item.amount > 0 ) if (validItems.length === 0) { newErrors.items = 'Minimal harus ada 1 item yang valid dengan bahan, satuan, kuantitas dan harga yang terisi' } setErrors(newErrors) return Object.keys(newErrors).length === 0 } // Handler Functions const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => { setFormData(prev => ({ ...prev, [field]: value })) if (errors[field as keyof ValidationErrors]) { setErrors(prev => ({ ...prev, [field]: undefined })) } } const handleItemChange = (index: number, field: keyof PurchaseOrderFormItem, value: any): void => { setFormData(prev => { const newItems = [...prev.items] newItems[index] = { ...newItems[index], [field]: value } if (field === 'amount' || field === 'quantity') { const item = newItems[index] item.total = item.amount * item.quantity } return { ...prev, items: newItems } }) if (errors.items) { setErrors(prev => ({ ...prev, items: undefined })) } } const handleIngredientSelection = (index: number, selectedIngredient: any) => { handleItemChange(index, 'ingredient', selectedIngredient) if (selectedIngredient) { const ingredientData: IngredientItem = selectedIngredient.originalData || selectedIngredient if (ingredientData.unit_id || ingredientData.unit) { let unitToFind = null if (ingredientData.unit && typeof ingredientData.unit === 'object') { unitToFind = ingredientData.unit } else if (ingredientData.unit_id) { unitToFind = unitOptions.find(option => option.value === ingredientData.unit_id) } if (unitToFind) { const unitOption = { label: (unitToFind as any).label || (unitToFind as any).name || (unitToFind as any).unit_name, value: (unitToFind as any).value || ingredientData.unit_id } handleItemChange(index, 'unit', unitOption) } } if (ingredientData.cost !== undefined && ingredientData.cost !== null) { handleItemChange(index, 'amount', ingredientData.cost) } if (ingredientData.name) { handleItemChange(index, 'description', ingredientData.name) } } } const addItem = (): void => { const newItem: PurchaseOrderFormItem = { id: Date.now(), ingredient: null, description: '', quantity: 1, unit: null, amount: 0, total: 0 } setFormData(prev => ({ ...prev, items: [...prev.items, newItem] })) } const removeItem = (index: number): void => { setFormData(prev => ({ ...prev, items: prev.items.filter((_, i) => i !== index) })) } const getSelectedVendorData = () => { if (!formData.vendor?.value || !vendors) return null const selectedVendor = vendors.find(vendor => vendor.id === (formData?.vendor?.value ?? '')) return selectedVendor } const upsertAttachment = (attachments: string[], newId: string, index = 0) => { if (attachments.length === 0) { return [newId] } return attachments.map((id, i) => (i === index ? newId : id)) } const handleUpload = async (file: File): Promise => { return new Promise((resolve, reject) => { const formData = new FormData() formData.append('file', file) formData.append('file_type', 'image') formData.append('description', 'Gambar Purchase Order') mutate(formData, { onSuccess: data => { setFormData(prev => ({ ...prev, attachment_file_ids: upsertAttachment(prev.attachment_file_ids, data.id) })) setImageUrl(data.file_url) resolve(data.id) }, onError: error => { reject(error) } }) }) } const subtotal = formData.items.reduce((sum, item) => sum + (item.total || 0), 0) const convertToApiRequest = (): PurchaseOrderRequest => { return { vendor_id: formData.vendor?.value || '', po_number: formData.po_number, transaction_date: formData.transaction_date, due_date: formData.due_date, reference: formData.reference || undefined, status: formData.status, message: formData.message || undefined, items: formData.items .filter(item => item.ingredient && item.unit) .map(item => ({ ingredient_id: item.ingredient!.value, description: item.description || undefined, quantity: item.quantity, unit_id: item.unit!.value, amount: item.amount })), attachment_file_ids: formData.attachment_file_ids.length > 0 ? formData.attachment_file_ids : undefined } } const handleSave = () => { if (!validateForm()) { setErrors(prev => ({ ...prev, general: 'Mohon lengkapi semua field yang wajib diisi' })) return } createPurchaseOrder.mutate(convertToApiRequest(), { onSuccess: () => { window.history.back() }, onError: error => { setErrors(prev => ({ ...prev, general: 'Terjadi kesalahan saat menyimpan data. Silakan coba lagi.' })) } }) } // Get current ingredient data for popover const getCurrentIngredientData = () => { if (popoverState.itemIndex !== null) { return formData.items[popoverState.itemIndex]?.ingredient } return null } return ( {errors.general && ( {errors.general} )} {/* BASIC INFO SECTION */} { handleInputChange('vendor', newValue) if (newValue?.value) { const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value) console.log('Vendor terpilih:', selectedVendorData) } }} loading={isLoadingVendors} renderInput={params => ( )} /> {getSelectedVendorData() && ( {getSelectedVendorData()?.contact_person ?? ''} {getSelectedVendorData()?.address ?? '-'} {getSelectedVendorData()?.phone_number ?? '-'} )} ) => handleInputChange('po_number', e.target.value)} error={!!errors.po_number} helperText={errors.po_number} /> {/* Row 2 - Transaction Date, Due Date, Status */} ) => handleInputChange('transaction_date', e.target.value) } InputLabelProps={{ shrink: true }} error={!!errors.transaction_date} helperText={errors.transaction_date} /> ) => handleInputChange('due_date', e.target.value)} InputLabelProps={{ shrink: true }} error={!!errors.due_date} helperText={errors.due_date} /> ) => handleInputChange('reference', e.target.value)} /> {/* ITEMS TABLE SECTION */} Item Purchase Order {errors.items && ( {errors.items} )} Bahan Deskripsi Kuantitas Satuan Harga Total {formData.items.map((item: PurchaseOrderFormItem, index: number) => ( handleIngredientSelection(index, newValue)} loading={isLoadingIngredients} getOptionLabel={(option: any) => { if (!option) return '' return option.label || option.name || option.nama || '' }} isOptionEqualToValue={(option: any, value: any) => { if (!option || !value) return false const optionId = option.value || option.id const valueId = value.value || value.id return optionId === valueId }} renderInput={params => ( {isLoadingIngredients ? : null} {params.InputProps.endAdornment} ) }} /> )} disabled={isLoadingIngredients} /> ) => handleItemChange(index, 'description', e.target.value) } placeholder='Deskripsi' /> ) => handleItemChange(index, 'quantity', parseInt(e.target.value) || 1) } inputProps={{ min: 1 }} /> handleItemChange(index, 'unit', newValue)} loading={isLoadingUnits} getOptionLabel={(option: any) => { if (!option) return '' return option.label || option.name || option.nama || '' }} renderInput={params => ( {isLoadingUnits ? : null} {params.InputProps.endAdornment} ) }} /> )} disabled={isLoadingUnits} /> ) => { const value = e.target.value if (value === '') { handleItemChange(index, 'amount', 0) return } const numericValue = parseFloat(value) handleItemChange(index, 'amount', isNaN(numericValue) ? 0 : numericValue) }} onClick={e => handlePriceFieldClick(e, index)} inputProps={{ min: 0, step: 'any' }} placeholder='0' sx={{ cursor: item.ingredient ? 'pointer' : 'text' }} /> removeItem(index)} disabled={formData.items.length === 1} > ))}
{/* Add New Item Button */}
{/* SUMMARY SECTION */} {/* Left Side - Message and Attachment */} {/* Message Section */} {formData.showPesan && ( ) => handleInputChange('message', e.target.value) } /> )} {/* Attachment Section */} {formData.showAttachment && ( )} {/* Right Side - Totals */} {/* Sub Total */} Sub Total {new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(subtotal)} {/* Total */} Total {new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(subtotal)} {/* Save Button */}
{/* Price Popover */}
) } export default PurchaseAddForm