From 3af8be89e31f1ad59dc66d7db427db53ac66f3e6 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 12 Mar 2023 09:55:55 +0100 Subject: [PATCH] Feature/improve usability of fire calculator (#1779) * Improve usability * Add debounce * Persist annualInterestRate * Partially disable date picker * Update changelog --- CHANGELOG.md | 6 ++ .../src/app/user/update-user-setting.dto.ts | 5 +- .../portfolio/fire/fire-page.component.ts | 28 +++++++ .../app/pages/portfolio/fire/fire-page.html | 18 +++-- .../lib/interfaces/user-settings.interface.ts | 1 + .../fire-calculator.component.html | 9 +++ .../fire-calculator.component.scss | 27 +++++++ .../fire-calculator.component.ts | 75 +++++++------------ 8 files changed, 111 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76bdb537f..93d1f3054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Improved the usability of the _FIRE_ calculator + ## 1.244.0 - 2023-03-09 ### Added 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 668c1bc66..0bb391a35 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -3,7 +3,6 @@ import type { DateRange, ViewMode } from '@ghostfolio/common/types'; -import { Type } from 'class-transformer'; import { IsBoolean, IsIn, @@ -14,6 +13,10 @@ import { } from 'class-validator'; export class UpdateUserSettingDto { + @IsNumber() + @IsOptional() + annualInterestRate?: number; + @IsOptional() @IsString() baseCurrency?: string; 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 f1b316df7..f5d796f54 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 @@ -1,5 +1,6 @@ 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 { PortfolioReportRule, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -20,6 +21,7 @@ export class FirePageComponent implements OnDestroy, OnInit { public deviceType: string; public feeRules: PortfolioReportRule[]; public fireWealth: Big; + public hasImpersonationId: boolean; public hasPermissionToCreateOrder: boolean; public hasPermissionToUpdateUserSettings: boolean; public isLoading = false; @@ -33,6 +35,7 @@ export class FirePageComponent implements OnDestroy, OnInit { private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, private deviceService: DeviceDetectorService, + private impersonationStorageService: ImpersonationStorageService, private userService: UserService ) {} @@ -70,6 +73,13 @@ export class FirePageComponent implements OnDestroy, OnInit { this.changeDetectorRef.markForCheck(); }); + this.impersonationStorageService + .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((impersonationId) => { + this.hasImpersonationId = !!impersonationId; + }); + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { @@ -91,6 +101,24 @@ export class FirePageComponent implements OnDestroy, OnInit { }); } + public onAnnualInterestRateChange(annualInterestRate: number) { + this.dataService + .putUserSetting({ annualInterestRate }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.userService.remove(); + + this.userService + .get() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((user) => { + this.user = user; + + this.changeDetectorRef.markForCheck(); + }); + }); + } + public onRetirementDateChange(retirementDate: Date) { this.dataService .putUserSetting({ 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 9a0196c7f..4269c3d70 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -11,15 +11,17 @@ > per month, based on your total assets of - + and a withdrawal rate of 4%. diff --git a/libs/common/src/lib/interfaces/user-settings.interface.ts b/libs/common/src/lib/interfaces/user-settings.interface.ts index 217d7ba4b..d3864ab64 100644 --- a/libs/common/src/lib/interfaces/user-settings.interface.ts +++ b/libs/common/src/lib/interfaces/user-settings.interface.ts @@ -1,6 +1,7 @@ import { ColorScheme, DateRange, ViewMode } from '@ghostfolio/common/types'; export interface UserSettings { + annualInterestRate?: number; baseCurrency?: string; benchmark?: string; colorScheme?: ColorScheme; diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.html b/libs/ui/src/lib/fire-calculator/fire-calculator.component.html index 31451872b..8037c9ad9 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.html +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.html @@ -30,18 +30,27 @@ Retirement Date +
+ {{ + calculatorForm.controls['retirementDate'].value + | date : 'MMMM YYYY' + }} +
diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.scss b/libs/ui/src/lib/fire-calculator/fire-calculator.component.scss index e02c91e3d..d44f914f4 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.scss +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.scss @@ -8,4 +8,31 @@ height: 100%; } } + + ::ng-deep { + .mdc-text-field--disabled { + .mdc-floating-label, + .mdc-text-field__input { + color: inherit; + } + + .mdc-notched-outline__leading, + .mdc-notched-outline__notch, + .mdc-notched-outline__trailing { + border-color: rgba(var(--dark-disabled-text)); + } + } + } +} + +:host-context(.is-dark-theme) { + ::ng-deep { + .mdc-text-field--disabled { + .mdc-notched-outline__leading, + .mdc-notched-outline__notch, + .mdc-notched-outline__trailing { + border-color: rgba(var(--dark-disabled-text)); + } + } + } } diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index 53a382ae3..f336d19d9 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -1,7 +1,6 @@ import 'chartjs-adapter-date-fns'; import { - AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -39,7 +38,7 @@ import { sub } from 'date-fns'; import { isNumber } from 'lodash'; -import { Subject, takeUntil } from 'rxjs'; +import { debounceTime, Subject, takeUntil } from 'rxjs'; import { FireCalculatorService } from './fire-calculator.service'; @@ -49,9 +48,8 @@ import { FireCalculatorService } from './fire-calculator.service'; styleUrls: ['./fire-calculator.component.scss'], templateUrl: './fire-calculator.component.html' }) -export class FireCalculatorComponent - implements AfterViewInit, OnChanges, OnDestroy -{ +export class FireCalculatorComponent implements OnChanges, OnDestroy { + @Input() annualInterestRate = 5; @Input() colorScheme: ColorScheme; @Input() currency: string; @Input() deviceType: string; @@ -62,6 +60,7 @@ export class FireCalculatorComponent @Input() retirementDate: Date; @Input() savingsRate = 0; + @Output() annualInterestRateChanged = new EventEmitter(); @Output() projectedTotalAmountChanged = new EventEmitter(); @Output() retirementDateChanged = new EventEmitter(); @Output() savingsRateChanged = new EventEmitter(); @@ -100,7 +99,7 @@ export class FireCalculatorComponent this.calculatorForm.setValue( { - annualInterestRate: 5, + annualInterestRate: this.annualInterestRate, paymentPerPeriod: this.savingsRate, principalInvestmentAmount: 0, projectedTotalAmount: this.projectedTotalAmount, @@ -117,75 +116,45 @@ export class FireCalculatorComponent this.initialize(); }); + this.calculatorForm + .get('annualInterestRate') + .valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) + .subscribe((annualInterestRate) => { + this.annualInterestRateChanged.emit(annualInterestRate); + }); this.calculatorForm .get('paymentPerPeriod') - .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) + .valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) .subscribe((savingsRate) => { this.savingsRateChanged.emit(savingsRate); }); this.calculatorForm .get('projectedTotalAmount') - .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) + .valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) .subscribe((projectedTotalAmount) => { this.projectedTotalAmountChanged.emit(projectedTotalAmount); }); this.calculatorForm .get('retirementDate') - .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) + .valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) .subscribe((retirementDate) => { this.retirementDateChanged.emit(retirementDate); }); } - public ngAfterViewInit() { - if (isNumber(this.fireWealth) && this.fireWealth >= 0) { - setTimeout(() => { - // Wait for the chartCanvas - this.calculatorForm.patchValue( - { - paymentPerPeriod: this.getPMT(), - principalInvestmentAmount: this.getP(), - projectedTotalAmount: - Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0, - retirementDate: - this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE - }, - { - emitEvent: false - } - ); - this.calculatorForm.get('principalInvestmentAmount').disable(); - - this.changeDetectorRef.markForCheck(); - }); - } - - if (this.hasPermissionToUpdateUserSettings === true) { - this.calculatorForm.get('paymentPerPeriod').enable({ emitEvent: false }); - this.calculatorForm - .get('projectedTotalAmount') - .enable({ emitEvent: false }); - this.calculatorForm.get('retirementDate').enable({ emitEvent: false }); - } else { - this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false }); - this.calculatorForm - .get('projectedTotalAmount') - .disable({ emitEvent: false }); - this.calculatorForm.get('retirementDate').disable({ emitEvent: false }); - } - } - public ngOnChanges() { this.periodsToRetire = this.getPeriodsToRetire(); + if (isNumber(this.fireWealth) && this.fireWealth >= 0) { setTimeout(() => { // Wait for the chartCanvas this.calculatorForm.patchValue( { + annualInterestRate: this.annualInterestRate, principalInvestmentAmount: this.fireWealth, paymentPerPeriod: this.savingsRate ?? 0, projectedTotalAmount: - Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0, + Number(this.getProjectedTotalAmount().toFixed(0)) ?? 0, retirementDate: this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE }, @@ -200,18 +169,24 @@ export class FireCalculatorComponent } if (this.hasPermissionToUpdateUserSettings === true) { + this.calculatorForm + .get('annualInterestRate') + .enable({ emitEvent: false }); this.calculatorForm.get('paymentPerPeriod').enable({ emitEvent: false }); this.calculatorForm .get('projectedTotalAmount') .enable({ emitEvent: false }); - this.calculatorForm.get('retirementDate').enable({ emitEvent: false }); } else { + this.calculatorForm + .get('annualInterestRate') + .disable({ emitEvent: false }); this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false }); this.calculatorForm .get('projectedTotalAmount') .disable({ emitEvent: false }); - this.calculatorForm.get('retirementDate').disable({ emitEvent: false }); } + + this.calculatorForm.get('retirementDate').disable({ emitEvent: false }); } public setMonthAndYear(