Compare commits

..

4 Commits

Author SHA1 Message Date
efrilm
2d69a024c1 Get Account 2025-09-12 19:51:35 +07:00
efrilm
27fe48e99e integrate chart of account to sub akun dari 2025-09-12 19:30:14 +07:00
efrilm
5a0d96ee43 feat: update 2025-09-12 19:19:43 +07:00
efrilm
9cbb41c2ad Get Chart of Account Types 2025-09-12 19:18:43 +07:00
6 changed files with 280 additions and 216 deletions

View File

@ -0,0 +1,36 @@
import { useQuery } from '@tanstack/react-query'
import { api } from '../api'
import { Accounts } from '@/types/services/chartOfAccount'
interface AccountQueryParams {
page?: number
limit?: number
search?: string
}
export function useAccounts(params: AccountQueryParams = {}) {
const { page = 1, limit = 10, search = '', ...filters } = params
return useQuery<Accounts>({
queryKey: ['accounts', { page, limit, search, ...filters }],
queryFn: async () => {
const queryParams = new URLSearchParams()
queryParams.append('page', page.toString())
queryParams.append('limit', limit.toString())
if (search) {
queryParams.append('search', search)
}
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
queryParams.append(key, value.toString())
}
})
const res = await api.get(`/accounts?${queryParams.toString()}`)
return res.data.data
}
})
}

View File

@ -0,0 +1,36 @@
import { useQuery } from '@tanstack/react-query'
import { api } from '../api'
import { ChartOfAccounts } from '@/types/services/chartOfAccount'
interface ChartOfAccountQueryParams {
page?: number
limit?: number
search?: string
}
export function useChartOfAccount(params: ChartOfAccountQueryParams = {}) {
const { page = 1, limit = 10, search = '', ...filters } = params
return useQuery<ChartOfAccounts>({
queryKey: ['chart-of-accounts', { page, limit, search, ...filters }],
queryFn: async () => {
const queryParams = new URLSearchParams()
queryParams.append('page', page.toString())
queryParams.append('limit', limit.toString())
if (search) {
queryParams.append('search', search)
}
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
queryParams.append(key, value.toString())
}
})
const res = await api.get(`/chart-of-accounts?${queryParams.toString()}`)
return res.data.data
}
})
}

View File

@ -0,0 +1,36 @@
import { ChartOfAccountTypes } from '@/types/services/chartOfAccount'
import { useQuery } from '@tanstack/react-query'
import { api } from '../api'
interface ChartOfAccountQueryParams {
page?: number
limit?: number
search?: string
}
export function useChartOfAccountTypes(params: ChartOfAccountQueryParams = {}) {
const { page = 1, limit = 10, search = '', ...filters } = params
return useQuery<ChartOfAccountTypes>({
queryKey: ['chart-of-account-types', { page, limit, search, ...filters }],
queryFn: async () => {
const queryParams = new URLSearchParams()
queryParams.append('page', page.toString())
queryParams.append('limit', limit.toString())
if (search) {
queryParams.append('search', search)
}
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
queryParams.append(key, value.toString())
}
})
const res = await api.get(`/chart-of-account-types?${queryParams.toString()}`)
return res.data.data
}
})
}

View File

@ -0,0 +1,64 @@
export interface ChartOfAccountType {
id: string
name: string
code: string
description: string
is_active: boolean
created_at: string
updated_at: string
}
export interface ChartOfAccountTypes {
data: ChartOfAccountType[]
limit: number
page: number
total: number
}
export interface ChartOfAccount {
id: string
organization_id: string
outlet_id: string
chart_of_account_type_id: string
parent_id: string
name: string
code: string
description: string
is_active: boolean
is_system: boolean
created_at: string
updated_at: string
chart_of_account_type: ChartOfAccountType
}
export interface ChartOfAccounts {
data: ChartOfAccount[]
limit: number
page: number
total: number
}
export interface Account {
id: string
organization_id: string
outlet_id: string
chart_of_account_id: string
name: string
number: string
account_type: string
opening_balance: number
current_balance: number
description: string
is_active: true
is_system: false
created_at: string
updated_at: string
chart_of_account: ChartOfAccount
}
export interface Accounts {
data: Account[]
limit: number
page: number
total: number
}

