Feature/improve usability of tabs on home page (#283)

* Improve usability: lazy load endpoints on tab change

* Feature/improve portfolio summary (#285)

* Update changelog
pull/287/head
Thomas 3 years ago committed by GitHub
parent 8adacd9760
commit 98f44323da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,15 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Fixed
### Added
- Fixed the position detail chart if there are missing historical data around the first buy date
- Fixed the snack bar background color in dark mode
- Added the calculated net worth to the portfolio summary tab on the home page
- Added the calculated time in market to the portfolio summary tab on the home page
### Changed
- Improved the usability of the tabs on the home page
- Restructured the portfolio summary tab on the home page
- Upgraded `angular-material-css-vars` from version `2.1.0` to `2.1.2`
### Fixed
- Fixed the position detail chart if there are missing historical data around the first buy date
- Fixed the snack bar background color in dark mode
## 1.36.0 - 09.08.2021
### Changed

@ -5,10 +5,10 @@ import {
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import {
PortfolioOverview,
PortfolioPerformance,
PortfolioPosition,
PortfolioReport
PortfolioReport,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import {
@ -30,6 +30,7 @@ import {
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import Big from 'big.js';
import { Response } from 'express';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -202,32 +203,6 @@ export class PortfolioController {
return <any>res.json(details);
}
@Get('overview')
@UseGuards(AuthGuard('jwt'))
public async getOverview(
@Headers('impersonation-id') impersonationId
): Promise<PortfolioOverview> {
let overview = await this.portfolioService.getOverview(impersonationId);
if (
impersonationId &&
!hasPermission(
getPermissions(this.request.user.role),
permissions.readForeignPortfolio
)
) {
overview = nullifyValuesInObject(overview, [
'cash',
'committedFunds',
'fees',
'totalBuy',
'totalSell'
]);
}
return overview;
}
@Get('performance')
@UseGuards(AuthGuard('jwt'))
public async getPerformance(
@ -281,6 +256,35 @@ export class PortfolioController {
return <any>res.json(result);
}
@Get('summary')
@UseGuards(AuthGuard('jwt'))
public async getSummary(
@Headers('impersonation-id') impersonationId
): Promise<PortfolioSummary> {
let summary = await this.portfolioService.getSummary(impersonationId);
if (
impersonationId &&
!hasPermission(
getPermissions(this.request.user.role),
permissions.readForeignPortfolio
)
) {
summary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'fees',
'totalBuy',
'totalSell'
]);
}
return summary;
}
@Get('position/:symbol')
@UseGuards(AuthGuard('jwt'))
public async getPosition(

@ -25,10 +25,10 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.se
import { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import {
PortfolioOverview,
PortfolioPerformance,
PortfolioPosition,
PortfolioReport,
PortfolioSummary,
Position,
TimelinePosition
} from '@ghostfolio/common/interfaces';
@ -151,31 +151,6 @@ export class PortfolioService {
}));
}
public async getOverview(
aImpersonationId: string
): Promise<PortfolioOverview> {
const userId = await this.getUserId(aImpersonationId);
const currency = this.request.user.Settings.currency;
const { balance } = await this.accountService.getCashDetails(
userId,
currency
);
const orders = await this.orderService.getOrders({ userId });
const fees = this.getFees(orders);
const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY);
const totalSell = this.getTotalByType(orders, currency, TypeOfOrder.SELL);
return {
committedFunds: totalBuy - totalSell,
fees,
cash: balance,
ordersCount: orders.length,
totalBuy: totalBuy,
totalSell: totalSell
};
}
public async getDetails(
aImpersonationId: string,
aDateRange: DateRange = 'max'
@ -689,6 +664,42 @@ export class PortfolioService {
};
}
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
const currency = this.request.user.Settings.currency;
const userId = await this.getUserId(aImpersonationId);
const performanceInformation = await this.getPerformance(userId);
const { balance } = await this.accountService.getCashDetails(
userId,
currency
);
const orders = await this.orderService.getOrders({ userId });
const fees = this.getFees(orders);
const firstOrderDate = orders[0]?.date;
const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY);
const totalSell = this.getTotalByType(orders, currency, TypeOfOrder.SELL);
const committedFunds = new Big(totalBuy).sub(totalSell);
const netWorth = new Big(balance)
.plus(performanceInformation.performance.currentValue)
.toNumber();
return {
...performanceInformation.performance,
fees,
firstOrderDate,
netWorth,
cash: balance,
committedFunds: committedFunds.toNumber(),
ordersCount: orders.length,
totalBuy: totalBuy,
totalSell: totalSell
};
}
private async getCashPosition({
cashDetails,
investment,

@ -1,74 +0,0 @@
<div class="container p-0">
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Cash</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : overview?.cash"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Buy</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : overview?.totalBuy"
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Sell</div>
<div class="d-flex justify-content-end">
<span
*ngIf="overview?.totalSell || overview?.totalSell === 0"
class="mr-1"
>-</span
>
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : overview?.totalSell"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3">
<div class="d-flex flex-grow-1" i18n>Investment</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : overview?.committedFunds"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3">
<div class="d-flex flex-grow-1" i18n>
Fees for {{ overview?.ordersCount }} {overview?.ordersCount, plural, =1
{order} other {orders}}
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : overview?.fees"
></gf-value>
</div>
</div>
</div>

@ -1,28 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
OnInit
} from '@angular/core';
import { PortfolioOverview } from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client';
@Component({
selector: 'gf-portfolio-overview',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './portfolio-overview.component.html',
styleUrls: ['./portfolio-overview.component.scss']
})
export class PortfolioOverviewComponent implements OnChanges, OnInit {
@Input() baseCurrency: Currency;
@Input() isLoading: boolean;
@Input() locale: string;
@Input() overview: PortfolioOverview;
public constructor() {}
public ngOnInit() {}
public ngOnChanges() {}
}

@ -1,54 +0,0 @@
<div class="container p-0">
<div class="row no-gutters">
<div class="flex-grow-1"></div>
<div *ngIf="isLoading" class="align-items-center d-flex">
<ngx-skeleton-loader
animation="pulse"
class="mb-2"
[theme]="{
height: '4rem',
width: '15rem'
}"
></ngx-skeleton-loader>
</div>
<div
[hidden]="isLoading"
class="display-4 font-weight-bold m-0 text-center value-container"
>
<span #value id="value"></span>
</div>
<div class="flex-grow-1 px-1">
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
[theme]="{
height: '1.3rem',
width: '2.5rem'
}"
></ngx-skeleton-loader>
<div *ngIf="!isLoading">
{{ unit }}
</div>
</div>
</div>
<div *ngIf="showDetails" class="row">
<div class="d-flex col justify-content-end">
<gf-value
[colorizeSign]="true"
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : performance?.currentNetPerformance"
></gf-value>
</div>
<div class="col">
<gf-value
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="
isLoading ? undefined : performance?.currentNetPerformancePercent
"
></gf-value>
</div>
</div>
</div>

