diff --git a/CHANGELOG.md b/CHANGELOG.md index cab185008..5af076a01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -### Fixed +### Added -- Fixed the position detail chart if there are missing historical data around the first buy date -- Fixed the snack bar background color in dark mode +- Added the calculated net worth to the portfolio summary tab on the home page +- Added the calculated time in market to the portfolio summary tab on the home page ### Changed +- Improved the usability of the tabs on the home page +- Restructured the portfolio summary tab on the home page - Upgraded `angular-material-css-vars` from version `2.1.0` to `2.1.2` +### Fixed + +- Fixed the position detail chart if there are missing historical data around the first buy date +- Fixed the snack bar background color in dark mode + ## 1.36.0 - 09.08.2021 ### Changed diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 57113dac5..96a2e2b55 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -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 res.json(details); } - @Get('overview') - @UseGuards(AuthGuard('jwt')) - public async getOverview( - @Headers('impersonation-id') impersonationId - ): Promise { - 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 res.json(result); } + @Get('summary') + @UseGuards(AuthGuard('jwt')) + public async getSummary( + @Headers('impersonation-id') impersonationId + ): Promise { + 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( diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 63d781f05..ccb287e27 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -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 { - 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 { + 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, diff --git a/apps/client/src/app/components/portfolio-overview/portfolio-overview.component.html b/apps/client/src/app/components/portfolio-overview/portfolio-overview.component.html deleted file mode 100644 index e3cb238d4..000000000 --- a/apps/client/src/app/components/portfolio-overview/portfolio-overview.component.html +++ /dev/null @@ -1,74 +0,0 @@ -
-
-
Cash
-
- -
-
-
-

-
-
-
Buy
-
- -
-
-
-
Sell
-
- - - -
-
-
-

-
-
-
Investment
-
- -
-
-
-

