Browse Source

feat: extend fire page with projection information

pull/6034/head
Johnson Towoju 3 days ago
parent
commit
d4930961db
  1. 27
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  2. 173
      apps/client/src/app/pages/portfolio/fire/fire-page.html
  3. 5
      libs/common/src/lib/interfaces/fire-calculation.interface.ts
  4. 2
      libs/common/src/lib/interfaces/index.ts
  5. 14
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts

27
apps/client/src/app/pages/portfolio/fire/fire-page.component.ts

@ -1,7 +1,11 @@
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 { FireWealth, User } from '@ghostfolio/common/interfaces';
import {
FireWealth,
User,
FireCalculation
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfFireCalculatorComponent } from '@ghostfolio/ui/fire-calculator';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -44,6 +48,10 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public withdrawalRatePerMonth: Big;
public withdrawalRatePerYear: Big;
public projectedWithdrawalRatePerMonth: Big;
public projectedWithdrawalRatePerYear: Big;
public fireCalculation: FireCalculation;
private unsubscribeSubject = new Subject<void>();
public constructor(
@ -211,6 +219,23 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
});
}
public onCalculationComplete(calculation: FireCalculation) {
this.fireCalculation = calculation;
if (
this.fireWealth &&
this.user?.settings?.safeWithdrawalRate &&
calculation.projectedTotalAmount
) {
this.projectedWithdrawalRatePerYear = new Big(
calculation.projectedTotalAmount
).mul(this.user.settings.safeWithdrawalRate);
this.projectedWithdrawalRatePerMonth =
this.projectedWithdrawalRatePerYear.div(12);
}
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();

173
apps/client/src/app/pages/portfolio/fire/fire-page.html

@ -28,6 +28,7 @@
[retirementDate]="user?.settings?.retirementDate"
[savingsRate]="user?.settings?.savingsRate"
(annualInterestRateChanged)="onAnnualInterestRateChange($event)"
(calculationComplete)="onCalculationComplete($event)"
(projectedTotalAmountChanged)="onProjectedTotalAmountChange($event)"
(retirementDateChanged)="onRetirementDateChange($event)"
(savingsRateChanged)="onSavingsRateChange($event)"
@ -62,75 +63,127 @@
</div>
} @else {
<div [ngClass]="{ 'text-muted': user?.subscription?.type === 'Basic' }">
<ng-container i18n
>If you retire today, you would be able to withdraw</ng-container
>
<ng-container>&nbsp;</ng-container>
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerYear?.toNumber()"
/>
<div>
<ng-container i18n
>If you retire today, you would be able to withdraw</ng-container
>
<ng-container>&nbsp;</ng-container>
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerYear?.toNumber()"
/>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>per year</ng-container></span
>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>or</ng-container>
<ng-container>&nbsp;</ng-container>
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerMonth?.toNumber()"
/>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>per month</ng-container></span
><ng-container i18n>,</ng-container>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>based on your total assets of</ng-container>
<ng-container>&nbsp;</ng-container>
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="fireWealth?.today.valueInBaseCurrency"
/>
</span>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>and a safe withdrawal rate (SWR) of</ng-container>
@if (
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
) {
<select
class="border-0 cursor-pointer d-inline-block font-weight-bold safe-withdrawal-rate-select"
[formControl]="safeWithdrawalRateControl"
>
@for (rate of safeWithdrawalRateOptions; track rate) {
<option [value]="rate">
{{ rate | percent: '1.1-1' }}
</option>
}</select
>.
} @else {
<ng-container>&nbsp;</ng-container>
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isPercent]="true"
[locale]="user?.settings?.locale"
[precision]="1"
[value]="user?.settings?.safeWithdrawalRate" /></span
>.
}
</div>
<div class="mt-2">
<ng-container i18n>By</ng-container>
<ng-container>&nbsp;</ng-container>
<span class="font-weight-bold">{{
fireCalculation?.retirementDate | date: 'MMMM yyyy'
}}</span>
<ng-container i18n>,</ng-container>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>per year</ng-container></span
>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>or</ng-container>
<ng-container>&nbsp;</ng-container>
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerMonth?.toNumber()"
/>
<ng-container i18n>this is projected to increase to</ng-container>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>per month</ng-container></span
><ng-container i18n>,</ng-container>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>based on your total assets of</ng-container>
<ng-container>&nbsp;</ng-container>
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="fireWealth?.today.valueInBaseCurrency"
/>
</span>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>and a safe withdrawal rate (SWR) of</ng-container>
@if (
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
) {
<select
class="border-0 cursor-pointer d-inline-block font-weight-bold safe-withdrawal-rate-select"
[formControl]="safeWithdrawalRateControl"
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="projectedWithdrawalRatePerYear?.toNumber()"
/>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>per year</ng-container></span
>
@for (rate of safeWithdrawalRateOptions; track rate) {
<option [value]="rate">
{{ rate | percent: '1.1-1' }}
</option>
}</select
>.
} @else {
<ng-container>&nbsp;</ng-container>
<ng-container i18n>or</ng-container>
<ng-container>&nbsp;</ng-container>
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="projectedWithdrawalRatePerMonth?.toNumber()"
/>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>per month</ng-container></span
><ng-container i18n>,</ng-container>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>assuming a</ng-container>
<ng-container>&nbsp;</ng-container>
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isPercent]="true"
[locale]="user?.settings?.locale"
[precision]="1"
[value]="user?.settings?.safeWithdrawalRate" /></span
>.
}
[precision]="2"
[value]="fireCalculation?.annualInterestRate"
/></span>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>annual interest rate</ng-container>.
</div>
</div>
}
</div>

