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
### Added
- Added an option to use the annualized performance as the annual interest rate in the _FIRE_ calculator
### Changed
- 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
);
protected annualizedPerformancePercent: number | undefined;
protected fireWealth: FireWealth;
protected hasImpersonationId: boolean;
protected hasPermissionToUpdateUserSettings: boolean;
@ -99,6 +100,9 @@ export class GfFirePageComponent implements OnInit {
.fetchPortfolioDetails()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ summary }) => {
this.annualizedPerformancePercent =
summary?.annualizedPerformancePercentWithCurrencyEffect;
this.fireWealth = {
today: {
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() {
if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) {
this.withdrawalRatePerYear = new Big(

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

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

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

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

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

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

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

@ -28,6 +28,15 @@
<div class="ml-2" matTextSuffix>%</div>
</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-label i18n>Retirement Date</mat-label>
<div>{{ retirementDateLabel }}</div>

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

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

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

@ -28,6 +28,7 @@ import {
ReactiveFormsModule
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import {
MatDatepicker,
MatDatepickerModule
@ -72,6 +73,7 @@ import { FireCalculatorService } from './fire-calculator.service';
FormsModule,
IonIcon,
MatButtonModule,
MatCheckboxModule,
MatDatepickerModule,
MatFormFieldModule,
MatInputModule,
@ -85,6 +87,7 @@ import { FireCalculatorService } from './fire-calculator.service';
})
export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@Input() annualInterestRate = 0;
@Input() annualizedPerformancePercent: number;
@Input() colorScheme: ColorScheme;
@Input() currency: string;
@Input() deviceType: string;
@ -94,13 +97,15 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@Input() projectedTotalAmount = 0;
@Input() retirementDate: Date;
@Input() savingsRate = 0;
@Input() useAnnualizedPerformanceRate = false;
public calculatorForm = this.formBuilder.group({
annualInterestRate: new FormControl<number | null>(null),
paymentPerPeriod: new FormControl<number | null>(null),
principalInvestmentAmount: 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'>;
@ -116,6 +121,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
protected readonly projectedTotalAmountChanged = output<number>();
protected readonly retirementDateChanged = output<Date>();
protected readonly savingsRateChanged = output<number>();
protected readonly useAnnualizedPerformanceRateChanged = output<boolean>();
private readonly CONTRIBUTION_PERIOD = 12;
@ -206,6 +212,27 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
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 {
@ -222,14 +249,24 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
}
public ngOnChanges() {
const useAnnualizedPerformanceRate =
this.useAnnualizedPerformanceRate ?? false;
const annualInterestRate =
useAnnualizedPerformanceRate &&
this.isAnnualizedPerformancePercentAvailable()
? this.getAnnualizedInterestRatePercent()
: this.annualInterestRate;
if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
this.calculatorForm.setValue(
{
annualInterestRate: this.annualInterestRate,
annualInterestRate,
paymentPerPeriod: this.savingsRate,
principalInvestmentAmount: this.fireWealth,
projectedTotalAmount: this.projectedTotalAmount,
retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE
retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE,
useAnnualizedPerformanceRate
},
{
emitEvent: false
@ -271,6 +308,16 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.calculatorForm
.get('projectedTotalAmount')
?.enable({ emitEvent: false });
if (this.isAnnualizedPerformancePercentAvailable()) {
this.calculatorForm
.get('useAnnualizedPerformanceRate')
?.enable({ emitEvent: false });
} else {
this.calculatorForm
.get('useAnnualizedPerformanceRate')
?.disable({ emitEvent: false });
}
} else {
this.calculatorForm
.get('annualInterestRate')
@ -281,6 +328,15 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.calculatorForm
.get('projectedTotalAmount')
?.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 });
@ -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() {
return this.fireWealth || 0;
}

Loading…
Cancel
Save