diff --git a/packages/server/src/modules/BankingAccounts/BankAccounts.controller.ts b/packages/server/src/modules/BankingAccounts/BankAccounts.controller.ts index 40244408e..2bfeb35ab 100644 --- a/packages/server/src/modules/BankingAccounts/BankAccounts.controller.ts +++ b/packages/server/src/modules/BankingAccounts/BankAccounts.controller.ts @@ -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); } diff --git a/packages/server/src/modules/BankingAccounts/BankAccountsApplication.service.ts b/packages/server/src/modules/BankingAccounts/BankAccountsApplication.service.ts index f53f7fd64..e3d0a583f 100644 --- a/packages/server/src/modules/BankingAccounts/BankAccountsApplication.service.ts +++ b/packages/server/src/modules/BankingAccounts/BankAccountsApplication.service.ts @@ -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); } diff --git a/packages/server/src/modules/BankingAccounts/dtos/BankAccountResponse.dto.ts b/packages/server/src/modules/BankingAccounts/dtos/BankAccountResponse.dto.ts new file mode 100644 index 000000000..57604d7b8 --- /dev/null +++ b/packages/server/src/modules/BankingAccounts/dtos/BankAccountResponse.dto.ts @@ -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; +} diff --git a/packages/server/src/modules/BankingAccounts/dtos/BankAccountsQuery.dto.ts b/packages/server/src/modules/BankingAccounts/dtos/BankAccountsQuery.dto.ts new file mode 100644 index 000000000..8f2653be3 --- /dev/null +++ b/packages/server/src/modules/BankingAccounts/dtos/BankAccountsQuery.dto.ts @@ -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; +} diff --git a/packages/server/src/modules/BankingAccounts/queries/GetBankAccounts.ts b/packages/server/src/modules/BankingAccounts/queries/GetBankAccounts.ts index 74d2052d6..42c82cd80 100644 --- a/packages/server/src/modules/BankingAccounts/queries/GetBankAccounts.ts +++ b/packages/server/src/modules/BankingAccounts/queries/GetBankAccounts.ts @@ -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, - ) {} + ) { } /** * 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( + _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() diff --git a/packages/server/src/modules/DynamicListing/DynamicList.service.ts b/packages/server/src/modules/DynamicListing/DynamicList.service.ts index 7c4947b7c..05575ea6c 100644 --- a/packages/server/src/modules/DynamicListing/DynamicList.service.ts +++ b/packages/server/src/modules/DynamicListing/DynamicList.service.ts @@ -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( + public parseStringifiedFilter( filterRoles: T, ): T { return { diff --git a/packages/webapp/src/containers/CashFlow/CashFlowAccounts/CashFlowAccountsActionsBar.tsx b/packages/webapp/src/containers/CashFlow/CashFlowAccounts/CashFlowAccountsActionsBar.tsx index f923a16cc..867f82f85 100644 --- a/packages/webapp/src/containers/CashFlow/CashFlowAccounts/CashFlowAccountsActionsBar.tsx +++ b/packages/webapp/src/containers/CashFlow/CashFlowAccounts/CashFlowAccountsActionsBar.tsx @@ -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({ /> -