Purchase create
This commit is contained in:
parent
98d6446b0c
commit
222beb5043
@ -10,46 +10,6 @@ export type PurchaseOrderType = {
|
|||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IngredientItem {
|
|
||||||
id: number
|
|
||||||
ingredient: { label: string; value: string } | null
|
|
||||||
deskripsi: string
|
|
||||||
kuantitas: number
|
|
||||||
satuan: { label: string; value: string } | null
|
|
||||||
harga: number
|
|
||||||
total: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PurchaseOrderFormData {
|
|
||||||
vendor: { label: string; value: string } | null
|
|
||||||
nomor: string
|
|
||||||
tglTransaksi: string
|
|
||||||
tglJatuhTempo: string
|
|
||||||
referensi: string
|
|
||||||
termin: { label: string; value: string } | null
|
|
||||||
hargaTermasukPajak: boolean
|
|
||||||
showShippingInfo: boolean
|
|
||||||
tanggalPengiriman: string
|
|
||||||
ekspedisi: { label: string; value: string } | null
|
|
||||||
noResi: string
|
|
||||||
showPesan: boolean
|
|
||||||
showAttachment: boolean
|
|
||||||
showTambahDiskon: boolean
|
|
||||||
showBiayaPengiriman: boolean
|
|
||||||
showBiayaTransaksi: boolean
|
|
||||||
showUangMuka: boolean
|
|
||||||
pesan: string
|
|
||||||
ingredientItems: IngredientItem[]
|
|
||||||
transactionCosts?: TransactionCost[]
|
|
||||||
subtotal?: number
|
|
||||||
discountType?: 'percentage' | 'fixed'
|
|
||||||
downPaymentType?: 'percentage' | 'fixed'
|
|
||||||
discountValue?: string
|
|
||||||
shippingCost?: string
|
|
||||||
transactionCost?: string
|
|
||||||
downPayment?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionCost {
|
export interface TransactionCost {
|
||||||
id: string
|
id: string
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { IngredientItem } from './ingredient'
|
||||||
import { Vendor } from './vendor'
|
import { Vendor } from './vendor'
|
||||||
|
|
||||||
export interface PurchaseOrderRequest {
|
export interface PurchaseOrderRequest {
|
||||||
@ -93,3 +94,27 @@ export interface PurchaseOrderFile {
|
|||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderFormData {
|
||||||
|
vendor: { label: string; value: string } | null
|
||||||
|
po_number: string
|
||||||
|
transaction_date: string
|
||||||
|
due_date: string
|
||||||
|
reference: string
|
||||||
|
status: 'draft' | 'sent' | 'approved' | 'received' | 'cancelled'
|
||||||
|
showPesan: boolean
|
||||||
|
showAttachment: boolean
|
||||||
|
message: string
|
||||||
|
items: PurchaseOrderFormItem[]
|
||||||
|
attachment_file_ids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurchaseOrderFormItem {
|
||||||
|
id: number // for UI tracking
|
||||||
|
ingredient: { label: string; value: string; originalData?: IngredientItem } | null
|
||||||
|
description: string
|
||||||
|
quantity: number
|
||||||
|
unit: { label: string; value: string } | null
|
||||||
|
amount: number
|
||||||
|
total: number // calculated field for UI
|
||||||
|
}
|
||||||
|
|||||||
@ -15,7 +15,10 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
Paper,
|
Paper,
|
||||||
CircularProgress
|
CircularProgress,
|
||||||
|
Alert,
|
||||||
|
Popover,
|
||||||
|
Divider
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from '@mui/material/Grid2'
|
||||||
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
||||||
@ -27,88 +30,113 @@ import { useIngredients } from '@/services/queries/ingredients'
|
|||||||
import { useUnits } from '@/services/queries/units'
|
import { useUnits } from '@/services/queries/units'
|
||||||
import { useFilesMutation } from '@/services/mutations/files'
|
import { useFilesMutation } from '@/services/mutations/files'
|
||||||
import { usePurchaseOrdersMutation } from '@/services/mutations/purchaseOrder'
|
import { usePurchaseOrdersMutation } from '@/services/mutations/purchaseOrder'
|
||||||
|
import { PurchaseOrderFormData, PurchaseOrderFormItem, PurchaseOrderRequest } from '@/types/services/purchaseOrder'
|
||||||
export interface PurchaseOrderRequest {
|
import { IngredientItem } from '@/types/services/ingredient'
|
||||||
vendor_id: string // uuid.UUID
|
|
||||||
po_number: string
|
|
||||||
transaction_date: string // ISO date string
|
|
||||||
due_date: string // ISO date string
|
|
||||||
reference?: string
|
|
||||||
status?: 'draft' | 'sent' | 'approved' | 'received' | 'cancelled'
|
|
||||||
message?: string
|
|
||||||
items: PurchaseOrderItemRequest[]
|
|
||||||
attachment_file_ids?: string[] // uuid.UUID[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PurchaseOrderItemRequest {
|
|
||||||
ingredient_id: string // uuid.UUID
|
|
||||||
description?: string
|
|
||||||
quantity: number
|
|
||||||
unit_id: string // uuid.UUID
|
|
||||||
amount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IngredientItem = {
|
|
||||||
id: string
|
|
||||||
organization_id: string
|
|
||||||
outlet_id: string
|
|
||||||
name: string
|
|
||||||
unit_id: string
|
|
||||||
cost: number
|
|
||||||
stock: number
|
|
||||||
is_semi_finished: boolean
|
|
||||||
is_active: boolean
|
|
||||||
metadata: Record<string, unknown>
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
unit: Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Unit = {
|
export type Unit = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
// Add other unit properties as needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal form state interface for UI management
|
interface ValidationErrors {
|
||||||
interface PurchaseOrderFormData {
|
vendor?: string
|
||||||
vendor: { label: string; value: string } | null
|
po_number?: string
|
||||||
po_number: string
|
transaction_date?: string
|
||||||
transaction_date: string
|
due_date?: string
|
||||||
due_date: string
|
items?: string
|
||||||
reference: string
|
general?: string
|
||||||
status: 'draft' | 'sent' | 'approved' | 'received' | 'cancelled'
|
|
||||||
showPesan: boolean
|
|
||||||
showAttachment: boolean
|
|
||||||
message: string
|
|
||||||
items: PurchaseOrderFormItem[]
|
|
||||||
attachment_file_ids: string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PurchaseOrderFormItem {
|
interface PopoverState {
|
||||||
id: number // for UI tracking
|
isOpen: boolean
|
||||||
ingredient: { label: string; value: string; originalData?: IngredientItem } | null
|
anchorEl: HTMLElement | null
|
||||||
description: string
|
itemIndex: number | null
|
||||||
quantity: number
|
}
|
||||||
unit: { label: string; value: string } | null
|
|
||||||
amount: number
|
// Komponen PricePopover
|
||||||
total: number // calculated field for UI
|
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 (
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={onClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
minWidth: 300,
|
||||||
|
maxWidth: 350,
|
||||||
|
boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
|
||||||
|
borderRadius: 2
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
Harga beli terakhir
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='h6' color='primary' fontWeight={600}>
|
||||||
|
{new Intl.NumberFormat('id-ID').format(lastPrice)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
size='small'
|
||||||
|
sx={{
|
||||||
|
color: 'primary.main',
|
||||||
|
textTransform: 'none',
|
||||||
|
p: 0,
|
||||||
|
minWidth: 'auto'
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
console.log('Navigate to purchase history')
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Riwayat harga beli
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const PurchaseAddForm: React.FC = () => {
|
const PurchaseAddForm: React.FC = () => {
|
||||||
const [imageUrl, setImageUrl] = useState<string>('')
|
const [imageUrl, setImageUrl] = useState<string>('')
|
||||||
|
const [errors, setErrors] = useState<ValidationErrors>({})
|
||||||
|
const [popoverState, setPopoverState] = useState<PopoverState>({
|
||||||
|
isOpen: false,
|
||||||
|
anchorEl: null,
|
||||||
|
itemIndex: null
|
||||||
|
})
|
||||||
const [formData, setFormData] = useState<PurchaseOrderFormData>({
|
const [formData, setFormData] = useState<PurchaseOrderFormData>({
|
||||||
vendor: null,
|
vendor: null,
|
||||||
po_number: '',
|
po_number: '',
|
||||||
transaction_date: '',
|
transaction_date: '',
|
||||||
due_date: '',
|
due_date: '',
|
||||||
reference: '',
|
reference: '',
|
||||||
status: 'draft',
|
status: 'sent',
|
||||||
// Bottom section toggles
|
|
||||||
showPesan: false,
|
showPesan: false,
|
||||||
showAttachment: false,
|
showAttachment: false,
|
||||||
message: '',
|
message: '',
|
||||||
// Items
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -154,7 +182,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
label: ingredient.name,
|
label: ingredient.name,
|
||||||
value: ingredient.id,
|
value: ingredient.id,
|
||||||
id: ingredient.id,
|
id: ingredient.id,
|
||||||
originalData: ingredient // This includes the full IngredientItem with unit, cost, etc.
|
originalData: ingredient
|
||||||
}))
|
}))
|
||||||
}, [ingredients, isLoadingIngredients])
|
}, [ingredients, isLoadingIngredients])
|
||||||
|
|
||||||
@ -172,12 +200,78 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}, [units, isLoadingUnits])
|
}, [units, isLoadingUnits])
|
||||||
|
|
||||||
|
// Handle price field click untuk menampilkan popover
|
||||||
|
const handlePriceFieldClick = (event: React.MouseEvent<HTMLElement>, 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
|
// Handler Functions
|
||||||
const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => {
|
const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value
|
[field]: value
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
if (errors[field as keyof ValidationErrors]) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: undefined
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleItemChange = (index: number, field: keyof PurchaseOrderFormItem, value: any): void => {
|
const handleItemChange = (index: number, field: keyof PurchaseOrderFormItem, value: any): void => {
|
||||||
@ -185,7 +279,6 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
const newItems = [...prev.items]
|
const newItems = [...prev.items]
|
||||||
newItems[index] = { ...newItems[index], [field]: value }
|
newItems[index] = { ...newItems[index], [field]: value }
|
||||||
|
|
||||||
// Auto-calculate total if amount or quantity changes
|
|
||||||
if (field === 'amount' || field === 'quantity') {
|
if (field === 'amount' || field === 'quantity') {
|
||||||
const item = newItems[index]
|
const item = newItems[index]
|
||||||
item.total = item.amount * item.quantity
|
item.total = item.amount * item.quantity
|
||||||
@ -193,30 +286,31 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
|
|
||||||
return { ...prev, items: newItems }
|
return { ...prev, items: newItems }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (errors.items) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
items: undefined
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIngredientSelection = (index: number, selectedIngredient: any) => {
|
const handleIngredientSelection = (index: number, selectedIngredient: any) => {
|
||||||
handleItemChange(index, 'ingredient', selectedIngredient)
|
handleItemChange(index, 'ingredient', selectedIngredient)
|
||||||
|
|
||||||
// Auto-populate related fields if available in the ingredient data
|
|
||||||
if (selectedIngredient) {
|
if (selectedIngredient) {
|
||||||
const ingredientData: IngredientItem = selectedIngredient.originalData || selectedIngredient
|
const ingredientData: IngredientItem = selectedIngredient.originalData || selectedIngredient
|
||||||
|
|
||||||
// Auto-fill unit based on IngredientItem structure
|
|
||||||
if (ingredientData.unit_id || ingredientData.unit) {
|
if (ingredientData.unit_id || ingredientData.unit) {
|
||||||
let unitToFind = null
|
let unitToFind = null
|
||||||
|
|
||||||
// If ingredient has unit object (populated relation)
|
|
||||||
if (ingredientData.unit && typeof ingredientData.unit === 'object') {
|
if (ingredientData.unit && typeof ingredientData.unit === 'object') {
|
||||||
unitToFind = ingredientData.unit
|
unitToFind = ingredientData.unit
|
||||||
}
|
} else if (ingredientData.unit_id) {
|
||||||
// If ingredient has unit_id, find the unit from unitOptions
|
|
||||||
else if (ingredientData.unit_id) {
|
|
||||||
unitToFind = unitOptions.find(option => option.value === ingredientData.unit_id)
|
unitToFind = unitOptions.find(option => option.value === ingredientData.unit_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unitToFind) {
|
if (unitToFind) {
|
||||||
// Create unit option object
|
|
||||||
const unitOption = {
|
const unitOption = {
|
||||||
label: (unitToFind as any).label || (unitToFind as any).name || (unitToFind as any).unit_name,
|
label: (unitToFind as any).label || (unitToFind as any).name || (unitToFind as any).unit_name,
|
||||||
value: (unitToFind as any).value || ingredientData.unit_id
|
value: (unitToFind as any).value || ingredientData.unit_id
|
||||||
@ -226,12 +320,10 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-fill amount with cost from IngredientItem
|
|
||||||
if (ingredientData.cost !== undefined && ingredientData.cost !== null) {
|
if (ingredientData.cost !== undefined && ingredientData.cost !== null) {
|
||||||
handleItemChange(index, 'amount', ingredientData.cost)
|
handleItemChange(index, 'amount', ingredientData.cost)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-fill description with ingredient name
|
|
||||||
if (ingredientData.name) {
|
if (ingredientData.name) {
|
||||||
handleItemChange(index, 'description', ingredientData.name)
|
handleItemChange(index, 'description', ingredientData.name)
|
||||||
}
|
}
|
||||||
@ -261,7 +353,6 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to get selected vendor data
|
|
||||||
const getSelectedVendorData = () => {
|
const getSelectedVendorData = () => {
|
||||||
if (!formData.vendor?.value || !vendors) return null
|
if (!formData.vendor?.value || !vendors) return null
|
||||||
const selectedVendor = vendors.find(vendor => vendor.id === (formData?.vendor?.value ?? ''))
|
const selectedVendor = vendors.find(vendor => vendor.id === (formData?.vendor?.value ?? ''))
|
||||||
@ -280,29 +371,26 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('file_type', 'image')
|
formData.append('file_type', 'image')
|
||||||
formData.append('description', 'Purchase image')
|
formData.append('description', 'Gambar Purchase Order')
|
||||||
|
|
||||||
mutate(formData, {
|
mutate(formData, {
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
// pemakaian:
|
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
attachment_file_ids: upsertAttachment(prev.attachment_file_ids, data.id)
|
attachment_file_ids: upsertAttachment(prev.attachment_file_ids, data.id)
|
||||||
}))
|
}))
|
||||||
setImageUrl(data.file_url)
|
setImageUrl(data.file_url)
|
||||||
resolve(data.id) // <-- balikin id file yang berhasil diupload
|
resolve(data.id)
|
||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
reject(error) // biar async/await bisa tangkep error
|
reject(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate subtotal from items
|
|
||||||
const subtotal = formData.items.reduce((sum, item) => sum + (item.total || 0), 0)
|
const subtotal = formData.items.reduce((sum, item) => sum + (item.total || 0), 0)
|
||||||
|
|
||||||
// Convert form data to API request format
|
|
||||||
const convertToApiRequest = (): PurchaseOrderRequest => {
|
const convertToApiRequest = (): PurchaseOrderRequest => {
|
||||||
return {
|
return {
|
||||||
vendor_id: formData.vendor?.value || '',
|
vendor_id: formData.vendor?.value || '',
|
||||||
@ -313,7 +401,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
status: formData.status,
|
status: formData.status,
|
||||||
message: formData.message || undefined,
|
message: formData.message || undefined,
|
||||||
items: formData.items
|
items: formData.items
|
||||||
.filter(item => item.ingredient && item.unit) // Only include valid items
|
.filter(item => item.ingredient && item.unit)
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
ingredient_id: item.ingredient!.value,
|
ingredient_id: item.ingredient!.value,
|
||||||
description: item.description || undefined,
|
description: item.description || undefined,
|
||||||
@ -326,19 +414,46 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
general: 'Mohon lengkapi semua field yang wajib diisi'
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
createPurchaseOrder.mutate(convertToApiRequest(), {
|
createPurchaseOrder.mutate(convertToApiRequest(), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
window.history.back()
|
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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{errors.general && (
|
||||||
|
<Alert severity='error' sx={{ mb: 3 }}>
|
||||||
|
{errors.general}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{/* BASIC INFO SECTION */}
|
{/* BASIC INFO SECTION */}
|
||||||
{/* Row 1 - Vendor and PO Number */}
|
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
<CustomAutocomplete
|
<CustomAutocomplete
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -348,16 +463,18 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
handleInputChange('vendor', newValue)
|
handleInputChange('vendor', newValue)
|
||||||
if (newValue?.value) {
|
if (newValue?.value) {
|
||||||
const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value)
|
const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value)
|
||||||
console.log('Vendor selected:', selectedVendorData)
|
console.log('Vendor terpilih:', selectedVendorData)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
loading={isLoadingVendors}
|
loading={isLoadingVendors}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
{...params}
|
||||||
label='Vendor'
|
label='Vendor *'
|
||||||
placeholder={isLoadingVendors ? 'Memuat vendor...' : 'Pilih kontak'}
|
placeholder={isLoadingVendors ? 'Memuat vendor...' : 'Pilih vendor'}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
error={!!errors.vendor}
|
||||||
|
helperText={errors.vendor}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -387,9 +504,11 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='PO Number'
|
label='Nomor PO *'
|
||||||
value={formData.po_number}
|
value={formData.po_number}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('po_number', e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('po_number', e.target.value)}
|
||||||
|
error={!!errors.po_number}
|
||||||
|
helperText={errors.po_number}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@ -397,7 +516,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='Transaction Date'
|
label='Tanggal Transaksi *'
|
||||||
type='date'
|
type='date'
|
||||||
value={formData.transaction_date}
|
value={formData.transaction_date}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
@ -406,25 +525,29 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
shrink: true
|
shrink: true
|
||||||
}}
|
}}
|
||||||
|
error={!!errors.transaction_date}
|
||||||
|
helperText={errors.transaction_date}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='Due Date'
|
label='Tanggal Jatuh Tempo *'
|
||||||
type='date'
|
type='date'
|
||||||
value={formData.due_date}
|
value={formData.due_date}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('due_date', e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('due_date', e.target.value)}
|
||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
shrink: true
|
shrink: true
|
||||||
}}
|
}}
|
||||||
|
error={!!errors.due_date}
|
||||||
|
helperText={errors.due_date}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='Reference'
|
label='Referensi'
|
||||||
placeholder='Reference'
|
placeholder='Referensi'
|
||||||
value={formData.reference}
|
value={formData.reference}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('reference', e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('reference', e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -433,18 +556,24 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
{/* ITEMS TABLE SECTION */}
|
{/* ITEMS TABLE SECTION */}
|
||||||
<Grid size={{ xs: 12 }} sx={{ mt: 4 }}>
|
<Grid size={{ xs: 12 }} sx={{ mt: 4 }}>
|
||||||
<Typography variant='h6' sx={{ mb: 2, fontWeight: 600 }}>
|
<Typography variant='h6' sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
Purchase Order Items
|
Item Purchase Order
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
{errors.items && (
|
||||||
|
<Alert severity='error' sx={{ mb: 2 }}>
|
||||||
|
{errors.items}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<TableContainer component={Paper} variant='outlined'>
|
<TableContainer component={Paper} variant='outlined'>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow sx={{ backgroundColor: 'grey.50' }}>
|
<TableRow sx={{ backgroundColor: 'grey.50' }}>
|
||||||
<TableCell sx={{ fontWeight: 'bold', minWidth: 180 }}>Ingredient</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', minWidth: 180 }}>Bahan</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 'bold', minWidth: 150 }}>Description</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', minWidth: 150 }}>Deskripsi</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 100 }}>Quantity</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', width: 100 }}>Kuantitas</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Unit</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Satuan</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Amount</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Harga</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 100, textAlign: 'right' }}>Total</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', width: 100, textAlign: 'right' }}>Total</TableCell>
|
||||||
<TableCell sx={{ width: 50 }}></TableCell>
|
<TableCell sx={{ width: 50 }}></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -472,7 +601,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
{...params}
|
||||||
placeholder={isLoadingIngredients ? 'Loading ingredients...' : 'Select Ingredient'}
|
placeholder={isLoadingIngredients ? 'Memuat bahan...' : 'Pilih Bahan'}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
...params.InputProps,
|
...params.InputProps,
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
@ -495,7 +624,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
handleItemChange(index, 'description', e.target.value)
|
handleItemChange(index, 'description', e.target.value)
|
||||||
}
|
}
|
||||||
placeholder='Description'
|
placeholder='Deskripsi'
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -524,7 +653,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
{...params}
|
{...params}
|
||||||
placeholder={isLoadingUnits ? 'Loading units...' : 'Select...'}
|
placeholder={isLoadingUnits ? 'Memuat satuan...' : 'Pilih satuan...'}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
...params.InputProps,
|
...params.InputProps,
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
@ -554,8 +683,10 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
const numericValue = parseFloat(value)
|
const numericValue = parseFloat(value)
|
||||||
handleItemChange(index, 'amount', isNaN(numericValue) ? 0 : numericValue)
|
handleItemChange(index, 'amount', isNaN(numericValue) ? 0 : numericValue)
|
||||||
}}
|
}}
|
||||||
|
onClick={e => handlePriceFieldClick(e, index)}
|
||||||
inputProps={{ min: 0, step: 'any' }}
|
inputProps={{ min: 0, step: 'any' }}
|
||||||
placeholder='0'
|
placeholder='0'
|
||||||
|
sx={{ cursor: item.ingredient ? 'pointer' : 'text' }}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -596,7 +727,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
sx={{ mt: 1 }}
|
sx={{ mt: 1 }}
|
||||||
disabled={isLoadingIngredients || isLoadingUnits}
|
disabled={isLoadingIngredients || isLoadingUnits}
|
||||||
>
|
>
|
||||||
Add Item
|
Tambah Item
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@ -636,7 +767,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
<i className='tabler-chevron-right w-4 h-4' />
|
<i className='tabler-chevron-right w-4 h-4' />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
Message
|
Pesan
|
||||||
</Button>
|
</Button>
|
||||||
{formData.showPesan && (
|
{formData.showPesan && (
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mt: 2 }}>
|
||||||
@ -644,7 +775,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
multiline
|
multiline
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder='Add message...'
|
placeholder='Tambahkan pesan...'
|
||||||
value={formData.message || ''}
|
value={formData.message || ''}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
handleInputChange('message', e.target.value)
|
handleInputChange('message', e.target.value)
|
||||||
@ -685,7 +816,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
<i className='tabler-chevron-right w-4 h-4' />
|
<i className='tabler-chevron-right w-4 h-4' />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
Attachment
|
Lampiran
|
||||||
</Button>
|
</Button>
|
||||||
{formData.showAttachment && (
|
{formData.showAttachment && (
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
@ -693,8 +824,8 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
maxFileSize={1 * 1024 * 1024}
|
maxFileSize={1 * 1024 * 1024}
|
||||||
showUrlOption={false}
|
showUrlOption={false}
|
||||||
currentImageUrl={imageUrl}
|
currentImageUrl={imageUrl}
|
||||||
dragDropText='Drop your image here'
|
dragDropText='Letakkan gambar Anda di sini'
|
||||||
browseButtonText='Choose Image'
|
browseButtonText='Pilih Gambar'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@ -759,6 +890,7 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
color='primary'
|
color='primary'
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
disabled={createPurchaseOrder.isPending}
|
||||||
sx={{
|
sx={{
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@ -770,13 +902,28 @@ const PurchaseAddForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
{createPurchaseOrder.isPending ? (
|
||||||
|
<>
|
||||||
|
<CircularProgress size={16} sx={{ mr: 1 }} />
|
||||||
|
Menyimpan...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Simpan'
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* Price Popover */}
|
||||||
|
<PricePopover
|
||||||
|
anchorEl={popoverState.anchorEl}
|
||||||
|
open={popoverState.isOpen}
|
||||||
|
onClose={handleClosePopover}
|
||||||
|
ingredientData={getCurrentIngredientData()}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user