@ -1,9 +0,0 @@
:host {
display: block;
.value-container {
#value {
font-variant-numeric: tabular-nums;
}
}
}

@ -1,65 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnChanges,
OnInit,
ViewChild
} from '@angular/core';
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client';
import { CountUp } from 'countup.js';
import { isNumber } from 'lodash';
@Component({
selector: 'gf-portfolio-performance-summary',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './portfolio-performance-summary.component.html',
styleUrls: ['./portfolio-performance-summary.component.scss']
})
export class PortfolioPerformanceSummaryComponent implements OnChanges, OnInit {
@Input() baseCurrency: Currency;
@Input() isLoading: boolean;
@Input() locale: string;
@Input() performance: PortfolioPerformance;
@Input() showDetails: boolean;
@ViewChild('value') value: ElementRef;
public unit: string;
public constructor() {}
public ngOnInit() {}
public ngOnChanges() {
if (this.isLoading) {
if (this.value?.nativeElement) {
this.value.nativeElement.innerHTML = '';
}
} else {
if (isNumber(this.performance?.currentValue)) {
this.unit = this.baseCurrency;
new CountUp('value', this.performance?.currentValue, {
decimalPlaces: 2,
duration: 1,
separator: `'`
}).start();
} else if (this.performance?.currentValue === null) {
this.unit = '%';
new CountUp(
'value',
this.performance?.currentNetPerformancePercent * 100,
{
decimalPlaces: 2,
duration: 0.75,
separator: `'`
}
).start();
}
}
}
}