View File

@ -5,7 +5,6 @@ import { useState, useEffect } from 'react'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import Drawer from '@mui/material/Drawer' import Drawer from '@mui/material/Drawer'
import IconButton from '@mui/material/IconButton' import IconButton from '@mui/material/IconButton'
import MenuItem from '@mui/material/MenuItem'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
@ -15,6 +14,8 @@ import { useForm, Controller } from 'react-hook-form'
// Component Imports // Component Imports
import CustomTextField from '@core/components/mui/TextField' import CustomTextField from '@core/components/mui/TextField'
import CustomAutocomplete from '@/@core/components/mui/Autocomplete' import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
import { useChartOfAccountTypes } from '@/services/queries/chartOfAccountType'
import { useChartOfAccount } from '@/services/queries/chartOfAccount'
// Account Type // Account Type
export type AccountType = { export type AccountType = {
@ -40,31 +41,6 @@ type FormValidateType = {
parentAccount?: string parentAccount?: string
} }
// Categories available for accounts
const accountCategories = [
'Kas & Bank',
'Piutang',
'Persediaan',
'Aset Tetap',
'Hutang',
'Ekuitas',
'Pendapatan',
'Beban'
]
// Parent accounts (dummy data for dropdown)
const parentAccounts = [
{ id: 1, code: '1-10001', name: 'Kas' },
{ id: 2, code: '1-10002', name: 'Bank BCA' },
{ id: 3, code: '1-10003', name: 'Bank Mandiri' },
{ id: 4, code: '1-10101', name: 'Piutang Usaha' },
{ id: 5, code: '1-10201', name: 'Persediaan Barang' },
{ id: 6, code: '2-20001', name: 'Hutang Usaha' },
{ id: 7, code: '3-30001', name: 'Modal Pemilik' },
{ id: 8, code: '4-40001', name: 'Penjualan' },
{ id: 9, code: '5-50001', name: 'Beban Gaji' }
]
// Vars // Vars
const initialData = { const initialData = {
name: '', name: '',
@ -80,6 +56,38 @@ const AccountFormDrawer = (props: Props) => {
// Determine if we're editing // Determine if we're editing
const isEdit = !!editingAccount const isEdit = !!editingAccount
const { data: accountTypes, isLoading } = useChartOfAccountTypes()
const { data: accounts, isLoading: isLoadingAccounts } = useChartOfAccount({
page: 1,
limit: 100
})
// Process account types for the dropdown
const categoryOptions = accountTypes?.data.length
? accountTypes.data
.filter(type => type.is_active) // Only show active types
.map(type => ({
id: type.id,
name: type.name,
code: type.code,
description: type.description
}))
: []
// Process accounts for parent account dropdown
const parentAccountOptions = accounts?.data.length
? accounts.data
.filter(account => account.is_active) // Only show active accounts
.filter(account => (editingAccount ? account.id !== editingAccount.id.toString() : true)) // Exclude current account when editing
.map(account => ({
id: account.id,
code: account.code,
name: account.name,
description: account.description
}))
: []
// Hooks // Hooks
const { const {
control, control,
@ -237,17 +245,29 @@ const AccountFormDrawer = (props: Props) => {
render={({ field: { onChange, value, ...field } }) => ( render={({ field: { onChange, value, ...field } }) => (
<CustomAutocomplete <CustomAutocomplete
{...field} {...field}
options={accountCategories} loading={isLoading}
value={value || null} options={categoryOptions}
onChange={(_, newValue) => onChange(newValue || '')} value={categoryOptions.find(option => option.name === value) || null}
onChange={(_, newValue) => onChange(newValue?.name || '')}
getOptionLabel={option => option.name}
renderOption={(props, option) => (
<Box component='li' {...props}>
<div>
<Typography variant='body2'>
{option.code} - {option.name}
</Typography>
</div>
</Box>
)}
renderInput={params => ( renderInput={params => (
<CustomTextField <CustomTextField
{...params} {...params}
placeholder='Pilih kategori' placeholder={isLoading ? 'Loading categories...' : 'Pilih kategori'}
{...(errors.category && { error: true, helperText: 'Field ini wajib diisi.' })} {...(errors.category && { error: true, helperText: 'Field ini wajib diisi.' })}
/> />
)} )}
isOptionEqualToValue={(option, value) => option === value} isOptionEqualToValue={(option, value) => option.name === value.name}
disabled={isLoading}
/> />
)} )}
/> />
@ -264,14 +284,36 @@ const AccountFormDrawer = (props: Props) => {
render={({ field: { onChange, value, ...field } }) => ( render={({ field: { onChange, value, ...field } }) => (
<CustomAutocomplete <CustomAutocomplete
{...field} {...field}
options={parentAccounts} loading={isLoadingAccounts}
value={parentAccounts.find(account => `${account.code} ${account.name}` === value) || null} options={parentAccountOptions}
value={parentAccountOptions.find(account => `${account.code} ${account.name}` === value) || null}
onChange={(_, newValue) => onChange(newValue ? `${newValue.code} ${newValue.name}` : '')} onChange={(_, newValue) => onChange(newValue ? `${newValue.code} ${newValue.name}` : '')}
getOptionLabel={option => `${option.code} ${option.name}`} getOptionLabel={option => `${option.code} - ${option.name}`}
renderInput={params => <CustomTextField {...params} placeholder='Pilih akun' />} renderOption={(props, option) => (
<Box component='li' {...props}>
<div>
<Typography variant='body2'>
{option.code} - {option.name}
</Typography>
{option.description && (
<Typography variant='caption' color='textSecondary'>
{option.description}
</Typography>
)}
</div>
</Box>
)}
renderInput={params => (
<CustomTextField
{...params}
placeholder={isLoadingAccounts ? 'Loading accounts...' : 'Pilih akun parent'}
/>
)}
isOptionEqualToValue={(option, value) => isOptionEqualToValue={(option, value) =>
`${option.code} ${option.name}` === `${value.code} ${value.name}` `${option.code} ${option.name}` === `${value.code} ${value.name}`
} }
disabled={isLoadingAccounts}
noOptionsText={isLoadingAccounts ? 'Loading...' : 'Tidak ada akun tersedia'}
/> />
)} )}
/> />

View File

@ -41,6 +41,11 @@ import TablePaginationComponent from '@/components/TablePaginationComponent'
import Loading from '@/components/layout/shared/Loading' import Loading from '@/components/layout/shared/Loading'
import { getLocalizedUrl } from '@/utils/i18n' import { getLocalizedUrl } from '@/utils/i18n'
import AccountFormDrawer from './AccountFormDrawer' import AccountFormDrawer from './AccountFormDrawer'
import { useChartOfAccount } from '@/services/queries/chartOfAccount'
import { Account, ChartOfAccount } from '@/types/services/chartOfAccount'
import { useAccounts } from '@/services/queries/account'
import { formatCurrency } from '@/utils/transform'
import { useChartOfAccountTypes } from '@/services/queries/chartOfAccountType'
// Account Type // Account Type
export type AccountType = { export type AccountType = {
@ -60,119 +65,10 @@ declare module '@tanstack/table-core' {
} }
} }
type AccountTypeWithAction = AccountType & { type AccountTypeWithAction = Account & {
actions?: string actions?: string
} }
// Dummy Account Data
export const accountsData: AccountType[] = [
{
id: 1,
code: '1-10001',
name: 'Kas',
category: 'Kas & Bank',
balance: '20000000'
},
{
id: 2,
code: '1-10002',
name: 'Bank BCA',
category: 'Kas & Bank',
balance: '150000000'
},
{
id: 3,
code: '1-10003',
name: 'Bank Mandiri',
category: 'Kas & Bank',
balance: '75000000'
},
{
id: 4,
code: '1-10101',
name: 'Piutang Usaha',
category: 'Piutang',
balance: '50000000'
},
{
id: 5,
code: '1-10102',
name: 'Piutang Karyawan',
category: 'Piutang',
balance: '5000000'
},
{
id: 6,
code: '1-10201',
name: 'Persediaan Barang',
category: 'Persediaan',
balance: '100000000'
},
{
id: 7,
code: '1-10301',
name: 'Peralatan Kantor',
category: 'Aset Tetap',
balance: '25000000'
},
{
id: 8,
code: '1-10302',
name: 'Kendaraan',
category: 'Aset Tetap',
balance: '200000000'
},
{
id: 9,
code: '2-20001',
name: 'Hutang Usaha',
category: 'Hutang',
balance: '-30000000'
},
{
id: 10,
code: '2-20002',
name: 'Hutang Gaji',
category: 'Hutang',
balance: '-15000000'
},
{
id: 11,
code: '3-30001',
name: 'Modal Pemilik',
category: 'Ekuitas',
balance: '500000000'
},
{
id: 12,
code: '4-40001',
name: 'Penjualan',
category: 'Pendapatan',
balance: '250000000'
},
{
id: 13,
code: '5-50001',
name: 'Beban Gaji',
category: 'Beban',
balance: '-80000000'
},
{
id: 14,
code: '5-50002',
name: 'Beban Listrik',
category: 'Beban',
balance: '-5000000'
},
{
id: 15,
code: '5-50003',
name: 'Beban Telepon',
category: 'Beban',
balance: '-2000000'
}
]
// Styled Components // Styled Components
const Icon = styled('i')({}) const Icon = styled('i')({})
@ -242,16 +138,6 @@ const getCategoryColor = (category: string) => {
} }
} }
// Format currency
const formatCurrency = (amount: string) => {
const numAmount = parseInt(amount)
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(Math.abs(numAmount))
}
// Column Definitions // Column Definitions
const columnHelper = createColumnHelper<AccountTypeWithAction>() const columnHelper = createColumnHelper<AccountTypeWithAction>()
@ -261,53 +147,25 @@ const AccountListTable = () => {
// States // States
const [addAccountOpen, setAddAccountOpen] = useState(false) const [addAccountOpen, setAddAccountOpen] = useState(false)
const [rowSelection, setRowSelection] = useState({}) const [rowSelection, setRowSelection] = useState({})
const [currentPage, setCurrentPage] = useState(0) const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10) const [pageSize, setPageSize] = useState(10)
const [openConfirm, setOpenConfirm] = useState(false) const [openConfirm, setOpenConfirm] = useState(false)
const [accountId, setAccountId] = useState('') const [accountId, setAccountId] = useState('')
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [categoryFilter, setCategoryFilter] = useState<string>('Semua') const [categoryFilter, setCategoryFilter] = useState<string>('Semua')
const [filteredData, setFilteredData] = useState<AccountType[]>(accountsData) const [editingAccount, setEditingAccount] = useState<Account | null>(null)
const [data, setData] = useState<AccountType[]>(accountsData)
const [editingAccount, setEditingAccount] = useState<AccountType | null>(null) const { data, isLoading } = useAccounts({
page: currentPage,
limit: pageSize,
search
})
// Hooks // Hooks
const { lang: locale } = useParams() const { lang: locale } = useParams()
// Get unique categories for filter const accounts = data?.data ?? []
const categories = useMemo(() => { const totalCount = data?.total ?? 0
const uniqueCategories = [...new Set(data.map(account => account.category))]
return ['Semua', ...uniqueCategories]
}, [data])
// Filter data based on search and category
useEffect(() => {
let filtered = data
// Filter by search
if (search) {
filtered = filtered.filter(
account =>
account.code.toLowerCase().includes(search.toLowerCase()) ||
account.name.toLowerCase().includes(search.toLowerCase()) ||
account.category.toLowerCase().includes(search.toLowerCase())
)
}
// Filter by category
if (categoryFilter !== 'Semua') {
filtered = filtered.filter(account => account.category === categoryFilter)
}
setFilteredData(filtered)
setCurrentPage(0)
}, [search, categoryFilter, data])
const totalCount = filteredData.length
const paginatedData = useMemo(() => {
const startIndex = currentPage * pageSize
return filteredData.slice(startIndex, startIndex + pageSize)
}, [filteredData, currentPage, pageSize])
const handlePageChange = useCallback((event: unknown, newPage: number) => { const handlePageChange = useCallback((event: unknown, newPage: number) => {
setCurrentPage(newPage) setCurrentPage(newPage)
@ -319,12 +177,8 @@ const AccountListTable = () => {
setCurrentPage(0) setCurrentPage(0)
}, []) }, [])
const handleDelete = () => {
setOpenConfirm(false)
}
// Handle row click for edit // Handle row click for edit
const handleRowClick = (account: AccountType, event: React.MouseEvent) => { const handleRowClick = (account: Account, event: React.MouseEvent) => {
// Don't trigger row click if clicking on checkbox or link // Don't trigger row click if clicking on checkbox or link
const target = event.target as HTMLElement const target = event.target as HTMLElement
if (target.closest('input[type="checkbox"]') || target.closest('a') || target.closest('button')) { if (target.closest('input[type="checkbox"]') || target.closest('a') || target.closest('button')) {
@ -365,7 +219,7 @@ const AccountListTable = () => {
/> />
) )
}, },
columnHelper.accessor('code', { columnHelper.accessor('number', {
header: 'Kode Akun', header: 'Kode Akun',
cell: ({ row }) => ( cell: ({ row }) => (
<Button <Button
@ -381,7 +235,7 @@ const AccountListTable = () => {
} }
}} }}
> >
{row.original.code} {row.original.number}
</Button> </Button>
) )
}), }),
@ -393,26 +247,21 @@ const AccountListTable = () => {
</Typography> </Typography>
) )
}), }),
columnHelper.accessor('category', { columnHelper.accessor('chart_of_account.name', {
header: 'Kategori', header: 'Kategori',
cell: ({ row }) => ( cell: ({ row }) => (
<Chip <Typography color='text.primary' className='font-medium'>
variant='tonal' {row.original.chart_of_account.name}
label={row.original.category} </Typography>
size='small'
color={getCategoryColor(row.original.category) as any}
className='capitalize'
/>
) )
}), }),
columnHelper.accessor('balance', { columnHelper.accessor('current_balance', {
header: 'Saldo', header: 'Saldo',
cell: ({ row }) => { cell: ({ row }) => {
const balance = parseInt(row.original.balance)
return ( return (
<Typography className='font-medium text-right text-primary'> <Typography className='font-medium text-right text-primary'>
{balance < 0 ? '-' : ''} {row.original.current_balance < 0 ? '-' : ''}
{formatCurrency(row.original.balance)} {formatCurrency(row.original.current_balance)}
</Typography> </Typography>
) )
} }
@ -422,7 +271,7 @@ const AccountListTable = () => {
) )
const table = useReactTable({ const table = useReactTable({
data: paginatedData as AccountType[], data: accounts as Account[],
columns, columns,
filterFns: { filterFns: {
fuzzy: fuzzyFilter fuzzy: fuzzyFilter
@ -512,7 +361,7 @@ const AccountListTable = () => {
</tr> </tr>
))} ))}
</thead> </thead>
{filteredData.length === 0 ? ( {accounts.length === 0 ? (
<tbody> <tbody>
<tr> <tr>
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'> <td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
@ -558,15 +407,16 @@ const AccountListTable = () => {
onPageChange={handlePageChange} onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange} onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[10, 25, 50]} rowsPerPageOptions={[10, 25, 50]}
disabled={isLoading}
/> />
</Card> </Card>
<AccountFormDrawer {/* <AccountFormDrawer
open={addAccountOpen} open={addAccountOpen}
handleClose={handleCloseDrawer} handleClose={handleCloseDrawer}
accountData={data} accountData={data}
setData={setData} setData={setData}
editingAccount={editingAccount} editingAccount={editingAccount}
/> /> */}
</> </>
) )
} }