'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 */}
}
onClick={addItem}
variant='outlined'
size='small'
sx={{ mt: 1 }}
disabled={isLoadingIngredients || isLoadingUnits}
>
Tambah Item
{/* 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