diff --git a/CHANGELOG.md b/CHANGELOG.md index 82faee5c..65930f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added the footer row with buying power and net worth to the accounts table + ## 1.75.0 - 13.11.2021 ### Added diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index d6a4ff29..a832bfca 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -1,16 +1,17 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; -import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper'; +import { + nullifyValuesInObject, + nullifyValuesInObjects +} from '@ghostfolio/api/helper/object.helper'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; +import { Accounts } from '@ghostfolio/common/interfaces'; import { getPermissions, hasPermission, permissions } from '@ghostfolio/common/permissions'; -import type { - AccountWithValue, - RequestWithUser -} from '@ghostfolio/common/types'; +import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, Controller, @@ -90,32 +91,39 @@ export class AccountController { @UseGuards(AuthGuard('jwt')) public async getAllAccounts( @Headers('impersonation-id') impersonationId - ): Promise { + ): Promise { const impersonationUserId = await this.impersonationService.validateImpersonationId( impersonationId, this.request.user.id ); - let accounts = await this.portfolioService.getAccounts( - impersonationUserId || this.request.user.id - ); + let accountsWithAggregations = + await this.portfolioService.getAccountsWithAggregations( + impersonationUserId || this.request.user.id + ); if ( impersonationUserId || this.userService.isRestrictedView(this.request.user) ) { - accounts = nullifyValuesInObjects(accounts, [ - 'balance', - 'convertedBalance', - 'fee', - 'quantity', - 'unitPrice', - 'value' - ]); + accountsWithAggregations = { + ...nullifyValuesInObject(accountsWithAggregations, [ + 'totalBalance', + 'totalValue' + ]), + accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [ + 'balance', + 'convertedBalance', + 'fee', + 'quantity', + 'unitPrice', + 'value' + ]) + }; } - return accounts; + return accountsWithAggregations; } @Get(':id') diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 6fbcf9a7..a1b752e3 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -28,6 +28,7 @@ import { } from '@ghostfolio/common/config'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { + Accounts, PortfolioDetails, PortfolioPerformance, PortfolioReport, @@ -101,7 +102,7 @@ export class PortfolioService { userCurrency ), transactionCount: account.Order.length, - value: details.accounts[account.name].current + value: details.accounts[account.name]?.current ?? 0 }; delete result.Order; @@ -110,6 +111,21 @@ export class PortfolioService { }); } + public async getAccountsWithAggregations(aUserId: string): Promise { + const accounts = await this.getAccounts(aUserId); + let totalBalance = 0; + let totalValue = 0; + let transactionCount = 0; + + for (const account of accounts) { + totalBalance += account.convertedBalance; + totalValue += account.value; + transactionCount += account.transactionCount; + } + + return { accounts, totalBalance, totalValue, transactionCount }; + } + public async getInvestments( aImpersonationId: string ): Promise { @@ -924,16 +940,9 @@ export class PortfolioService { }; for (const order of ordersByAccount) { - let currentValueOfSymbol = this.exchangeRateDataService.toCurrency( - order.quantity * portfolioItemsNow[order.symbol].marketPrice, - order.currency, - userCurrency - ); - let originalValueOfSymbol = this.exchangeRateDataService.toCurrency( - order.quantity * order.unitPrice, - order.currency, - userCurrency - ); + let currentValueOfSymbol = + order.quantity * portfolioItemsNow[order.symbol].marketPrice; + let originalValueOfSymbol = order.quantity * order.unitPrice; if (order.type === 'SELL') { currentValueOfSymbol *= -1; diff --git a/apps/client/src/app/components/accounts-table/accounts-table.component.html b/apps/client/src/app/components/accounts-table/accounts-table.component.html index d3c01060..d16aeb2f 100644 --- a/apps/client/src/app/components/accounts-table/accounts-table.component.html +++ b/apps/client/src/app/components/accounts-table/accounts-table.component.html @@ -15,6 +15,7 @@ >(Default) + Total @@ -29,6 +30,7 @@ {{ element.currency }} + {{ baseCurrency }} @@ -51,6 +53,7 @@ {{ element.Platform?.name }} + @@ -63,6 +66,9 @@ element.transactionCount }} + + {{ transactionCount }} + @@ -77,6 +83,14 @@ [value]="element.convertedBalance" > + + + @@ -91,6 +105,14 @@ [value]="element.value" > + + + @@ -118,10 +140,16 @@ + + (); @Output() accountToUpdate = new EventEmitter(); diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index cf9324c1..81ede2dc 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -28,6 +28,9 @@ export class AccountsPageComponent implements OnDestroy, OnInit { public hasPermissionToCreateAccount: boolean; public hasPermissionToDeleteAccount: boolean; public routeQueryParams: Subscription; + public totalBalance = 0; + public totalValue = 0; + public transactionCount = 0; public user: User; private unsubscribeSubject = new Subject(); @@ -103,8 +106,11 @@ export class AccountsPageComponent implements OnDestroy, OnInit { this.dataService .fetchAccounts() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.accounts = response; + .subscribe(({ accounts, totalBalance, totalValue, transactionCount }) => { + this.accounts = accounts; + this.totalBalance = totalBalance; + this.totalValue = totalValue; + this.transactionCount = transactionCount; if (this.accounts?.length <= 0) { this.router.navigate([], { queryParams: { createDialog: true } }); diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html index b398c698..2afbfc95 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.html +++ b/apps/client/src/app/pages/accounts/accounts-page.html @@ -8,6 +8,9 @@ [deviceType]="deviceType" [locale]="user?.settings?.locale" [showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView" + [totalBalance]="totalBalance" + [totalValue]="totalValue" + [transactionCount]="transactionCount" (accountDeleted)="onDeleteAccount($event)" (accountToUpdate)="onUpdateAccount($event)" > diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 890c75c3..0f45d8e8 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -17,6 +17,7 @@ import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setti import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto'; import { Access, + Accounts, AdminData, Export, InfoItem, @@ -62,7 +63,7 @@ export class DataService { } public fetchAccounts() { - return this.http.get('/api/account'); + return this.http.get('/api/account'); } public fetchAdminData() { diff --git a/apps/client/src/styles/table.scss b/apps/client/src/styles/table.scss index 19d9033f..49fcd6df 100644 --- a/apps/client/src/styles/table.scss +++ b/apps/client/src/styles/table.scss @@ -1,8 +1,12 @@ @mixin gf-table($darkTheme: false) { background: transparent !important; - td { - border: 0 !important; + .mat-footer-row, + .mat-row { + .mat-cell, + .mat-footer-cell { + border-bottom: 0; + } } .mat-row { diff --git a/libs/common/src/lib/interfaces/accounts.interface.ts b/libs/common/src/lib/interfaces/accounts.interface.ts new file mode 100644 index 00000000..14732f41 --- /dev/null +++ b/libs/common/src/lib/interfaces/accounts.interface.ts @@ -0,0 +1,8 @@ +import { AccountWithValue } from '@ghostfolio/common/types'; + +export interface Accounts { + accounts: AccountWithValue[]; + totalBalance: number; + totalValue: number; + transactionCount: number; +} diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index d11782f8..b2c973be 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -1,4 +1,5 @@ import { Access } from './access.interface'; +import { Accounts } from './accounts.interface'; import { AdminData } from './admin-data.interface'; import { Export } from './export.interface'; import { InfoItem } from './info-item.interface'; @@ -19,6 +20,7 @@ import { User } from './user.interface'; export { Access, + Accounts, AdminData, Export, InfoItem, diff --git a/libs/common/src/lib/types/account-with-value.type.ts b/libs/common/src/lib/types/account-with-value.type.ts index 3c6ac173..a3b81f15 100644 --- a/libs/common/src/lib/types/account-with-value.type.ts +++ b/libs/common/src/lib/types/account-with-value.type.ts @@ -2,5 +2,6 @@ import { Account as AccountModel } from '@prisma/client'; export type AccountWithValue = AccountModel & { convertedBalance: number; + transactionCount: number; value: number; };