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
|
||||
|
||||
### 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 the position detail chart if there are missing historical data around the first buy date
|
||||
- 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
|
||||
|
||||
### 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();
|
||||
});
|
||||
} 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
|
||||
.fetchPortfolioOverview()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.overview = response;
|
||||
this.isLoadingOverview = false;
|
||||
this.dataService
|
||||
.fetchPortfolioSummary()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.summary = response;
|
||||
this.isLoadingSummary = false;
|
||||
|
||||
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"
|
||||
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.dataService
|
||||
.fetchPortfolioPerformance({ range: this.dateRange })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.performance = response;
|
||||
this.isLoadingPerformance = 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.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…
x
Reference in New Issue
Block a user