Feature/improve usability of fire calculator (#1779)
* Improve usability * Add debounce * Persist annualInterestRate * Partially disable date picker * Update changelog
This commit is contained in:
parent
0f1db71604
commit
3af8be89e3
@ -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/),
|
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).
|
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
|
## 1.244.0 - 2023-03-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -3,7 +3,6 @@ import type {
|
|||||||
DateRange,
|
DateRange,
|
||||||
ViewMode
|
ViewMode
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
import { Type } from 'class-transformer';
|
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsIn,
|
IsIn,
|
||||||
@ -14,6 +13,10 @@ import {
|
|||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
export class UpdateUserSettingDto {
|
export class UpdateUserSettingDto {
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
annualInterestRate?: number;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
baseCurrency?: string;
|
baseCurrency?: string;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
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 { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { PortfolioReportRule, User } from '@ghostfolio/common/interfaces';
|
import { PortfolioReportRule, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
@ -20,6 +21,7 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public feeRules: PortfolioReportRule[];
|
public feeRules: PortfolioReportRule[];
|
||||||
public fireWealth: Big;
|
public fireWealth: Big;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
@ -33,6 +35,7 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -70,6 +73,13 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((impersonationId) => {
|
||||||
|
this.hasImpersonationId = !!impersonationId;
|
||||||
|
});
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.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) {
|
public onRetirementDateChange(retirementDate: Date) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.putUserSetting({
|
.putUserSetting({
|
||||||
|
@ -11,15 +11,17 @@
|
|||||||
></gf-premium-indicator>
|
></gf-premium-indicator>
|
||||||
</h4>
|
</h4>
|
||||||
<gf-fire-calculator
|
<gf-fire-calculator
|
||||||
|
[annualInterestRate]="user?.settings?.annualInterestRate"
|
||||||
[colorScheme]="user?.settings?.colorScheme"
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[currency]="user?.settings?.baseCurrency"
|
[currency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[fireWealth]="fireWealth?.toNumber()"
|
[fireWealth]="fireWealth?.toNumber()"
|
||||||
[hasPermissionToUpdateUserSettings]="hasPermissionToUpdateUserSettings"
|
[hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[projectedTotalAmount]="user?.settings?.projectedTotalAmount"
|
[projectedTotalAmount]="user?.settings?.projectedTotalAmount"
|
||||||
[retirementDate]="user?.settings?.retirementDate"
|
[retirementDate]="user?.settings?.retirementDate"
|
||||||
[savingsRate]="user?.settings?.savingsRate"
|
[savingsRate]="user?.settings?.savingsRate"
|
||||||
|
(annualInterestRateChanged)="onAnnualInterestRateChange($event)"
|
||||||
(projectedTotalAmountChanged)="onProjectedTotalAmountChange($event)"
|
(projectedTotalAmountChanged)="onProjectedTotalAmountChange($event)"
|
||||||
(retirementDateChanged)="onRetirementDateChange($event)"
|
(retirementDateChanged)="onRetirementDateChange($event)"
|
||||||
(savingsRateChanged)="onSavingsRateChange($event)"
|
(savingsRateChanged)="onSavingsRateChange($event)"
|
||||||
@ -73,12 +75,14 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
per month</span
|
per month</span
|
||||||
>, based on your total assets of
|
>, based on your total assets of
|
||||||
<gf-value
|
<span class="font-weight-bold"
|
||||||
class="d-inline-block"
|
><gf-value
|
||||||
[currency]="user?.settings?.baseCurrency"
|
class="d-inline-block"
|
||||||
[locale]="user?.settings?.locale"
|
[currency]="user?.settings?.baseCurrency"
|
||||||
[value]="fireWealth?.toNumber()"
|
[locale]="user?.settings?.locale"
|
||||||
></gf-value>
|
[value]="fireWealth?.toNumber()"
|
||||||
|
></gf-value
|
||||||
|
></span>
|
||||||
and a withdrawal rate of 4%.
|
and a withdrawal rate of 4%.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ColorScheme, DateRange, ViewMode } from '@ghostfolio/common/types';
|
import { ColorScheme, DateRange, ViewMode } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
|
annualInterestRate?: number;
|
||||||
baseCurrency?: string;
|
baseCurrency?: string;
|
||||||
benchmark?: string;
|
benchmark?: string;
|
||||||
colorScheme?: ColorScheme;
|
colorScheme?: ColorScheme;
|
||||||
|
@ -30,18 +30,27 @@
|
|||||||
|
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Retirement Date</mat-label>
|
<mat-label i18n>Retirement Date</mat-label>
|
||||||
|
<div>
|
||||||
|
{{
|
||||||
|
calculatorForm.controls['retirementDate'].value
|
||||||
|
| date : 'MMMM YYYY'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
|
class="d-none"
|
||||||
formControlName="retirementDate"
|
formControlName="retirementDate"
|
||||||
matInput
|
matInput
|
||||||
[matDatepicker]="datepicker"
|
[matDatepicker]="datepicker"
|
||||||
/>
|
/>
|
||||||
<mat-datepicker-toggle
|
<mat-datepicker-toggle
|
||||||
matIconSuffix
|
matIconSuffix
|
||||||
|
[disabled]="hasPermissionToUpdateUserSettings !== true"
|
||||||
[for]="datepicker"
|
[for]="datepicker"
|
||||||
></mat-datepicker-toggle>
|
></mat-datepicker-toggle>
|
||||||
<mat-datepicker
|
<mat-datepicker
|
||||||
#datepicker
|
#datepicker
|
||||||
startView="multi-year"
|
startView="multi-year"
|
||||||
|
[disabled]="hasPermissionToUpdateUserSettings !== true"
|
||||||
(monthSelected)="setMonthAndYear($event, datepicker)"
|
(monthSelected)="setMonthAndYear($event, datepicker)"
|
||||||
>
|
>
|
||||||
</mat-datepicker>
|
</mat-datepicker>
|
||||||
|
@ -8,4 +8,31 @@
|
|||||||
height: 100%;
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'chartjs-adapter-date-fns';
|
import 'chartjs-adapter-date-fns';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
@ -39,7 +38,7 @@ import {
|
|||||||
sub
|
sub
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { debounceTime, Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { FireCalculatorService } from './fire-calculator.service';
|
import { FireCalculatorService } from './fire-calculator.service';
|
||||||
|
|
||||||
@ -49,9 +48,8 @@ import { FireCalculatorService } from './fire-calculator.service';
|
|||||||
styleUrls: ['./fire-calculator.component.scss'],
|
styleUrls: ['./fire-calculator.component.scss'],
|
||||||
templateUrl: './fire-calculator.component.html'
|
templateUrl: './fire-calculator.component.html'
|
||||||
})
|
})
|
||||||
export class FireCalculatorComponent
|
export class FireCalculatorComponent implements OnChanges, OnDestroy {
|
||||||
implements AfterViewInit, OnChanges, OnDestroy
|
@Input() annualInterestRate = 5;
|
||||||
{
|
|
||||||
@Input() colorScheme: ColorScheme;
|
@Input() colorScheme: ColorScheme;
|
||||||
@Input() currency: string;
|
@Input() currency: string;
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
@ -62,6 +60,7 @@ export class FireCalculatorComponent
|
|||||||
@Input() retirementDate: Date;
|
@Input() retirementDate: Date;
|
||||||
@Input() savingsRate = 0;
|
@Input() savingsRate = 0;
|
||||||
|
|
||||||
|
@Output() annualInterestRateChanged = new EventEmitter<number>();
|
||||||
@Output() projectedTotalAmountChanged = new EventEmitter<number>();
|
@Output() projectedTotalAmountChanged = new EventEmitter<number>();
|
||||||
@Output() retirementDateChanged = new EventEmitter<Date>();
|
@Output() retirementDateChanged = new EventEmitter<Date>();
|
||||||
@Output() savingsRateChanged = new EventEmitter<number>();
|
@Output() savingsRateChanged = new EventEmitter<number>();
|
||||||
@ -100,7 +99,7 @@ export class FireCalculatorComponent
|
|||||||
|
|
||||||
this.calculatorForm.setValue(
|
this.calculatorForm.setValue(
|
||||||
{
|
{
|
||||||
annualInterestRate: 5,
|
annualInterestRate: this.annualInterestRate,
|
||||||
paymentPerPeriod: this.savingsRate,
|
paymentPerPeriod: this.savingsRate,
|
||||||
principalInvestmentAmount: 0,
|
principalInvestmentAmount: 0,
|
||||||
projectedTotalAmount: this.projectedTotalAmount,
|
projectedTotalAmount: this.projectedTotalAmount,
|
||||||
@ -117,75 +116,45 @@ export class FireCalculatorComponent
|
|||||||
this.initialize();
|
this.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.calculatorForm
|
||||||
|
.get('annualInterestRate')
|
||||||
|
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((annualInterestRate) => {
|
||||||
|
this.annualInterestRateChanged.emit(annualInterestRate);
|
||||||
|
});
|
||||||
this.calculatorForm
|
this.calculatorForm
|
||||||
.get('paymentPerPeriod')
|
.get('paymentPerPeriod')
|
||||||
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
|
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((savingsRate) => {
|
.subscribe((savingsRate) => {
|
||||||
this.savingsRateChanged.emit(savingsRate);
|
this.savingsRateChanged.emit(savingsRate);
|
||||||
});
|
});
|
||||||
this.calculatorForm
|
this.calculatorForm
|
||||||
.get('projectedTotalAmount')
|
.get('projectedTotalAmount')
|
||||||
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
|
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((projectedTotalAmount) => {
|
.subscribe((projectedTotalAmount) => {
|
||||||
this.projectedTotalAmountChanged.emit(projectedTotalAmount);
|
this.projectedTotalAmountChanged.emit(projectedTotalAmount);
|
||||||
});
|
});
|
||||||
this.calculatorForm
|
this.calculatorForm
|
||||||
.get('retirementDate')
|
.get('retirementDate')
|
||||||
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
|
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((retirementDate) => {
|
.subscribe((retirementDate) => {
|
||||||
this.retirementDateChanged.emit(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() {
|
public ngOnChanges() {
|
||||||
this.periodsToRetire = this.getPeriodsToRetire();
|
this.periodsToRetire = this.getPeriodsToRetire();
|
||||||
|
|
||||||
if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
|
if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Wait for the chartCanvas
|
// Wait for the chartCanvas
|
||||||
this.calculatorForm.patchValue(
|
this.calculatorForm.patchValue(
|
||||||
{
|
{
|
||||||
|
annualInterestRate: this.annualInterestRate,
|
||||||
principalInvestmentAmount: this.fireWealth,
|
principalInvestmentAmount: this.fireWealth,
|
||||||
paymentPerPeriod: this.savingsRate ?? 0,
|
paymentPerPeriod: this.savingsRate ?? 0,
|
||||||
projectedTotalAmount:
|
projectedTotalAmount:
|
||||||
Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0,
|
Number(this.getProjectedTotalAmount().toFixed(0)) ?? 0,
|
||||||
retirementDate:
|
retirementDate:
|
||||||
this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE
|
this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE
|
||||||
},
|
},
|
||||||
@ -200,18 +169,24 @@ export class FireCalculatorComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasPermissionToUpdateUserSettings === true) {
|
if (this.hasPermissionToUpdateUserSettings === true) {
|
||||||
|
this.calculatorForm
|
||||||
|
.get('annualInterestRate')
|
||||||
|
.enable({ emitEvent: false });
|
||||||
this.calculatorForm.get('paymentPerPeriod').enable({ emitEvent: false });
|
this.calculatorForm.get('paymentPerPeriod').enable({ emitEvent: false });
|
||||||
this.calculatorForm
|
this.calculatorForm
|
||||||
.get('projectedTotalAmount')
|
.get('projectedTotalAmount')
|
||||||
.enable({ emitEvent: false });
|
.enable({ emitEvent: false });
|
||||||
this.calculatorForm.get('retirementDate').enable({ emitEvent: false });
|
|
||||||
} else {
|
} else {
|
||||||
|
this.calculatorForm
|
||||||
|
.get('annualInterestRate')
|
||||||
|
.disable({ emitEvent: false });
|
||||||
this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false });
|
this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false });
|
||||||
this.calculatorForm
|
this.calculatorForm
|
||||||
.get('projectedTotalAmount')
|
.get('projectedTotalAmount')
|
||||||
.disable({ emitEvent: false });
|
.disable({ emitEvent: false });
|
||||||
this.calculatorForm.get('retirementDate').disable({ emitEvent: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.calculatorForm.get('retirementDate').disable({ emitEvent: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
public setMonthAndYear(
|
public setMonthAndYear(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user