@ -1,14 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfValueModule } from '../value/value.module';
import { PortfolioPerformanceSummaryComponent } from './portfolio-performance-summary.component';
@NgModule({
declarations: [PortfolioPerformanceSummaryComponent],
exports: [PortfolioPerformanceSummaryComponent],
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
providers: []
})
export class GfPortfolioPerformanceSummaryModule {}

@ -1,67 +1,54 @@
<div class="container p-0">
<div class="row px-3 py-2">
<div class="d-flex flex-grow-1" i18n>Value</div>
<div class="d-flex flex-column flex-wrap justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : performance?.currentValue"
></gf-value>
<div class="row no-gutters">
<div class="flex-grow-1"></div>
<div *ngIf="isLoading" class="align-items-center d-flex">
<ngx-skeleton-loader
animation="pulse"
class="mb-2"
[theme]="{
height: '4rem',
width: '15rem'
}"
></ngx-skeleton-loader>
</div>
<div
[hidden]="isLoading"
class="display-4 font-weight-bold m-0 text-center value-container"
>
<span #value id="value"></span>
</div>
<div class="flex-grow-1 px-1">
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
[theme]="{
height: '1.3rem',
width: '2.5rem'
}"
></ngx-skeleton-loader>
<div *ngIf="!isLoading">
{{ unit }}
</div>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Absolute Performance</div>
<div class="d-flex flex-column flex-wrap justify-content-end">
<div *ngIf="showDetails" class="row">
<div class="d-flex col justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[currency]="baseCurrency"
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : performance?.currentGrossPerformance"
[value]="isLoading ? undefined : performance?.currentNetPerformance"
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Performance (TWR)</div>
<div class="d-flex flex-column flex-wrap justify-content-end">
<div class="col">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="
isLoading ? undefined : performance?.currentGrossPerformancePercent
isLoading ? undefined : performance?.currentNetPerformancePercent
"
></gf-value>
</div>
</div>
<!--
<div class="row px-3 py-2">
<div class="d-flex flex-grow-1" i18n>Net performance</div>
<div class="d-flex flex-column flex-wrap justify-content-end">
<gf-value
class="justify-content-end mb-2"
position="end"
[colorizeSign]="true"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : performance?.currentNetPerformance"
></gf-value>
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="
isLoading ? undefined : performance?.currentNetPerformancePercent
"
></gf-value>
</div>
</div>
-->
</div>

@ -1,3 +1,9 @@
:host {
display: block;
.value-container {
#value {
font-variant-numeric: tabular-nums;
}
}
}