5
libs/common/src/lib/interfaces/fire-calculation.interface.ts

@ -0,0 +1,5 @@
export interface FireCalculation {
retirementDate: Date;
projectedTotalAmount: number;
annualInterestRate: number;
}

2
libs/common/src/lib/interfaces/index.ts

@ -18,6 +18,7 @@ import type { DataProviderInfo } from './data-provider-info.interface';
import type { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
import type { FilterGroup } from './filter-group.interface';
import type { Filter } from './filter.interface';
import type { FireCalculation } from './fire-calculation.interface';
import type { FireWealth } from './fire-wealth.interface';
import type { HistoricalDataItem } from './historical-data-item.interface';
import type { HoldingWithParents } from './holding-with-parents.interface';
@ -141,6 +142,7 @@ export {
Filter,
FilterGroup,
FireWealth,
FireCalculation,
HistoricalDataItem,
HistoricalResponse,
Holding,

14
libs/ui/src/lib/fire-calculator/fire-calculator.component.ts

@ -4,6 +4,7 @@ import {
} from '@ghostfolio/common/chart-helper';
import { primaryColorRgb } from '@ghostfolio/common/config';
import { getLocale } from '@ghostfolio/common/helper';
import { FireCalculation } from '@ghostfolio/common/interfaces';
import { ColorScheme } from '@ghostfolio/common/types';
import { CommonModule } from '@angular/common';
@ -91,6 +92,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@Output() projectedTotalAmountChanged = new EventEmitter<number>();
@Output() retirementDateChanged = new EventEmitter<Date>();
@Output() savingsRateChanged = new EventEmitter<number>();
@Output() calculationComplete = new EventEmitter<FireCalculation>();
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
@ -131,6 +133,18 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.initialize();
});
this.calculatorForm.valueChanges
.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
.subscribe(() => {
const { retirementDate, projectedTotalAmount } =
this.calculatorForm.getRawValue();
this.calculationComplete.emit({
retirementDate,
projectedTotalAmount,
annualInterestRate: this.getR()
});
});
this.calculatorForm
.get('annualInterestRate')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))

Loading…
Cancel
Save