Feature/add footer row to accounts table (#471)
* Add footer row to accounts table with total balance and value * Update changelog
This commit is contained in:
parent
a50b55da75
commit
3032126508
@ -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 {
|
||||||
|
8
libs/common/src/lib/interfaces/accounts.interface.ts
Normal file
8
libs/common/src/lib/interfaces/accounts.interface.ts
Normal file
@ -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…
x
Reference in New Issue
Block a user