@ -1,11 +1,16 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnInit
OnChanges,
OnInit,
ViewChild
} from '@angular/core';
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client';
import { CountUp } from 'countup.js';
import { isNumber } from 'lodash';
@Component({
selector: 'gf-portfolio-performance',
@ -13,13 +18,48 @@ import { Currency } from '@prisma/client';
templateUrl: './portfolio-performance.component.html',
styleUrls: ['./portfolio-performance.component.scss']
})
export class PortfolioPerformanceComponent implements OnInit {
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() baseCurrency: Currency;
@Input() isLoading: boolean;
@Input() locale: string;
@Input() performance: PortfolioPerformance;
@Input() showDetails: boolean;
@ViewChild('value') value: ElementRef;
public unit: string;
public constructor() {}
public ngOnInit() {}
public ngOnChanges() {
if (this.isLoading) {
if (this.value?.nativeElement) {
this.value.nativeElement.innerHTML = '';
}
} else {
if (isNumber(this.performance?.currentValue)) {
this.unit = this.baseCurrency;
new CountUp('value', this.performance?.currentValue, {
decimalPlaces: 2,
duration: 1,
separator: `'`
}).start();
} else if (this.performance?.currentValue === null) {
this.unit = '%';
new CountUp(
'value',
this.performance?.currentNetPerformancePercent * 100,
{
decimalPlaces: 2,
duration: 0.75,
separator: `'`
}
).start();
}
}
}
}

@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfValueModule } from '../value/value.module';
import { PortfolioPerformanceComponent } from './portfolio-performance.component';
@ -7,7 +8,7 @@ import { PortfolioPerformanceComponent } from './portfolio-performance.component
@NgModule({
declarations: [PortfolioPerformanceComponent],
exports: [PortfolioPerformanceComponent],
imports: [CommonModule, GfValueModule],
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
providers: []
})
export class GfPortfolioPerformanceModule {}

@ -0,0 +1,134 @@
<div class="container p-0">
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Time in Market</div>
<div class="d-flex justify-content-end">
{{ timeInMarket }}
<gf-value class="justify-content-end" [value]="timeInMarket"></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3">
<div class="d-flex flex-grow-1" i18n>
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
{order} other {orders}}
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.fees"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Buy</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.totalBuy"
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Sell</div>
<div class="d-flex justify-content-end">
<span *ngIf="summary?.totalSell || summary?.totalSell === 0" class="mr-1"
>-</span
>
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.totalSell"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Investment</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.committedFunds"
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Absolute Performance</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.currentGrossPerformance"
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1 ml-3" i18n>Performance (TWR)</div>
<div class="d-flex flex-column flex-wrap justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="
isLoading ? undefined : summary?.currentGrossPerformancePercent
"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Value</div>
<div class="d-flex flex-column flex-wrap justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.currentValue"
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Cash</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.cash"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Net Worth</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.netWorth"
></gf-value>
</div>
</div>
</div>

@ -0,0 +1,41 @@
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
OnInit
} from '@angular/core';
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client';
import { formatDistanceToNow } from 'date-fns';
@Component({
selector: 'gf-portfolio-summary',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './portfolio-summary.component.html',
styleUrls: ['./portfolio-summary.component.scss']
})
export class PortfolioSummaryComponent implements OnChanges, OnInit {
@Input() baseCurrency: Currency;
@Input() isLoading: boolean;
@Input() locale: string;
@Input() summary: PortfolioSummary;
public timeInMarket: string;
public constructor() {}
public ngOnInit() {}
public ngOnChanges() {
if (this.summary) {
if (this.summary.firstOrderDate) {
this.timeInMarket = formatDistanceToNow(this.summary.firstOrderDate);
} else {
this.timeInMarket = '-';
}
} else {
this.timeInMarket = undefined;
}
}
}

@ -2,12 +2,12 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { GfValueModule } from '../value/value.module';
import { PortfolioOverviewComponent } from './portfolio-overview.component';
import { PortfolioSummaryComponent } from './portfolio-summary.component';
@NgModule({
declarations: [PortfolioOverviewComponent],
exports: [PortfolioOverviewComponent],
declarations: [PortfolioSummaryComponent],
exports: [PortfolioSummaryComponent],
imports: [CommonModule, GfValueModule],
providers: []
})
export class GfPortfolioOverviewModule {}
export class GfPortfolioSummaryModule {}

