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 { 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';

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 { 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';

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

@ -1,3 +1,5 @@
export interface UserSettings {
emergencyFund?: number;
isNewCalculationEngine?: 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 {
@IsNumber()
@IsOptional()
emergencyFund?: number;
@IsBoolean()
@IsOptional()
isNewCalculationEngine?: boolean;

1
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';

9
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();

1
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)"
></gf-portfolio-summary>
</mat-card-content>
</mat-card>

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

@ -130,6 +130,25 @@
></gf-value>
</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="d-flex flex-grow-1" i18n>Cash (Buying Power)</div>
<div class="d-flex justify-content-end">

18
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<number>();
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);
}
}
}

4
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 {}

1
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;

Loading…
Cancel
Save