Browse Source

Add support for emergency fund

pull/749/head
Thomas 3 years ago
parent
commit
603ce4fb8d
  1. 8
      apps/api/src/app/portfolio/portfolio.service-new.ts
  2. 8
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 2
      apps/api/src/app/user/interfaces/user-settings.interface.ts
  4. 6
      apps/api/src/app/user/update-user-setting.dto.ts
  5. 1
      apps/api/src/app/user/user.controller.ts
  6. 9
      apps/client/src/app/components/home-summary/home-summary.component.ts
  7. 1
      apps/client/src/app/components/home-summary/home-summary.html
  8. 19
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  9. 18
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts
  10. 4
      apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts
  11. 1
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts

8
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 { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface'; import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.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 { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-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'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
@ -895,6 +896,9 @@ export class PortfolioServiceNew {
userId userId
}); });
const dividend = this.getDividend(orders).toNumber(); const dividend = this.getDividend(orders).toNumber();
const emergencyFund =
(this.request.user?.Settings?.settings as UserSettings).emergencyFund ??
0;
const fees = this.getFees(orders).toNumber(); const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date; const firstOrderDate = orders[0]?.date;
const items = this.getItems(orders).toNumber(); const items = this.getItems(orders).toNumber();
@ -902,6 +906,7 @@ export class PortfolioServiceNew {
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
const committedFunds = new Big(totalBuy).minus(totalSell); const committedFunds = new Big(totalBuy).minus(totalSell);
const netWorth = new Big(balanceInBaseCurrency) const netWorth = new Big(balanceInBaseCurrency)
@ -927,14 +932,15 @@ export class PortfolioServiceNew {
return { return {
...performanceInformation.performance, ...performanceInformation.performance,
annualizedPerformancePercent, annualizedPerformancePercent,
cash,
dividend, dividend,
emergencyFund,
fees, fees,
firstOrderDate, firstOrderDate,
items, items,
netWorth, netWorth,
totalBuy, totalBuy,
totalSell, totalSell,
cash: balanceInBaseCurrency,
committedFunds: committedFunds.toNumber(), committedFunds: committedFunds.toNumber(),
ordersCount: orders.filter((order) => { ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL'; return order.type === 'BUY' || order.type === 'SELL';

8
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 { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/portfolio-calculator'; 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 { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-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'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
@ -873,6 +874,9 @@ export class PortfolioService {
userId userId
}); });
const dividend = this.getDividend(orders).toNumber(); const dividend = this.getDividend(orders).toNumber();
const emergencyFund =
(this.request.user?.Settings?.settings as UserSettings).emergencyFund ??
0;
const fees = this.getFees(orders).toNumber(); const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date; const firstOrderDate = orders[0]?.date;
const items = this.getItems(orders).toNumber(); const items = this.getItems(orders).toNumber();
@ -880,6 +884,7 @@ export class PortfolioService {
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
const committedFunds = new Big(totalBuy).minus(totalSell); const committedFunds = new Big(totalBuy).minus(totalSell);
const netWorth = new Big(balanceInBaseCurrency) const netWorth = new Big(balanceInBaseCurrency)
@ -889,7 +894,9 @@ export class PortfolioService {
return { return {
...performanceInformation.performance, ...performanceInformation.performance,
cash,
dividend, dividend,
emergencyFund,
fees, fees,
firstOrderDate, firstOrderDate,
items, items,
@ -898,7 +905,6 @@ export class PortfolioService {
totalSell, totalSell,
annualizedPerformancePercent: annualizedPerformancePercent:
performanceInformation.performance.annualizedPerformancePercent, performanceInformation.performance.annualizedPerformancePercent,
cash: balanceInBaseCurrency,
committedFunds: committedFunds.toNumber(), committedFunds: committedFunds.toNumber(),
ordersCount: orders.filter((order) => { ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL'; return order.type === 'BUY' || order.type === 'SELL';

2
apps/api/src/app/user/interfaces/user-settings.interface.ts

@ -1,3 +1,5 @@
export interface UserSettings { export interface UserSettings {
emergencyFund?: number;
isNewCalculationEngine?: boolean;
isRestrictedView?: boolean; isRestrictedView?: boolean;
} }

6
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 { export class UpdateUserSettingDto {
@IsNumber()
@IsOptional()
emergencyFund?: number;
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
isNewCalculationEngine?: boolean; isNewCalculationEngine?: boolean;

1
apps/api/src/app/user/user.controller.ts

@ -23,7 +23,6 @@ import {
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { Provider, Role } from '@prisma/client';
import { User as UserModel } from '@prisma/client'; import { User as UserModel } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';

9
apps/client/src/app/components/home-summary/home-summary.component.ts

@ -43,6 +43,15 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
this.update(); this.update();
} }
public onChangeEmergencyFund(emergencyFund: number) {
this.dataService
.putUserSetting({ emergencyFund })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.update();
});
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();

1
apps/client/src/app/components/home-summary/home-summary.html

@ -11,6 +11,7 @@
[isLoading]="isLoading" [isLoading]="isLoading"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[summary]="summary" [summary]="summary"
(emergencyFundChanged)="onChangeEmergencyFund($event)"
></gf-portfolio-summary> ></gf-portfolio-summary>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

19
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -130,6 +130,25 @@
></gf-value> ></gf-value>
</div> </div>
</div> </div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Emergency Fund</div>
<div
class="align-items-center cursor-pointer d-flex justify-content-end"
(click)="onEditEmergencyFund()"
>
<ion-icon
*ngIf="!isLoading"
class="mr-1 text-muted"
name="create-outline"
></ion-icon>
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.emergencyFund"
></gf-value>
</div>
</div>
<div class="row px-3 py-1"> <div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Cash (Buying Power)</div> <div class="d-flex flex-grow-1" i18n>Cash (Buying Power)</div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">

18
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts

@ -1,9 +1,11 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
EventEmitter,
Input, Input,
OnChanges, OnChanges,
OnInit OnInit,
Output
} from '@angular/core'; } from '@angular/core';
import { PortfolioSummary } from '@ghostfolio/common/interfaces'; import { PortfolioSummary } from '@ghostfolio/common/interfaces';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
@ -20,6 +22,8 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
@Input() locale: string; @Input() locale: string;
@Input() summary: PortfolioSummary; @Input() summary: PortfolioSummary;
@Output() emergencyFundChanged = new EventEmitter<number>();
public timeInMarket: string; public timeInMarket: string;
public constructor() {} public constructor() {}
@ -37,4 +41,16 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
this.timeInMarket = undefined; 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);
}
}
} }

4
apps/client/src/app/components/portfolio-summary/portfolio-summary.module.ts

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; 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 { GfValueModule } from '@ghostfolio/ui/value';
import { PortfolioSummaryComponent } from './portfolio-summary.component'; import { PortfolioSummaryComponent } from './portfolio-summary.component';
@ -8,6 +8,6 @@ import { PortfolioSummaryComponent } from './portfolio-summary.component';
declarations: [PortfolioSummaryComponent], declarations: [PortfolioSummaryComponent],
exports: [PortfolioSummaryComponent], exports: [PortfolioSummaryComponent],
imports: [CommonModule, GfValueModule], imports: [CommonModule, GfValueModule],
providers: [] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfPortfolioSummaryModule {} export class GfPortfolioSummaryModule {}

1
libs/common/src/lib/interfaces/portfolio-summary.interface.ts

@ -5,6 +5,7 @@ export interface PortfolioSummary extends PortfolioPerformance {
cash: number; cash: number;
dividend: number; dividend: number;
committedFunds: number; committedFunds: number;
emergencyFund: number;
fees: number; fees: number;
firstOrderDate: Date; firstOrderDate: Date;
items: number; items: number;

Loading…
Cancel
Save