Feature/improve usability of tabs on home page (#283)
* Improve usability: lazy load endpoints on tab change * Feature/improve portfolio summary (#285) * Update changelog
This commit is contained in:
parent
8adacd9760
commit
98f44323da
15
CHANGELOG.md
15
CHANGELOG.md
@ -7,15 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
- Fixed the position detail chart if there are missing historical data around the first buy date
|
- 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
|
- Fixed the snack bar background color in dark mode
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Upgraded `angular-material-css-vars` from version `2.1.0` to `2.1.2`
|
|
||||||
|
|
||||||
## 1.36.0 - 09.08.2021
|
## 1.36.0 - 09.08.2021
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -5,10 +5,10 @@ import {
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
import {
|
import {
|
||||||
PortfolioOverview,
|
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
PortfolioReport
|
PortfolioReport,
|
||||||
|
PortfolioSummary
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import {
|
import {
|
||||||
@ -30,6 +30,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import Big from 'big.js';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
@ -202,32 +203,6 @@ export class PortfolioController {
|
|||||||
return <any>res.json(details);
|
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')
|
@Get('performance')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getPerformance(
|
public async getPerformance(
|
||||||
@ -281,6 +256,35 @@ export class PortfolioController {
|
|||||||
return <any>res.json(result);
|
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')
|
@Get('position/:symbol')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getPosition(
|
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 { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
PortfolioOverview,
|
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
TimelinePosition
|
TimelinePosition
|
||||||
} from '@ghostfolio/common/interfaces';
|
} 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(
|
public async getDetails(
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aDateRange: DateRange = 'max'
|
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({
|
private async getCashPosition({
|
||||||
cashDetails,
|
cashDetails,
|
||||||
investment,
|
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="container p-0">
|
||||||
<div class="row px-3 py-2">
|
<div class="row no-gutters">
|
||||||
<div class="d-flex flex-grow-1" i18n>Value</div>
|
<div class="flex-grow-1"></div>
|
||||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
<div *ngIf="isLoading" class="align-items-center d-flex">
|
||||||
<gf-value
|
<ngx-skeleton-loader
|
||||||
class="justify-content-end"
|
animation="pulse"
|
||||||
position="end"
|
class="mb-2"
|
||||||
[currency]="baseCurrency"
|
[theme]="{
|
||||||
[locale]="locale"
|
height: '4rem',
|
||||||
[value]="isLoading ? undefined : performance?.currentValue"
|
width: '15rem'
|
||||||
></gf-value>
|
}"
|
||||||
|
></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>
|
</div>
|
||||||
<div class="row px-3 py-1">
|
<div *ngIf="showDetails" class="row">
|
||||||
<div class="d-flex flex-grow-1" i18n>Absolute Performance</div>
|
<div class="d-flex col justify-content-end">
|
||||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
|
||||||
position="end"
|
|
||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[currency]="baseCurrency"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : performance?.currentGrossPerformance"
|
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col">
|
||||||
<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">
|
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
|
||||||
position="end"
|
|
||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="
|
[value]="
|
||||||
isLoading ? undefined : performance?.currentGrossPerformancePercent
|
isLoading ? undefined : performance?.currentNetPerformancePercent
|
||||||
"
|
"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
.value-container {
|
||||||
|
#value {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
ElementRef,
|
||||||
Input,
|
Input,
|
||||||
OnInit
|
OnChanges,
|
||||||
|
OnInit,
|
||||||
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
import { CountUp } from 'countup.js';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-portfolio-performance',
|
selector: 'gf-portfolio-performance',
|
||||||
@ -13,13 +18,48 @@ import { Currency } from '@prisma/client';
|
|||||||
templateUrl: './portfolio-performance.component.html',
|
templateUrl: './portfolio-performance.component.html',
|
||||||
styleUrls: ['./portfolio-performance.component.scss']
|
styleUrls: ['./portfolio-performance.component.scss']
|
||||||
})
|
})
|
||||||
export class PortfolioPerformanceComponent implements OnInit {
|
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||||
@Input() baseCurrency: Currency;
|
@Input() baseCurrency: Currency;
|
||||||
@Input() isLoading: boolean;
|
@Input() isLoading: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() performance: PortfolioPerformance;
|
@Input() performance: PortfolioPerformance;
|
||||||
|
@Input() showDetails: boolean;
|
||||||
|
|
||||||
|
@ViewChild('value') value: ElementRef;
|
||||||
|
|
||||||
|
public unit: string;
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
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 { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfValueModule } from '../value/value.module';
|
import { GfValueModule } from '../value/value.module';
|
||||||
import { PortfolioPerformanceComponent } from './portfolio-performance.component';
|
import { PortfolioPerformanceComponent } from './portfolio-performance.component';
|
||||||
@ -7,7 +8,7 @@ import { PortfolioPerformanceComponent } from './portfolio-performance.component
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [PortfolioPerformanceComponent],
|
declarations: [PortfolioPerformanceComponent],
|
||||||
exports: [PortfolioPerformanceComponent],
|
exports: [PortfolioPerformanceComponent],
|
||||||
imports: [CommonModule, GfValueModule],
|
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
|
||||||
providers: []
|
providers: []
|
||||||
})
|
})
|
||||||
export class GfPortfolioPerformanceModule {}
|
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 { NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { GfValueModule } from '../value/value.module';
|
import { GfValueModule } from '../value/value.module';
|
||||||
import { PortfolioOverviewComponent } from './portfolio-overview.component';
|
import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [PortfolioOverviewComponent],
|
declarations: [PortfolioSummaryComponent],
|
||||||
exports: [PortfolioOverviewComponent],
|
exports: [PortfolioSummaryComponent],
|
||||||
imports: [CommonModule, GfValueModule],
|
imports: [CommonModule, GfValueModule],
|
||||||
providers: []
|
providers: []
|
||||||
})
|
})
|
||||||
export class GfPortfolioOverviewModule {}
|
export class GfPortfolioSummaryModule {}
|
@ -85,14 +85,18 @@ export class ValueComponent implements OnChanges, OnInit {
|
|||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
} else if (isDate(new Date(this.value))) {
|
} else {
|
||||||
this.isDate = true;
|
try {
|
||||||
this.isNumber = false;
|
if (isDate(new Date(this.value))) {
|
||||||
|
this.isDate = true;
|
||||||
|
this.isNumber = false;
|
||||||
|
|
||||||
this.formattedDate = format(
|
this.formattedDate = format(
|
||||||
new Date(<string>this.value),
|
new Date(<string>this.value),
|
||||||
DEFAULT_DATE_FORMAT
|
DEFAULT_DATE_FORMAT
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatTabChangeEvent } from '@angular/material/tabs';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
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';
|
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';
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import {
|
import {
|
||||||
PortfolioOverview,
|
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
@ -44,6 +45,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
@ViewChild('positionsContainer') positionsContainer: ElementRef;
|
@ViewChild('positionsContainer') positionsContainer: ElementRef;
|
||||||
|
|
||||||
public canCreateAccount: boolean;
|
public canCreateAccount: boolean;
|
||||||
|
public currentTabIndex = 0;
|
||||||
public dateRange: DateRange;
|
public dateRange: DateRange;
|
||||||
public dateRangeOptions: ToggleOption[] = [
|
public dateRangeOptions: ToggleOption[] = [
|
||||||
{ label: 'Today', value: '1d' },
|
{ label: 'Today', value: '1d' },
|
||||||
@ -57,14 +59,14 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||||
public hasPermissionToReadForeignPortfolio: boolean;
|
public hasPermissionToReadForeignPortfolio: boolean;
|
||||||
public hasPositions = false;
|
public hasPositions: boolean;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public isLoadingOverview = true;
|
|
||||||
public isLoadingPerformance = true;
|
public isLoadingPerformance = true;
|
||||||
public overview: PortfolioOverview;
|
public isLoadingSummary = true;
|
||||||
public performance: PortfolioPerformance;
|
public performance: PortfolioPerformance;
|
||||||
public positions: Position[];
|
public positions: Position[];
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
|
public summary: PortfolioSummary;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -153,7 +155,9 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onTabChanged() {
|
public onTabChanged(event: MatTabChangeEvent) {
|
||||||
|
this.currentTabIndex = event.index;
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,54 +186,55 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.hasPositions = undefined;
|
if (this.currentTabIndex === 0) {
|
||||||
this.isLoadingOverview = true;
|
this.isLoadingPerformance = true;
|
||||||
this.isLoadingPerformance = true;
|
|
||||||
this.positions = undefined;
|
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchChart({ range: this.dateRange })
|
.fetchChart({ range: this.dateRange })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((chartData) => {
|
.subscribe((chartData) => {
|
||||||
this.historicalDataItems = chartData.map((chartDataItem) => {
|
this.historicalDataItems = chartData.map((chartDataItem) => {
|
||||||
return {
|
return {
|
||||||
date: chartDataItem.date,
|
date: chartDataItem.date,
|
||||||
value: chartDataItem.value
|
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
|
this.changeDetectorRef.markForCheck();
|
||||||
.fetchPortfolioPerformance({ range: this.dateRange })
|
});
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
} else if (this.currentTabIndex === 1) {
|
||||||
.subscribe((response) => {
|
this.dataService
|
||||||
this.performance = response;
|
.fetchPositions({ range: this.dateRange })
|
||||||
this.isLoadingPerformance = false;
|
.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
|
this.dataService
|
||||||
.fetchPortfolioOverview()
|
.fetchPortfolioSummary()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe((response) => {
|
||||||
this.overview = response;
|
this.summary = response;
|
||||||
this.isLoadingOverview = false;
|
this.isLoadingSummary = false;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
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"
|
headerPosition="below"
|
||||||
mat-align-tabs="center"
|
mat-align-tabs="center"
|
||||||
[disablePagination]="true"
|
[disablePagination]="true"
|
||||||
(selectedTabChange)="onTabChanged()"
|
(selectedTabChange)="onTabChanged($event)"
|
||||||
>
|
>
|
||||||
<mat-tab>
|
<mat-tab>
|
||||||
<ng-template mat-tab-label>
|
<ng-template mat-tab-label>
|
||||||
@ -55,14 +55,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="overview-container row mt-1">
|
<div class="overview-container row mt-1">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<gf-portfolio-performance-summary
|
<gf-portfolio-performance
|
||||||
class="pb-4"
|
class="pb-4"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isLoading]="isLoadingPerformance"
|
[isLoading]="isLoadingPerformance"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[performance]="performance"
|
[performance]="performance"
|
||||||
[showDetails]="!hasImpersonationId || hasPermissionToReadForeignPortfolio"
|
[showDetails]="!hasImpersonationId || hasPermissionToReadForeignPortfolio"
|
||||||
></gf-portfolio-performance-summary>
|
></gf-portfolio-performance>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="dateRange"
|
[defaultValue]="dateRange"
|
||||||
@ -82,7 +82,7 @@
|
|||||||
<div class="container justify-content-center pb-3 px-3 positions">
|
<div class="container justify-content-center pb-3 px-3 positions">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
|
||||||
<div class="row">
|
<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">
|
<div class="pb-2 text-center">
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="dateRange"
|
[defaultValue]="dateRange"
|
||||||
@ -119,33 +119,18 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="container pb-3 px-3 positions">
|
<div class="container pb-3 px-3 positions">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||||
<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">
|
|
||||||
<mat-card class="h-100">
|
<mat-card class="h-100">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title i18n>Summary</mat-card-title>
|
<mat-card-title i18n>Summary</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-overview
|
<gf-portfolio-summary
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isLoading]="isLoadingOverview"
|
[isLoading]="isLoadingSummary"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[overview]="overview"
|
[summary]="summary"
|
||||||
></gf-portfolio-overview>
|
></gf-portfolio-summary>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,9 +7,8 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
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 { 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 { 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 { 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 { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
|
|
||||||
@ -24,9 +23,8 @@ import { HomePageComponent } from './home-page.component';
|
|||||||
GfLineChartModule,
|
GfLineChartModule,
|
||||||
GfNoTransactionsInfoModule,
|
GfNoTransactionsInfoModule,
|
||||||
GfPerformanceChartDialogModule,
|
GfPerformanceChartDialogModule,
|
||||||
GfPortfolioOverviewModule,
|
|
||||||
GfPortfolioPerformanceModule,
|
GfPortfolioPerformanceModule,
|
||||||
GfPortfolioPerformanceSummaryModule,
|
GfPortfolioSummaryModule,
|
||||||
GfPositionsModule,
|
GfPositionsModule,
|
||||||
GfToggleModule,
|
GfToggleModule,
|
||||||
HomePageRoutingModule,
|
HomePageRoutingModule,
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { MatTabChangeEvent } from '@angular/material/tabs';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
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 {
|
export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
||||||
@ViewChild('positionsContainer') positionsContainer: ElementRef;
|
@ViewChild('positionsContainer') positionsContainer: ElementRef;
|
||||||
|
|
||||||
|
public currentTabIndex = 0;
|
||||||
public dateRange: DateRange = 'max';
|
public dateRange: DateRange = 'max';
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToReadForeignPortfolio: boolean;
|
public hasPermissionToReadForeignPortfolio: boolean;
|
||||||
public hasPositions = false;
|
public hasPositions: boolean;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public isLoadingPerformance = true;
|
public isLoadingPerformance = true;
|
||||||
public performance: PortfolioPerformance;
|
public performance: PortfolioPerformance;
|
||||||
@ -92,7 +94,9 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
.subscribe((fragment) => this.viewportScroller.scrollToAnchor(fragment));
|
.subscribe((fragment) => this.viewportScroller.scrollToAnchor(fragment));
|
||||||
}
|
}
|
||||||
|
|
||||||
public onTabChanged() {
|
public onTabChanged(event: MatTabChangeEvent) {
|
||||||
|
this.currentTabIndex = event.index;
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,43 +106,43 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.hasPositions = undefined;
|
if (this.currentTabIndex === 0) {
|
||||||
this.isLoadingPerformance = true;
|
this.isLoadingPerformance = true;
|
||||||
this.positions = undefined;
|
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchChart({ range: this.dateRange })
|
.fetchChart({ range: this.dateRange })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((chartData) => {
|
.subscribe((chartData) => {
|
||||||
this.historicalDataItems = chartData.map((chartDataItem) => {
|
this.historicalDataItems = chartData.map((chartDataItem) => {
|
||||||
return {
|
return {
|
||||||
date: chartDataItem.date,
|
date: chartDataItem.date,
|
||||||
value: chartDataItem.value
|
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
|
this.changeDetectorRef.markForCheck();
|
||||||
.fetchPortfolioPerformance({ range: this.dateRange })
|
});
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
} else if (this.currentTabIndex === 1) {
|
||||||
.subscribe((response) => {
|
this.dataService
|
||||||
this.performance = response;
|
.fetchPositions({ range: this.dateRange })
|
||||||
this.isLoadingPerformance = false;
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((response) => {
|
||||||
|
this.positions = response.positions;
|
||||||
|
this.hasPositions = this.positions?.length > 0;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
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"
|
headerPosition="below"
|
||||||
mat-align-tabs="center"
|
mat-align-tabs="center"
|
||||||
[disablePagination]="true"
|
[disablePagination]="true"
|
||||||
(selectedTabChange)="onTabChanged()"
|
(selectedTabChange)="onTabChanged($event)"
|
||||||
>
|
>
|
||||||
<mat-tab>
|
<mat-tab>
|
||||||
<ng-template mat-tab-label>
|
<ng-template mat-tab-label>
|
||||||
@ -43,14 +43,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="overview-container row mb-5 mt-1">
|
<div class="overview-container row mb-5 mt-1">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<gf-portfolio-performance-summary
|
<gf-portfolio-performance
|
||||||
class="pb-4"
|
class="pb-4"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isLoading]="isLoadingPerformance"
|
[isLoading]="isLoadingPerformance"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[performance]="performance"
|
[performance]="performance"
|
||||||
[showDetails]="!hasImpersonationId || hasPermissionToReadForeignPortfolio"
|
[showDetails]="!hasImpersonationId || hasPermissionToReadForeignPortfolio"
|
||||||
></gf-portfolio-performance-summary>
|
></gf-portfolio-performance>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,7 @@ import { MatTabsModule } from '@angular/material/tabs';
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
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 { 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 { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
||||||
|
|
||||||
import { ZenPageRoutingModule } from './zen-page-routing.module';
|
import { ZenPageRoutingModule } from './zen-page-routing.module';
|
||||||
@ -19,7 +19,7 @@ import { ZenPageComponent } from './zen-page.component';
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
GfLineChartModule,
|
GfLineChartModule,
|
||||||
GfNoTransactionsInfoModule,
|
GfNoTransactionsInfoModule,
|
||||||
GfPortfolioPerformanceSummaryModule,
|
GfPortfolioPerformanceModule,
|
||||||
GfPositionsModule,
|
GfPositionsModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
@ -19,11 +19,10 @@ import {
|
|||||||
AdminData,
|
AdminData,
|
||||||
Export,
|
Export,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
PortfolioItem,
|
|
||||||
PortfolioOverview,
|
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
|
PortfolioSummary,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
@ -148,10 +147,6 @@ export class DataService {
|
|||||||
return this.http.get<InvestmentItem[]>('/api/portfolio/investments');
|
return this.http.get<InvestmentItem[]>('/api/portfolio/investments');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPortfolioOverview() {
|
|
||||||
return this.http.get<PortfolioOverview>('/api/portfolio/overview');
|
|
||||||
}
|
|
||||||
|
|
||||||
public fetchPortfolioPerformance(aParams: { [param: string]: any }) {
|
public fetchPortfolioPerformance(aParams: { [param: string]: any }) {
|
||||||
return this.http.get<PortfolioPerformance>('/api/portfolio/performance', {
|
return this.http.get<PortfolioPerformance>('/api/portfolio/performance', {
|
||||||
params: aParams
|
params: aParams
|
||||||
@ -169,6 +164,18 @@ export class DataService {
|
|||||||
return this.http.get<PortfolioReport>('/api/portfolio/report');
|
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) {
|
public fetchPositionDetail(aSymbol: string) {
|
||||||
return this.http.get<PortfolioPositionDetail>(
|
return this.http.get<PortfolioPositionDetail>(
|
||||||
`/api/portfolio/position/${aSymbol}`
|
`/api/portfolio/position/${aSymbol}`
|
||||||
|
@ -8,6 +8,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
|
|||||||
import { PortfolioPosition } from './portfolio-position.interface';
|
import { PortfolioPosition } from './portfolio-position.interface';
|
||||||
import { PortfolioReportRule } from './portfolio-report-rule.interface';
|
import { PortfolioReportRule } from './portfolio-report-rule.interface';
|
||||||
import { PortfolioReport } from './portfolio-report.interface';
|
import { PortfolioReport } from './portfolio-report.interface';
|
||||||
|
import { PortfolioSummary } from './portfolio-summary.interface';
|
||||||
import { Position } from './position.interface';
|
import { Position } from './position.interface';
|
||||||
import { TimelinePosition } from './timeline-position.interface';
|
import { TimelinePosition } from './timeline-position.interface';
|
||||||
import { UserSettings } from './user-settings.interface';
|
import { UserSettings } from './user-settings.interface';
|
||||||
@ -25,6 +26,7 @@ export {
|
|||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioReportRule,
|
PortfolioReportRule,
|
||||||
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
TimelinePosition,
|
TimelinePosition,
|
||||||
User,
|
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…
x
Reference in New Issue
Block a user