Merge pull request #895 from bigcapitalhq/fix-bank-accounts-filter
fix(server): bank accounts filter
This commit is contained in:
@@ -1,16 +1,28 @@
|
||||
import { Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { BankAccountsApplication } from './BankAccountsApplication.service';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { ICashflowAccountsFilter } from './types/BankAccounts.types';
|
||||
import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { BankAccountsQueryDto } from './dtos/BankAccountsQuery.dto';
|
||||
import { BankAccountResponseDto } from './dtos/BankAccountResponse.dto';
|
||||
|
||||
@Controller('banking/accounts')
|
||||
@ApiTags('Bank Accounts')
|
||||
export class BankAccountsController {
|
||||
constructor(private bankAccountsApplication: BankAccountsApplication) {}
|
||||
constructor(private bankAccountsApplication: BankAccountsApplication) { }
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Retrieve the bank accounts.' })
|
||||
getBankAccounts(@Query() filterDto: ICashflowAccountsFilter) {
|
||||
@ApiQuery({
|
||||
name: 'query',
|
||||
description: 'Query parameters for the bank accounts list.',
|
||||
type: BankAccountsQueryDto,
|
||||
required: false,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'List of bank accounts retrieved successfully.',
|
||||
type: [BankAccountResponseDto],
|
||||
})
|
||||
getBankAccounts(@Query() filterDto: BankAccountsQueryDto) {
|
||||
return this.bankAccountsApplication.getBankAccounts(filterDto);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PauseBankAccountFeeds } from './commands/PauseBankAccountFeeds.service'
|
||||
import { GetBankAccountsService } from './queries/GetBankAccounts';
|
||||
import { ICashflowAccountsFilter } from './types/BankAccounts.types';
|
||||
import { GetBankAccountSummary } from './queries/GetBankAccountSummary';
|
||||
import { BankAccountsQueryDto } from './dtos/BankAccountsQuery.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BankAccountsApplication {
|
||||
@@ -16,13 +17,13 @@ export class BankAccountsApplication {
|
||||
private readonly refreshBankAccountService: RefreshBankAccountService,
|
||||
private readonly resumeBankAccountFeedsService: ResumeBankAccountFeedsService,
|
||||
private readonly pauseBankAccountFeedsService: PauseBankAccountFeeds,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Retrieves the bank accounts.
|
||||
* @param {ICashflowAccountsFilter} filterDto -
|
||||
*/
|
||||
getBankAccounts(filterDto: ICashflowAccountsFilter) {
|
||||
getBankAccounts(filterDto: BankAccountsQueryDto) {
|
||||
return this.getBankAccountsService.getCashflowAccounts(filterDto);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* Bank Account Response DTO
|
||||
* Based on AccountResponseDto but excludes fields that are filtered out by CashflowAccountTransformer:
|
||||
* - predefined
|
||||
* - index
|
||||
* - accountTypeLabel
|
||||
*/
|
||||
export class BankAccountResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'The unique identifier of the account',
|
||||
example: 1,
|
||||
})
|
||||
id: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The name of the account',
|
||||
example: 'Cash Account',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The slug of the account',
|
||||
example: 'cash-account',
|
||||
})
|
||||
slug: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The code of the account',
|
||||
example: '1001',
|
||||
})
|
||||
code: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The type of the account',
|
||||
example: 'bank',
|
||||
})
|
||||
accountType: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The parent account ID',
|
||||
example: null,
|
||||
})
|
||||
parentAccountId: number | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The currency code of the account',
|
||||
example: 'USD',
|
||||
})
|
||||
currencyCode: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether the account is active',
|
||||
example: true,
|
||||
})
|
||||
active: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The bank balance of the account',
|
||||
example: 5000.0,
|
||||
})
|
||||
bankBalance: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The formatted bank balance',
|
||||
example: '$5,000.00',
|
||||
})
|
||||
bankBalanceFormatted: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The last feeds update timestamp',
|
||||
example: '2024-03-20T10:30:00Z',
|
||||
})
|
||||
lastFeedsUpdatedAt: string | Date | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The formatted last feeds update timestamp',
|
||||
example: 'Mar 20, 2024 10:30 AM',
|
||||
})
|
||||
lastFeedsUpdatedAtFormatted: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The last feeds updated from now (relative time)',
|
||||
example: '2 hours ago',
|
||||
})
|
||||
lastFeedsUpdatedFromNow: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The amount of the account',
|
||||
example: 5000.0,
|
||||
})
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The formatted amount',
|
||||
example: '$5,000.00',
|
||||
})
|
||||
formattedAmount: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The Plaid item ID',
|
||||
example: 'plaid-item-123',
|
||||
})
|
||||
plaidItemId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The Plaid account ID',
|
||||
example: 'plaid-account-456',
|
||||
})
|
||||
plaidAccountId: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether the feeds are active',
|
||||
example: true,
|
||||
})
|
||||
isFeedsActive: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether the account is syncing owner',
|
||||
example: true,
|
||||
})
|
||||
isSyncingOwner: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether the feeds are paused',
|
||||
example: false,
|
||||
})
|
||||
isFeedsPaused: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The account normal',
|
||||
example: 'debit',
|
||||
})
|
||||
accountNormal: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The formatted account normal',
|
||||
example: 'Debit',
|
||||
})
|
||||
accountNormalFormatted: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The flatten name with all dependant accounts names',
|
||||
example: 'Assets: Cash Account',
|
||||
})
|
||||
flattenName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The account level in the hierarchy',
|
||||
example: 2,
|
||||
})
|
||||
accountLevel?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The creation timestamp',
|
||||
example: '2024-03-20T10:00:00Z',
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The update timestamp',
|
||||
example: '2024-03-20T10:30:00Z',
|
||||
})
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, Min } from 'class-validator';
|
||||
import { ToNumber } from '@/common/decorators/Validators';
|
||||
import { IFilterRole, ISortOrder } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
import { parseBoolean } from '@/utils/parse-boolean';
|
||||
|
||||
export class BankAccountsQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Custom view ID',
|
||||
type: Number,
|
||||
example: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
customViewId?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Filter roles array',
|
||||
type: Array,
|
||||
isArray: true,
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
filterRoles?: IFilterRole[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Column to sort by',
|
||||
type: String,
|
||||
example: 'created_at',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
columnSortBy?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Sort order',
|
||||
enum: ISortOrder,
|
||||
example: ISortOrder.DESC,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(ISortOrder)
|
||||
sortOrder?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Stringified filter roles',
|
||||
type: String,
|
||||
example: '{"fieldKey":"status","value":"active"}',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
stringifiedFilterRoles?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Search keyword',
|
||||
type: String,
|
||||
example: 'bank account',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
searchKeyword?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'View slug',
|
||||
type: String,
|
||||
example: 'active-accounts',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
viewSlug?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Page number',
|
||||
type: Number,
|
||||
example: 1,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Page size',
|
||||
type: Number,
|
||||
example: 25,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
pageSize?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Include inactive accounts',
|
||||
type: Boolean,
|
||||
example: false,
|
||||
default: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
@IsBoolean()
|
||||
inactiveMode?: boolean;
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { ICashflowAccountsFilter } from '../types/BankAccounts.types';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
|
||||
import { BankAccountsQueryDto } from '../dtos/BankAccountsQuery.dto';
|
||||
import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
|
||||
@Injectable()
|
||||
export class GetBankAccountsService {
|
||||
@@ -15,14 +17,14 @@ export class GetBankAccountsService {
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow accounts.
|
||||
* @param {ICashflowAccountsFilter} filterDTO - Filter DTO.
|
||||
* @returns {ICashflowAccount[]}
|
||||
*/
|
||||
public async getCashflowAccounts(filterDTO: ICashflowAccountsFilter) {
|
||||
public async getCashflowAccounts(filterDTO: BankAccountsQueryDto) {
|
||||
const _filterDto = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
@@ -30,12 +32,14 @@ export class GetBankAccountsService {
|
||||
...filterDTO,
|
||||
};
|
||||
// Parsees accounts list filter DTO.
|
||||
const filter = this.dynamicListService.parseStringifiedFilter(_filterDto);
|
||||
const filter = this.dynamicListService.parseStringifiedFilter<BankAccountsQueryDto>(
|
||||
_filterDto,
|
||||
);
|
||||
|
||||
// Dynamic list service.
|
||||
const dynamicList = await this.dynamicListService.dynamicList(
|
||||
this.accountModel(),
|
||||
filter,
|
||||
filter as IDynamicListFilter,
|
||||
);
|
||||
// Retrieve accounts model based on the given query.
|
||||
const accounts = await this.accountModel()
|
||||
|
||||
@@ -16,7 +16,7 @@ export class DynamicListService {
|
||||
private dynamicListSearch: DynamicListSearch,
|
||||
private dynamicListSortBy: DynamicListSortBy,
|
||||
private dynamicListView: DynamicListCustomView,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Parses filter DTO.
|
||||
@@ -31,9 +31,9 @@ export class DynamicListService {
|
||||
// Merges the default properties with filter object.
|
||||
...(model.defaultSort
|
||||
? {
|
||||
sortOrder: model.defaultSort.sortOrder,
|
||||
columnSortBy: model.defaultSort.sortOrder,
|
||||
}
|
||||
sortOrder: model.defaultSort.sortOrder,
|
||||
columnSortBy: model.defaultSort.sortOrder,
|
||||
}
|
||||
: {}),
|
||||
...filterDTO,
|
||||
};
|
||||
@@ -93,7 +93,7 @@ export class DynamicListService {
|
||||
* Parses stringified filter roles.
|
||||
* @param {string} stringifiedFilterRoles - Stringified filter roles.
|
||||
*/
|
||||
public parseStringifiedFilter<T extends IDynamicListFilter>(
|
||||
public parseStringifiedFilter<T extends { stringifiedFilterRoles?: string }>(
|
||||
filterRoles: T,
|
||||
): T {
|
||||
return {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { AccountDialogAction } from '@/containers/Dialogs/AccountDialog/utils';
|
||||
|
||||
import { ACCOUNT_TYPE, Features } from '@/constants';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { CreditCard2Icon } from '@/icons/CreditCard2';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
|
||||
@@ -89,21 +90,6 @@ function CashFlowAccountsActionsBar({
|
||||
/>
|
||||
<NavbarDivider />
|
||||
</Can>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" iconSize={16} />}
|
||||
text={<T id={'print'} />}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||
text={<T id={'import'} />}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="file-export-16" iconSize={16} />}
|
||||
text={<T id={'export'} />}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
<Can I={CashflowAction.Edit} a={AbilitySubject.Cashflow}>
|
||||
<Switch
|
||||
@@ -118,7 +104,8 @@ function CashFlowAccountsActionsBar({
|
||||
<FeatureCan feature={Features.BankSyncing}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
text={'Connect to Bank / Credit Card'}
|
||||
text={'Connect Bank/Credit Card'}
|
||||
icon={<CreditCard2Icon />}
|
||||
onClick={handleConnectToBank}
|
||||
disabled={isPlaidLoading}
|
||||
/>
|
||||
|
||||
@@ -317,8 +317,13 @@ const AccountsEmptyStateBase = styled.div`
|
||||
margin: 2rem 0;
|
||||
`;
|
||||
const AccountsEmptyStateTitle = styled.h1`
|
||||
font-size: 16px;
|
||||
color: #626b76;
|
||||
--x-text-color: #626b76;
|
||||
|
||||
.bp4-dark & {
|
||||
--x-text-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
font-size: 18px;
|
||||
color: var(--x-text-color);
|
||||
opacity: 0.8;
|
||||
line-height: 1.6;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -6,6 +6,8 @@ import { createTableStateReducers } from '@/store/tableState.reducer';
|
||||
import t from '@/store/types';
|
||||
|
||||
export const defaultTableQuery = {
|
||||
pageSize: 9999,
|
||||
pageIndex: 0,
|
||||
filterRoles: [],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user