333 lines
10 KiB
TypeScript
333 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useMemo, useEffect } from 'react'
|
|
import Grid from '@mui/material/Grid2'
|
|
import TextField, { TextFieldProps } from '@mui/material/TextField'
|
|
import InputAdornment from '@mui/material/InputAdornment'
|
|
import Box from '@mui/material/Box'
|
|
import Typography from '@mui/material/Typography'
|
|
import Chip from '@mui/material/Chip'
|
|
import FormControl from '@mui/material/FormControl'
|
|
import InputLabel from '@mui/material/InputLabel'
|
|
import Select from '@mui/material/Select'
|
|
import MenuItem from '@mui/material/MenuItem'
|
|
import CircularProgress from '@mui/material/CircularProgress'
|
|
import CashBankCard from './CashBankCard' // Adjust import path as needed
|
|
import CustomTextField from '@/@core/components/mui/TextField'
|
|
import { getLocalizedUrl } from '@/utils/i18n'
|
|
import { Locale } from '@/configs/i18n'
|
|
import { useParams } from 'next/navigation'
|
|
import AccountFormDrawer from '../account/AccountFormDrawer'
|
|
import { Button } from '@mui/material'
|
|
import { Account } from '@/types/services/chartOfAccount'
|
|
import { useAccounts } from '@/services/queries/account'
|
|
import { formatCurrency } from '@/utils/transform'
|
|
|
|
interface BankAccount {
|
|
id: string
|
|
title: string
|
|
accountNumber: string
|
|
balances: Array<{
|
|
amount: string | number
|
|
label: string
|
|
}>
|
|
chartData: Array<{
|
|
name: string
|
|
data: number[]
|
|
}>
|
|
categories: string[]
|
|
chartColor?: string
|
|
currency: 'IDR' | 'USD' | 'EUR'
|
|
accountType: 'giro' | 'savings' | 'investment' | 'credit' | 'cash'
|
|
bank: string
|
|
status: 'active' | 'inactive' | 'blocked'
|
|
}
|
|
|
|
// Static chart data for fallback/demo purposes
|
|
const generateChartData = (accountType: string, balance: number) => {
|
|
const baseValue = balance || 1000000
|
|
const variation = baseValue * 0.2
|
|
|
|
return Array.from({ length: 12 }, (_, i) => {
|
|
const randomVariation = (Math.random() - 0.5) * variation
|
|
return Math.max(baseValue + randomVariation, baseValue * 0.5)
|
|
})
|
|
}
|
|
|
|
const getChartColor = (accountType: string) => {
|
|
const colors = {
|
|
giro: '#ff6b9d',
|
|
savings: '#4285f4',
|
|
investment: '#00bcd4',
|
|
credit: '#ff9800',
|
|
cash: '#4caf50'
|
|
}
|
|
return colors[accountType as keyof typeof colors] || '#757575'
|
|
}
|
|
|
|
const DebouncedInput = ({
|
|
value: initialValue,
|
|
onChange,
|
|
debounce = 500,
|
|
...props
|
|
}: {
|
|
value: string | number
|
|
onChange: (value: string | number) => void
|
|
debounce?: number
|
|
} & Omit<TextFieldProps, 'onChange'>) => {
|
|
// States
|
|
const [value, setValue] = useState(initialValue)
|
|
|
|
useEffect(() => {
|
|
setValue(initialValue)
|
|
}, [initialValue])
|
|
|
|
useEffect(() => {
|
|
const timeout = setTimeout(() => {
|
|
onChange(value)
|
|
}, debounce)
|
|
|
|
return () => clearTimeout(timeout)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [value])
|
|
|
|
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
|
}
|
|
|
|
const CashBankList = () => {
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
const [editingAccount, setEditingAccount] = useState<Account | null>(null)
|
|
const [addAccountOpen, setAddAccountOpen] = useState(false)
|
|
const [data, setData] = useState<Account[]>([])
|
|
const { lang: locale } = useParams()
|
|
|
|
// Use the accounts hook with search parameter
|
|
const { data: accountsResponse, isLoading } = useAccounts({
|
|
page: 1,
|
|
limit: 10,
|
|
search: searchQuery
|
|
})
|
|
|
|
const handleCloseDrawer = () => {
|
|
setAddAccountOpen(false)
|
|
setEditingAccount(null)
|
|
}
|
|
|
|
// Transform API data to match our BankAccount interface
|
|
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(() => {
|
|
if (!searchQuery || accountsResponse) {
|
|
// If using API search or no search, return transformed accounts as is
|
|
return transformedAccounts
|
|
}
|
|
|
|
// Local filtering fallback
|
|
return transformedAccounts.filter(account => {
|
|
const matchesSearch =
|
|
account.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
account.accountNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
account.bank.toLowerCase().includes(searchQuery.toLowerCase())
|
|
return matchesSearch
|
|
})
|
|
}, [transformedAccounts, searchQuery, accountsResponse])
|
|
|
|
return (
|
|
<>
|
|
<Box sx={{ p: 3 }}>
|
|
{/* Search and Filters */}
|
|
<Box sx={{ mb: 4 }}>
|
|
<div className='flex justify-between items-center'>
|
|
<DebouncedInput
|
|
value={searchQuery}
|
|
onChange={value => setSearchQuery(value as string)}
|
|
placeholder='Cari akun...'
|
|
className='max-sm:is-full'
|
|
disabled={isLoading}
|
|
InputProps={{
|
|
startAdornment: (
|
|
<InputAdornment position='start'>
|
|
<i className='tabler-search' />
|
|
</InputAdornment>
|
|
)
|
|
}}
|
|
/>
|
|
<Box>
|
|
<Button
|
|
variant='contained'
|
|
className='max-sm:is-full is-auto'
|
|
startIcon={<i className='tabler-plus' />}
|
|
onClick={() => {
|
|
setEditingAccount(null)
|
|
setAddAccountOpen(true)
|
|
}}
|
|
disabled={isLoading}
|
|
>
|
|
Tambah Akun
|
|
</Button>
|
|
</Box>
|
|
</div>
|
|
</Box>
|
|
|
|
{/* Loading State */}
|
|
{isLoading && (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
)}
|
|
|
|
{/* Account Cards */}
|
|
{!isLoading && (
|
|
<Grid container spacing={3}>
|
|
{filteredAccounts.length > 0 ? (
|
|
filteredAccounts.map(account => (
|
|
<Grid key={account.id} size={{ xs: 12, lg: 6, xl: 4 }}>
|
|
<CashBankCard
|
|
title={account.title}
|
|
accountNumber={account.accountNumber}
|
|
balances={account.balances}
|
|
chartData={account.chartData}
|
|
categories={account.categories}
|
|
chartColor={account.chartColor}
|
|
currency={account.currency}
|
|
showButton={account.accountType !== 'cash'}
|
|
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>
|
|
)}
|
|
|
|
{/* Error State (if needed) */}
|
|
{!isLoading && !accountsResponse && (
|
|
<Grid size={{ xs: 12 }}>
|
|
<Box
|
|
sx={{
|
|
textAlign: 'center',
|
|
py: 8,
|
|
backgroundColor: 'error.light',
|
|
borderRadius: 2,
|
|
color: 'error.contrastText'
|
|
}}
|
|
>
|
|
<Typography variant='h6' gutterBottom>
|
|
Terjadi kesalahan saat memuat data
|
|
</Typography>
|
|
<Typography variant='body2'>Silakan coba lagi atau hubungi administrator</Typography>
|
|
</Box>
|
|
</Grid>
|
|
)}
|
|
</Box>
|
|
|
|
<AccountFormDrawer
|
|
open={addAccountOpen}
|
|
handleClose={handleCloseDrawer}
|
|
accountData={data}
|
|
setData={setData}
|
|
editingAccount={editingAccount}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default CashBankList
|