Browse Source

Merge 2687229ca4 into 0512b8ee51

pull/7452/merge
Max Dietrich 5 hours ago
committed by GitHub
parent
commit
e4cd87c352
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 22
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  3. 7
      apps/client/src/app/pages/portfolio/fire/fire-page.html
  4. 4
      libs/common/src/lib/dtos/update-user-setting.dto.ts
  5. 1
      libs/common/src/lib/interfaces/user-settings.interface.ts
  6. 9
      libs/ui/src/lib/fire-calculator/fire-calculator.component.html
  7. 4
      libs/ui/src/lib/fire-calculator/fire-calculator.component.scss
  8. 101
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts

4
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
# Unreleased # Unreleased
### Added
- Added an option to use the annualized performance as the annual interest rate in the _FIRE_ calculator
### Changed ### Changed
- Moved the improved symbol lookup results by removing the currency from the name of cryptocurrencies from experimental to general availability - Moved the improved symbol lookup results by removing the currency from the name of cryptocurrencies from experimental to general availability

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

@ -50,6 +50,7 @@ export class GfFirePageComponent implements OnInit {
() => this.deviceDetectorService.deviceInfo().deviceType () => this.deviceDetectorService.deviceInfo().deviceType
); );
protected annualizedPerformancePercent: number | undefined;
protected fireWealth: FireWealth; protected fireWealth: FireWealth;
protected hasImpersonationId: boolean; protected hasImpersonationId: boolean;
protected hasPermissionToUpdateUserSettings: boolean; protected hasPermissionToUpdateUserSettings: boolean;
@ -99,6 +100,9 @@ export class GfFirePageComponent implements OnInit {
.fetchPortfolioDetails() .fetchPortfolioDetails()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ summary }) => { .subscribe(({ summary }) => {
this.annualizedPerformancePercent =
summary?.annualizedPerformancePercentWithCurrencyEffect;
this.fireWealth = { this.fireWealth = {
today: { today: {
valueInBaseCurrency: summary?.fireWealth valueInBaseCurrency: summary?.fireWealth
@ -243,6 +247,24 @@ export class GfFirePageComponent implements OnInit {
}); });
} }
protected onUseAnnualizedPerformanceRateChange(
useAnnualizedPerformanceRate: boolean
) {
this.dataService
.putUserSetting({ useAnnualizedPerformanceRate })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
private calculateWithdrawalRates() { private calculateWithdrawalRates() {
if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) { if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) {
this.withdrawalRatePerYear = new Big( this.withdrawalRatePerYear = new Big(

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

@ -11,6 +11,7 @@
</h4> </h4>
<gf-fire-calculator <gf-fire-calculator
[annualInterestRate]="user?.settings?.annualInterestRate" [annualInterestRate]="user?.settings?.annualInterestRate"
[annualizedPerformancePercent]="annualizedPerformancePercent"
[colorScheme]="user?.settings?.colorScheme" [colorScheme]="user?.settings?.colorScheme"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[deviceType]="deviceType()" [deviceType]="deviceType()"
@ -28,11 +29,17 @@
[style.pointer-events]=" [style.pointer-events]="
user?.subscription?.type === 'Basic' ? 'none' : 'initial' user?.subscription?.type === 'Basic' ? 'none' : 'initial'
" "
[useAnnualizedPerformanceRate]="
user?.settings?.useAnnualizedPerformanceRate
"
(annualInterestRateChanged)="onAnnualInterestRateChange($event)" (annualInterestRateChanged)="onAnnualInterestRateChange($event)"
(calculationCompleted)="onCalculationComplete($event)" (calculationCompleted)="onCalculationComplete($event)"
(projectedTotalAmountChanged)="onProjectedTotalAmountChange($event)" (projectedTotalAmountChanged)="onProjectedTotalAmountChange($event)"
(retirementDateChanged)="onRetirementDateChange($event)" (retirementDateChanged)="onRetirementDateChange($event)"
(savingsRateChanged)="onSavingsRateChange($event)" (savingsRateChanged)="onSavingsRateChange($event)"
(useAnnualizedPerformanceRateChanged)="
onUseAnnualizedPerformanceRateChange($event)
"
/> />
</div> </div>
</div> </div>

4
libs/common/src/lib/dtos/update-user-setting.dto.ts

@ -120,6 +120,10 @@ export class UpdateUserSettingDto {
@IsOptional() @IsOptional()
savingsRate?: number; savingsRate?: number;
@IsBoolean()
@IsOptional()
useAnnualizedPerformanceRate?: boolean;
@IsIn(['DEFAULT', 'ZEN'] as ViewMode[]) @IsIn(['DEFAULT', 'ZEN'] as ViewMode[])
@IsOptional() @IsOptional()
viewMode?: ViewMode; viewMode?: ViewMode;

1
libs/common/src/lib/interfaces/user-settings.interface.ts

@ -31,6 +31,7 @@ export interface UserSettings {
retirementDate?: string; retirementDate?: string;
safeWithdrawalRate?: number; safeWithdrawalRate?: number;
savingsRate?: number; savingsRate?: number;
useAnnualizedPerformanceRate?: boolean;
viewMode?: ViewMode; viewMode?: ViewMode;
xRayRules?: XRayRulesSettings; xRayRules?: XRayRulesSettings;
} }

9
libs/ui/src/lib/fire-calculator/fire-calculator.component.html

@ -28,6 +28,15 @@
<div class="ml-2" matTextSuffix>%</div> <div class="ml-2" matTextSuffix>%</div>
</mat-form-field> </mat-form-field>
<div class="mb-3 px-2 use-annualized-performance-rate">
<mat-checkbox
color="primary"
formControlName="useAnnualizedPerformanceRate"
i18n
>Use annualized performance</mat-checkbox
>
</div>
<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>{{ retirementDateLabel }}</div> <div>{{ retirementDateLabel }}</div>

4
libs/ui/src/lib/fire-calculator/fire-calculator.component.scss

@ -1,6 +1,10 @@
:host { :host {
display: block; display: block;
.use-annualized-performance-rate {
margin-top: -0.75rem;
}
.chart-container { .chart-container {
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;

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

@ -28,6 +28,7 @@ import {
ReactiveFormsModule ReactiveFormsModule
} from '@angular/forms'; } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { import {
MatDatepicker, MatDatepicker,
MatDatepickerModule MatDatepickerModule
@ -72,6 +73,7 @@ import { FireCalculatorService } from './fire-calculator.service';
FormsModule, FormsModule,
IonIcon, IonIcon,
MatButtonModule, MatButtonModule,
MatCheckboxModule,
MatDatepickerModule, MatDatepickerModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
@ -85,6 +87,7 @@ import { FireCalculatorService } from './fire-calculator.service';
}) })
export class GfFireCalculatorComponent implements OnChanges, OnDestroy { export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@Input() annualInterestRate = 0; @Input() annualInterestRate = 0;
@Input() annualizedPerformancePercent: number;
@Input() colorScheme: ColorScheme; @Input() colorScheme: ColorScheme;
@Input() currency: string; @Input() currency: string;
@Input() deviceType: string; @Input() deviceType: string;
@ -94,13 +97,15 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@Input() projectedTotalAmount = 0; @Input() projectedTotalAmount = 0;
@Input() retirementDate: Date; @Input() retirementDate: Date;
@Input() savingsRate = 0; @Input() savingsRate = 0;
@Input() useAnnualizedPerformanceRate = false;
public calculatorForm = this.formBuilder.group({ public calculatorForm = this.formBuilder.group({
annualInterestRate: new FormControl<number | null>(null), annualInterestRate: new FormControl<number | null>(null),
paymentPerPeriod: new FormControl<number | null>(null), paymentPerPeriod: new FormControl<number | null>(null),
principalInvestmentAmount: new FormControl<number | null>(null), principalInvestmentAmount: new FormControl<number | null>(null),
projectedTotalAmount: new FormControl<number | null>(null), projectedTotalAmount: new FormControl<number | null>(null),
retirementDate: new FormControl<Date | null>(null) retirementDate: new FormControl<Date | null>(null),
useAnnualizedPerformanceRate: new FormControl<boolean>(false)
}); });
public chart: Chart<'bar'>; public chart: Chart<'bar'>;
@ -116,6 +121,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
protected readonly projectedTotalAmountChanged = output<number>(); protected readonly projectedTotalAmountChanged = output<number>();
protected readonly retirementDateChanged = output<Date>(); protected readonly retirementDateChanged = output<Date>();
protected readonly savingsRateChanged = output<number>(); protected readonly savingsRateChanged = output<number>();
protected readonly useAnnualizedPerformanceRateChanged = output<boolean>();
private readonly CONTRIBUTION_PERIOD = 12; private readonly CONTRIBUTION_PERIOD = 12;
@ -206,6 +212,27 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.retirementDateChanged.emit(retirementDate); this.retirementDateChanged.emit(retirementDate);
} }
}); });
this.calculatorForm
.get('useAnnualizedPerformanceRate')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((useAnnualizedPerformanceRate) => {
if (useAnnualizedPerformanceRate !== null) {
this.applyAnnualizedPerformanceRate(useAnnualizedPerformanceRate);
}
});
this.calculatorForm
.get('useAnnualizedPerformanceRate')
?.valueChanges.pipe(
debounceTime(500),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((useAnnualizedPerformanceRate) => {
if (useAnnualizedPerformanceRate !== null) {
this.useAnnualizedPerformanceRateChanged.emit(
useAnnualizedPerformanceRate
);
}
});
} }
protected get retirementDateLabel(): string { protected get retirementDateLabel(): string {
@ -222,14 +249,24 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
} }
public ngOnChanges() { public ngOnChanges() {
const useAnnualizedPerformanceRate =
this.useAnnualizedPerformanceRate ?? false;
const annualInterestRate =
useAnnualizedPerformanceRate &&
this.isAnnualizedPerformancePercentAvailable()
? this.getAnnualizedInterestRatePercent()
: this.annualInterestRate;
if (isNumber(this.fireWealth) && this.fireWealth >= 0) { if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
this.calculatorForm.setValue( this.calculatorForm.setValue(
{ {
annualInterestRate: this.annualInterestRate, annualInterestRate,
paymentPerPeriod: this.savingsRate, paymentPerPeriod: this.savingsRate,
principalInvestmentAmount: this.fireWealth, principalInvestmentAmount: this.fireWealth,
projectedTotalAmount: this.projectedTotalAmount, projectedTotalAmount: this.projectedTotalAmount,
retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE,
useAnnualizedPerformanceRate
}, },
{ {
emitEvent: false emitEvent: false
@ -271,6 +308,16 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.calculatorForm this.calculatorForm
.get('projectedTotalAmount') .get('projectedTotalAmount')
?.enable({ emitEvent: false }); ?.enable({ emitEvent: false });
if (this.isAnnualizedPerformancePercentAvailable()) {
this.calculatorForm
.get('useAnnualizedPerformanceRate')
?.enable({ emitEvent: false });
} else {
this.calculatorForm
.get('useAnnualizedPerformanceRate')
?.disable({ emitEvent: false });
}
} else { } else {
this.calculatorForm this.calculatorForm
.get('annualInterestRate') .get('annualInterestRate')
@ -281,6 +328,15 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.calculatorForm this.calculatorForm
.get('projectedTotalAmount') .get('projectedTotalAmount')
?.disable({ emitEvent: false }); ?.disable({ emitEvent: false });
this.calculatorForm
.get('useAnnualizedPerformanceRate')
?.disable({ emitEvent: false });
}
if (useAnnualizedPerformanceRate) {
this.calculatorForm
.get('annualInterestRate')
?.disable({ emitEvent: false });
} }
this.calculatorForm.get('retirementDate')?.disable({ emitEvent: false }); this.calculatorForm.get('retirementDate')?.disable({ emitEvent: false });
@ -467,6 +523,45 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
}; };
} }
private applyAnnualizedPerformanceRate(
useAnnualizedPerformanceRate: boolean
) {
const annualInterestRateControl =
this.calculatorForm.get('annualInterestRate');
if (
useAnnualizedPerformanceRate &&
this.isAnnualizedPerformancePercentAvailable()
) {
annualInterestRateControl?.setValue(
this.getAnnualizedInterestRatePercent(),
{ emitEvent: false }
);
annualInterestRateControl?.disable({ emitEvent: false });
} else {
annualInterestRateControl?.setValue(this.annualInterestRate, {
emitEvent: false
});
if (this.hasPermissionToUpdateUserSettings === true) {
annualInterestRateControl?.enable({ emitEvent: false });
}
}
this.initialize();
}
private isAnnualizedPerformancePercentAvailable(): boolean {
return (
isNumber(this.annualizedPerformancePercent) &&
!Number.isNaN(this.annualizedPerformancePercent)
);
}
private getAnnualizedInterestRatePercent(): number {
return Math.round(this.annualizedPerformancePercent * 10000) / 100;
}
private getP() { private getP() {
return this.fireWealth || 0; return this.fireWealth || 0;
} }

Loading…
Cancel
Save