diff --git a/CHANGELOG.md b/CHANGELOG.md index 544a7bdc6..58a7b3b48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for configuring the safe withdrawal rate in the _FIRE_ section (experimental) + ### Changed - Changed the _As seen in_ section on the landing page to an animated carousel 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 index c80b55c45..63187c05c 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -7,8 +7,10 @@ import { GfFireCalculatorComponent } from '@ghostfolio/ui/fire-calculator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfValueComponent } from '@ghostfolio/ui/value'; -import { NgStyle } from '@angular/common'; +import { CommonModule, NgStyle } from '@angular/common'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormControl } from '@angular/forms'; import { Big } from 'big.js'; import { DeviceDetectorService } from 'ngx-device-detector'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -17,11 +19,14 @@ import { takeUntil } from 'rxjs/operators'; @Component({ imports: [ + CommonModule, + FormsModule, GfFireCalculatorComponent, GfPremiumIndicatorComponent, GfValueComponent, NgStyle, - NgxSkeletonLoaderModule + NgxSkeletonLoaderModule, + ReactiveFormsModule ], selector: 'gf-fire-page', styleUrls: ['./fire-page.scss'], @@ -33,6 +38,8 @@ export class GfFirePageComponent implements OnDestroy, OnInit { public hasImpersonationId: boolean; public hasPermissionToUpdateUserSettings: boolean; public isLoading = false; + public safeWithdrawalRateControl = new FormControl(undefined); + public safeWithdrawalRateOptions = [0.025, 0.03, 0.035, 0.04, 0.045]; public user: User; public withdrawalRatePerMonth: Big; public withdrawalRatePerYear: Big; @@ -70,11 +77,7 @@ export class GfFirePageComponent implements OnDestroy, OnInit { }; } - this.withdrawalRatePerYear = Big( - this.fireWealth.today.valueInBaseCurrency - ).mul(this.user.settings.safeWithdrawalRate); - - this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12); + this.calculateWithdrawalRates(); this.isLoading = false; @@ -88,6 +91,12 @@ export class GfFirePageComponent implements OnDestroy, OnInit { this.hasImpersonationId = !!impersonationId; }); + this.safeWithdrawalRateControl.valueChanges + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((value) => { + this.onSafeWithdrawalRateChange(Number(value)); + }); + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { @@ -102,6 +111,13 @@ export class GfFirePageComponent implements OnDestroy, OnInit { permissions.updateUserSettings ); + this.safeWithdrawalRateControl.setValue( + this.user.settings.safeWithdrawalRate, + { emitEvent: false } + ); + + this.calculateWithdrawalRates(); + this.changeDetectorRef.markForCheck(); } }); @@ -141,6 +157,25 @@ export class GfFirePageComponent implements OnDestroy, OnInit { }); }); } + + public onSafeWithdrawalRateChange(safeWithdrawalRate: number) { + this.dataService + .putUserSetting({ safeWithdrawalRate }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.userService + .get(true) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((user) => { + this.user = user; + + this.calculateWithdrawalRates(); + + this.changeDetectorRef.markForCheck(); + }); + }); + } + public onSavingsRateChange(savingsRate: number) { this.dataService .putUserSetting({ savingsRate }) @@ -180,4 +215,14 @@ export class GfFirePageComponent implements OnDestroy, OnInit { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); } + + private calculateWithdrawalRates() { + if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) { + this.withdrawalRatePerYear = new Big( + this.fireWealth.today.valueInBaseCurrency + ).mul(this.user.settings.safeWithdrawalRate); + + this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12); + } + } } diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index d6548f761..ce51717fa 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -105,15 +105,32 @@   and a safe withdrawal rate (SWR) of -   - . + @if ( + !hasImpersonationId && + hasPermissionToUpdateUserSettings && + user?.settings?.isExperimentalFeatures + ) { + . + } @else { +   + . + } } diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.scss b/apps/client/src/app/pages/portfolio/fire/fire-page.scss index 5d4e87f30..2892885c9 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.scss +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.scss @@ -1,3 +1,19 @@ :host { display: block; + + .safe-withdrawal-rate-select { + background-color: transparent; + color: rgb(var(--dark-primary-text)); + + &:focus { + box-shadow: none; + outline: 0; + } + } +} + +:host-context(.theme-dark) { + .safe-withdrawal-rate-select { + color: rgb(var(--light-primary-text)); + } }