|
|
|
@ -13,6 +13,7 @@ import {
|
|
|
|
|
ViewChild
|
|
|
|
|
} from '@angular/core';
|
|
|
|
|
import { FormBuilder, FormControl } from '@angular/forms';
|
|
|
|
|
import { MatDatepicker } from '@angular/material/datepicker';
|
|
|
|
|
import {
|
|
|
|
|
getTooltipOptions,
|
|
|
|
|
transformTickToAbbreviation
|
|
|
|
@ -28,17 +29,25 @@ import {
|
|
|
|
|
Tooltip
|
|
|
|
|
} from 'chart.js';
|
|
|
|
|
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 { Subject, takeUntil } from 'rxjs';
|
|
|
|
|
|
|
|
|
|
import { FireCalculatorService } from './fire-calculator.service';
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'gf-fire-calculator',
|
|
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
|
|
templateUrl: './fire-calculator.component.html',
|
|
|
|
|
styleUrls: ['./fire-calculator.component.scss']
|
|
|
|
|
selector: 'gf-fire-calculator',
|
|
|
|
|
styleUrls: ['./fire-calculator.component.scss'],
|
|
|
|
|
templateUrl: './fire-calculator.component.html'
|
|
|
|
|
})
|
|
|
|
|
export class FireCalculatorComponent
|
|
|
|
|
implements AfterViewInit, OnChanges, OnDestroy
|
|
|
|
@ -49,8 +58,12 @@ export class FireCalculatorComponent
|
|
|
|
|
@Input() fireWealth: number;
|
|
|
|
|
@Input() hasPermissionToUpdateUserSettings: boolean;
|
|
|
|
|
@Input() locale: string;
|
|
|
|
|
@Input() projectedTotalAmount = 0;
|
|
|
|
|
@Input() retirementDate: Date;
|
|
|
|
|
@Input() savingsRate = 0;
|
|
|
|
|
|
|
|
|
|
@Output() projectedTotalAmountChanged = new EventEmitter<number>();
|
|
|
|
|
@Output() retirementDateChanged = new EventEmitter<Date>();
|
|
|
|
|
@Output() savingsRateChanged = new EventEmitter<number>();
|
|
|
|
|
|
|
|
|
|
@ViewChild('chartCanvas') chartCanvas;
|
|
|
|
@ -59,13 +72,17 @@ export class FireCalculatorComponent
|
|
|
|
|
annualInterestRate: new FormControl<number>(undefined),
|
|
|
|
|
paymentPerPeriod: 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 isLoading = true;
|
|
|
|
|
public projectedTotalAmount: number;
|
|
|
|
|
public periodsToRetire = 0;
|
|
|
|
|
|
|
|
|
|
private readonly CONTRIBUTION_PERIOD = 12;
|
|
|
|
|
private readonly DEFAULT_RETIREMENT_DATE = startOfMonth(
|
|
|
|
|
addYears(new Date(), 10)
|
|
|
|
|
);
|
|
|
|
|
private unsubscribeSubject = new Subject<void>();
|
|
|
|
|
|
|
|
|
|
public constructor(
|
|
|
|
@ -86,7 +103,8 @@ export class FireCalculatorComponent
|
|
|
|
|
annualInterestRate: 5,
|
|
|
|
|
paymentPerPeriod: this.savingsRate,
|
|
|
|
|
principalInvestmentAmount: 0,
|
|
|
|
|
time: 10
|
|
|
|
|
projectedTotalAmount: this.projectedTotalAmount,
|
|
|
|
|
retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
emitEvent: false
|
|
|
|
@ -105,6 +123,18 @@ export class FireCalculatorComponent
|
|
|
|
|
.subscribe((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() {
|
|
|
|
@ -113,8 +143,12 @@ export class FireCalculatorComponent
|
|
|
|
|
// Wait for the chartCanvas
|
|
|
|
|
this.calculatorForm.patchValue(
|
|
|
|
|
{
|
|
|
|
|
principalInvestmentAmount: this.fireWealth,
|
|
|
|
|
paymentPerPeriod: this.savingsRate ?? 0
|
|
|
|
|
paymentPerPeriod: this.getPMT(),
|
|
|
|
|
principalInvestmentAmount: this.getP(),
|
|
|
|
|
projectedTotalAmount:
|
|
|
|
|
Number(this.getProjectedTotalAmount().toFixed(2)) ?? 0,
|
|
|
|
|
retirementDate:
|
|
|
|
|
this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
emitEvent: false
|
|
|
|
@ -128,19 +162,32 @@ export class FireCalculatorComponent
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
{
|
|
|
|
|
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
|
|
|
|
@ -154,11 +201,32 @@ export class FireCalculatorComponent
|
|
|
|
|
|
|
|
|
|
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 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() {
|
|
|
|
|
this.chart?.destroy();
|
|
|
|
|
|
|
|
|
@ -261,17 +329,22 @@ export class FireCalculatorComponent
|
|
|
|
|
const labels = [];
|
|
|
|
|
|
|
|
|
|
// Principal investment amount
|
|
|
|
|
const P: number =
|
|
|
|
|
this.calculatorForm.get('principalInvestmentAmount').value || 0;
|
|
|
|
|
const P: number = this.getP();
|
|
|
|
|
|
|
|
|
|
// Payment per period
|
|
|
|
|
const PMT = this.calculatorForm.get('paymentPerPeriod').value;
|
|
|
|
|
const PMT = this.getPMT();
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
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++) {
|
|
|
|
|
labels.push(year);
|
|
|
|
@ -308,7 +381,7 @@ export class FireCalculatorComponent
|
|
|
|
|
for (let period = 1; period <= t; period++) {
|
|
|
|
|
const periodInMonths =
|
|
|
|
|
period * this.CONTRIBUTION_PERIOD - monthsPassedInCurrentYear;
|
|
|
|
|
const { interest, principal, totalAmount } =
|
|
|
|
|
const { interest, principal } =
|
|
|
|
|
this.fireCalculatorService.calculateCompoundInterest({
|
|
|
|
|
P,
|
|
|
|
|
periodInMonths,
|
|
|
|
@ -319,10 +392,6 @@ export class FireCalculatorComponent
|
|
|
|
|
datasetDeposit.data.push(this.fireWealth);
|
|
|
|
|
datasetInterest.data.push(interest.toNumber());
|
|
|
|
|
datasetSavings.data.push(principal.minus(this.fireWealth).toNumber());
|
|
|
|
|
|
|
|
|
|
if (period === t) {
|
|
|
|
|
this.projectedTotalAmount = totalAmount.toNumber();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
@ -330,4 +399,67 @@ export class FireCalculatorComponent
|
|
|
|
|
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
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|