-
-
-
- Fees for {{ overview?.ordersCount }} {overview?.ordersCount, plural, =1 - {order} other {orders}} -
-
- -
-
-
diff --git a/apps/client/src/app/components/portfolio-overview/portfolio-overview.component.ts b/apps/client/src/app/components/portfolio-overview/portfolio-overview.component.ts deleted file mode 100644 index d8f2c86d5..000000000 --- a/apps/client/src/app/components/portfolio-overview/portfolio-overview.component.ts +++ /dev/null @@ -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() {} -} diff --git a/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.component.html b/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.component.html deleted file mode 100644 index 42aa5a683..000000000 --- a/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.component.html +++ /dev/null @@ -1,54 +0,0 @@ -
-
-
-
- -
-
- -
-
- -
- {{ unit }} -
-
-
-
-
- -
-
- -
-
-
diff --git a/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.component.scss b/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.component.scss deleted file mode 100644 index 850cce442..000000000 --- a/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.component.scss +++ /dev/null @@ -1,9 +0,0 @@ -:host { - display: block; - - .value-container { - #value { - font-variant-numeric: tabular-nums; - } - } -} diff --git a/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.component.ts b/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.component.ts deleted file mode 100644 index 580f48147..000000000 --- a/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.component.ts +++ /dev/null @@ -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(); - } - } - } -} diff --git a/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.module.ts b/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.module.ts deleted file mode 100644 index 3b95e3547..000000000 --- a/apps/client/src/app/components/portfolio-performance-summary/portfolio-performance-summary.module.ts +++ /dev/null @@ -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 {} diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html index b855cd743..42aa5a683 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html @@ -1,67 +1,54 @@
-
-
Value
-
- +
+
+
+ +
+
+ +
+
+ +
+ {{ unit }} +
-
-
Absolute Performance
-
+
+
-
-
-
Performance (TWR)
-
+
-
diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.scss b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.scss index 5d4e87f30..850cce442 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.scss +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.scss @@ -1,3 +1,9 @@ :host { display: block; + + .value-container { + #value { + font-variant-numeric: tabular-nums; + } + } } diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts index 735bfbd19..90d134d45 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts @@ -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(); + } + } + } } diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.module.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.module.ts index 3d83a61f6..e0ea5c92c 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.module.ts +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.module.ts @@ -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 {} diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html new file mode 100644 index 000000000..37193396d --- /dev/null +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -0,0 +1,134 @@ +
+
+
Time in Market
+
+ {{ timeInMarket }} + +
+
+
+

+
+
+
+ Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1 + {order} other {orders}} +
+
+ +
+
+
+

+
+
+
Buy
+
+ +
+
+
+
Sell
+
+ - + +
+
+
+

+
+
+
Investment
+
+ +
+
+
+
Absolute Performance
+
+ +
+
+
+
Performance (TWR)
+
+ +
+
+
+

+
+
+
Value
+
+ +
+
+
+
Cash
+
+ +
+
+
+

+
+
+
Net Worth
+
+ +
+
+
diff --git a/apps/client/src/app/components/portfolio-overview/portfolio-overview.component.scss b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.scss similarity index 100% rename from apps/client/src/app/components/portfolio-overview/portfolio-overview.component.scss rename to apps/client/src/app/components/portfolio-summary/portfolio-summary.component.scss diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts new file mode 100644 index 000000000..7ce3f33d8 --- /dev/null +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts @@ -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; + } + } +} diff --git a/apps/client/src/app/components/portfolio-overview/portfolio-overview.module.ts b/apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts similarity index 51% rename from apps/client/src/app/components/portfolio-overview/portfolio-overview.module.ts rename to apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts index 9f86a9bbf..451984adf 100644 --- a/apps/client/src/app/components/portfolio-overview/portfolio-overview.module.ts +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts @@ -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 {} diff --git a/apps/client/src/app/components/value/value.component.ts b/apps/client/src/app/components/value/value.component.ts index 863ec9806..b4b619c5d 100644 --- a/apps/client/src/app/components/value/value.component.ts +++ b/apps/client/src/app/components/value/value.component.ts @@ -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(this.value), - DEFAULT_DATE_FORMAT - ); + this.formattedDate = format( + new Date(this.value), + DEFAULT_DATE_FORMAT + ); + } + } catch {} } } } diff --git a/apps/client/src/app/pages/home/home-page.component.ts b/apps/client/src/app/pages/home/home-page.component.ts index 036ff8eb4..035794d6b 100644 --- a/apps/client/src/app/pages/home/home-page.component.ts +++ b/apps/client/src/app/pages/home/home-page.component.ts @@ -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(); @@ -153,7 +155,9 @@ export class HomePageComponent implements OnDestroy, OnInit { this.update(); } - public onTabChanged() { + public onTabChanged(event: MatTabChangeEvent) { + this.currentTabIndex = event.index; + this.update(); } @@ -182,54 +186,55 @@ export class HomePageComponent implements OnDestroy, OnInit { } private update() { - this.hasPositions = undefined; - this.isLoadingOverview = true; - this.isLoadingPerformance = true; - this.positions = undefined; + if (this.currentTabIndex === 0) { + this.isLoadingPerformance = true; + + this.dataService + .fetchChart({ range: this.dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((chartData) => { + this.historicalDataItems = chartData.map((chartDataItem) => { + return { + date: chartDataItem.date, + value: chartDataItem.value + }; + }); - this.dataService - .fetchChart({ range: this.dateRange }) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((chartData) => { - this.historicalDataItems = chartData.map((chartDataItem) => { - return { - date: chartDataItem.date, - value: chartDataItem.value - }; + this.changeDetectorRef.markForCheck(); }); - this.changeDetectorRef.markForCheck(); - }); + this.dataService + .fetchPortfolioPerformance({ range: this.dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + this.performance = response; + this.isLoadingPerformance = false; - this.dataService - .fetchPortfolioPerformance({ range: this.dateRange }) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.performance = response; - this.isLoadingPerformance = false; - - this.changeDetectorRef.markForCheck(); - }); - - this.dataService - .fetchPortfolioOverview() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.overview = response; - this.isLoadingOverview = false; + this.changeDetectorRef.markForCheck(); + }); + } else if (this.currentTabIndex === 1) { + this.dataService + .fetchPositions({ range: this.dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + this.positions = response.positions; + this.hasPositions = this.positions?.length > 0; - this.changeDetectorRef.markForCheck(); - }); + this.changeDetectorRef.markForCheck(); + }); + } else if (this.currentTabIndex === 2) { + this.isLoadingSummary = true; - this.dataService - .fetchPositions({ range: this.dateRange }) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.positions = response.positions; - this.hasPositions = this.positions?.length > 0; + this.dataService + .fetchPortfolioSummary() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + this.summary = response; + this.isLoadingSummary = false; - this.changeDetectorRef.markForCheck(); - }); + this.changeDetectorRef.markForCheck(); + }); + } this.changeDetectorRef.markForCheck(); } diff --git a/apps/client/src/app/pages/home/home-page.html b/apps/client/src/app/pages/home/home-page.html index 7eaea908f..cebf1f072 100644 --- a/apps/client/src/app/pages/home/home-page.html +++ b/apps/client/src/app/pages/home/home-page.html @@ -4,7 +4,7 @@ headerPosition="below" mat-align-tabs="center" [disablePagination]="true" - (selectedTabChange)="onTabChanged()" + (selectedTabChange)="onTabChanged($event)" > @@ -55,14 +55,14 @@
- + >

Holdings

