feat: Purchase Order Add
This commit is contained in:
parent
dfa8218017
commit
4c4579f009
@ -0,0 +1,19 @@
|
|||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
|
||||||
|
import PurchaseOrderAddHeader from '@/views/apps/purchase/purchase-orders/add/PurchaseOrderAddHeader'
|
||||||
|
import PurchaseOrderAddForm from '@/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm'
|
||||||
|
|
||||||
|
const PurchaseOrderAddPage = () => {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={6}>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<PurchaseOrderAddHeader />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<PurchaseOrderAddForm />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PurchaseOrderAddPage
|
||||||
291
src/components/ImageUpload.tsx
Normal file
291
src/components/ImageUpload.tsx
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// React Imports
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
// MUI Imports
|
||||||
|
import type { BoxProps } from '@mui/material/Box'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
import List from '@mui/material/List'
|
||||||
|
import ListItem from '@mui/material/ListItem'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
|
||||||
|
// Third-party Imports
|
||||||
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
|
||||||
|
// Component Imports
|
||||||
|
import Link from '@components/Link'
|
||||||
|
import CustomAvatar from '@core/components/mui/Avatar'
|
||||||
|
|
||||||
|
// Styled Component Imports
|
||||||
|
import AppReactDropzone from '@/libs/styles/AppReactDropzone'
|
||||||
|
|
||||||
|
type FileProp = {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageUploadProps {
|
||||||
|
// Required props
|
||||||
|
onUpload: (file: File) => Promise<string> | string // Returns image URL
|
||||||
|
|
||||||
|
// Optional customization props
|
||||||
|
title?: string | null // Made nullable
|
||||||
|
currentImageUrl?: string
|
||||||
|
onImageRemove?: () => void
|
||||||
|
onImageChange?: (url: string) => void
|
||||||
|
|
||||||
|
// Upload state
|
||||||
|
isUploading?: boolean
|
||||||
|
|
||||||
|
// UI customization
|
||||||
|
maxFileSize?: number // in bytes
|
||||||
|
acceptedFileTypes?: string[]
|
||||||
|
showUrlOption?: boolean
|
||||||
|
uploadButtonText?: string
|
||||||
|
browseButtonText?: string
|
||||||
|
dragDropText?: string
|
||||||
|
replaceText?: string
|
||||||
|
|
||||||
|
// Style customization
|
||||||
|
className?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styled Dropzone Component
|
||||||
|
const Dropzone = styled(AppReactDropzone)<BoxProps>(({ theme }) => ({
|
||||||
|
'& .dropzone': {
|
||||||
|
minHeight: 'unset',
|
||||||
|
padding: theme.spacing(12),
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
paddingInline: theme.spacing(5)
|
||||||
|
},
|
||||||
|
'&+.MuiList-root .MuiListItem-root .file-name': {
|
||||||
|
fontWeight: theme.typography.body1.fontWeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const ImageUpload: React.FC<ImageUploadProps> = ({
|
||||||
|
onUpload,
|
||||||
|
title = null, // Default to null
|
||||||
|
currentImageUrl = '',
|
||||||
|
onImageRemove,
|
||||||
|
onImageChange,
|
||||||
|
isUploading = false,
|
||||||
|
maxFileSize = 5 * 1024 * 1024, // 5MB default
|
||||||
|
acceptedFileTypes = ['image/*'],
|
||||||
|
showUrlOption = true,
|
||||||
|
uploadButtonText = 'Upload',
|
||||||
|
browseButtonText = 'Browse Image',
|
||||||
|
dragDropText = 'Drag and Drop Your Image Here.',
|
||||||
|
replaceText = 'Drop New Image to Replace',
|
||||||
|
className = '',
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
// States
|
||||||
|
const [files, setFiles] = useState<File[]>([])
|
||||||
|
const [error, setError] = useState<string>('')
|
||||||
|
|
||||||
|
const handleUpload = async () => {
|
||||||
|
if (!files.length) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setError('')
|
||||||
|
const imageUrl = await onUpload(files[0])
|
||||||
|
|
||||||
|
if (typeof imageUrl === 'string') {
|
||||||
|
onImageChange?.(imageUrl)
|
||||||
|
setFiles([]) // Clear files after successful upload
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Upload failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
|
onDrop: (acceptedFiles: File[]) => {
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
if (acceptedFiles.length === 0) return
|
||||||
|
|
||||||
|
const file = acceptedFiles[0]
|
||||||
|
|
||||||
|
// Validate file size
|
||||||
|
if (file.size > maxFileSize) {
|
||||||
|
setError(`File size should be less than ${formatFileSize(maxFileSize)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace files instead of adding to them
|
||||||
|
setFiles([file])
|
||||||
|
},
|
||||||
|
accept: acceptedFileTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}),
|
||||||
|
disabled: disabled || isUploading
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 Bytes'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderFilePreview = (file: FileProp) => {
|
||||||
|
if (file.type.startsWith('image')) {
|
||||||
|
return <img width={38} height={38} alt={file.name} src={URL.createObjectURL(file as any)} />
|
||||||
|
} else {
|
||||||
|
return <i className='tabler-file-description' />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveFile = (file: FileProp) => {
|
||||||
|
const filtered = files.filter((i: FileProp) => i.name !== file.name)
|
||||||
|
setFiles(filtered)
|
||||||
|
setError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveCurrentImage = () => {
|
||||||
|
onImageRemove?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveAllFiles = () => {
|
||||||
|
setFiles([])
|
||||||
|
setError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileList = files.map((file: FileProp) => (
|
||||||
|
<ListItem key={file.name} className='pis-4 plb-3'>
|
||||||
|
<div className='file-details'>
|
||||||
|
<div className='file-preview'>{renderFilePreview(file)}</div>
|
||||||
|
<div>
|
||||||
|
<Typography className='file-name font-medium' color='text.primary'>
|
||||||
|
{file.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography className='file-size' variant='body2'>
|
||||||
|
{formatFileSize(file.size)}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<IconButton onClick={() => handleRemoveFile(file)} disabled={isUploading}>
|
||||||
|
<i className='tabler-x text-xl' />
|
||||||
|
</IconButton>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropzone className={className}>
|
||||||
|
{/* Conditional title and URL option header */}
|
||||||
|
{title && (
|
||||||
|
<div className='flex justify-between items-center mb-4'>
|
||||||
|
<Typography variant='h6' component='h2'>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
{showUrlOption && (
|
||||||
|
<Typography component={Link} color='primary.main' className='font-medium'>
|
||||||
|
Add media from URL
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div {...getRootProps({ className: 'dropzone' })}>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<div className='flex items-center flex-col gap-2 text-center'>
|
||||||
|
<CustomAvatar variant='rounded' skin='light' color='secondary'>
|
||||||
|
<i className='tabler-upload' />
|
||||||
|
</CustomAvatar>
|
||||||
|
<Typography variant='h4'>{currentImageUrl && !files.length ? replaceText : dragDropText}</Typography>
|
||||||
|
<Typography color='text.disabled'>or</Typography>
|
||||||
|
<Button variant='tonal' size='small' disabled={disabled || isUploading}>
|
||||||
|
{browseButtonText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<Typography color='error' variant='body2' className='mt-2 text-center'>
|
||||||
|
{error}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show current image if it exists */}
|
||||||
|
{currentImageUrl && !files.length && (
|
||||||
|
<div className='current-image mb-4'>
|
||||||
|
<Typography variant='subtitle2' className='mb-2'>
|
||||||
|
Current Image:
|
||||||
|
</Typography>
|
||||||
|
<div className='flex items-center justify-between p-3 border border-gray-200 rounded'>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<img width={60} height={60} alt='Current image' src={currentImageUrl} className='rounded object-cover' />
|
||||||
|
<div>
|
||||||
|
<Typography className='font-medium' color='text.primary'>
|
||||||
|
Current image
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
Uploaded image
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{onImageRemove && (
|
||||||
|
<IconButton onClick={handleRemoveCurrentImage} color='error' disabled={isUploading}>
|
||||||
|
<i className='tabler-x text-xl' />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* File list and upload buttons */}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<>
|
||||||
|
<List>{fileList}</List>
|
||||||
|
<div className='buttons'>
|
||||||
|
<Button color='error' variant='tonal' onClick={handleRemoveAllFiles} disabled={isUploading}>
|
||||||
|
Remove All
|
||||||
|
</Button>
|
||||||
|
<Button variant='contained' onClick={handleUpload} disabled={isUploading}>
|
||||||
|
{isUploading ? 'Uploading...' : uploadButtonText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dropzone>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageUpload
|
||||||
|
|
||||||
|
// ===== USAGE EXAMPLES =====
|
||||||
|
|
||||||
|
// 1. Without title
|
||||||
|
// <ImageUpload
|
||||||
|
// onUpload={handleUpload}
|
||||||
|
// currentImageUrl={imageUrl}
|
||||||
|
// onImageChange={setImageUrl}
|
||||||
|
// onImageRemove={() => setImageUrl('')}
|
||||||
|
// />
|
||||||
|
|
||||||
|
// 2. With title
|
||||||
|
// <ImageUpload
|
||||||
|
// title="Product Image"
|
||||||
|
// onUpload={handleUpload}
|
||||||
|
// currentImageUrl={imageUrl}
|
||||||
|
// onImageChange={setImageUrl}
|
||||||
|
// onImageRemove={() => setImageUrl('')}
|
||||||
|
// />
|
||||||
|
|
||||||
|
// 3. Explicitly set title to null
|
||||||
|
// <ImageUpload
|
||||||
|
// title={null}
|
||||||
|
// onUpload={handleUpload}
|
||||||
|
// currentImageUrl={imageUrl}
|
||||||
|
// onImageChange={setImageUrl}
|
||||||
|
// onImageRemove={() => setImageUrl('')}
|
||||||
|
// />
|
||||||
@ -9,3 +9,58 @@ export type PurchaseOrderType = {
|
|||||||
status: string
|
status: string
|
||||||
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
|
||||||
|
discount: string
|
||||||
|
harga: number
|
||||||
|
pajak: { label: string; value: string } | null
|
||||||
|
waste: { label: string; value: string } | null
|
||||||
|
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 {
|
||||||
|
id: string
|
||||||
|
type: string
|
||||||
|
name: string
|
||||||
|
amount: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DropdownOption {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,121 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Card, CardContent } from '@mui/material'
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
||||||
|
import PurchaseOrderBasicInfo from './PurchaseOrderBasicInfo'
|
||||||
|
import PurchaseOrderIngredientsTable from './PurchaseOrderIngredientsTable'
|
||||||
|
import PurchaseOrderSummary from './PurchaseOrderSummary'
|
||||||
|
|
||||||
|
const PurchaseOrderAddForm: React.FC = () => {
|
||||||
|
const [formData, setFormData] = useState<PurchaseOrderFormData>({
|
||||||
|
vendor: null,
|
||||||
|
nomor: 'PO/00043',
|
||||||
|
tglTransaksi: '2025-09-09',
|
||||||
|
tglJatuhTempo: '2025-09-10',
|
||||||
|
referensi: '',
|
||||||
|
termin: null,
|
||||||
|
hargaTermasukPajak: true,
|
||||||
|
// Shipping info
|
||||||
|
showShippingInfo: false,
|
||||||
|
tanggalPengiriman: '',
|
||||||
|
ekspedisi: null,
|
||||||
|
noResi: '',
|
||||||
|
// Bottom section toggles
|
||||||
|
showPesan: false,
|
||||||
|
showAttachment: false,
|
||||||
|
showTambahDiskon: false,
|
||||||
|
showBiayaPengiriman: false,
|
||||||
|
showBiayaTransaksi: false,
|
||||||
|
showUangMuka: false,
|
||||||
|
pesan: '',
|
||||||
|
// Ingredient items (updated from productItems)
|
||||||
|
ingredientItems: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
ingredient: null,
|
||||||
|
deskripsi: '',
|
||||||
|
kuantitas: 1,
|
||||||
|
satuan: null,
|
||||||
|
discount: '0',
|
||||||
|
harga: 0,
|
||||||
|
pajak: null,
|
||||||
|
waste: null,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleIngredientChange = (index: number, field: keyof IngredientItem, value: any): void => {
|
||||||
|
setFormData(prev => {
|
||||||
|
const newItems = [...prev.ingredientItems]
|
||||||
|
newItems[index] = { ...newItems[index], [field]: value }
|
||||||
|
|
||||||
|
// Auto-calculate total if price or quantity changes
|
||||||
|
if (field === 'harga' || field === 'kuantitas') {
|
||||||
|
const item = newItems[index]
|
||||||
|
item.total = item.harga * item.kuantitas
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...prev, ingredientItems: newItems }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addIngredientItem = (): void => {
|
||||||
|
const newItem: IngredientItem = {
|
||||||
|
id: Date.now(),
|
||||||
|
ingredient: null,
|
||||||
|
deskripsi: '',
|
||||||
|
kuantitas: 1,
|
||||||
|
satuan: null,
|
||||||
|
discount: '0%',
|
||||||
|
harga: 0,
|
||||||
|
pajak: null,
|
||||||
|
waste: null,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
ingredientItems: [...prev.ingredientItems, newItem]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeIngredientItem = (index: number): void => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
ingredientItems: prev.ingredientItems.filter((_, i) => i !== index)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{/* Basic Info Section */}
|
||||||
|
<PurchaseOrderBasicInfo formData={formData} handleInputChange={handleInputChange} />
|
||||||
|
|
||||||
|
{/* Ingredients Table Section */}
|
||||||
|
<PurchaseOrderIngredientsTable
|
||||||
|
formData={formData}
|
||||||
|
handleIngredientChange={handleIngredientChange}
|
||||||
|
addIngredientItem={addIngredientItem}
|
||||||
|
removeIngredientItem={removeIngredientItem}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Summary Section */}
|
||||||
|
<PurchaseOrderSummary formData={formData} handleInputChange={handleInputChange} />
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PurchaseOrderAddForm
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// MUI Imports
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
|
||||||
|
const PurchaseOrderAddHeader = () => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-wrap sm:items-center justify-between max-sm:flex-col gap-6'>
|
||||||
|
<div>
|
||||||
|
<Typography variant='h4' className='mbe-1'>
|
||||||
|
Tambah Pesanan Pembelian
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PurchaseOrderAddHeader
|
||||||
@ -0,0 +1,197 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { Button, Switch, FormControlLabel } from '@mui/material'
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
||||||
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
|
import { DropdownOption, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
||||||
|
|
||||||
|
interface PurchaseOrderBasicInfoProps {
|
||||||
|
formData: PurchaseOrderFormData
|
||||||
|
handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PurchaseOrderBasicInfo: React.FC<PurchaseOrderBasicInfoProps> = ({ formData, handleInputChange }) => {
|
||||||
|
// Sample data for dropdowns
|
||||||
|
const vendorOptions: DropdownOption[] = [
|
||||||
|
{ label: 'Vendor A', value: 'vendor_a' },
|
||||||
|
{ label: 'Vendor B', value: 'vendor_b' },
|
||||||
|
{ label: 'Vendor C', value: 'vendor_c' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const terminOptions: DropdownOption[] = [
|
||||||
|
{ label: 'Net 30', value: 'net_30' },
|
||||||
|
{ label: 'Net 15', value: 'net_15' },
|
||||||
|
{ label: 'Net 60', value: 'net_60' },
|
||||||
|
{ label: 'Cash on Delivery', value: 'cod' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const ekspedisiOptions: DropdownOption[] = [
|
||||||
|
{ label: 'JNE', value: 'jne' },
|
||||||
|
{ label: 'J&T Express', value: 'jnt' },
|
||||||
|
{ label: 'SiCepat', value: 'sicepat' },
|
||||||
|
{ label: 'Pos Indonesia', value: 'pos' },
|
||||||
|
{ label: 'TIKI', value: 'tiki' }
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Row 1 - Vendor dan Nomor */}
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
|
<CustomAutocomplete
|
||||||
|
fullWidth
|
||||||
|
options={vendorOptions}
|
||||||
|
value={formData.vendor}
|
||||||
|
onChange={(event, newValue) => handleInputChange('vendor', newValue)}
|
||||||
|
renderInput={params => <CustomTextField {...params} label='Vendor' placeholder='Pilih kontak' fullWidth />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Nomor'
|
||||||
|
value={formData.nomor}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('nomor', e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
readOnly: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Row 2 - Tgl. Transaksi, Tgl. Jatuh Tempo, Termin */}
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Tgl. Transaksi'
|
||||||
|
type='date'
|
||||||
|
value={formData.tglTransaksi}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('tglTransaksi', e.target.value)}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Tgl. Jatuh Tempo'
|
||||||
|
type='date'
|
||||||
|
value={formData.tglJatuhTempo}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('tglJatuhTempo', e.target.value)}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomAutocomplete
|
||||||
|
fullWidth
|
||||||
|
options={terminOptions}
|
||||||
|
value={formData.termin}
|
||||||
|
onChange={(event, newValue) => handleInputChange('termin', newValue)}
|
||||||
|
renderInput={params => <CustomTextField {...params} label='Termin' placeholder='Net 30' fullWidth />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Row 3 - Tampilkan Informasi Pengiriman */}
|
||||||
|
<Grid size={12}>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
onClick={() => handleInputChange('showShippingInfo', !formData.showShippingInfo)}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: '8px 0',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formData.showShippingInfo ? '−' : '+'} {formData.showShippingInfo ? 'Sembunyikan' : 'Tampilkan'} Informasi
|
||||||
|
Pengiriman
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Shipping Information - Conditional */}
|
||||||
|
{formData.showShippingInfo && (
|
||||||
|
<>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Tanggal Pengiriman'
|
||||||
|
type='date'
|
||||||
|
placeholder='Pilih tanggal'
|
||||||
|
value={formData.tanggalPengiriman}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('tanggalPengiriman', e.target.value)
|
||||||
|
}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomAutocomplete
|
||||||
|
fullWidth
|
||||||
|
options={ekspedisiOptions}
|
||||||
|
value={formData.ekspedisi}
|
||||||
|
onChange={(event, newValue) => handleInputChange('ekspedisi', newValue)}
|
||||||
|
renderInput={params => (
|
||||||
|
<CustomTextField {...params} label='Ekspedisi' placeholder='Pilih ekspedisi' fullWidth />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='No. Resi'
|
||||||
|
placeholder='No. Resi'
|
||||||
|
value={formData.noResi}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('noResi', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Row 4 - Referensi, SKU, Switch Pajak */}
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='Referensi'
|
||||||
|
placeholder='Referensi'
|
||||||
|
value={formData.referensi}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('referensi', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }}>
|
||||||
|
<CustomTextField fullWidth label='SKU' placeholder='Scan Barcode/SKU' variant='outlined' />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 4, md: 4 }} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={formData.hargaTermasukPajak}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('hargaTermasukPajak', e.target.checked)
|
||||||
|
}
|
||||||
|
color='primary'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label='Harga termasuk pajak'
|
||||||
|
sx={{
|
||||||
|
marginLeft: 0,
|
||||||
|
'& .MuiFormControlLabel-label': {
|
||||||
|
fontSize: '14px',
|
||||||
|
color: 'text.secondary'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PurchaseOrderBasicInfo
|
||||||
@ -0,0 +1,249 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { Button, Typography } from '@mui/material'
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
||||||
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
|
import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
||||||
|
|
||||||
|
interface PurchaseOrderIngredientsTableProps {
|
||||||
|
formData: PurchaseOrderFormData
|
||||||
|
handleIngredientChange: (index: number, field: keyof IngredientItem, value: any) => void
|
||||||
|
addIngredientItem: () => void
|
||||||
|
removeIngredientItem: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PurchaseOrderIngredientsTable: React.FC<PurchaseOrderIngredientsTableProps> = ({
|
||||||
|
formData,
|
||||||
|
handleIngredientChange,
|
||||||
|
addIngredientItem,
|
||||||
|
removeIngredientItem
|
||||||
|
}) => {
|
||||||
|
const ingredientOptions = [
|
||||||
|
{ label: 'Tepung Terigu Premium', value: 'tepung_terigu_premium' },
|
||||||
|
{ label: 'Gula Pasir Halus', value: 'gula_pasir_halus' },
|
||||||
|
{ label: 'Mentega Unsalted', value: 'mentega_unsalted' },
|
||||||
|
{ label: 'Telur Ayam Grade A', value: 'telur_ayam_grade_a' },
|
||||||
|
{ label: 'Vanilla Extract', value: 'vanilla_extract' },
|
||||||
|
{ label: 'Coklat Chips', value: 'coklat_chips' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const satuanOptions = [
|
||||||
|
{ label: 'KG', value: 'kg' },
|
||||||
|
{ label: 'GRAM', value: 'gram' },
|
||||||
|
{ label: 'LITER', value: 'liter' },
|
||||||
|
{ label: 'ML', value: 'ml' },
|
||||||
|
{ label: 'PCS', value: 'pcs' },
|
||||||
|
{ label: 'PACK', value: 'pack' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const pajakOptions = [
|
||||||
|
{ label: 'PPN 11%', value: 'ppn_11' },
|
||||||
|
{ label: 'PPN 0%', value: 'ppn_0' },
|
||||||
|
{ label: 'Bebas Pajak', value: 'tax_free' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const wasteOptions = [
|
||||||
|
{ label: '2%', value: '2' },
|
||||||
|
{ label: '5%', value: '5' },
|
||||||
|
{ label: '10%', value: '10' },
|
||||||
|
{ label: '15%', value: '15' },
|
||||||
|
{ label: 'Custom', value: 'custom' }
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid size={12} sx={{ mt: 4 }}>
|
||||||
|
<Typography variant='h6' sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
|
Bahan Baku / Ingredients
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Table Header */}
|
||||||
|
<Grid container spacing={1} sx={{ mb: 2, px: 1 }}>
|
||||||
|
<Grid size={1.8}>
|
||||||
|
<Typography variant='subtitle2' fontWeight={600}>
|
||||||
|
Bahan Baku
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1.8}>
|
||||||
|
<Typography variant='subtitle2' fontWeight={600}>
|
||||||
|
Deskripsi
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1}>
|
||||||
|
<Typography variant='subtitle2' fontWeight={600}>
|
||||||
|
Kuantitas
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1.2}>
|
||||||
|
<Typography variant='subtitle2' fontWeight={600}>
|
||||||
|
Satuan
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1}>
|
||||||
|
<Typography variant='subtitle2' fontWeight={600}>
|
||||||
|
Discount
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1.2}>
|
||||||
|
<Typography variant='subtitle2' fontWeight={600}>
|
||||||
|
Harga
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1.2}>
|
||||||
|
<Typography variant='subtitle2' fontWeight={600}>
|
||||||
|
Pajak
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1.2}>
|
||||||
|
<Typography variant='subtitle2' fontWeight={600}>
|
||||||
|
Waste
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1}>
|
||||||
|
<Typography variant='subtitle2' fontWeight={600}>
|
||||||
|
Total
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={0.6}></Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Ingredient Items */}
|
||||||
|
{formData.ingredientItems.map((item: IngredientItem, index: number) => (
|
||||||
|
<Grid container spacing={1} key={item.id} sx={{ mb: 2, alignItems: 'center' }}>
|
||||||
|
<Grid size={1.8}>
|
||||||
|
<CustomAutocomplete
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
options={ingredientOptions}
|
||||||
|
value={item.ingredient}
|
||||||
|
onChange={(event, newValue) => handleIngredientChange(index, 'ingredient', newValue)}
|
||||||
|
renderInput={params => (
|
||||||
|
<CustomTextField {...params} placeholder='Pilih Bahan Baku' size='small' fullWidth />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1.8}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
value={item.deskripsi}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleIngredientChange(index, 'deskripsi', e.target.value)
|
||||||
|
}
|
||||||
|
placeholder='Deskripsi'
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
type='number'
|
||||||
|
value={item.kuantitas}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleIngredientChange(index, 'kuantitas', parseInt(e.target.value) || 1)
|
||||||
|
}
|
||||||
|
inputProps={{ min: 1 }}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1.2}>
|
||||||
|
<CustomAutocomplete
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
options={satuanOptions}
|
||||||
|
value={item.satuan}
|
||||||
|
onChange={(event, newValue) => handleIngredientChange(index, 'satuan', newValue)}
|
||||||
|
renderInput={params => <CustomTextField {...params} placeholder='Pilih...' size='small' fullWidth />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
value={item.discount}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleIngredientChange(index, 'discount', e.target.value)
|
||||||
|
}
|
||||||
|
placeholder='0%'
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1.2}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
type='number'
|
||||||
|
value={item.harga === 0 ? '' : item.harga?.toString() || ''} // Ubah ini: 0 jadi empty string
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value
|
||||||
|
|
||||||
|
if (value === '') {
|
||||||
|
// Jika kosong, set ke null atau undefined, bukan 0
|
||||||
|
handleIngredientChange(index, 'harga', null) // atau undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericValue = parseFloat(value)
|
||||||
|
handleIngredientChange(index, 'harga', isNaN(numericValue) ? 0 : numericValue)
|
||||||
|
}}
|
||||||
|
inputProps={{ min: 0, step: 'any' }}
|
||||||
|
placeholder='0'
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1.2}>
|
||||||
|
<CustomAutocomplete
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
options={pajakOptions}
|
||||||
|
value={item.pajak}
|
||||||
|
onChange={(event, newValue) => handleIngredientChange(index, 'pajak', newValue)}
|
||||||
|
renderInput={params => <CustomTextField {...params} placeholder='...' size='small' fullWidth />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1.2}>
|
||||||
|
<CustomAutocomplete
|
||||||
|
fullWidth
|
||||||
|
size='small'
|
||||||
|
options={wasteOptions}
|
||||||
|
value={item.waste}
|
||||||
|
onChange={(event, newValue) => handleIngredientChange(index, 'waste', newValue)}
|
||||||
|
renderInput={params => <CustomTextField {...params} placeholder='...' size='small' fullWidth />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={1}>
|
||||||
|
<CustomTextField fullWidth size='small' value={item.total} InputProps={{ readOnly: true }} />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={0.6}>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
color='error'
|
||||||
|
size='small'
|
||||||
|
onClick={() => removeIngredientItem(index)}
|
||||||
|
sx={{ minWidth: 'auto', p: 1 }}
|
||||||
|
>
|
||||||
|
<i className='tabler-trash' />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Add New Item Button */}
|
||||||
|
<Grid container spacing={1}>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
onClick={addIngredientItem}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
color: 'primary.main',
|
||||||
|
borderColor: 'primary.main'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Tambah bahan baku
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PurchaseOrderIngredientsTable
|
||||||
@ -0,0 +1,578 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { Button, Typography, Box, ToggleButton, ToggleButtonGroup, InputAdornment, IconButton } from '@mui/material'
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
|
import { PurchaseOrderFormData, TransactionCost } from '@/types/apps/purchaseOrderTypes'
|
||||||
|
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
|
||||||
|
import ImageUpload from '@/components/ImageUpload'
|
||||||
|
|
||||||
|
interface PurchaseOrderSummaryProps {
|
||||||
|
formData: PurchaseOrderFormData
|
||||||
|
handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PurchaseOrderSummary: React.FC<PurchaseOrderSummaryProps> = ({ formData, handleInputChange }) => {
|
||||||
|
// Initialize transaction costs if not exist
|
||||||
|
const transactionCosts = formData.transactionCosts || []
|
||||||
|
|
||||||
|
// Options for transaction cost types
|
||||||
|
const transactionCostOptions = [
|
||||||
|
{ label: 'Biaya Admin', value: 'admin' },
|
||||||
|
{ label: 'Pajak', value: 'pajak' },
|
||||||
|
{ label: 'Materai', value: 'materai' },
|
||||||
|
{ label: 'Lainnya', value: 'lainnya' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Add new transaction cost
|
||||||
|
const addTransactionCost = () => {
|
||||||
|
const newCost: TransactionCost = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: '',
|
||||||
|
name: '',
|
||||||
|
amount: ''
|
||||||
|
}
|
||||||
|
handleInputChange('transactionCosts', [...transactionCosts, newCost])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove transaction cost
|
||||||
|
const removeTransactionCost = (id: string) => {
|
||||||
|
const filtered = transactionCosts.filter((cost: TransactionCost) => cost.id !== id)
|
||||||
|
handleInputChange('transactionCosts', filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update transaction cost
|
||||||
|
const updateTransactionCost = (id: string, field: keyof TransactionCost, value: string) => {
|
||||||
|
const updated = transactionCosts.map((cost: TransactionCost) =>
|
||||||
|
cost.id === id ? { ...cost, [field]: value } : cost
|
||||||
|
)
|
||||||
|
handleInputChange('transactionCosts', updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate discount amount based on percentage or fixed amount
|
||||||
|
const calculateDiscount = () => {
|
||||||
|
if (!formData.discountValue) return 0
|
||||||
|
|
||||||
|
const subtotal = formData.subtotal || 0
|
||||||
|
if (formData.discountType === 'percentage') {
|
||||||
|
return (subtotal * parseFloat(formData.discountValue)) / 100
|
||||||
|
}
|
||||||
|
return parseFloat(formData.discountValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const discountAmount = calculateDiscount()
|
||||||
|
const shippingCost = parseFloat(formData.shippingCost || '0')
|
||||||
|
|
||||||
|
// Calculate total transaction costs
|
||||||
|
const totalTransactionCost = transactionCosts.reduce((sum: number, cost: TransactionCost) => {
|
||||||
|
return sum + parseFloat(cost.amount || '0')
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const downPayment = parseFloat(formData.downPayment || '0')
|
||||||
|
|
||||||
|
// Calculate total (subtotal - discount + shipping + transaction costs)
|
||||||
|
const total = (formData.subtotal || 0) - discountAmount + shippingCost + totalTransactionCost
|
||||||
|
|
||||||
|
// Calculate remaining balance (total - down payment)
|
||||||
|
const remainingBalance = total - downPayment
|
||||||
|
|
||||||
|
const handleUpload = async (file: File): Promise<string> => {
|
||||||
|
// Simulate upload
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(URL.createObjectURL(file))
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid size={12} sx={{ mt: 4 }}>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{/* Left Side - Pesan and Attachment */}
|
||||||
|
<Grid size={{ xs: 12, md: 7 }}>
|
||||||
|
{/* Pesan Section */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='inherit'
|
||||||
|
onClick={() => handleInputChange('showPesan', !formData.showPesan)}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: '12px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: 'text.primary',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#eeeeee'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box component='span' sx={{ mr: 1 }}>
|
||||||
|
{formData.showPesan ? (
|
||||||
|
<i className='tabler-chevron-down w-4 h-4' />
|
||||||
|
) : (
|
||||||
|
<i className='tabler-chevron-right w-4 h-4' />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
Pesan
|
||||||
|
</Button>
|
||||||
|
{formData.showPesan && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
placeholder='Tambahkan pesan...'
|
||||||
|
value={formData.pesan || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange('pesan', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Attachment Section */}
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='inherit'
|
||||||
|
onClick={() => handleInputChange('showAttachment', !formData.showAttachment)}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: '12px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: 'text.primary',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#eeeeee'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box component='span' sx={{ mr: 1 }}>
|
||||||
|
{formData.showAttachment ? (
|
||||||
|
<i className='tabler-chevron-down w-4 h-4' />
|
||||||
|
) : (
|
||||||
|
<i className='tabler-chevron-right w-4 h-4' />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
Attachment
|
||||||
|
</Button>
|
||||||
|
{formData.showAttachment && (
|
||||||
|
<ImageUpload
|
||||||
|
onUpload={handleUpload}
|
||||||
|
maxFileSize={1 * 1024 * 1024} // 1MB
|
||||||
|
showUrlOption={false}
|
||||||
|
dragDropText='Drop your image here'
|
||||||
|
browseButtonText='Choose Image'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Right Side - Totals */}
|
||||||
|
<Grid size={{ xs: 12, md: 5 }}>
|
||||||
|
<Box sx={{ backgroundColor: '#ffffff', p: 3, borderRadius: '8px' }}>
|
||||||
|
{/* Sub Total */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='body1' color='text.secondary' sx={{ fontSize: '16px' }}>
|
||||||
|
Sub Total
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' fontWeight={600} sx={{ fontSize: '16px', textAlign: 'right' }}>
|
||||||
|
{new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
minimumFractionDigits: 0
|
||||||
|
}).format(formData.subtotal || 0)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Additional Options */}
|
||||||
|
<Box>
|
||||||
|
{/* Tambah Diskon */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
size='small'
|
||||||
|
sx={{ textTransform: 'none', fontSize: '14px', p: 0, textAlign: 'left' }}
|
||||||
|
onClick={() => handleInputChange('showTambahDiskon', !formData.showTambahDiskon)}
|
||||||
|
>
|
||||||
|
{formData.showTambahDiskon ? '- Sembunyikan Diskon' : '+ Tambahan Diskon'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Show input form when showTambahDiskon is true */}
|
||||||
|
{formData.showTambahDiskon && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 1 }}>
|
||||||
|
<CustomTextField
|
||||||
|
size='small'
|
||||||
|
placeholder='0'
|
||||||
|
value={formData.discountValue || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('discountValue', e.target.value)
|
||||||
|
}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment:
|
||||||
|
formData.discountType === 'percentage' ? (
|
||||||
|
<InputAdornment position='end'>%</InputAdornment>
|
||||||
|
) : undefined
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={formData.discountType || 'percentage'}
|
||||||
|
exclusive
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
if (newValue) handleInputChange('discountType', newValue)
|
||||||
|
}}
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
<ToggleButton value='percentage' sx={{ px: 2 }}>
|
||||||
|
%
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value='fixed' sx={{ px: 2 }}>
|
||||||
|
Rp
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Biaya Pengiriman */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
size='small'
|
||||||
|
sx={{ textTransform: 'none', fontSize: '14px', p: 0, textAlign: 'left' }}
|
||||||
|
onClick={() => handleInputChange('showBiayaPengiriman', !formData.showBiayaPengiriman)}
|
||||||
|
>
|
||||||
|
{formData.showBiayaPengiriman ? '- Sembunyikan Biaya Pengiriman' : '+ Biaya pengiriman'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Show input form when showBiayaPengiriman is true */}
|
||||||
|
{formData.showBiayaPengiriman && (
|
||||||
|
<Box sx={{ mt: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Typography variant='body2' sx={{ minWidth: '140px' }}>
|
||||||
|
Biaya pengiriman
|
||||||
|
</Typography>
|
||||||
|
<CustomTextField
|
||||||
|
size='small'
|
||||||
|
placeholder='0'
|
||||||
|
value={formData.shippingCost || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('shippingCost', e.target.value)
|
||||||
|
}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: <InputAdornment position='start'>Rp</InputAdornment>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Biaya Transaksi - Multiple */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
size='small'
|
||||||
|
sx={{ textTransform: 'none', fontSize: '14px', p: 0, textAlign: 'left' }}
|
||||||
|
onClick={() => {
|
||||||
|
if (!formData.showBiayaTransaksi) {
|
||||||
|
handleInputChange('showBiayaTransaksi', true)
|
||||||
|
if (transactionCosts.length === 0) {
|
||||||
|
addTransactionCost()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleInputChange('showBiayaTransaksi', false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formData.showBiayaTransaksi ? '- Sembunyikan Biaya Transaksi' : '+ Biaya Transaksi'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Show multiple transaction cost inputs */}
|
||||||
|
{formData.showBiayaTransaksi && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
{transactionCosts.map((cost: TransactionCost, index: number) => (
|
||||||
|
<Box key={cost.id} sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 2 }}>
|
||||||
|
{/* Remove button */}
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
onClick={() => removeTransactionCost(cost.id)}
|
||||||
|
sx={{
|
||||||
|
color: 'error.main',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'error.main',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'error.lighter'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className='tabler-trash' />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{/* Type AutoComplete */}
|
||||||
|
<CustomAutocomplete
|
||||||
|
size='small'
|
||||||
|
options={transactionCostOptions}
|
||||||
|
getOptionLabel={option => (typeof option === 'string' ? option : option.label)}
|
||||||
|
value={transactionCostOptions.find(option => option.value === cost.type) || null}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
updateTransactionCost(cost.id, 'type', newValue ? newValue.value : '')
|
||||||
|
}}
|
||||||
|
renderInput={params => (
|
||||||
|
<CustomTextField {...params} size='small' placeholder='Pilih biaya transaksi...' />
|
||||||
|
)}
|
||||||
|
sx={{ minWidth: 180 }}
|
||||||
|
noOptionsText='Tidak ada pilihan'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Name input */}
|
||||||
|
<CustomTextField
|
||||||
|
size='small'
|
||||||
|
placeholder='Nama'
|
||||||
|
value={cost.name}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
updateTransactionCost(cost.id, 'name', e.target.value)
|
||||||
|
}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Amount input */}
|
||||||
|
<CustomTextField
|
||||||
|
size='small'
|
||||||
|
placeholder='0'
|
||||||
|
value={cost.amount}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
updateTransactionCost(cost.id, 'amount', e.target.value)
|
||||||
|
}
|
||||||
|
sx={{ width: 120 }}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: <InputAdornment position='start'>Rp</InputAdornment>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Add more button */}
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='primary'
|
||||||
|
size='small'
|
||||||
|
onClick={addTransactionCost}
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '13px',
|
||||||
|
mt: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Tambah biaya transaksi lain
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Total */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='h6' fontWeight={600} sx={{ fontSize: '18px' }}>
|
||||||
|
Total
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='h6' fontWeight={600} sx={{ fontSize: '18px', textAlign: 'right' }}>
|
||||||
|
{new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
minimumFractionDigits: 0
|
||||||
|
}).format(total)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Uang Muka */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f8f8f8'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
{/* Dropdown */}
|
||||||
|
<CustomAutocomplete
|
||||||
|
size='small'
|
||||||
|
options={[{ label: '1-10003 Gi...', value: '1-10003' }]}
|
||||||
|
getOptionLabel={option => (typeof option === 'string' ? option : option.label)}
|
||||||
|
value={{ label: '1-10003 Gi...', value: '1-10003' }}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
// Handle change if needed
|
||||||
|
}}
|
||||||
|
renderInput={params => <CustomTextField {...params} size='small' />}
|
||||||
|
sx={{ minWidth: 120 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Amount input */}
|
||||||
|
<CustomTextField
|
||||||
|
size='small'
|
||||||
|
placeholder='0'
|
||||||
|
value={formData.downPayment || '0'}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInputChange('downPayment', e.target.value)
|
||||||
|
}
|
||||||
|
sx={{ width: '80px' }}
|
||||||
|
inputProps={{
|
||||||
|
style: { textAlign: 'center' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Percentage/Fixed toggle */}
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={formData.downPaymentType || 'fixed'}
|
||||||
|
exclusive
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
if (newValue) handleInputChange('downPaymentType', newValue)
|
||||||
|
}}
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
<ToggleButton value='percentage' sx={{ px: 1.5 }}>
|
||||||
|
%
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value='fixed' sx={{ px: 1.5 }}>
|
||||||
|
Rp
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Right side text */}
|
||||||
|
<Typography
|
||||||
|
variant='body1'
|
||||||
|
sx={{
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 400
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Uang muka {downPayment > 0 ? downPayment.toLocaleString('id-ID') : '0'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Sisa Tagihan */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
py: 2,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
borderRadius: '4px',
|
||||||
|
mb: 3,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#eeeeee'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='body1' color='text.primary' sx={{ fontSize: '16px', fontWeight: 600 }}>
|
||||||
|
Sisa Tagihan
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body1' fontWeight={600} sx={{ fontSize: '16px', textAlign: 'right' }}>
|
||||||
|
{new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
minimumFractionDigits: 0
|
||||||
|
}).format(remainingBalance)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
fontWeight: 600,
|
||||||
|
py: 1.5,
|
||||||
|
boxShadow: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PurchaseOrderSummary
|
||||||
@ -41,6 +41,7 @@ import TablePaginationComponent from '@/components/TablePaginationComponent'
|
|||||||
import Loading from '@/components/layout/shared/Loading'
|
import Loading from '@/components/layout/shared/Loading'
|
||||||
import { PurchaseOrderType } from '@/types/apps/purchaseOrderTypes'
|
import { PurchaseOrderType } from '@/types/apps/purchaseOrderTypes'
|
||||||
import { purchaseOrdersData } from '@/data/dummy/purchase-order'
|
import { purchaseOrdersData } from '@/data/dummy/purchase-order'
|
||||||
|
import { getLocalizedUrl } from '@/utils/i18n'
|
||||||
|
|
||||||
declare module '@tanstack/table-core' {
|
declare module '@tanstack/table-core' {
|
||||||
interface FilterFns {
|
interface FilterFns {
|
||||||
@ -367,11 +368,12 @@ const PurchaseOrderListTable = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='contained'
|
variant='contained'
|
||||||
|
component={Link}
|
||||||
|
className='max-sm:is-full is-auto'
|
||||||
startIcon={<i className='tabler-plus' />}
|
startIcon={<i className='tabler-plus' />}
|
||||||
onClick={() => setAddPOOpen(!addPOOpen)}
|
href={getLocalizedUrl('/apps/purchase/purchase-orders/add', locale as Locale)}
|
||||||
className='max-sm:is-full'
|
|
||||||
>
|
>
|
||||||
Tambah PO Baru
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user