Feature/extend fire calculator by retirement date (#1748)
* Extend fire calculator by retirement date * Update changelog --------- Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
This commit is contained in:
parent
6301c0c21c
commit
fce9e7fb0c
@ -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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the _FIRE_ calculator by a retirement date setting
|
||||||
|
|
||||||
## 1.243.0 - 2023-03-08
|
## 1.243.0 - 2023-03-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -3,9 +3,11 @@ 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,
|
||||||
|
IsISO8601,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString
|
IsString
|
||||||
@ -48,6 +50,14 @@ export class UpdateUserSettingDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
projectedTotalAmount?: number;
|
||||||
|
|
||||||
|
@IsISO8601()
|
||||||
|
@IsOptional()
|
||||||
|
retirementDate?: string;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
savingsRate?: number;
|
savingsRate?: number;
|
||||||
|
@ -91,6 +91,27 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onRetirementDateChange(retirementDate: Date) {
|
||||||
|
this.dataService
|
||||||
|
.putUserSetting({
|
||||||
|
retirementDate: retirementDate.toISOString(),
|
||||||
|
projectedTotalAmount: null
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.userService.remove();
|
||||||
|
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onSavingsRateChange(savingsRate: number) {
|
public onSavingsRateChange(savingsRate: number) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.putUserSetting({ savingsRate })
|
.putUserSetting({ savingsRate })
|
||||||
@ -109,6 +130,27 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onProjectedTotalAmountChange(projectedTotalAmount: number) {
|
||||||
|
this.dataService
|
||||||
|
.putUserSetting({
|
||||||
|
projectedTotalAmount,
|
||||||
|
retirementDate: null
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.userService.remove();
|
||||||
|
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -17,7 +17,11 @@
|
|||||||
[fireWealth]="fireWealth?.toNumber()"
|
[fireWealth]="fireWealth?.toNumber()"
|
||||||
[hasPermissionToUpdateUserSettings]="hasPermissionToUpdateUserSettings"
|
[hasPermissionToUpdateUserSettings]="hasPermissionToUpdateUserSettings"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
|
[projectedTotalAmount]="user?.settings?.projectedTotalAmount"
|
||||||
|
[retirementDate]="user?.settings?.retirementDate"
|
||||||
[savingsRate]="user?.settings?.savingsRate"
|
[savingsRate]="user?.settings?.savingsRate"
|
||||||
|
(projectedTotalAmountChanged)="onProjectedTotalAmountChange($event)"
|
||||||
|
(retirementDateChanged)="onRetirementDateChange($event)"
|
||||||
(savingsRateChanged)="onSavingsRateChange($event)"
|
(savingsRateChanged)="onSavingsRateChange($event)"
|
||||||
></gf-fire-calculator>
|
></gf-fire-calculator>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,8 +6,9 @@ import { SubscriptionInterstitialDialogParams } from '@ghostfolio/client/compone
|
|||||||
import { SubscriptionInterstitialDialog } from '@ghostfolio/client/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component';
|
import { SubscriptionInterstitialDialog } from '@ghostfolio/client/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import { parseISO } from 'date-fns';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, of } from 'rxjs';
|
import { Subject, of, Observable } from 'rxjs';
|
||||||
import { throwError } from 'rxjs';
|
import { throwError } from 'rxjs';
|
||||||
import { catchError, map, takeUntil } from 'rxjs/operators';
|
import { catchError, map, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -49,9 +50,13 @@ export class UserService extends ObservableStore<UserStoreState> {
|
|||||||
this.setState({ user: null }, UserStoreActions.RemoveUser);
|
this.setState({ user: null }, UserStoreActions.RemoveUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchUser() {
|
private fetchUser(): Observable<User> {
|
||||||
return this.http.get<User>('/api/v1/user').pipe(
|
return this.http.get<any>('/api/v1/user').pipe(
|
||||||
map((user) => {
|
map((user) => {
|
||||||
|
if (user.settings?.retirementDate) {
|
||||||
|
user.settings.retirementDate = parseISO(user.settings.retirementDate);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ user }, UserStoreActions.GetUser);
|
this.setState({ user }, UserStoreActions.GetUser);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -3505,6 +3505,22 @@
|
|||||||
<context context-type="linenumber">199,201</context>
|
<context context-type="linenumber">199,201</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="46d3a0f17b741c93f9e61aa7157820da41506f53" datatype="html">
|
||||||
|
<source>Target Net Worth</source>
|
||||||
|
<target state="new">Angestrebtes Nettovermögen</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context>
|
||||||
|
<context context-type="linenumber">38</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7383cd391b1967e03f0636c231d20f036d5c37ee" datatype="html">
|
||||||
|
<source>Retirement Date</source>
|
||||||
|
<target state="translated">Pensionierungsdatum</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context>
|
||||||
|
<context context-type="linenumber">32</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
@ -2434,7 +2434,7 @@
|
|||||||
<target state="translated">Verwacht totaalbedrag</target>
|
<target state="translated">Verwacht totaalbedrag</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context>
|
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context>
|
||||||
<context context-type="linenumber">44</context>
|
<context context-type="linenumber">52</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1054498214311181686" datatype="html">
|
<trans-unit id="1054498214311181686" datatype="html">
|
||||||
@ -3505,6 +3505,22 @@
|
|||||||
<context context-type="linenumber">199,201</context>
|
<context context-type="linenumber">199,201</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="46d3a0f17b741c93f9e61aa7157820da41506f53" datatype="html">
|
||||||
|
<source>Target Net Worth</source>
|
||||||
|
<target state="translated">Beoogd Netto Vermogen</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context>
|
||||||
|
<context context-type="linenumber">38</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7383cd391b1967e03f0636c231d20f036d5c37ee" datatype="html">
|
||||||
|
<source>Retirement Date</source>
|
||||||
|
<target state="translated">Pensioen Datum</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context>
|
||||||
|
<context context-type="linenumber">32</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
@ -2193,7 +2193,7 @@
|
|||||||
<source>Projected Total Amount</source>
|
<source>Projected Total Amount</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context>
|
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context>
|
||||||
<context context-type="linenumber">44</context>
|
<context context-type="linenumber">52</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1054498214311181686" datatype="html">
|
<trans-unit id="1054498214311181686" datatype="html">
|
||||||
@ -3146,6 +3146,20 @@
|
|||||||
<context context-type="linenumber">199,201</context>
|
<context context-type="linenumber">199,201</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="46d3a0f17b741c93f9e61aa7157820da41506f53" datatype="html">
|
||||||
|
<source>Target Net Worth</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context>
|
||||||
|
<context context-type="linenumber">38</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7383cd391b1967e03f0636c231d20f036d5c37ee" datatype="html">
|
||||||
|
<source>Retirement Date</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">libs/ui/src/lib/fire-calculator/fire-calculator.component.html</context>
|
||||||
|
<context context-type="linenumber">32</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
@ -10,6 +10,8 @@ export interface UserSettings {
|
|||||||
isRestrictedView?: boolean;
|
isRestrictedView?: boolean;
|
||||||
language?: string;
|
language?: string;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
projectedTotalAmount?: number;
|
||||||
|
retirementDate?: string;
|
||||||
savingsRate?: number;
|
savingsRate?: number;
|
||||||
viewMode?: ViewMode;
|
viewMode?: ViewMode;
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,6 @@
|
|||||||
<span class="ml-2" matTextSuffix>{{ currency }}</span>
|
<span class="ml-2" matTextSuffix>{{ currency }}</span>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
|
||||||
<mat-label i18n>Investment Horizon</mat-label>
|
|
||||||
<input formControlName="time" matInput type="number" />
|
|
||||||
<span class="ml-2" i18n matTextSuffix>years</span>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Annual Interest Rate</mat-label>
|
<mat-label i18n>Annual Interest Rate</mat-label>
|
||||||
<input
|
<input
|
||||||
@ -34,15 +28,35 @@
|
|||||||
<div class="ml-2" matTextSuffix>%</div>
|
<div class="ml-2" matTextSuffix>%</div>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<gf-value
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
i18n
|
<mat-label i18n>Retirement Date</mat-label>
|
||||||
size="large"
|
<input
|
||||||
[currency]="currency"
|
formControlName="retirementDate"
|
||||||
[isCurrency]="true"
|
matInput
|
||||||
[locale]="locale"
|
[matDatepicker]="datepicker"
|
||||||
[value]="projectedTotalAmount"
|
/>
|
||||||
>Projected Total Amount</gf-value
|
<mat-datepicker-toggle
|
||||||
>
|
matIconSuffix
|
||||||
|
[for]="datepicker"
|
||||||
|
></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker
|
||||||
|
#datepicker
|
||||||
|
startView="multi-year"
|
||||||
|
(monthSelected)="setMonthAndYear($event, datepicker)"
|
||||||
|
>
|
||||||
|
</mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Projected Total Amount</mat-label>
|
||||||
|
<input
|
||||||
|
formControlName="projectedTotalAmount"
|
||||||
|
matInput
|
||||||
|
step="100"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<span class="ml-2" matTextSuffix>{{ currency }}</span>
|
||||||
|
</mat-form-field>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9 text-center">
|
<div class="col-md-9 text-center">
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormBuilder, FormControl } from '@angular/forms';
|
import { FormBuilder, FormControl } from '@angular/forms';
|
||||||
|
import { MatDatepicker } from '@angular/material/datepicker';
|
||||||
import {
|
import {
|
||||||
getTooltipOptions,
|
getTooltipOptions,
|
||||||
transformTickToAbbreviation
|
transformTickToAbbreviation
|
||||||
@ -28,17 +29,25 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import * as Color from 'color';
|
import * as Color from 'color';
|
||||||
import { getMonth } from 'date-fns';
|
import {
|
||||||
|
add,
|
||||||
|
addYears,
|
||||||
|
getMonth,
|
||||||
|
setMonth,
|
||||||
|
setYear,
|
||||||
|
startOfMonth,
|
||||||
|
sub
|
||||||
|
} from 'date-fns';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { FireCalculatorService } from './fire-calculator.service';
|
import { FireCalculatorService } from './fire-calculator.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-fire-calculator',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
templateUrl: './fire-calculator.component.html',
|
selector: 'gf-fire-calculator',
|
||||||
styleUrls: ['./fire-calculator.component.scss']
|
styleUrls: ['./fire-calculator.component.scss'],
|
||||||
|
templateUrl: './fire-calculator.component.html'
|
||||||
})
|
})
|
||||||
export class FireCalculatorComponent
|
export class FireCalculatorComponent
|
||||||
implements AfterViewInit, OnChanges, OnDestroy
|
implements AfterViewInit, OnChanges, OnDestroy
|
||||||
@ -49,8 +58,12 @@ export class FireCalculatorComponent
|
|||||||
@Input() fireWealth: number;
|
@Input() fireWealth: number;
|
||||||
@Input() hasPermissionToUpdateUserSettings: boolean;
|
@Input() hasPermissionToUpdateUserSettings: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
|
@Input() projectedTotalAmount = 0;
|
||||||
|
@Input() retirementDate: Date;
|
||||||
@Input() savingsRate = 0;
|
@Input() savingsRate = 0;
|
||||||
|
|
||||||
|
@Output() projectedTotalAmountChanged = new EventEmitter<number>();
|
||||||
|
@Output() retirementDateChanged = new EventEmitter<Date>();
|
||||||
@Output() savingsRateChanged = new EventEmitter<number>();
|
@Output() savingsRateChanged = new EventEmitter<number>();
|
||||||
|
|
||||||
@ViewChild('chartCanvas') chartCanvas;
|
@ViewChild('chartCanvas') chartCanvas;
|
||||||
@ -59,13 +72,17 @@ export class FireCalculatorComponent
|
|||||||
annualInterestRate: new FormControl<number>(undefined),
|
annualInterestRate: new FormControl<number>(undefined),
|
||||||
paymentPerPeriod: new FormControl<number>(undefined),
|
paymentPerPeriod: new FormControl<number>(undefined),
|
||||||
principalInvestmentAmount: new FormControl<number>(undefined),
|
principalInvestmentAmount: new FormControl<number>(undefined),
|
||||||
time: new FormControl<number>(undefined)
|
projectedTotalAmount: new FormControl<number>(undefined),
|
||||||
|
retirementDate: new FormControl<Date>(undefined)
|
||||||
});
|
});
|
||||||
public chart: Chart<'bar'>;
|
public chart: Chart<'bar'>;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public projectedTotalAmount: number;
|
public periodsToRetire = 0;
|
||||||
|
|
||||||
private readonly CONTRIBUTION_PERIOD = 12;
|
private readonly CONTRIBUTION_PERIOD = 12;
|
||||||
|
private readonly DEFAULT_RETIREMENT_DATE = startOfMonth(
|
||||||
|
addYears(new Date(), 10)
|
||||||
|
);
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -86,7 +103,8 @@ export class FireCalculatorComponent
|
|||||||
annualInterestRate: 5,
|
annualInterestRate: 5,
|
||||||
paymentPerPeriod: this.savingsRate,
|
paymentPerPeriod: this.savingsRate,
|
||||||
principalInvestmentAmount: 0,
|
principalInvestmentAmount: 0,
|
||||||
time: 10
|
projectedTotalAmount: this.projectedTotalAmount,
|
||||||
|
retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
emitEvent: false
|
emitEvent: false
|
||||||
@ -105,6 +123,18 @@ export class FireCalculatorComponent
|
|||||||
.subscribe((savingsRate) => {
|
.subscribe((savingsRate) => {
|
||||||
this.savingsRateChanged.emit(savingsRate);
|
this.savingsRateChanged.emit(savingsRate);
|
||||||
});
|
});
|
||||||
|
this.calculatorForm
|
||||||
|
.get('projectedTotalAmount')
|
||||||
|
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((projectedTotalAmount) => {
|
||||||
|
this.projectedTotalAmountChanged.emit(projectedTotalAmount);
|
||||||
|
});
|
||||||
|
this.calculatorForm
|
||||||
|
.get('retirementDate')
|
||||||
|
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((retirementDate) => {
|
||||||
|
this.retirementDateChanged.emit(retirementDate);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngAfterViewInit() {
|
public ngAfterViewInit() {
|
||||||
@ -113,8 +143,12 @@ export class FireCalculatorComponent
|
|||||||
// Wait for the chartCanvas
|
// Wait for the chartCanvas
|
||||||
this.calculatorForm.patchValue(
|
this.calculatorForm.patchValue(
|
||||||
{
|
{
|
||||||
principalInvestmentAmount: this.fireWealth,
|
paymentPerPeriod: this.getPMT(),
|
||||||
paymentPerPeriod: this.savingsRate ?? 0
|
principalInvestmentAmount: this.getP(),
|
||||||
|
projectedTotalAmount:
|
||||||
|
Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0,
|
||||||
|
retirementDate:
|
||||||
|
this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
emitEvent: false
|
emitEvent: false
|
||||||
@ -128,19 +162,32 @@ export class FireCalculatorComponent
|
|||||||
|
|
||||||
if (this.hasPermissionToUpdateUserSettings === true) {
|
if (this.hasPermissionToUpdateUserSettings === true) {
|
||||||
this.calculatorForm.get('paymentPerPeriod').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 {
|
} else {
|
||||||
this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false });
|
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();
|
||||||
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(
|
||||||
{
|
{
|
||||||
principalInvestmentAmount: this.fireWealth,
|
principalInvestmentAmount: this.fireWealth,
|
||||||
paymentPerPeriod: this.savingsRate ?? 0
|
paymentPerPeriod: this.savingsRate ?? 0,
|
||||||
|
projectedTotalAmount:
|
||||||
|
Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0,
|
||||||
|
retirementDate:
|
||||||
|
this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
emitEvent: false
|
emitEvent: false
|
||||||
@ -154,11 +201,32 @@ export class FireCalculatorComponent
|
|||||||
|
|
||||||
if (this.hasPermissionToUpdateUserSettings === true) {
|
if (this.hasPermissionToUpdateUserSettings === true) {
|
||||||
this.calculatorForm.get('paymentPerPeriod').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 {
|
} else {
|
||||||
this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false });
|
this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false });
|
||||||
|
this.calculatorForm
|
||||||
|
.get('projectedTotalAmount')
|
||||||
|
.disable({ emitEvent: false });
|
||||||
|
this.calculatorForm.get('retirementDate').disable({ emitEvent: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setMonthAndYear(
|
||||||
|
normalizedMonthAndYear: Date,
|
||||||
|
datepicker: MatDatepicker<Date>
|
||||||
|
) {
|
||||||
|
const retirementDate = this.calculatorForm.get('retirementDate').value;
|
||||||
|
const newRetirementDate = setMonth(
|
||||||
|
setYear(retirementDate, normalizedMonthAndYear.getFullYear()),
|
||||||
|
normalizedMonthAndYear.getMonth()
|
||||||
|
);
|
||||||
|
this.calculatorForm.get('retirementDate').setValue(newRetirementDate);
|
||||||
|
datepicker.close();
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.chart?.destroy();
|
this.chart?.destroy();
|
||||||
|
|
||||||
@ -261,17 +329,22 @@ export class FireCalculatorComponent
|
|||||||
const labels = [];
|
const labels = [];
|
||||||
|
|
||||||
// Principal investment amount
|
// Principal investment amount
|
||||||
const P: number =
|
const P: number = this.getP();
|
||||||
this.calculatorForm.get('principalInvestmentAmount').value || 0;
|
|
||||||
|
|
||||||
// Payment per period
|
// Payment per period
|
||||||
const PMT = this.calculatorForm.get('paymentPerPeriod').value;
|
const PMT = this.getPMT();
|
||||||
|
|
||||||
// Annual interest rate
|
// Annual interest rate
|
||||||
const r: number = this.calculatorForm.get('annualInterestRate').value / 100;
|
const r: number = this.getR();
|
||||||
|
|
||||||
|
// Calculate retirement date
|
||||||
|
// if we want to retire at month x, we need the projectedTotalAmount at month x-1
|
||||||
|
const lastPeriodDate = sub(this.getRetirementDate(), { months: 1 });
|
||||||
|
const yearsToRetire = lastPeriodDate.getFullYear() - currentYear;
|
||||||
|
|
||||||
// Time
|
// Time
|
||||||
const t = this.calculatorForm.get('time').value;
|
// +1 to take into account the current year
|
||||||
|
const t = yearsToRetire + 1;
|
||||||
|
|
||||||
for (let year = currentYear; year < currentYear + t; year++) {
|
for (let year = currentYear; year < currentYear + t; year++) {
|
||||||
labels.push(year);
|
labels.push(year);
|
||||||
@ -308,7 +381,7 @@ export class FireCalculatorComponent
|
|||||||
for (let period = 1; period <= t; period++) {
|
for (let period = 1; period <= t; period++) {
|
||||||
const periodInMonths =
|
const periodInMonths =
|
||||||
period * this.CONTRIBUTION_PERIOD - monthsPassedInCurrentYear;
|
period * this.CONTRIBUTION_PERIOD - monthsPassedInCurrentYear;
|
||||||
const { interest, principal, totalAmount } =
|
const { interest, principal } =
|
||||||
this.fireCalculatorService.calculateCompoundInterest({
|
this.fireCalculatorService.calculateCompoundInterest({
|
||||||
P,
|
P,
|
||||||
periodInMonths,
|
periodInMonths,
|
||||||
@ -319,10 +392,6 @@ export class FireCalculatorComponent
|
|||||||
datasetDeposit.data.push(this.fireWealth);
|
datasetDeposit.data.push(this.fireWealth);
|
||||||
datasetInterest.data.push(interest.toNumber());
|
datasetInterest.data.push(interest.toNumber());
|
||||||
datasetSavings.data.push(principal.minus(this.fireWealth).toNumber());
|
datasetSavings.data.push(principal.minus(this.fireWealth).toNumber());
|
||||||
|
|
||||||
if (period === t) {
|
|
||||||
this.projectedTotalAmount = totalAmount.toNumber();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -330,4 +399,67 @@ export class FireCalculatorComponent
|
|||||||
datasets: [datasetDeposit, datasetSavings, datasetInterest]
|
datasets: [datasetDeposit, datasetSavings, datasetInterest]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getP() {
|
||||||
|
return this.fireWealth || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPeriodsToRetire(): number {
|
||||||
|
if (this.projectedTotalAmount) {
|
||||||
|
const periods = this.fireCalculatorService.calculatePeriodsToRetire({
|
||||||
|
P: this.getP(),
|
||||||
|
totalAmount: this.projectedTotalAmount,
|
||||||
|
PMT: this.getPMT(),
|
||||||
|
r: this.getR()
|
||||||
|
});
|
||||||
|
|
||||||
|
return periods;
|
||||||
|
} else {
|
||||||
|
const today = new Date();
|
||||||
|
const retirementDate =
|
||||||
|
this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE;
|
||||||
|
|
||||||
|
return (
|
||||||
|
12 * (retirementDate.getFullYear() - today.getFullYear()) +
|
||||||
|
retirementDate.getMonth() -
|
||||||
|
today.getMonth()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPMT() {
|
||||||
|
return this.savingsRate ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getProjectedTotalAmount() {
|
||||||
|
if (this.projectedTotalAmount) {
|
||||||
|
return this.projectedTotalAmount || 0;
|
||||||
|
} else {
|
||||||
|
const { totalAmount } =
|
||||||
|
this.fireCalculatorService.calculateCompoundInterest({
|
||||||
|
P: this.getP(),
|
||||||
|
periodInMonths: this.periodsToRetire,
|
||||||
|
PMT: this.getPMT(),
|
||||||
|
r: this.getR()
|
||||||
|
});
|
||||||
|
|
||||||
|
return totalAmount.toNumber();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getR() {
|
||||||
|
return this.calculatorForm.get('annualInterestRate').value / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRetirementDate(): Date {
|
||||||
|
const monthsToRetire = this.periodsToRetire % 12;
|
||||||
|
const yearsToRetire = Math.floor(this.periodsToRetire / 12);
|
||||||
|
|
||||||
|
return startOfMonth(
|
||||||
|
add(new Date(), {
|
||||||
|
months: monthsToRetire,
|
||||||
|
years: yearsToRetire
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,11 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfValueModule } from '../value';
|
|
||||||
import { FireCalculatorComponent } from './fire-calculator.component';
|
import { FireCalculatorComponent } from './fire-calculator.component';
|
||||||
import { FireCalculatorService } from './fire-calculator.service';
|
import { FireCalculatorService } from './fire-calculator.service';
|
||||||
|
|
||||||
@ -16,8 +16,8 @@ import { FireCalculatorService } from './fire-calculator.service';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
GfValueModule,
|
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatDatepickerModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
NgxSkeletonLoaderModule,
|
NgxSkeletonLoaderModule,
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
|
import { FireCalculatorService } from './fire-calculator.service';
|
||||||
|
|
||||||
|
describe('FireCalculatorService', () => {
|
||||||
|
let fireCalculatorService: FireCalculatorService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [FireCalculatorService]
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
fireCalculatorService = module.get<FireCalculatorService>(
|
||||||
|
FireCalculatorService
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test periods to retire', () => {
|
||||||
|
it('should return the correct amount of periods to retire with no interst rate', async () => {
|
||||||
|
const r = 0;
|
||||||
|
const P = 1000;
|
||||||
|
const totalAmount = 1900;
|
||||||
|
const PMT = 100;
|
||||||
|
|
||||||
|
const periodsToRetire = fireCalculatorService.calculatePeriodsToRetire({
|
||||||
|
P,
|
||||||
|
r,
|
||||||
|
PMT,
|
||||||
|
totalAmount
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(periodsToRetire).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the 0 when total amount is 0', async () => {
|
||||||
|
const r = 0.05;
|
||||||
|
const P = 100000;
|
||||||
|
const totalAmount = 0;
|
||||||
|
const PMT = 10000;
|
||||||
|
|
||||||
|
const periodsToRetire = fireCalculatorService.calculatePeriodsToRetire({
|
||||||
|
P,
|
||||||
|
r,
|
||||||
|
PMT,
|
||||||
|
totalAmount
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(periodsToRetire).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct amount of periods to retire with interst rate', async () => {
|
||||||
|
const r = 0.05;
|
||||||
|
const P = 598478.96;
|
||||||
|
const totalAmount = 812399.66;
|
||||||
|
const PMT = 6000;
|
||||||
|
const expectedPeriods = 24;
|
||||||
|
|
||||||
|
const periodsToRetire = fireCalculatorService.calculatePeriodsToRetire({
|
||||||
|
P,
|
||||||
|
r,
|
||||||
|
PMT,
|
||||||
|
totalAmount
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Math.round(periodsToRetire)).toBe(expectedPeriods);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -40,4 +40,36 @@ export class FireCalculatorService {
|
|||||||
totalAmount
|
totalAmount
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public calculatePeriodsToRetire({
|
||||||
|
P,
|
||||||
|
PMT,
|
||||||
|
r,
|
||||||
|
totalAmount
|
||||||
|
}: {
|
||||||
|
P: number;
|
||||||
|
PMT: number;
|
||||||
|
r: number;
|
||||||
|
totalAmount: number;
|
||||||
|
}) {
|
||||||
|
if (r == 0) {
|
||||||
|
// No compound interest
|
||||||
|
return (totalAmount - P) / PMT;
|
||||||
|
} else if (totalAmount <= P) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const periodInterest = new Big(r).div(this.COMPOUND_PERIOD);
|
||||||
|
const numerator1: number = Math.log10(
|
||||||
|
new Big(totalAmount).plus(new Big(PMT).div(periodInterest)).toNumber()
|
||||||
|
);
|
||||||
|
const numerator2: number = Math.log10(
|
||||||
|
new Big(P).plus(new Big(PMT).div(periodInterest)).toNumber()
|
||||||
|
);
|
||||||
|
const denominator: number = Math.log10(
|
||||||
|
new Big(1).plus(periodInterest).toNumber()
|
||||||
|
);
|
||||||
|
|
||||||
|
return (numerator1 - numerator2) / denominator;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user