-
+
-
- - - Performance - - - - - -
-
+
Summary - + [summary]="summary" + >
diff --git a/apps/client/src/app/pages/home/home-page.module.ts b/apps/client/src/app/pages/home/home-page.module.ts index df4e2b61e..a549b0476 100644 --- a/apps/client/src/app/pages/home/home-page.module.ts +++ b/apps/client/src/app/pages/home/home-page.module.ts @@ -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, diff --git a/apps/client/src/app/pages/zen/zen-page.component.ts b/apps/client/src/app/pages/zen/zen-page.component.ts index 39e072f3a..53abe15c1 100644 --- a/apps/client/src/app/pages/zen/zen-page.component.ts +++ b/apps/client/src/app/pages/zen/zen-page.component.ts @@ -8,6 +8,7 @@ import { OnInit, ViewChild } from '@angular/core'; +import { MatTabChangeEvent } from '@angular/material/tabs'; import { ActivatedRoute } from '@angular/router'; import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface'; import { DataService } from '@ghostfolio/client/services/data.service'; @@ -32,11 +33,12 @@ import { first, takeUntil } from 'rxjs/operators'; export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit { @ViewChild('positionsContainer') positionsContainer: ElementRef; + public currentTabIndex = 0; public dateRange: DateRange = 'max'; public deviceType: string; public hasImpersonationId: boolean; public hasPermissionToReadForeignPortfolio: boolean; - public hasPositions = false; + public hasPositions: boolean; public historicalDataItems: LineChartItem[]; public isLoadingPerformance = true; public performance: PortfolioPerformance; @@ -92,7 +94,9 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit { .subscribe((fragment) => this.viewportScroller.scrollToAnchor(fragment)); } - public onTabChanged() { + public onTabChanged(event: MatTabChangeEvent) { + this.currentTabIndex = event.index; + this.update(); } @@ -102,43 +106,43 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit { } private update() { - this.hasPositions = undefined; - this.isLoadingPerformance = true; - this.positions = undefined; + if (this.currentTabIndex === 0) { + this.isLoadingPerformance = true; + + this.dataService + .fetchChart({ range: this.dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((chartData) => { + this.historicalDataItems = chartData.map((chartDataItem) => { + return { + date: chartDataItem.date, + value: chartDataItem.value + }; + }); - this.dataService - .fetchChart({ range: this.dateRange }) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((chartData) => { - this.historicalDataItems = chartData.map((chartDataItem) => { - return { - date: chartDataItem.date, - value: chartDataItem.value - }; + this.changeDetectorRef.markForCheck(); }); - this.changeDetectorRef.markForCheck(); - }); - - this.dataService - .fetchPortfolioPerformance({ range: this.dateRange }) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.performance = response; - this.isLoadingPerformance = false; - - this.changeDetectorRef.markForCheck(); - }); + this.dataService + .fetchPortfolioPerformance({ range: this.dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + this.performance = response; + this.isLoadingPerformance = false; - this.dataService - .fetchPositions({ range: this.dateRange }) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.positions = response.positions; - this.hasPositions = this.positions?.length > 0; + this.changeDetectorRef.markForCheck(); + }); + } else if (this.currentTabIndex === 1) { + this.dataService + .fetchPositions({ range: this.dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + this.positions = response.positions; + this.hasPositions = this.positions?.length > 0; - this.changeDetectorRef.markForCheck(); - }); + this.changeDetectorRef.markForCheck(); + }); + } this.changeDetectorRef.markForCheck(); } diff --git a/apps/client/src/app/pages/zen/zen-page.html b/apps/client/src/app/pages/zen/zen-page.html index f57b2a2bb..ccaec4712 100644 --- a/apps/client/src/app/pages/zen/zen-page.html +++ b/apps/client/src/app/pages/zen/zen-page.html @@ -4,7 +4,7 @@ headerPosition="below" mat-align-tabs="center" [disablePagination]="true" - (selectedTabChange)="onTabChanged()" + (selectedTabChange)="onTabChanged($event)" > @@ -43,14 +43,14 @@
- + >
diff --git a/apps/client/src/app/pages/zen/zen-page.module.ts b/apps/client/src/app/pages/zen/zen-page.module.ts index cf8d50a85..2b0a650b3 100644 --- a/apps/client/src/app/pages/zen/zen-page.module.ts +++ b/apps/client/src/app/pages/zen/zen-page.module.ts @@ -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, diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 6dabe681b..9fb6da566 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -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('/api/portfolio/investments'); } - public fetchPortfolioOverview() { - return this.http.get('/api/portfolio/overview'); - } - public fetchPortfolioPerformance(aParams: { [param: string]: any }) { return this.http.get('/api/portfolio/performance', { params: aParams @@ -169,6 +164,18 @@ export class DataService { return this.http.get('/api/portfolio/report'); } + public fetchPortfolioSummary(): Observable { + return this.http.get('/api/portfolio/summary').pipe( + map((summary) => { + if (summary.firstOrderDate) { + summary.firstOrderDate = parseISO(summary.firstOrderDate); + } + + return summary; + }) + ); + } + public fetchPositionDetail(aSymbol: string) { return this.http.get( `/api/portfolio/position/${aSymbol}` diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index d087eae7b..725b33921 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -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, diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts new file mode 100644 index 000000000..e34bd0e6b --- /dev/null +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -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; +}