From 7f0c98cae68f6656f55fdbe086f85b88e117fef9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 30 Mar 2022 21:14:49 +0200 Subject: [PATCH] Feature/setup fire page with 4 percent rule (#773) * Setup page for FIRE * Update changelog --- CHANGELOG.md | 1 + apps/client/src/app/app-routing.module.ts | 7 ++ .../fire/fire-page-routing.module.ts | 15 ++++ .../portfolio/fire/fire-page.component.ts | 86 +++++++++++++++++++ .../app/pages/portfolio/fire/fire-page.html | 53 ++++++++++++ .../pages/portfolio/fire/fire-page.module.ts | 19 ++++ .../app/pages/portfolio/fire/fire-page.scss | 3 + .../app/pages/portfolio/portfolio-page.html | 27 ++++++ 8 files changed, 211 insertions(+) create mode 100644 apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts create mode 100644 apps/client/src/app/pages/portfolio/fire/fire-page.component.ts create mode 100644 apps/client/src/app/pages/portfolio/fire/fire-page.html create mode 100644 apps/client/src/app/pages/portfolio/fire/fire-page.module.ts create mode 100644 apps/client/src/app/pages/portfolio/fire/fire-page.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index 06fd92002..dc2de273e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added a _FIRE_ (Financial Independence, Retire Early) section including the 4% rule - Added more durations in the coupon system ### Fixed diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index 0c6e7c2ab..28a9d1b49 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -113,6 +113,13 @@ const routes: Routes = [ (m) => m.AnalysisPageModule ) }, + { + path: 'portfolio/fire', + loadChildren: () => + import('./pages/portfolio/fire/fire-page.module').then( + (m) => m.FirePageModule + ) + }, { path: 'portfolio/report', loadChildren: () => diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts b/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts new file mode 100644 index 000000000..445457e01 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; + +import { FirePageComponent } from './fire-page.component'; + +const routes: Routes = [ + { path: '', component: FirePageComponent, canActivate: [AuthGuard] } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class FirePageRoutingModule {} diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts new file mode 100644 index 000000000..1eb132dfd --- /dev/null +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -0,0 +1,86 @@ +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { User } from '@ghostfolio/common/interfaces'; +import Big from 'big.js'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + host: { class: 'page' }, + selector: 'gf-fire-page', + styleUrls: ['./fire-page.scss'], + templateUrl: './fire-page.html' +}) +export class FirePageComponent implements OnDestroy, OnInit { + public fireWealth: number; + public hasImpersonationId: boolean; + public isLoading = false; + public user: User; + public withdrawalRatePerMonth: number; + public withdrawalRatePerYear: number; + + private unsubscribeSubject = new Subject(); + + /** + * @constructor + */ + public constructor( + private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService, + private impersonationStorageService: ImpersonationStorageService, + private userService: UserService + ) {} + + /** + * Initializes the controller + */ + public ngOnInit() { + this.isLoading = true; + + this.impersonationStorageService + .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((aId) => { + this.hasImpersonationId = !!aId; + }); + + this.dataService + .fetchPortfolioSummary() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ cash, currentValue }) => { + if (cash === null || currentValue === null) { + return; + } + + this.fireWealth = new Big(currentValue).plus(cash).toNumber(); + this.withdrawalRatePerYear = new Big(this.fireWealth) + .mul(4) + .div(100) + .toNumber(); + this.withdrawalRatePerMonth = new Big(this.withdrawalRatePerYear) + .div(12) + .toNumber(); + + this.isLoading = false; + + this.changeDetectorRef.markForCheck(); + }); + + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + + this.changeDetectorRef.markForCheck(); + } + }); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html new file mode 100644 index 000000000..69735c196 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -0,0 +1,53 @@ +
+
+
+

FIRE

+
+

4% Rule

+
+ + +
+
+ If you retire today, you would be able to withdraw + + per year + or + + per month, based on your net worth of + + (excluding emergency fund) and a withdrawal rate of 4%. +
+
+
+
+
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts new file mode 100644 index 000000000..86fb0a953 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { GfValueModule } from '@ghostfolio/ui/value'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + +import { FirePageRoutingModule } from './fire-page-routing.module'; +import { FirePageComponent } from './fire-page.component'; + +@NgModule({ + declarations: [FirePageComponent], + imports: [ + CommonModule, + FirePageRoutingModule, + GfValueModule, + NgxSkeletonLoaderModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class FirePageModule {} diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.scss b/apps/client/src/app/pages/portfolio/fire/fire-page.scss new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.html b/apps/client/src/app/pages/portfolio/portfolio-page.html index 7c221d9d5..d0717805d 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page.html +++ b/apps/client/src/app/pages/portfolio/portfolio-page.html @@ -101,5 +101,32 @@ +
+ +

+ FIRE + +

+
+ Ghostfolio FIRE calculates metrics for the + Financial Independence, Retire Early lifestyle. +
+ +
+