Create And Update Account

This commit is contained in:
efrilm 2025-09-12 20:35:49 +07:00
parent ce344e4a98
commit b2840ca27c
4 changed files with 202 additions and 91 deletions

View File

@ -0,0 +1,52 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'react-toastify'
import { api } from '../api'
import { AccountRequest } from '../queries/chartOfAccountType'
export const useAccountsMutation = () => {
const queryClient = useQueryClient()
const createAccount = useMutation({
mutationFn: async (newAccount: AccountRequest) => {
const response = await api.post('/accounts', newAccount)
return response.data
},
onSuccess: () => {
toast.success('Account created successfully!')
queryClient.invalidateQueries({ queryKey: ['accounts'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed')
}
})
const updateAccount = useMutation({
mutationFn: async ({ id, payload }: { id: string; payload: AccountRequest }) => {
const response = await api.put(`/accounts/${id}`, payload)
return response.data
},
onSuccess: () => {
toast.success('Account updated successfully!')
queryClient.invalidateQueries({ queryKey: ['accounts'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed')
}
})
const deleteAccount = useMutation({
mutationFn: async (id: string) => {
const response = await api.delete(`/accounts/${id}`)
return response.data
},
onSuccess: () => {
toast.success('Account deleted successfully!')
queryClient.invalidateQueries({ queryKey: ['accounts'] })
},
onError: (error: any) => {
toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed')
}
})
return { createAccount, updateAccount, deleteAccount }
}

View File

@ -34,3 +34,12 @@ export function useChartOfAccountTypes(params: ChartOfAccountQueryParams = {}) {
}
})
}
export interface AccountRequest {
chart_of_account_id: string
name: string
number: string
account_type: string
opening_balance: number
description: string
}

View File

@ -14,41 +14,52 @@ import { useForm, Controller } from 'react-hook-form'
// Component Imports
import CustomTextField from '@core/components/mui/TextField'
import CustomAutocomplete from '@/@core/components/mui/Autocomplete'
import { useChartOfAccountTypes } from '@/services/queries/chartOfAccountType'
import { AccountRequest } from '@/services/queries/chartOfAccountType'
import { useChartOfAccount } from '@/services/queries/chartOfAccount'
// Account Type
export type AccountType = {
id: number
code: string
name: string
category: string
balance: string
}
import { Account, ChartOfAccount } from '@/types/services/chartOfAccount'
import { useAccountsMutation } from '@/services/mutations/account'
type Props = {
open: boolean
handleClose: () => void
accountData?: AccountType[]
setData: (data: AccountType[]) => void
editingAccount?: AccountType | null
accountData?: Account[]
setData: (data: Account[]) => void
editingAccount?: Account | null
}
type FormValidateType = {
name: string
code: string
category: string
parentAccount?: string
account_type: string
opening_balance: number
description: string
chart_of_account_id: string
}
// Vars
const initialData = {
name: '',
code: '',
category: '',
parentAccount: ''
account_type: '',
opening_balance: 0,
description: '',
chart_of_account_id: ''
}
// Static Account Types
const staticAccountTypes = [
{ id: '1', name: 'Cash', code: 'cash', description: 'Cash account' },
{ id: '2', name: 'Wallet', code: 'wallet', description: 'Digital wallet account' },
{ id: '3', name: 'Bank', code: 'bank', description: 'Bank account' },
{ id: '4', name: 'Credit', code: 'credit', description: 'Credit account' },
{ id: '5', name: 'Debit', code: 'debit', description: 'Debit account' },
{ id: '6', name: 'Asset', code: 'asset', description: 'Asset account' },
{ id: '7', name: 'Liability', code: 'liability', description: 'Liability account' },
{ id: '8', name: 'Equity', code: 'equity', description: 'Equity account' },
{ id: '9', name: 'Revenue', code: 'revenue', description: 'Revenue account' },
{ id: '10', name: 'Expense', code: 'expense', description: 'Expense account' }
]
const AccountFormDrawer = (props: Props) => {
// Props
const { open, handleClose, accountData, setData, editingAccount } = props
@ -56,30 +67,20 @@ const AccountFormDrawer = (props: Props) => {
// Determine if we're editing
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
}))
: []
const { createAccount, updateAccount } = useAccountsMutation()
// Process accounts for parent account dropdown
const parentAccountOptions = accounts?.data.length
// Use static account types
const accountTypeOptions = staticAccountTypes
// Process chart of accounts for the dropdown
const chartOfAccountOptions = 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,
@ -105,9 +106,11 @@ const AccountFormDrawer = (props: Props) => {
// Populate form with existing data
resetForm({
name: editingAccount.name,
code: editingAccount.code,
category: editingAccount.category,
parentAccount: ''
code: editingAccount.number,
account_type: editingAccount.account_type,
opening_balance: editingAccount.opening_balance,
description: editingAccount.description || '',
chart_of_account_id: editingAccount.chart_of_account_id
})
} else {
// Reset to initial data for new account
@ -118,35 +121,40 @@ const AccountFormDrawer = (props: Props) => {
const onSubmit = (data: FormValidateType) => {
if (isEdit && editingAccount) {
// Update existing account
const updatedAccounts =
accountData?.map(account =>
account.id === editingAccount.id
? {
...account,
code: data.code,
name: data.name,
category: data.category
}
: account
) || []
setData(updatedAccounts)
} else {
// Create new account
const newAccount: AccountType = {
id: accountData?.length ? Math.max(...accountData.map(a => a.id)) + 1 : 1,
code: data.code,
const accountRequest: AccountRequest = {
chart_of_account_id: data.chart_of_account_id,
name: data.name,
category: data.category,
balance: '0'
number: data.code,
account_type: data.account_type,
opening_balance: data.opening_balance,
description: data.description
}
setData([...(accountData ?? []), newAccount])
updateAccount.mutate(
{ id: editingAccount.id, payload: accountRequest },
{
onSuccess: () => {
handleClose()
resetForm(initialData)
}
}
)
} else {
// Create new account - this would typically be sent as AccountRequest to API
const accountRequest: AccountRequest = {
chart_of_account_id: data.chart_of_account_id,
name: data.name,
number: data.code,
account_type: data.account_type,
opening_balance: data.opening_balance,
description: data.description
}
createAccount.mutate(accountRequest, {
onSuccess: () => {
handleClose()
resetForm(initialData)
}
})
}
handleClose()
resetForm(initialData)
}
const handleReset = () => {
@ -233,61 +241,58 @@ const AccountFormDrawer = (props: Props) => {
/>
</div>
{/* Kategori */}
{/* Tipe Akun */}
<div>
<Typography variant='body2' className='mb-2'>
Kategori <span className='text-red-500'>*</span>
Tipe Akun <span className='text-red-500'>*</span>
</Typography>
<Controller
name='category'
name='account_type'
control={control}
rules={{ required: true }}
render={({ field: { onChange, value, ...field } }) => (
<CustomAutocomplete
{...field}
loading={isLoading}
options={categoryOptions}
value={categoryOptions.find(option => option.name === value) || null}
onChange={(_, newValue) => onChange(newValue?.name || '')}
options={accountTypeOptions}
value={accountTypeOptions.find(option => option.code === value) || null}
onChange={(_, newValue) => onChange(newValue?.code || '')}
getOptionLabel={option => option.name}
renderOption={(props, option) => (
<Box component='li' {...props}>
<div>
<Typography variant='body2'>
{option.code} - {option.name}
</Typography>
<Typography variant='body2'>{option.name}</Typography>
</div>
</Box>
)}
renderInput={params => (
<CustomTextField
{...params}
placeholder={isLoading ? 'Loading categories...' : 'Pilih kategori'}
{...(errors.category && { error: true, helperText: 'Field ini wajib diisi.' })}
placeholder='Pilih tipe akun'
{...(errors.account_type && { error: true, helperText: 'Field ini wajib diisi.' })}
/>
)}
isOptionEqualToValue={(option, value) => option.name === value.name}
disabled={isLoading}
isOptionEqualToValue={(option, value) => option.code === value.code}
/>
)}
/>
</div>
{/* Sub Akun dari */}
{/* Chart of Account */}
<div>
<Typography variant='body2' className='mb-2'>
Sub Akun dari
Chart of Account <span className='text-red-500'>*</span>
</Typography>
<Controller
name='parentAccount'
name='chart_of_account_id'
control={control}
rules={{ required: true }}
render={({ field: { onChange, value, ...field } }) => (
<CustomAutocomplete
{...field}
loading={isLoadingAccounts}
options={parentAccountOptions}
value={parentAccountOptions.find(account => `${account.code} ${account.name}` === value) || null}
onChange={(_, newValue) => onChange(newValue ? `${newValue.code} ${newValue.name}` : '')}
options={chartOfAccountOptions}
value={chartOfAccountOptions.find(option => option.id === value) || null}
onChange={(_, newValue) => onChange(newValue?.id || '')}
getOptionLabel={option => `${option.code} - ${option.name}`}
renderOption={(props, option) => (
<Box component='li' {...props}>
@ -306,18 +311,59 @@ const AccountFormDrawer = (props: Props) => {
renderInput={params => (
<CustomTextField
{...params}
placeholder={isLoadingAccounts ? 'Loading accounts...' : 'Pilih akun parent'}
placeholder={isLoadingAccounts ? 'Loading chart of accounts...' : 'Pilih chart of account'}
{...(errors.chart_of_account_id && { error: true, helperText: 'Field ini wajib diisi.' })}
/>
)}
isOptionEqualToValue={(option, value) =>
`${option.code} ${option.name}` === `${value.code} ${value.name}`
}
isOptionEqualToValue={(option, value) => option.id === value.id}
disabled={isLoadingAccounts}
noOptionsText={isLoadingAccounts ? 'Loading...' : 'Tidak ada akun tersedia'}
noOptionsText={isLoadingAccounts ? 'Loading...' : 'Tidak ada chart of account tersedia'}
/>
)}
/>
</div>
{/* Opening Balance */}
<div>
<Typography variant='body2' className='mb-2'>
Saldo Awal <span className='text-red-500'>*</span>
</Typography>
<Controller
name='opening_balance'
control={control}
rules={{ required: true, min: 0 }}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
type='number'
placeholder='0'
onChange={e => field.onChange(Number(e.target.value))}
{...(errors.opening_balance && {
error: true,
helperText:
errors.opening_balance.type === 'min'
? 'Saldo awal tidak boleh negatif.'
: 'Field ini wajib diisi.'
})}
/>
)}
/>
</div>
{/* Deskripsi */}
<div>
<Typography variant='body2' className='mb-2'>
Deskripsi
</Typography>
<Controller
name='description'
control={control}
render={({ field }) => (
<CustomTextField {...field} fullWidth multiline rows={3} placeholder='Deskripsi akun' />
)}
/>
</div>
</div>
</form>
</Box>

View File

@ -226,6 +226,10 @@ const AccountListTable = () => {
variant='text'
color='primary'
className='p-0 min-w-0 font-medium normal-case justify-start'
onClick={() => {
setEditingAccount(row.original)
setAddAccountOpen(true)
}}
sx={{
textTransform: 'none',
fontWeight: 500,
@ -414,13 +418,13 @@ const AccountListTable = () => {
disabled={isLoading}
/>
</Card>
{/* <AccountFormDrawer
<AccountFormDrawer
open={addAccountOpen}
handleClose={handleCloseDrawer}
accountData={data}
setData={setData}
accountData={accounts}
setData={() => {}}
editingAccount={editingAccount}
/> */}
/>
</>
)
}