From 603ce4fb8d13bc069d9e3d134cd360c1b6d1ad01 Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Thu, 10 Mar 2022 21:09:06 +0100 Subject: [PATCH] Add support for emergency fund --- .../app/portfolio/portfolio.service-new.ts | 8 +++++++- .../src/app/portfolio/portfolio.service.ts | 8 +++++++- .../interfaces/user-settings.interface.ts | 2 ++ .../src/app/user/update-user-setting.dto.ts | 6 +++++- apps/api/src/app/user/user.controller.ts | 1 - .../home-summary/home-summary.component.ts | 9 +++++++++ .../components/home-summary/home-summary.html | 1 + .../portfolio-summary.component.html | 19 +++++++++++++++++++ .../portfolio-summary.component.ts | 18 +++++++++++++++++- .../portfolio-summary.module.ts | 4 ++-- .../interfaces/portfolio-summary.interface.ts | 1 + 11 files changed, 70 insertions(+), 7 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index b34a9206f..4c02fd0d4 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -5,6 +5,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; +import { UserSettings } from '@ghostfolio/api/app/user/interfaces/user-settings.interface'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; @@ -895,6 +896,9 @@ export class PortfolioServiceNew { userId }); const dividend = this.getDividend(orders).toNumber(); + const emergencyFund = + (this.request.user?.Settings?.settings as UserSettings).emergencyFund ?? + 0; const fees = this.getFees(orders).toNumber(); const firstOrderDate = orders[0]?.date; const items = this.getItems(orders).toNumber(); @@ -902,6 +906,7 @@ export class PortfolioServiceNew { const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); + const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber(); const committedFunds = new Big(totalBuy).minus(totalSell); const netWorth = new Big(balanceInBaseCurrency) @@ -927,14 +932,15 @@ export class PortfolioServiceNew { return { ...performanceInformation.performance, annualizedPerformancePercent, + cash, dividend, + emergencyFund, fees, firstOrderDate, items, netWorth, totalBuy, totalSell, - cash: balanceInBaseCurrency, committedFunds: committedFunds.toNumber(), ordersCount: orders.filter((order) => { return order.type === 'BUY' || order.type === 'SELL'; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index f3df18c30..7192594ef 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -6,6 +6,7 @@ import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfol import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/portfolio-calculator'; +import { UserSettings } from '@ghostfolio/api/app/user/interfaces/user-settings.interface'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; @@ -873,6 +874,9 @@ export class PortfolioService { userId }); const dividend = this.getDividend(orders).toNumber(); + const emergencyFund = + (this.request.user?.Settings?.settings as UserSettings).emergencyFund ?? + 0; const fees = this.getFees(orders).toNumber(); const firstOrderDate = orders[0]?.date; const items = this.getItems(orders).toNumber(); @@ -880,6 +884,7 @@ export class PortfolioService { const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); + const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber(); const committedFunds = new Big(totalBuy).minus(totalSell); const netWorth = new Big(balanceInBaseCurrency) @@ -889,7 +894,9 @@ export class PortfolioService { return { ...performanceInformation.performance, + cash, dividend, + emergencyFund, fees, firstOrderDate, items, @@ -898,7 +905,6 @@ export class PortfolioService { totalSell, annualizedPerformancePercent: performanceInformation.performance.annualizedPerformancePercent, - cash: balanceInBaseCurrency, committedFunds: committedFunds.toNumber(), ordersCount: orders.filter((order) => { return order.type === 'BUY' || order.type === 'SELL'; diff --git a/apps/api/src/app/user/interfaces/user-settings.interface.ts b/apps/api/src/app/user/interfaces/user-settings.interface.ts index 7870abee9..fb4b2af35 100644 --- a/apps/api/src/app/user/interfaces/user-settings.interface.ts +++ b/apps/api/src/app/user/interfaces/user-settings.interface.ts @@ -1,3 +1,5 @@ export interface UserSettings { + emergencyFund?: number; + isNewCalculationEngine?: boolean; isRestrictedView?: boolean; } diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index 38dc9cafb..5af2f5f8d 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -1,6 +1,10 @@ -import { IsBoolean, IsOptional } from 'class-validator'; +import { IsBoolean, IsNumber, IsOptional } from 'class-validator'; export class UpdateUserSettingDto { + @IsNumber() + @IsOptional() + emergencyFund?: number; + @IsBoolean() @IsOptional() isNewCalculationEngine?: boolean; diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index a615d2f2b..97cf25b6e 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -23,7 +23,6 @@ import { import { REQUEST } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { AuthGuard } from '@nestjs/passport'; -import { Provider, Role } from '@prisma/client'; import { User as UserModel } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; diff --git a/apps/client/src/app/components/home-summary/home-summary.component.ts b/apps/client/src/app/components/home-summary/home-summary.component.ts index b2d8d91f4..976aa7549 100644 --- a/apps/client/src/app/components/home-summary/home-summary.component.ts +++ b/apps/client/src/app/components/home-summary/home-summary.component.ts @@ -43,6 +43,15 @@ export class HomeSummaryComponent implements OnDestroy, OnInit { this.update(); } + public onChangeEmergencyFund(emergencyFund: number) { + this.dataService + .putUserSetting({ emergencyFund }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.update(); + }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/home-summary/home-summary.html b/apps/client/src/app/components/home-summary/home-summary.html index fe0b99a2b..516925fb6 100644 --- a/apps/client/src/app/components/home-summary/home-summary.html +++ b/apps/client/src/app/components/home-summary/home-summary.html @@ -11,6 +11,7 @@ [isLoading]="isLoading" [locale]="user?.settings?.locale" [summary]="summary" + (emergencyFundChanged)="onChangeEmergencyFund($event)" > 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 index 1fc5e1f85..a1a6eaffa 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -130,6 +130,25 @@ > +
+
Emergency Fund
+
+ + +
+
Cash (Buying Power)
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 index 77debbb0f..8b438bc81 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts @@ -1,9 +1,11 @@ import { ChangeDetectionStrategy, Component, + EventEmitter, Input, OnChanges, - OnInit + OnInit, + Output } from '@angular/core'; import { PortfolioSummary } from '@ghostfolio/common/interfaces'; import { formatDistanceToNow } from 'date-fns'; @@ -20,6 +22,8 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit { @Input() locale: string; @Input() summary: PortfolioSummary; + @Output() emergencyFundChanged = new EventEmitter(); + public timeInMarket: string; public constructor() {} @@ -37,4 +41,16 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit { this.timeInMarket = undefined; } } + + public onEditEmergencyFund() { + const emergencyFundInput = prompt( + 'Please enter the amount of your emergency fund:', + this.summary.emergencyFund.toString() + ); + const emergencyFund = parseFloat(emergencyFundInput?.trim()); + + if (emergencyFund >= 0) { + this.emergencyFundChanged.emit(emergencyFund); + } + } } diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts b/apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts index 306de7f06..1724aa8c5 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { GfValueModule } from '@ghostfolio/ui/value'; import { PortfolioSummaryComponent } from './portfolio-summary.component'; @@ -8,6 +8,6 @@ import { PortfolioSummaryComponent } from './portfolio-summary.component'; declarations: [PortfolioSummaryComponent], exports: [PortfolioSummaryComponent], imports: [CommonModule, GfValueModule], - providers: [] + schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class GfPortfolioSummaryModule {} diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index 7ffec5d4d..b692e9ba9 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -5,6 +5,7 @@ export interface PortfolioSummary extends PortfolioPerformance { cash: number; dividend: number; committedFunds: number; + emergencyFund: number; fees: number; firstOrderDate: Date; items: number;