Merge pull request #895 from bigcapitalhq/fix-bank-accounts-filter

fix(server): bank accounts filter
This commit is contained in:
Ahmed Bouhuolia
2026-01-09 20:02:36 +02:00
committed by GitHub
9 changed files with 317 additions and 33 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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}
/>

View File

@@ -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;

View File

@@ -6,6 +6,8 @@ import { createTableStateReducers } from '@/store/tableState.reducer';
import t from '@/store/types';
export const defaultTableQuery = {
pageSize: 9999,
pageIndex: 0,
filterRoles: [],
};