Compare commits
No commits in common. "140822763ed6fd82500031b04c169351530b439e" and "b2840ca27c1c53660e3579f744ceafe11a3d7154" have entirely different histories.
140822763e
...
b2840ca27c
@ -1,22 +0,0 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { api } from '../api'
|
|
||||||
import { PurchaseOrderRequest } from '@/types/services/purchaseOrder'
|
|
||||||
|
|
||||||
export const useVendorsMutation = () => {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
const createVendor = useMutation({
|
|
||||||
mutationFn: async (newVendor: PurchaseOrderRequest) => {
|
|
||||||
const response = await api.post('/vendors', newVendor)
|
|
||||||
return response.data
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success('Vendor created successfully!')
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['vendors'] })
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -35,16 +35,6 @@ export function useVendors(params: VendorQueryParams = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useVendorActive() {
|
|
||||||
return useQuery<Vendor[]>({
|
|
||||||
queryKey: ['vendors/active'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await api.get(`/vendors/active`)
|
|
||||||
return res.data.data
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVendorById(id: string) {
|
export function useVendorById(id: string) {
|
||||||
return useQuery<Vendor>({
|
return useQuery<Vendor>({
|
||||||
queryKey: ['vendors', id],
|
queryKey: ['vendors', id],
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
export interface PurchaseOrderRequest {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@ -11,18 +11,16 @@ import FormControl from '@mui/material/FormControl'
|
|||||||
import InputLabel from '@mui/material/InputLabel'
|
import InputLabel from '@mui/material/InputLabel'
|
||||||
import Select from '@mui/material/Select'
|
import Select from '@mui/material/Select'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
import CircularProgress from '@mui/material/CircularProgress'
|
|
||||||
import CashBankCard from './CashBankCard' // Adjust import path as needed
|
import CashBankCard from './CashBankCard' // Adjust import path as needed
|
||||||
import CustomTextField from '@/@core/components/mui/TextField'
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
import { getLocalizedUrl } from '@/utils/i18n'
|
import { getLocalizedUrl } from '@/utils/i18n'
|
||||||
import { Locale } from '@/configs/i18n'
|
import { Locale } from '@/configs/i18n'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import AccountFormDrawer from '../account/AccountFormDrawer'
|
import AccountFormDrawer, { AccountType } from '../account/AccountFormDrawer'
|
||||||
|
import { accountsData } from '../account/AccountListTable'
|
||||||
import { Button } from '@mui/material'
|
import { Button } from '@mui/material'
|
||||||
import { Account } from '@/types/services/chartOfAccount'
|
|
||||||
import { useAccounts } from '@/services/queries/account'
|
|
||||||
import { formatCurrency } from '@/utils/transform'
|
|
||||||
|
|
||||||
|
// Types
|
||||||
interface BankAccount {
|
interface BankAccount {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
@ -43,28 +41,188 @@ interface BankAccount {
|
|||||||
status: 'active' | 'inactive' | 'blocked'
|
status: 'active' | 'inactive' | 'blocked'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static chart data for fallback/demo purposes
|
// Dummy Data
|
||||||
const generateChartData = (accountType: string, balance: number) => {
|
const dummyAccounts: BankAccount[] = [
|
||||||
const baseValue = balance || 1000000
|
{
|
||||||
const variation = baseValue * 0.2
|
id: '1',
|
||||||
|
title: 'Giro',
|
||||||
return Array.from({ length: 12 }, (_, i) => {
|
accountNumber: '1-10003',
|
||||||
const randomVariation = (Math.random() - 0.5) * variation
|
balances: [
|
||||||
return Math.max(baseValue + randomVariation, baseValue * 0.5)
|
{ amount: '7.313.321', label: 'Saldo di bank' },
|
||||||
})
|
{ amount: '30.631.261', label: 'Saldo di kledo' }
|
||||||
}
|
],
|
||||||
|
chartData: [
|
||||||
const getChartColor = (accountType: string) => {
|
{
|
||||||
const colors = {
|
name: 'Saldo',
|
||||||
giro: '#ff6b9d',
|
data: [
|
||||||
savings: '#4285f4',
|
20000000, 21000000, 20500000, 20800000, 21500000, 22000000, 25000000, 26000000, 28000000, 29000000, 30000000,
|
||||||
investment: '#00bcd4',
|
31000000
|
||||||
credit: '#ff9800',
|
]
|
||||||
cash: '#4caf50'
|
}
|
||||||
|
],
|
||||||
|
categories: ['Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des', 'Jan', 'Feb', 'Mar'],
|
||||||
|
chartColor: '#ff6b9d',
|
||||||
|
currency: 'IDR',
|
||||||
|
accountType: 'giro',
|
||||||
|
bank: 'Bank Mandiri',
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Tabungan Premium',
|
||||||
|
accountNumber: 'SAV-001234',
|
||||||
|
balances: [
|
||||||
|
{ amount: 15420000, label: 'Saldo Tersedia' },
|
||||||
|
{ amount: 18750000, label: 'Total Saldo' }
|
||||||
|
],
|
||||||
|
chartData: [
|
||||||
|
{
|
||||||
|
name: 'Balance',
|
||||||
|
data: [
|
||||||
|
12000000, 13500000, 14200000, 15000000, 15800000, 16200000, 17000000, 17500000, 18000000, 18200000, 18500000,
|
||||||
|
18750000
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
||||||
|
chartColor: '#4285f4',
|
||||||
|
currency: 'IDR',
|
||||||
|
accountType: 'savings',
|
||||||
|
bank: 'Bank BCA',
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Investment Portfolio',
|
||||||
|
accountNumber: 'INV-789012',
|
||||||
|
balances: [
|
||||||
|
{ amount: 125000, label: 'Portfolio Value' },
|
||||||
|
{ amount: 8750, label: 'Total Gains' }
|
||||||
|
],
|
||||||
|
chartData: [
|
||||||
|
{
|
||||||
|
name: 'Portfolio Value',
|
||||||
|
data: [110000, 115000, 112000, 118000, 122000, 119000, 125000, 128000, 126000, 130000, 127000, 125000]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||||
|
currency: 'USD',
|
||||||
|
accountType: 'investment',
|
||||||
|
bank: 'Charles Schwab',
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
title: 'Kartu Kredit Platinum',
|
||||||
|
accountNumber: 'CC-456789',
|
||||||
|
balances: [
|
||||||
|
{ amount: 2500000, label: 'Saldo Saat Ini' },
|
||||||
|
{ amount: 47500000, label: 'Limit Tersedia' }
|
||||||
|
],
|
||||||
|
chartData: [
|
||||||
|
{
|
||||||
|
name: 'Spending',
|
||||||
|
data: [
|
||||||
|
1200000, 1800000, 2200000, 1900000, 2100000, 2400000, 2800000, 2600000, 2300000, 2500000, 2700000, 2500000
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
||||||
|
currency: 'IDR',
|
||||||
|
accountType: 'credit',
|
||||||
|
bank: 'Bank BNI',
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
title: 'Deposito Berjangka',
|
||||||
|
accountNumber: 'DEP-334455',
|
||||||
|
balances: [
|
||||||
|
{ amount: 50000000, label: 'Pokok Deposito' },
|
||||||
|
{ amount: 2500000, label: 'Bunga Terkumpul' }
|
||||||
|
],
|
||||||
|
chartData: [
|
||||||
|
{
|
||||||
|
name: 'Deposito Growth',
|
||||||
|
data: [
|
||||||
|
50000000, 50200000, 50420000, 50650000, 50880000, 51120000, 51360000, 51610000, 51860000, 52120000, 52380000,
|
||||||
|
52500000
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
||||||
|
currency: 'IDR',
|
||||||
|
accountType: 'savings',
|
||||||
|
bank: 'Bank BRI',
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
title: 'Cash Management',
|
||||||
|
accountNumber: 'CSH-111222',
|
||||||
|
balances: [{ amount: 5000, label: 'Available Cash' }],
|
||||||
|
chartData: [
|
||||||
|
{
|
||||||
|
name: 'Cash Flow',
|
||||||
|
data: [4000, 4500, 4200, 4800, 5200, 4900, 5000, 5300, 5100, 5400, 5200, 5000]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
categories: ['Q1', 'Q2', 'Q3', 'Q4', 'Q1', 'Q2', 'Q3', 'Q4', 'Q1', 'Q2', 'Q3', 'Q4'],
|
||||||
|
chartColor: '#00bcd4',
|
||||||
|
currency: 'USD',
|
||||||
|
accountType: 'cash',
|
||||||
|
bank: 'Wells Fargo',
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
title: 'Rekening Bisnis',
|
||||||
|
accountNumber: 'BIZ-998877',
|
||||||
|
balances: [
|
||||||
|
{ amount: 85000000, label: 'Saldo Operasional' },
|
||||||
|
{ amount: 15000000, label: 'Dana Cadangan' }
|
||||||
|
],
|
||||||
|
chartData: [
|
||||||
|
{
|
||||||
|
name: 'Business Account',
|
||||||
|
data: [
|
||||||
|
70000000, 75000000, 80000000, 82000000, 85000000, 88000000, 90000000, 87000000, 85000000, 89000000, 92000000,
|
||||||
|
100000000
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
||||||
|
chartColor: '#ff9800',
|
||||||
|
currency: 'IDR',
|
||||||
|
accountType: 'giro',
|
||||||
|
bank: 'Bank Mandiri',
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8',
|
||||||
|
title: 'Tabungan Pendidikan',
|
||||||
|
accountNumber: 'EDU-567890',
|
||||||
|
balances: [
|
||||||
|
{ amount: 25000000, label: 'Dana Pendidikan' },
|
||||||
|
{ amount: 3500000, label: 'Bunga Terkumpul' }
|
||||||
|
],
|
||||||
|
chartData: [
|
||||||
|
{
|
||||||
|
name: 'Education Savings',
|
||||||
|
data: [
|
||||||
|
20000000, 21000000, 22000000, 23000000, 24000000, 24500000, 25000000, 25500000, 26000000, 27000000, 28000000,
|
||||||
|
28500000
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
||||||
|
chartColor: '#3f51b5',
|
||||||
|
currency: 'IDR',
|
||||||
|
accountType: 'savings',
|
||||||
|
bank: 'Bank BCA',
|
||||||
|
status: 'inactive'
|
||||||
}
|
}
|
||||||
return colors[accountType as keyof typeof colors] || '#757575'
|
]
|
||||||
}
|
|
||||||
|
|
||||||
const DebouncedInput = ({
|
const DebouncedInput = ({
|
||||||
value: initialValue,
|
value: initialValue,
|
||||||
onChange,
|
onChange,
|
||||||
@ -93,122 +251,28 @@ const DebouncedInput = ({
|
|||||||
|
|
||||||
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const CashBankList = () => {
|
const CashBankList = () => {
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [editingAccount, setEditingAccount] = useState<Account | null>(null)
|
const [editingAccount, setEditingAccount] = useState<AccountType | null>(null)
|
||||||
const [addAccountOpen, setAddAccountOpen] = useState(false)
|
const [addAccountOpen, setAddAccountOpen] = useState(false)
|
||||||
const [data, setData] = useState<Account[]>([])
|
const [data, setData] = useState<AccountType[]>(accountsData)
|
||||||
const { lang: locale } = useParams()
|
const { lang: locale } = useParams()
|
||||||
|
|
||||||
// Use the accounts hook with search parameter
|
|
||||||
const { data: accountsResponse, isLoading } = useAccounts({
|
|
||||||
page: 1,
|
|
||||||
limit: 10,
|
|
||||||
search: searchQuery
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleCloseDrawer = () => {
|
const handleCloseDrawer = () => {
|
||||||
setAddAccountOpen(false)
|
setAddAccountOpen(false)
|
||||||
setEditingAccount(null)
|
setEditingAccount(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform API data to match our BankAccount interface
|
// Filter and search logic
|
||||||
const transformedAccounts = useMemo((): BankAccount[] => {
|
|
||||||
if (!accountsResponse?.data) return []
|
|
||||||
|
|
||||||
return accountsResponse.data.map((account: Account) => {
|
|
||||||
const chartData = generateChartData(account.account_type, account.current_balance)
|
|
||||||
|
|
||||||
// Map account type to display type
|
|
||||||
const typeMapping = {
|
|
||||||
current_asset: 'giro' as const,
|
|
||||||
non_current_asset: 'investment' as const,
|
|
||||||
current_liability: 'credit' as const,
|
|
||||||
non_current_liability: 'credit' as const,
|
|
||||||
other_current_asset: 'cash' as const,
|
|
||||||
other_current_liability: 'credit' as const,
|
|
||||||
equity: 'savings' as const,
|
|
||||||
revenue: 'savings' as const,
|
|
||||||
expense: 'cash' as const
|
|
||||||
}
|
|
||||||
const displayAccountType = typeMapping[account.account_type as keyof typeof typeMapping] || 'giro'
|
|
||||||
|
|
||||||
// Get bank name from account
|
|
||||||
const getBankName = (acc: Account): string => {
|
|
||||||
if (acc.chart_of_account?.name) {
|
|
||||||
return acc.chart_of_account.name
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeToBank = {
|
|
||||||
current_asset: 'Bank Account',
|
|
||||||
non_current_asset: 'Investment Account',
|
|
||||||
current_liability: 'Credit Account',
|
|
||||||
other_current_asset: 'Cash Account',
|
|
||||||
equity: 'Equity Account',
|
|
||||||
revenue: 'Revenue Account',
|
|
||||||
expense: 'Expense Account'
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeToBank[acc.account_type as keyof typeof typeToBank] || 'General Account'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create balance information
|
|
||||||
const balances = []
|
|
||||||
|
|
||||||
if (account.current_balance !== account.opening_balance) {
|
|
||||||
balances.push({
|
|
||||||
amount: formatCurrency(account.current_balance),
|
|
||||||
label: 'Saldo Saat Ini'
|
|
||||||
})
|
|
||||||
balances.push({
|
|
||||||
amount: formatCurrency(account.opening_balance),
|
|
||||||
label: 'Saldo Awal'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
balances.push({
|
|
||||||
amount: formatCurrency(account.current_balance),
|
|
||||||
label: 'Saldo'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: account.id,
|
|
||||||
title: account.name,
|
|
||||||
accountNumber: account.number,
|
|
||||||
balances,
|
|
||||||
chartData: [
|
|
||||||
{
|
|
||||||
name: 'Saldo',
|
|
||||||
data: chartData
|
|
||||||
}
|
|
||||||
],
|
|
||||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
|
||||||
chartColor: getChartColor(account.account_type),
|
|
||||||
currency: 'IDR', // Assuming IDR as default, adjust as needed
|
|
||||||
accountType: displayAccountType,
|
|
||||||
bank: getBankName(account),
|
|
||||||
status: account.is_active ? 'active' : 'inactive'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [accountsResponse])
|
|
||||||
|
|
||||||
// Filter accounts based on search (if not handled by API)
|
|
||||||
const filteredAccounts = useMemo(() => {
|
const filteredAccounts = useMemo(() => {
|
||||||
if (!searchQuery || accountsResponse) {
|
return dummyAccounts.filter(account => {
|
||||||
// If using API search or no search, return transformed accounts as is
|
|
||||||
return transformedAccounts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local filtering fallback
|
|
||||||
return transformedAccounts.filter(account => {
|
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
account.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
account.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
account.accountNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
account.accountNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
account.bank.toLowerCase().includes(searchQuery.toLowerCase())
|
account.bank.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
return matchesSearch
|
return matchesSearch
|
||||||
})
|
})
|
||||||
}, [transformedAccounts, searchQuery, accountsResponse])
|
}, [searchQuery])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -219,16 +283,8 @@ const CashBankList = () => {
|
|||||||
<DebouncedInput
|
<DebouncedInput
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={value => setSearchQuery(value as string)}
|
onChange={value => setSearchQuery(value as string)}
|
||||||
placeholder='Cari akun...'
|
placeholder='Cari '
|
||||||
className='max-sm:is-full'
|
className='max-sm:is-full'
|
||||||
disabled={isLoading}
|
|
||||||
InputProps={{
|
|
||||||
startAdornment: (
|
|
||||||
<InputAdornment position='start'>
|
|
||||||
<i className='tabler-search' />
|
|
||||||
</InputAdornment>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
@ -239,7 +295,6 @@ const CashBankList = () => {
|
|||||||
setEditingAccount(null)
|
setEditingAccount(null)
|
||||||
setAddAccountOpen(true)
|
setAddAccountOpen(true)
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
|
||||||
>
|
>
|
||||||
Tambah Akun
|
Tambah Akun
|
||||||
</Button>
|
</Button>
|
||||||
@ -247,77 +302,45 @@ const CashBankList = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Loading State */}
|
|
||||||
{isLoading && (
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Account Cards */}
|
{/* Account Cards */}
|
||||||
{!isLoading && (
|
<Grid container spacing={3}>
|
||||||
<Grid container spacing={3}>
|
{filteredAccounts.length > 0 ? (
|
||||||
{filteredAccounts.length > 0 ? (
|
filteredAccounts.map(account => (
|
||||||
filteredAccounts.map(account => (
|
<Grid key={account.id} size={{ xs: 12, lg: 6, xl: 4 }}>
|
||||||
<Grid key={account.id} size={{ xs: 12, lg: 6, xl: 4 }}>
|
<CashBankCard
|
||||||
<CashBankCard
|
title={account.title}
|
||||||
title={account.title}
|
accountNumber={account.accountNumber}
|
||||||
accountNumber={account.accountNumber}
|
balances={account.balances}
|
||||||
balances={account.balances}
|
chartData={account.chartData}
|
||||||
chartData={account.chartData}
|
categories={account.categories}
|
||||||
categories={account.categories}
|
chartColor={account.chartColor}
|
||||||
chartColor={account.chartColor}
|
currency={account.currency}
|
||||||
currency={account.currency}
|
showButton={account.accountType !== 'cash'}
|
||||||
showButton={account.accountType !== 'cash'}
|
href={getLocalizedUrl(`/apps/cash-bank/${account.accountNumber}/detail`, locale as Locale)}
|
||||||
href={getLocalizedUrl(`/apps/cash-bank/${account.accountNumber}/detail`, locale as Locale)}
|
/>
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Grid size={{ xs: 12 }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
textAlign: 'center',
|
|
||||||
py: 8,
|
|
||||||
backgroundColor: 'grey.50',
|
|
||||||
borderRadius: 2
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant='h6' color='text.secondary' gutterBottom>
|
|
||||||
{searchQuery ? 'Tidak ada akun yang ditemukan' : 'Belum ada akun'}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='body2' color='text.secondary'>
|
|
||||||
{searchQuery
|
|
||||||
? 'Coba ubah kata kunci pencarian yang digunakan'
|
|
||||||
: 'Mulai dengan menambahkan akun baru'}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
))
|
||||||
</Grid>
|
) : (
|
||||||
)}
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<Box
|
||||||
{/* Error State (if needed) */}
|
sx={{
|
||||||
{!isLoading && !accountsResponse && (
|
textAlign: 'center',
|
||||||
<Grid size={{ xs: 12 }}>
|
py: 8,
|
||||||
<Box
|
backgroundColor: 'grey.50',
|
||||||
sx={{
|
borderRadius: 2
|
||||||
textAlign: 'center',
|
}}
|
||||||
py: 8,
|
>
|
||||||
backgroundColor: 'error.light',
|
<Typography variant='h6' color='text.secondary' gutterBottom>
|
||||||
borderRadius: 2,
|
Tidak ada akun yang ditemukan
|
||||||
color: 'error.contrastText'
|
</Typography>
|
||||||
}}
|
<Typography variant='body2' color='text.secondary'>
|
||||||
>
|
Coba ubah kata kunci pencarian atau filter yang digunakan
|
||||||
<Typography variant='h6' gutterBottom>
|
</Typography>
|
||||||
Terjadi kesalahan saat memuat data
|
</Box>
|
||||||
</Typography>
|
</Grid>
|
||||||
<Typography variant='body2'>Silakan coba lagi atau hubungi administrator</Typography>
|
)}
|
||||||
</Box>
|
</Grid>
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<AccountFormDrawer
|
<AccountFormDrawer
|
||||||
open={addAccountOpen}
|
open={addAccountOpen}
|
||||||
handleClose={handleCloseDrawer}
|
handleClose={handleCloseDrawer}
|
||||||
|
|||||||
@ -1,121 +0,0 @@
|
|||||||
'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 PurchaseBasicInfo from './PurchaseBasicInfo'
|
|
||||||
import PurchaseIngredientsTable from './PurchaseIngredientsTable'
|
|
||||||
import PurchaseSummary from './PurchaseSummary'
|
|
||||||
|
|
||||||
const PurchaseAddForm: 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 */}
|
|
||||||
<PurchaseBasicInfo formData={formData} handleInputChange={handleInputChange} />
|
|
||||||
|
|
||||||
{/* Ingredients Table Section */}
|
|
||||||
<PurchaseIngredientsTable
|
|
||||||
formData={formData}
|
|
||||||
handleIngredientChange={handleIngredientChange}
|
|
||||||
addIngredientItem={addIngredientItem}
|
|
||||||
removeIngredientItem={removeIngredientItem}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Summary Section */}
|
|
||||||
<PurchaseSummary formData={formData} handleInputChange={handleInputChange} />
|
|
||||||
</Grid>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PurchaseAddForm
|
|
||||||
@ -1,197 +0,0 @@
|
|||||||
'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 PurchaseBasicInfoProps {
|
|
||||||
formData: PurchaseOrderFormData
|
|
||||||
handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const PurchaseBasicInfo: React.FC<PurchaseBasicInfoProps> = ({ 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 PurchaseBasicInfo
|
|
||||||
@ -1,225 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Typography,
|
|
||||||
IconButton,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Paper
|
|
||||||
} 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 PurchaseIngredientsTableProps {
|
|
||||||
formData: PurchaseOrderFormData
|
|
||||||
handleIngredientChange: (index: number, field: keyof IngredientItem, value: any) => void
|
|
||||||
addIngredientItem: () => void
|
|
||||||
removeIngredientItem: (index: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const PurchaseIngredientsTable: React.FC<PurchaseIngredientsTableProps> = ({
|
|
||||||
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={{ xs: 12 }} sx={{ mt: 4 }}>
|
|
||||||
<Typography variant='h6' sx={{ mb: 2, fontWeight: 600 }}>
|
|
||||||
Bahan Baku / Ingredients
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<TableContainer component={Paper} variant='outlined'>
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow sx={{ backgroundColor: 'grey.50' }}>
|
|
||||||
<TableCell sx={{ fontWeight: 'bold', minWidth: 180 }}>Bahan Baku</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 'bold', minWidth: 150 }}>Deskripsi</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 100 }}>Kuantitas</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Satuan</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 100 }}>Discount</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Harga</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Pajak</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 120 }}>Waste</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 'bold', width: 100, textAlign: 'right' }}>Total</TableCell>
|
|
||||||
<TableCell sx={{ width: 50 }}></TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{formData.ingredientItems.map((item: IngredientItem, index: number) => (
|
|
||||||
<TableRow key={item.id}>
|
|
||||||
<TableCell>
|
|
||||||
<CustomAutocomplete
|
|
||||||
size='small'
|
|
||||||
options={ingredientOptions}
|
|
||||||
value={item.ingredient}
|
|
||||||
onChange={(event, newValue) => handleIngredientChange(index, 'ingredient', newValue)}
|
|
||||||
renderInput={params => <CustomTextField {...params} placeholder='Pilih Bahan Baku' />}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
size='small'
|
|
||||||
value={item.deskripsi}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
handleIngredientChange(index, 'deskripsi', e.target.value)
|
|
||||||
}
|
|
||||||
placeholder='Deskripsi'
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<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 }}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<CustomAutocomplete
|
|
||||||
size='small'
|
|
||||||
options={satuanOptions}
|
|
||||||
value={item.satuan}
|
|
||||||
onChange={(event, newValue) => handleIngredientChange(index, 'satuan', newValue)}
|
|
||||||
renderInput={params => <CustomTextField {...params} placeholder='Pilih...' />}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
size='small'
|
|
||||||
value={item.discount}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
handleIngredientChange(index, 'discount', e.target.value)
|
|
||||||
}
|
|
||||||
placeholder='0%'
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
size='small'
|
|
||||||
type='number'
|
|
||||||
value={item.harga === 0 ? '' : item.harga?.toString() || ''}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value
|
|
||||||
|
|
||||||
if (value === '') {
|
|
||||||
handleIngredientChange(index, 'harga', null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const numericValue = parseFloat(value)
|
|
||||||
handleIngredientChange(index, 'harga', isNaN(numericValue) ? 0 : numericValue)
|
|
||||||
}}
|
|
||||||
inputProps={{ min: 0, step: 'any' }}
|
|
||||||
placeholder='0'
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<CustomAutocomplete
|
|
||||||
size='small'
|
|
||||||
options={pajakOptions}
|
|
||||||
value={item.pajak}
|
|
||||||
onChange={(event, newValue) => handleIngredientChange(index, 'pajak', newValue)}
|
|
||||||
renderInput={params => <CustomTextField {...params} placeholder='...' />}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<CustomAutocomplete
|
|
||||||
size='small'
|
|
||||||
options={wasteOptions}
|
|
||||||
value={item.waste}
|
|
||||||
onChange={(event, newValue) => handleIngredientChange(index, 'waste', newValue)}
|
|
||||||
renderInput={params => <CustomTextField {...params} placeholder='...' />}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
size='small'
|
|
||||||
value={item.total}
|
|
||||||
InputProps={{ readOnly: true }}
|
|
||||||
sx={{
|
|
||||||
'& .MuiInputBase-input': {
|
|
||||||
textAlign: 'right'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<IconButton
|
|
||||||
size='small'
|
|
||||||
color='error'
|
|
||||||
onClick={() => removeIngredientItem(index)}
|
|
||||||
disabled={formData.ingredientItems.length === 1}
|
|
||||||
>
|
|
||||||
<i className='tabler-trash' />
|
|
||||||
</IconButton>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
|
|
||||||
{/* Add New Item Button */}
|
|
||||||
<Button
|
|
||||||
startIcon={<i className='tabler-plus' />}
|
|
||||||
onClick={addIngredientItem}
|
|
||||||
variant='outlined'
|
|
||||||
size='small'
|
|
||||||
sx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
Tambah bahan baku
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PurchaseIngredientsTable
|
|
||||||
@ -1,589 +0,0 @@
|
|||||||
'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 PurchaseSummaryProps {
|
|
||||||
formData: PurchaseOrderFormData
|
|
||||||
handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const PurchaseSummary: React.FC<PurchaseSummaryProps> = ({ 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'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant='text'
|
|
||||||
color='primary'
|
|
||||||
size='small'
|
|
||||||
sx={{ textTransform: 'none', fontSize: '14px', p: 0, textAlign: 'left' }}
|
|
||||||
onClick={() => handleInputChange('showUangMuka', !formData.showUangMuka)}
|
|
||||||
>
|
|
||||||
{formData.showUangMuka ? '- Sembunyikan Uang Muka' : '+ Uang Muka'}
|
|
||||||
</Button>
|
|
||||||
{formData.showUangMuka && (
|
|
||||||
<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 PurchaseSummary
|
|
||||||
@ -1,12 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Button, Switch, FormControlLabel, Box, Typography } from '@mui/material'
|
import { Button, Switch, FormControlLabel } 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'
|
||||||
import CustomTextField from '@/@core/components/mui/TextField'
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
import { DropdownOption, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
import { DropdownOption, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
||||||
import { useVendorActive } from '@/services/queries/vendor'
|
|
||||||
|
|
||||||
interface PurchaseBasicInfoProps {
|
interface PurchaseBasicInfoProps {
|
||||||
formData: PurchaseOrderFormData
|
formData: PurchaseOrderFormData
|
||||||
@ -14,22 +13,12 @@ interface PurchaseBasicInfoProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PurchaseBasicInfo: React.FC<PurchaseBasicInfoProps> = ({ formData, handleInputChange }) => {
|
const PurchaseBasicInfo: React.FC<PurchaseBasicInfoProps> = ({ formData, handleInputChange }) => {
|
||||||
const { data: vendors, isLoading } = useVendorActive()
|
// Sample data for dropdowns
|
||||||
|
const vendorOptions: DropdownOption[] = [
|
||||||
// Transform vendors data to dropdown options
|
{ label: 'Vendor A', value: 'vendor_a' },
|
||||||
const vendorOptions: DropdownOption[] =
|
{ label: 'Vendor B', value: 'vendor_b' },
|
||||||
vendors?.map(vendor => ({
|
{ label: 'Vendor C', value: 'vendor_c' }
|
||||||
label: vendor.name,
|
]
|
||||||
value: vendor.id
|
|
||||||
})) || []
|
|
||||||
|
|
||||||
// Function to get selected vendor data
|
|
||||||
const getSelectedVendorData = () => {
|
|
||||||
if (!formData.vendor?.value || !vendors) return null
|
|
||||||
|
|
||||||
const selectedVendor = vendors.find(vendor => vendor.id === (formData?.vendor?.value ?? ''))
|
|
||||||
return selectedVendor
|
|
||||||
}
|
|
||||||
|
|
||||||
const terminOptions: DropdownOption[] = [
|
const terminOptions: DropdownOption[] = [
|
||||||
{ label: 'Net 30', value: 'net_30' },
|
{ label: 'Net 30', value: 'net_30' },
|
||||||
@ -54,53 +43,9 @@ const PurchaseBasicInfo: React.FC<PurchaseBasicInfoProps> = ({ formData, handleI
|
|||||||
fullWidth
|
fullWidth
|
||||||
options={vendorOptions}
|
options={vendorOptions}
|
||||||
value={formData.vendor}
|
value={formData.vendor}
|
||||||
onChange={(event, newValue) => {
|
onChange={(event, newValue) => handleInputChange('vendor', newValue)}
|
||||||
handleInputChange('vendor', newValue)
|
renderInput={params => <CustomTextField {...params} label='Vendor' placeholder='Pilih kontak' fullWidth />}
|
||||||
|
|
||||||
// Optional: Bisa langsung akses full data vendor saat berubah
|
|
||||||
if (newValue?.value) {
|
|
||||||
const selectedVendorData = vendors?.find(vendor => vendor.id === newValue.value)
|
|
||||||
console.log('Vendor selected:', selectedVendorData)
|
|
||||||
// Atau bisa trigger callback lain jika dibutuhkan
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
loading={isLoading}
|
|
||||||
renderInput={params => (
|
|
||||||
<CustomTextField
|
|
||||||
{...params}
|
|
||||||
label='Vendor'
|
|
||||||
placeholder={isLoading ? 'Loading vendors...' : 'Pilih kontak'}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
{getSelectedVendorData() && (
|
|
||||||
<Box className='space-y-1 mt-3'>
|
|
||||||
{/* Nama Perum */}
|
|
||||||
<Box className='flex items-center space-x-2'>
|
|
||||||
<i className='tabler-user text-gray-500 w-3 h-3' />
|
|
||||||
<Typography className='text-gray-700 font-medium text-xs'>
|
|
||||||
{getSelectedVendorData()?.contact_person ?? ''}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Alamat */}
|
|
||||||
<Box className='flex items-start space-x-2'>
|
|
||||||
<i className='tabler-map text-gray-500 w-3 h-3' />
|
|
||||||
<Typography className='text-gray-700 font-medium text-xs'>
|
|
||||||
{getSelectedVendorData()?.address ?? '-'}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Nomor Telepon */}
|
|
||||||
<Box className='flex items-center space-x-2'>
|
|
||||||
<i className='tabler-phone text-gray-500 w-3 h-3' />
|
|
||||||
<Typography className='text-gray-700 font-medium text-xs'>
|
|
||||||
{getSelectedVendorData()?.phone_number ?? '-'}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
<Grid size={{ xs: 12, sm: 6, md: 6 }}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useMemo } from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Typography,
|
Typography,
|
||||||
@ -11,14 +11,12 @@ import {
|
|||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
Paper,
|
Paper
|
||||||
CircularProgress
|
|
||||||
} 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'
|
||||||
import CustomTextField from '@/@core/components/mui/TextField'
|
import CustomTextField from '@/@core/components/mui/TextField'
|
||||||
import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes'
|
||||||
import { useIngredients } from '@/services/queries/ingredients'
|
|
||||||
|
|
||||||
interface PurchaseIngredientsTableProps {
|
interface PurchaseIngredientsTableProps {
|
||||||
formData: PurchaseOrderFormData
|
formData: PurchaseOrderFormData
|
||||||
@ -33,21 +31,14 @@ const PurchaseIngredientsTable: React.FC<PurchaseIngredientsTableProps> = ({
|
|||||||
addIngredientItem,
|
addIngredientItem,
|
||||||
removeIngredientItem
|
removeIngredientItem
|
||||||
}) => {
|
}) => {
|
||||||
const { data: ingredients, isLoading } = useIngredients()
|
const ingredientOptions = [
|
||||||
|
{ label: 'Tepung Terigu Premium', value: 'tepung_terigu_premium' },
|
||||||
// Transform ingredients data to autocomplete options format
|
{ label: 'Gula Pasir Halus', value: 'gula_pasir_halus' },
|
||||||
const ingredientOptions = useMemo(() => {
|
{ label: 'Mentega Unsalted', value: 'mentega_unsalted' },
|
||||||
if (!ingredients || isLoading) {
|
{ label: 'Telur Ayam Grade A', value: 'telur_ayam_grade_a' },
|
||||||
return []
|
{ label: 'Vanilla Extract', value: 'vanilla_extract' },
|
||||||
}
|
{ label: 'Coklat Chips', value: 'coklat_chips' }
|
||||||
|
]
|
||||||
return ingredients?.data.map((ingredient: any) => ({
|
|
||||||
label: ingredient.name || ingredient.nama || ingredient.ingredient_name,
|
|
||||||
value: ingredient.id || ingredient.code || ingredient.value,
|
|
||||||
id: ingredient.id || ingredient.code || ingredient.value,
|
|
||||||
originalData: ingredient
|
|
||||||
}))
|
|
||||||
}, [ingredients, isLoading])
|
|
||||||
|
|
||||||
const satuanOptions = [
|
const satuanOptions = [
|
||||||
{ label: 'KG', value: 'kg' },
|
{ label: 'KG', value: 'kg' },
|
||||||
@ -72,40 +63,6 @@ const PurchaseIngredientsTable: React.FC<PurchaseIngredientsTableProps> = ({
|
|||||||
{ label: 'Custom', value: 'custom' }
|
{ label: 'Custom', value: 'custom' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// Handle ingredient selection with additional data population
|
|
||||||
const handleIngredientSelection = (index: number, selectedIngredient: any) => {
|
|
||||||
handleIngredientChange(index, 'ingredient', selectedIngredient)
|
|
||||||
|
|
||||||
// Auto-populate related fields if available in the ingredient data
|
|
||||||
if (selectedIngredient) {
|
|
||||||
// Get ingredient data from originalData or directly from selectedIngredient
|
|
||||||
const ingredientData = selectedIngredient.originalData || selectedIngredient
|
|
||||||
|
|
||||||
// Auto-fill unit if available
|
|
||||||
if (ingredientData.unit || ingredientData.satuan) {
|
|
||||||
const unit = ingredientData.unit || ingredientData.satuan
|
|
||||||
// Convert unit to string and make it safe
|
|
||||||
const unitString = String(unit).toLowerCase()
|
|
||||||
const unitOption = satuanOptions.find(
|
|
||||||
option => option.value === unit || option.label.toLowerCase() === unitString
|
|
||||||
)
|
|
||||||
if (unitOption) {
|
|
||||||
handleIngredientChange(index, 'satuan', unitOption)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-fill price if available
|
|
||||||
if (ingredientData.price || ingredientData.harga) {
|
|
||||||
handleIngredientChange(index, 'harga', ingredientData.price || ingredientData.harga)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-fill description if available
|
|
||||||
if (ingredientData.description || ingredientData.deskripsi) {
|
|
||||||
handleIngredientChange(index, 'deskripsi', ingredientData.description || ingredientData.deskripsi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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 }}>
|
||||||
@ -135,36 +92,9 @@ const PurchaseIngredientsTable: React.FC<PurchaseIngredientsTableProps> = ({
|
|||||||
<CustomAutocomplete
|
<CustomAutocomplete
|
||||||
size='small'
|
size='small'
|
||||||
options={ingredientOptions}
|
options={ingredientOptions}
|
||||||
value={item.ingredient || null}
|
value={item.ingredient}
|
||||||
onChange={(event, newValue) => handleIngredientSelection(index, newValue)}
|
onChange={(event, newValue) => handleIngredientChange(index, 'ingredient', newValue)}
|
||||||
loading={isLoading}
|
renderInput={params => <CustomTextField {...params} placeholder='Pilih Bahan Baku' />}
|
||||||
getOptionLabel={(option: any) => {
|
|
||||||
if (!option) return ''
|
|
||||||
return option.label || option.name || option.nama || ''
|
|
||||||
}}
|
|
||||||
isOptionEqualToValue={(option: any, value: any) => {
|
|
||||||
if (!option || !value) return false
|
|
||||||
// Handle different value structures
|
|
||||||
const optionId = option.value || option.id
|
|
||||||
const valueId = value.value || value.id
|
|
||||||
return optionId === valueId
|
|
||||||
}}
|
|
||||||
renderInput={params => (
|
|
||||||
<CustomTextField
|
|
||||||
{...params}
|
|
||||||
placeholder={isLoading ? 'Loading ingredients...' : 'Pilih Bahan Baku'}
|
|
||||||
InputProps={{
|
|
||||||
...params.InputProps,
|
|
||||||
endAdornment: (
|
|
||||||
<>
|
|
||||||
{isLoading ? <CircularProgress color='inherit' size={20} /> : null}
|
|
||||||
{params.InputProps.endAdornment}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -285,7 +215,6 @@ const PurchaseIngredientsTable: React.FC<PurchaseIngredientsTableProps> = ({
|
|||||||
variant='outlined'
|
variant='outlined'
|
||||||
size='small'
|
size='small'
|
||||||
sx={{ mt: 1 }}
|
sx={{ mt: 1 }}
|
||||||
disabled={isLoading}
|
|
||||||
>
|
>
|
||||||
Tambah bahan baku
|
Tambah bahan baku
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user