@ -85,14 +85,18 @@ export class ValueComponent implements OnChanges, OnInit {
});
} catch {}
}
} else if (isDate(new Date(this.value))) {
this.isDate = true;
this.isNumber = false;
} else {
try {
if (isDate(new Date(this.value))) {
this.isDate = true;
this.isNumber = false;
this.formattedDate = format(
new Date(<string>this.value),
DEFAULT_DATE_FORMAT
);
this.formattedDate = format(
new Date(<string>this.value),
DEFAULT_DATE_FORMAT
);
}
} catch {}
}
}
}

@ -8,6 +8,7 @@ import {
ViewChild
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTabChangeEvent } from '@angular/material/tabs';
import { ActivatedRoute, Router } from '@angular/router';
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
import { PerformanceChartDialog } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.component';
@ -20,8 +21,8 @@ import {
} from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
PortfolioOverview,
PortfolioPerformance,
PortfolioSummary,
Position,
User
} from '@ghostfolio/common/interfaces';
@ -44,6 +45,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
@ViewChild('positionsContainer') positionsContainer: ElementRef;
public canCreateAccount: boolean;
public currentTabIndex = 0;
public dateRange: DateRange;
public dateRangeOptions: ToggleOption[] = [
{ label: 'Today', value: '1d' },
@ -57,14 +59,14 @@ export class HomePageComponent implements OnDestroy, OnInit {
public hasImpersonationId: boolean;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public hasPermissionToReadForeignPortfolio: boolean;
public hasPositions = false;
public hasPositions: boolean;
public historicalDataItems: LineChartItem[];
public isLoadingOverview = true;
public isLoadingPerformance = true;
public overview: PortfolioOverview;
public isLoadingSummary = true;
public performance: PortfolioPerformance;
public positions: Position[];
public routeQueryParams: Subscription;
public summary: PortfolioSummary;
public user: User;
private unsubscribeSubject = new Subject<void>();
@ -153,7 +155,9 @@ export class HomePageComponent implements OnDestroy, OnInit {
this.update();
}
public onTabChanged() {
public onTabChanged(event: MatTabChangeEvent) {
this.currentTabIndex = event.index;
this.update();
}
@ -182,54 +186,55 @@ export class HomePageComponent implements OnDestroy, OnInit {
}
private update() {
this.hasPositions = undefined;
this.isLoadingOverview = true;
this.isLoadingPerformance = true;
this.positions = undefined;
if (this.currentTabIndex === 0) {
this.isLoadingPerformance = true;
this.dataService
.fetchChart({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
});
this.dataService
.fetchChart({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioPerformance({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.performance = response;
this.isLoadingPerformance = false;
this.dataService
.fetchPortfolioPerformance({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.performance = response;
this.isLoadingPerformance = false;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioOverview()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.overview = response;
this.isLoadingOverview = false;
this.changeDetectorRef.markForCheck();
});
} else if (this.currentTabIndex === 1) {
this.dataService
.fetchPositions({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.positions = response.positions;
this.hasPositions = this.positions?.length > 0;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
});
} else if (this.currentTabIndex === 2) {
this.isLoadingSummary = true;
this.dataService
.fetchPositions({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.positions = response.positions;
this.hasPositions = this.positions?.length > 0;
this.dataService
.fetchPortfolioSummary()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.summary = response;
this.isLoadingSummary = false;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck();
}

@ -4,7 +4,7 @@
headerPosition="below"
mat-align-tabs="center"
[disablePagination]="true"
(selectedTabChange)="onTabChanged()"
(selectedTabChange)="onTabChanged($event)"
>
<mat-tab>
<ng-template mat-tab-label>
@ -55,14 +55,14 @@
</div>
<div class="overview-container row mt-1">
<div class="col">
<gf-portfolio-performance-summary
<gf-portfolio-performance
class="pb-4"
[baseCurrency]="user?.settings?.baseCurrency"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"
[showDetails]="!hasImpersonationId || hasPermissionToReadForeignPortfolio"
></gf-portfolio-performance-summary>
></gf-portfolio-performance>
<div class="text-center">
<gf-toggle
[defaultValue]="dateRange"
@ -82,7 +82,7 @@
<div class="container justify-content-center pb-3 px-3 positions">
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
<div class="row">
<div class="align-items-center col">
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
<div class="pb-2 text-center">
<gf-toggle
[defaultValue]="dateRange"
@ -119,33 +119,18 @@
</ng-template>
<div class="container pb-3 px-3 positions">
<div class="row">
<div class="col-xs-12 col-md-6 mb-3">
<mat-card class="h-100">
<mat-card-header>
<mat-card-title i18n>Performance</mat-card-title>
</mat-card-header>
<mat-card-content>
<gf-portfolio-performance
[baseCurrency]="user?.settings?.baseCurrency"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"
></gf-portfolio-performance>
</mat-card-content>
</mat-card>
</div>
<div class="col-xs-12 col-md-6">
<div class="col-xs-12 col-md-8 offset-md-2">
<mat-card class="h-100">
<mat-card-header>
<mat-card-title i18n>Summary</mat-card-title>
</mat-card-header>
<mat-card-content>
<gf-portfolio-overview
<gf-portfolio-summary
[baseCurrency]="user?.settings?.baseCurrency"
[isLoading]="isLoadingOverview"
[isLoading]="isLoadingSummary"
[locale]="user?.settings?.locale"
[overview]="overview"
></gf-portfolio-overview>
[summary]="summary"
></gf-portfolio-summary>
</mat-card-content>
</mat-card>
</div>

@ -7,9 +7,8 @@ import { RouterModule } from '@angular/router';
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/client/components/no-transactions-info/no-transactions-info.module';
import { GfPerformanceChartDialogModule } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.module';
import { GfPortfolioOverviewModule } from '@ghostfolio/client/components/portfolio-overview/portfolio-overview.module';
import { GfPortfolioPerformanceSummaryModule } from '@ghostfolio/client/components/portfolio-performance-summary/portfolio-performance-summary.module';
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
@ -24,9 +23,8 @@ import { HomePageComponent } from './home-page.component';
GfLineChartModule,
GfNoTransactionsInfoModule,
GfPerformanceChartDialogModule,
GfPortfolioOverviewModule,
GfPortfolioPerformanceModule,
GfPortfolioPerformanceSummaryModule,
GfPortfolioSummaryModule,
GfPositionsModule,
GfToggleModule,
HomePageRoutingModule,

@ -8,6 +8,7 @@ import {
OnInit,
ViewChild
} from '@angular/core';
import { MatTabChangeEvent } from '@angular/material/tabs';
import { ActivatedRoute } from '@angular/router';
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
import { DataService } from '@ghostfolio/client/services/data.service';
@ -32,11 +33,12 @@ import { first, takeUntil } from 'rxjs/operators';
export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
@ViewChild('positionsContainer') positionsContainer: ElementRef;
public currentTabIndex = 0;
public dateRange: DateRange = 'max';
public deviceType: string;
public hasImpersonationId: boolean;
public hasPermissionToReadForeignPortfolio: boolean;
public hasPositions = false;
public hasPositions: boolean;
public historicalDataItems: LineChartItem[];
public isLoadingPerformance = true;
public performance: PortfolioPerformance;
@ -92,7 +94,9 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
.subscribe((fragment) => this.viewportScroller.scrollToAnchor(fragment));
}
public onTabChanged() {
public onTabChanged(event: MatTabChangeEvent) {
this.currentTabIndex = event.index;
this.update();
}
@ -102,43 +106,43 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
}
private update() {
this.hasPositions = undefined;
this.isLoadingPerformance = true;
this.positions = undefined;
if (this.currentTabIndex === 0) {
this.isLoadingPerformance = true;
this.dataService
.fetchChart({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
});
this.dataService
.fetchChart({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioPerformance({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.performance = response;
this.isLoadingPerformance = false;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioPerformance({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.performance = response;
this.isLoadingPerformance = false;
this.dataService
.fetchPositions({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.positions = response.positions;
this.hasPositions = this.positions?.length > 0;
this.changeDetectorRef.markForCheck();
});
} else if (this.currentTabIndex === 1) {
this.dataService
.fetchPositions({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.positions = response.positions;
this.hasPositions = this.positions?.length > 0;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck();
}

@ -4,7 +4,7 @@
headerPosition="below"
mat-align-tabs="center"
[disablePagination]="true"
(selectedTabChange)="onTabChanged()"
(selectedTabChange)="onTabChanged($event)"
>
<mat-tab>
<ng-template mat-tab-label>
@ -43,14 +43,14 @@
</div>
<div class="overview-container row mb-5 mt-1">
<div class="col">
<gf-portfolio-performance-summary
<gf-portfolio-performance
class="pb-4"
[baseCurrency]="user?.settings?.baseCurrency"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"
[showDetails]="!hasImpersonationId || hasPermissionToReadForeignPortfolio"
></gf-portfolio-performance-summary>
></gf-portfolio-performance>
</div>
</div>
</div>

@ -6,7 +6,7 @@ import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/client/components/no-transactions-info/no-transactions-info.module';
import { GfPortfolioPerformanceSummaryModule } from '@ghostfolio/client/components/portfolio-performance-summary/portfolio-performance-summary.module';
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
import { ZenPageRoutingModule } from './zen-page-routing.module';
@ -19,7 +19,7 @@ import { ZenPageComponent } from './zen-page.component';
CommonModule,
GfLineChartModule,
GfNoTransactionsInfoModule,
GfPortfolioPerformanceSummaryModule,
GfPortfolioPerformanceModule,
GfPositionsModule,
MatButtonModule,
MatCardModule,

@ -19,11 +19,10 @@ import {
AdminData,
Export,
InfoItem,
PortfolioItem,
PortfolioOverview,
PortfolioPerformance,
PortfolioPosition,
PortfolioReport,
PortfolioSummary,
User
} from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
@ -148,10 +147,6 @@ export class DataService {
return this.http.get<InvestmentItem[]>('/api/portfolio/investments');
}
public fetchPortfolioOverview() {
return this.http.get<PortfolioOverview>('/api/portfolio/overview');
}
public fetchPortfolioPerformance(aParams: { [param: string]: any }) {
return this.http.get<PortfolioPerformance>('/api/portfolio/performance', {
params: aParams
@ -169,6 +164,18 @@ export class DataService {
return this.http.get<PortfolioReport>('/api/portfolio/report');
}
public fetchPortfolioSummary(): Observable<PortfolioSummary> {
return this.http.get<any>('/api/portfolio/summary').pipe(
map((summary) => {
if (summary.firstOrderDate) {
summary.firstOrderDate = parseISO(summary.firstOrderDate);
}
return summary;
})
);
}
public fetchPositionDetail(aSymbol: string) {
return this.http.get<PortfolioPositionDetail>(
`/api/portfolio/position/${aSymbol}`

@ -8,6 +8,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
import { PortfolioPosition } from './portfolio-position.interface';
import { PortfolioReportRule } from './portfolio-report-rule.interface';
import { PortfolioReport } from './portfolio-report.interface';
import { PortfolioSummary } from './portfolio-summary.interface';
import { Position } from './position.interface';
import { TimelinePosition } from './timeline-position.interface';
import { UserSettings } from './user-settings.interface';
@ -25,6 +26,7 @@ export {
PortfolioPosition,
PortfolioReport,
PortfolioReportRule,
PortfolioSummary,
Position,
TimelinePosition,
User,

@ -0,0 +1,12 @@
import { PortfolioPerformance } from './portfolio-performance.interface';
export interface PortfolioSummary extends PortfolioPerformance {
cash: number;
committedFunds: number;
fees: number;
firstOrderDate: Date;
netWorth: number;
ordersCount: number;
totalBuy: number;
totalSell: number;
}
Loading…
Cancel
Save