From a04099b12d23e2cbf71274aadd2868ff6826cd32 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sat, 2 May 2026 15:02:43 +0700 Subject: [PATCH 1/6] Task/improve type safety in fire page component (#6807) * Improve type safety --- .../portfolio/fire/fire-page.component.ts | 121 ++++++++++-------- .../app/pages/portfolio/fire/fire-page.html | 2 +- 2 files changed, 66 insertions(+), 57 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index 7b5b10c7d..4ab7d6392 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -16,7 +16,9 @@ import { CommonModule, NgStyle } from '@angular/common'; import { ChangeDetectorRef, Component, + computed, DestroyRef, + inject, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -42,33 +44,40 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; templateUrl: './fire-page.html' }) export class GfFirePageComponent implements OnInit { - public deviceType: string; - public fireWealth: FireWealth; - public hasImpersonationId: boolean; - public hasPermissionToUpdateUserSettings: boolean; - public isLoading = false; - public projectedTotalAmount: number; - public retirementDate: Date; - public safeWithdrawalRateControl = new FormControl(undefined); - public safeWithdrawalRateOptions = [0.025, 0.03, 0.035, 0.04, 0.045]; - public user: User; - public withdrawalRatePerMonth: Big; - public withdrawalRatePerMonthProjected: Big; - public withdrawalRatePerYear: Big; - public withdrawalRatePerYearProjected: Big; - - public constructor( - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private destroyRef: DestroyRef, - private deviceService: DeviceDetectorService, - private impersonationStorageService: ImpersonationStorageService, - private userService: UserService - ) {} + protected readonly deviceType = computed( + () => this.deviceDetectorService.deviceInfo().deviceType + ); + + protected fireWealth: FireWealth; + protected hasImpersonationId: boolean; + protected hasPermissionToUpdateUserSettings: boolean; + protected isLoading = false; + protected retirementDate: Date; + protected readonly safeWithdrawalRateControl = new FormControl< + number | undefined + >(undefined); + protected readonly safeWithdrawalRateOptions = [ + 0.025, 0.03, 0.035, 0.04, 0.045 + ] as const; + protected user: User; + protected withdrawalRatePerMonth: Big; + protected withdrawalRatePerMonthProjected: Big; + protected withdrawalRatePerYear: Big; + protected withdrawalRatePerYearProjected: Big; + + private projectedTotalAmount: number; + + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly impersonationStorageService = inject( + ImpersonationStorageService + ); + private readonly userService = inject(UserService); public ngOnInit() { this.isLoading = true; - this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.dataService .fetchPortfolioDetails() @@ -76,7 +85,7 @@ export class GfFirePageComponent implements OnInit { .subscribe(({ summary }) => { this.fireWealth = { today: { - valueInBaseCurrency: summary.fireWealth + valueInBaseCurrency: summary?.fireWealth ? summary.fireWealth.today.valueInBaseCurrency : 0 } @@ -104,7 +113,7 @@ export class GfFirePageComponent implements OnInit { this.safeWithdrawalRateControl.valueChanges .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((value) => { - this.onSafeWithdrawalRateChange(Number(value)); + this.updateSafeWithdrawalRate(Number(value)); }); this.userService.stateChanged @@ -133,7 +142,7 @@ export class GfFirePageComponent implements OnInit { }); } - public onAnnualInterestRateChange(annualInterestRate: number) { + protected onAnnualInterestRateChange(annualInterestRate: number) { this.dataService .putUserSetting({ annualInterestRate }) .pipe(takeUntilDestroyed(this.destroyRef)) @@ -149,7 +158,7 @@ export class GfFirePageComponent implements OnInit { }); } - public onCalculationComplete({ + protected onCalculationComplete({ projectedTotalAmount, retirementDate }: FireCalculationCompleteEvent) { @@ -161,11 +170,11 @@ export class GfFirePageComponent implements OnInit { this.isLoading = false; } - public onRetirementDateChange(retirementDate: Date) { + protected onProjectedTotalAmountChange(projectedTotalAmount: number) { this.dataService .putUserSetting({ - retirementDate: retirementDate.toISOString(), - projectedTotalAmount: null + projectedTotalAmount, + retirementDate: undefined }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { @@ -180,9 +189,12 @@ export class GfFirePageComponent implements OnInit { }); } - public onSafeWithdrawalRateChange(safeWithdrawalRate: number) { + protected onRetirementDateChange(retirementDate: Date) { this.dataService - .putUserSetting({ safeWithdrawalRate }) + .putUserSetting({ + projectedTotalAmount: undefined, + retirementDate: retirementDate.toISOString() + }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.userService @@ -191,15 +203,12 @@ export class GfFirePageComponent implements OnInit { .subscribe((user) => { this.user = user; - this.calculateWithdrawalRates(); - this.calculateWithdrawalRatesProjected(); - this.changeDetectorRef.markForCheck(); }); }); } - public onSavingsRateChange(savingsRate: number) { + protected onSavingsRateChange(savingsRate: number) { this.dataService .putUserSetting({ savingsRate }) .pipe(takeUntilDestroyed(this.destroyRef)) @@ -215,25 +224,6 @@ export class GfFirePageComponent implements OnInit { }); } - public onProjectedTotalAmountChange(projectedTotalAmount: number) { - this.dataService - .putUserSetting({ - projectedTotalAmount, - retirementDate: null - }) - .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( @@ -258,4 +248,23 @@ export class GfFirePageComponent implements OnInit { this.withdrawalRatePerYearProjected.div(12); } } + + private updateSafeWithdrawalRate(safeWithdrawalRate: number) { + this.dataService + .putUserSetting({ safeWithdrawalRate }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.userService + .get(true) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((user) => { + this.user = user; + + this.calculateWithdrawalRates(); + this.calculateWithdrawalRatesProjected(); + + this.changeDetectorRef.markForCheck(); + }); + }); + } } diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index b441b2563..cabd7fcb4 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -13,7 +13,7 @@ [annualInterestRate]="user?.settings?.annualInterestRate" [colorScheme]="user?.settings?.colorScheme" [currency]="user?.settings?.baseCurrency" - [deviceType]="deviceType" + [deviceType]="deviceType()" [fireWealth]="fireWealth?.today.valueInBaseCurrency" [hasPermissionToUpdateUserSettings]=" !hasImpersonationId && hasPermissionToUpdateUserSettings From 2af973bedba2f7b429aa16c80fd8aa772db77e6b Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sat, 2 May 2026 22:26:34 +0700 Subject: [PATCH 2/6] Bugfix/reactivate auto calculate for retirement date and projected total amount in FIRE page (#6831) * fix(lib): update fields to accept null * fix(client): set to null --- .../app/pages/portfolio/fire/fire-page.component.ts | 4 ++-- libs/common/src/lib/dtos/update-user-setting.dto.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index 4ab7d6392..dc0a1d776 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -174,7 +174,7 @@ export class GfFirePageComponent implements OnInit { this.dataService .putUserSetting({ projectedTotalAmount, - retirementDate: undefined + retirementDate: null }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { @@ -192,7 +192,7 @@ export class GfFirePageComponent implements OnInit { protected onRetirementDateChange(retirementDate: Date) { this.dataService .putUserSetting({ - projectedTotalAmount: undefined, + projectedTotalAmount: null, retirementDate: retirementDate.toISOString() }) .pipe(takeUntilDestroyed(this.destroyRef)) diff --git a/libs/common/src/lib/dtos/update-user-setting.dto.ts b/libs/common/src/lib/dtos/update-user-setting.dto.ts index cf7dff7e8..f2781835c 100644 --- a/libs/common/src/lib/dtos/update-user-setting.dto.ts +++ b/libs/common/src/lib/dtos/update-user-setting.dto.ts @@ -96,13 +96,21 @@ export class UpdateUserSettingDto { @IsOptional() locale?: string; + /** + * The target financial amount the user aims to reach before retiring. + * Can be explicitly set to null to clear the value and calculate it dynamically. + */ @IsNumber() @IsOptional() - projectedTotalAmount?: number; + projectedTotalAmount?: number | null; + /** + * The target date when the user plans to retire. + * Can be explicitly set to null to clear the value and calculate it dynamically. + */ @IsISO8601() @IsOptional() - retirementDate?: string; + retirementDate?: string | null; @IsNumber() @IsOptional() From fff9c1667064f176b68530a79f9bb64dcbfec708 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 May 2026 19:02:06 +0200 Subject: [PATCH 3/6] Task/harmonize unit styling in value component (#6832) * Harmonize unit styling * Update changelog --- CHANGELOG.md | 1 + libs/ui/src/lib/value/value.component.html | 52 +++++++--------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 460f54d3f..045f92c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Harmonized the unit styling in the value component - Upgraded `stripe` from version `20.4.1` to `21.0.1` ## 3.1.0 - 2026-04-29 diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html index c4d6532a7..48e9c02e7 100644 --- a/libs/ui/src/lib/value/value.component.html +++ b/libs/ui/src/lib/value/value.component.html @@ -38,45 +38,23 @@
-
} } - @if (isPercent) { -
- @if (value === null) { - *****% - } @else { - {{ formattedValue }}% - } -
- } @else { -
- @if (value === null) { - ***** - } @else { - {{ formattedValue }} - } -
- } - @if (unit) { - @if (size === 'medium') { - - {{ unit }} - +
+ @if (value === null) { + ***** } @else { -
- {{ unit }} -
+ {{ formattedValue }} } +
+ @if (isPercent || unit) { +
+ {{ isPercent ? '%' : unit }} +
} } @if (isString) { From 4a81658f60226617dc6b8cf85d86e6c603885799 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sun, 3 May 2026 14:43:06 +0700 Subject: [PATCH 4/6] Bugfix/improve typography for client app and charts (#6833) * Improve typography for client app and charts * Update changelog --- CHANGELOG.md | 4 ++++ apps/client/src/app/app.component.ts | 4 ++++ apps/client/src/styles/theme.scss | 22 +++++++++++++++------- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 045f92c2c..e0b601427 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Harmonized the unit styling in the value component - Upgraded `stripe` from version `20.4.1` to `21.0.1` +### Fixed + +- Resolved an issue where charts and components defaulted to _Roboto_ instead of the preconfigured _Inter_ font family + ## 3.1.0 - 2026-04-29 ### Added diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index 0a9be3a75..e1967970d 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -28,6 +28,7 @@ import { RouterOutlet } from '@angular/router'; import { DataSource } from '@prisma/client'; +import { Chart } from 'chart.js'; import { addIcons } from 'ionicons'; import { openOutline } from 'ionicons/icons'; import { DeviceDetectorService } from 'ngx-device-detector'; @@ -255,6 +256,9 @@ export class GfAppComponent implements OnInit { this.toggleTheme(isDarkTheme); + // Default chart styles + Chart.defaults.font.family = getCssVariable('--font-family-sans-serif'); + window.matchMedia('(prefers-color-scheme: dark)').addListener((event) => { if (!this.user?.settings.colorScheme) { this.toggleTheme(event.matches); diff --git a/apps/client/src/styles/theme.scss b/apps/client/src/styles/theme.scss index dfde47c77..296270e7f 100644 --- a/apps/client/src/styles/theme.scss +++ b/apps/client/src/styles/theme.scss @@ -139,20 +139,28 @@ $_tertiary: map.merge(map.get($_palettes, tertiary), $_rest); @include mat.elevation-classes(); @include mat.table-density(-1); -// $gf-typography: mat.m2-define-typography-config(); +$gf-typography: ( + // Font families + brand-family: var(--font-family-sans-serif), + plain-family: var(--font-family-sans-serif), + // Font weights + bold-weight: 700, + medium-weight: 500, + regular-weight: 400 +); .theme-light { $gf-theme-default: mat.define-theme( ( color: ( primary: $_primary, - theme-type: light, - tertiary: $_tertiary + tertiary: $_tertiary, + theme-type: light ), density: ( scale: -3 ), - // typography: $gf-typography + typography: $gf-typography ) ); @@ -229,13 +237,13 @@ $_tertiary: map.merge(map.get($_palettes, tertiary), $_rest); ( color: ( primary: $_primary, - theme-type: dark, - tertiary: $_tertiary + tertiary: $_tertiary, + theme-type: dark ), density: ( scale: -3 ), - // typography: $gf-typography + typography: $gf-typography ) ); From 19abb6cbea84a592586ac5b875b4a6308a943f9d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 3 May 2026 10:00:46 +0200 Subject: [PATCH 5/6] Bugfix/handling of string inputs in form of asset profile dialog (#6834) * Fix string inputs handling * Update changelog --- CHANGELOG.md | 1 + .../asset-profile-dialog/asset-profile-dialog.component.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0b601427..534477a89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Resolved a validation error with an empty URL in the asset profile details dialog of the admin control panel - Resolved an issue where charts and components defaulted to _Roboto_ instead of the preconfigured _Inter_ font family ## 3.1.0 - 2026-04-29 diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index 76f6cb9c2..f0721c87f 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -575,13 +575,13 @@ export class GfAssetProfileDialogComponent implements OnInit { assetClass: this.assetProfileForm.controls.assetClass.value ?? undefined, assetSubClass: this.assetProfileForm.controls.assetSubClass.value ?? undefined, - comment: this.assetProfileForm.controls.comment.value ?? undefined, + comment: this.assetProfileForm.controls.comment.value || undefined, currency: this.assetProfileForm.controls.currency.value ?? undefined, isActive: isBoolean(this.assetProfileForm.controls.isActive.value) ? this.assetProfileForm.controls.isActive.value : undefined, - name: this.assetProfileForm.controls.name.value ?? undefined, - url: this.assetProfileForm.controls.url.value ?? undefined + name: this.assetProfileForm.controls.name.value || undefined, + url: this.assetProfileForm.controls.url.value || undefined }; try { From 03f10c35dfefda1cf1c1486df2dde4d291051910 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 3 May 2026 10:04:28 +0200 Subject: [PATCH 6/6] Release 3.2.0 (#6836) --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 534477a89..43119ff4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 3.2.0 - 2026-05-03 ### Added diff --git a/package-lock.json b/package-lock.json index 3e35d09fc..f507d8eb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.1.0", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.1.0", + "version": "3.2.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 99b774bfb..54c1f048f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.1.0", + "version": "3.2.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio",