Feature/add footer row to accounts table (#471)

* Add footer row to accounts table with total balance and value

* Update changelog
pull/472/head
Thomas Kaul 3 years ago committed by GitHub
parent a50b55da75
commit 3032126508
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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/), 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). 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 ## 1.75.0 - 13.11.2021
### Added ### Added

@ -1,16 +1,17 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { UserService } from '@ghostfolio/api/app/user/user.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 { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { Accounts } from '@ghostfolio/common/interfaces';
import { import {
getPermissions, getPermissions,
hasPermission, hasPermission,
permissions permissions
} from '@ghostfolio/common/permissions'; } from '@ghostfolio/common/permissions';
import type { import type { RequestWithUser } from '@ghostfolio/common/types';
AccountWithValue,
RequestWithUser
} from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
@ -90,32 +91,39 @@ export class AccountController {
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getAllAccounts( public async getAllAccounts(
@Headers('impersonation-id') impersonationId @Headers('impersonation-id') impersonationId
): Promise<AccountWithValue[]> { ): Promise<Accounts> {
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId( await this.impersonationService.validateImpersonationId(
impersonationId, impersonationId,
this.request.user.id this.request.user.id
); );
let accounts = await this.portfolioService.getAccounts( let accountsWithAggregations =
impersonationUserId || this.request.user.id await this.portfolioService.getAccountsWithAggregations(
); impersonationUserId || this.request.user.id
);
if ( if (
impersonationUserId || impersonationUserId ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
accounts = nullifyValuesInObjects(accounts, [ accountsWithAggregations = {
'balance', ...nullifyValuesInObject(accountsWithAggregations, [
'convertedBalance', 'totalBalance',
'fee', 'totalValue'
'quantity', ]),
'unitPrice', accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
'value' 'balance',
]); 'convertedBalance',
'fee',
'quantity',
'unitPrice',
'value'
])
};
} }
return accounts; return accountsWithAggregations;
} }
@Get(':id') @Get(':id')

@ -28,6 +28,7 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { import {
Accounts,
PortfolioDetails, PortfolioDetails,
PortfolioPerformance, PortfolioPerformance,
PortfolioReport, PortfolioReport,
@ -101,7 +102,7 @@ export class PortfolioService {
userCurrency userCurrency
), ),
transactionCount: account.Order.length, transactionCount: account.Order.length,
value: details.accounts[account.name].current value: details.accounts[account.name]?.current ?? 0
}; };
delete result.Order; delete result.Order;
@ -110,6 +111,21 @@ export class PortfolioService {
}); });
} }
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
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( public async getInvestments(
aImpersonationId: string aImpersonationId: string
): Promise<InvestmentItem[]> { ): Promise<InvestmentItem[]> {
@ -924,16 +940,9 @@ export class PortfolioService {
}; };
for (const order of ordersByAccount) { for (const order of ordersByAccount) {
let currentValueOfSymbol = this.exchangeRateDataService.toCurrency( let currentValueOfSymbol =
order.quantity * portfolioItemsNow[order.symbol].marketPrice, order.quantity * portfolioItemsNow[order.symbol].marketPrice;
order.currency, let originalValueOfSymbol = order.quantity * order.unitPrice;
userCurrency
);
let originalValueOfSymbol = this.exchangeRateDataService.toCurrency(
order.quantity * order.unitPrice,
order.currency,
userCurrency
);
if (order.type === 'SELL') { if (order.type === 'SELL') {
currentValueOfSymbol *= -1; currentValueOfSymbol *= -1;

@ -15,6 +15,7 @@
>(Default)</span >(Default)</span
> >
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell>Total</td>
</ng-container> </ng-container>
<ng-container matColumnDef="currency"> <ng-container matColumnDef="currency">
@ -29,6 +30,7 @@
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell> <td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
{{ element.currency }} {{ element.currency }}
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell>{{ baseCurrency }}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="platform"> <ng-container matColumnDef="platform">
@ -51,6 +53,7 @@
<span>{{ element.Platform?.name }}</span> <span>{{ element.Platform?.name }}</span>
</div> </div>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
<ng-container matColumnDef="transactions"> <ng-container matColumnDef="transactions">
@ -63,6 +66,9 @@
element.transactionCount element.transactionCount
}}</ng-container> }}</ng-container>
</td> </td>
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
{{ transactionCount }}
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="balance"> <ng-container matColumnDef="balance">
@ -77,6 +83,14 @@
[value]="element.convertedBalance" [value]="element.convertedBalance"
></gf-value> ></gf-value>
</td> </td>
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
<gf-value
class="d-inline-block justify-content-end"
[isCurrency]="true"
[locale]="locale"
[value]="totalBalance"
></gf-value>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="value"> <ng-container matColumnDef="value">
@ -91,6 +105,14 @@
[value]="element.value" [value]="element.value"
></gf-value> ></gf-value>
</td> </td>
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
<gf-value
class="d-inline-block justify-content-end"
[isCurrency]="true"
[locale]="locale"
[value]="totalValue"
></gf-value>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
@ -118,10 +140,16 @@
</button> </button>
</mat-menu> </mat-menu>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> <tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> <tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
<tr
*matFooterRowDef="displayedColumns"
mat-footer-row
[ngClass]="{ 'd-none': isLoading }"
></tr>
</table> </table>
<ngx-skeleton-loader <ngx-skeleton-loader

@ -10,6 +10,16 @@
} }
.mat-table { .mat-table {
td {
&.mat-footer-cell {
border-top: 1px solid
rgba(
var(--palette-foreground-divider),
var(--palette-foreground-divider-alpha)
);
}
}
th { th {
::ng-deep { ::ng-deep {
.mat-sort-header-container { .mat-sort-header-container {

@ -24,6 +24,9 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() deviceType: string; @Input() deviceType: string;
@Input() locale: string; @Input() locale: string;
@Input() showActions: boolean; @Input() showActions: boolean;
@Input() totalBalance: number;
@Input() totalValue: number;
@Input() transactionCount: number;
@Output() accountDeleted = new EventEmitter<string>(); @Output() accountDeleted = new EventEmitter<string>();
@Output() accountToUpdate = new EventEmitter<AccountModel>(); @Output() accountToUpdate = new EventEmitter<AccountModel>();

@ -28,6 +28,9 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
public hasPermissionToCreateAccount: boolean; public hasPermissionToCreateAccount: boolean;
public hasPermissionToDeleteAccount: boolean; public hasPermissionToDeleteAccount: boolean;
public routeQueryParams: Subscription; public routeQueryParams: Subscription;
public totalBalance = 0;
public totalValue = 0;
public transactionCount = 0;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -103,8 +106,11 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchAccounts() .fetchAccounts()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe(({ accounts, totalBalance, totalValue, transactionCount }) => {
this.accounts = response; this.accounts = accounts;
this.totalBalance = totalBalance;
this.totalValue = totalValue;
this.transactionCount = transactionCount;
if (this.accounts?.length <= 0) { if (this.accounts?.length <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } }); this.router.navigate([], { queryParams: { createDialog: true } });

@ -8,6 +8,9 @@
[deviceType]="deviceType" [deviceType]="deviceType"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView" [showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView"
[totalBalance]="totalBalance"
[totalValue]="totalValue"
[transactionCount]="transactionCount"
(accountDeleted)="onDeleteAccount($event)" (accountDeleted)="onDeleteAccount($event)"
(accountToUpdate)="onUpdateAccount($event)" (accountToUpdate)="onUpdateAccount($event)"
></gf-accounts-table> ></gf-accounts-table>

@ -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 { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
import { import {
Access, Access,
Accounts,
AdminData, AdminData,
Export, Export,
InfoItem, InfoItem,
@ -62,7 +63,7 @@ export class DataService {
} }
public fetchAccounts() { public fetchAccounts() {
return this.http.get<AccountWithValue[]>('/api/account'); return this.http.get<Accounts>('/api/account');
} }
public fetchAdminData() { public fetchAdminData() {

@ -1,8 +1,12 @@
@mixin gf-table($darkTheme: false) { @mixin gf-table($darkTheme: false) {
background: transparent !important; background: transparent !important;
td { .mat-footer-row,
border: 0 !important; .mat-row {
.mat-cell,
.mat-footer-cell {
border-bottom: 0;
}
} }
.mat-row { .mat-row {

@ -0,0 +1,8 @@
import { AccountWithValue } from '@ghostfolio/common/types';
export interface Accounts {
accounts: AccountWithValue[];
totalBalance: number;
totalValue: number;
transactionCount: number;
}

@ -1,4 +1,5 @@
import { Access } from './access.interface'; import { Access } from './access.interface';
import { Accounts } from './accounts.interface';
import { AdminData } from './admin-data.interface'; import { AdminData } from './admin-data.interface';
import { Export } from './export.interface'; import { Export } from './export.interface';
import { InfoItem } from './info-item.interface'; import { InfoItem } from './info-item.interface';
@ -19,6 +20,7 @@ import { User } from './user.interface';
export { export {
Access, Access,
Accounts,
AdminData, AdminData,
Export, Export,
InfoItem, InfoItem,

@ -2,5 +2,6 @@ import { Account as AccountModel } from '@prisma/client';
export type AccountWithValue = AccountModel & { export type AccountWithValue = AccountModel & {
convertedBalance: number; convertedBalance: number;
transactionCount: number;
value: number; value: number;
}; };

Loading…
Cancel
Save