Browse Source

Merge branch 'main' into task/upgrade-nx-to-version-22.7.1

pull/6825/head
Thomas Kaul 2 weeks ago
committed by GitHub
parent
commit
c55ce2d4aa
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      CHANGELOG.md
  2. 4
      apps/client/src/app/app.component.ts
  3. 6
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  4. 121
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  5. 2
      apps/client/src/app/pages/portfolio/fire/fire-page.html
  6. 22
      apps/client/src/styles/theme.scss
  7. 12
      libs/common/src/lib/dtos/update-user-setting.dto.ts
  8. 26
      libs/ui/src/lib/value/value.component.html
  9. 4
      package-lock.json
  10. 2
      package.json

13
CHANGELOG.md

@ -7,15 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Changed
- Upgraded `Nx` from version `22.6.5` to `22.7.1`
## 3.2.0 - 2026-05-03
### Added ### Added
- Added `angular-developer` skills - Added `angular-developer` skills
### Changed ### Changed
- Upgraded `Nx` from version `22.6.5` to `22.7.1` - Harmonized the unit styling in the value component
- Upgraded `stripe` from version `20.4.1` to `21.0.1` - Upgraded `stripe` from version `20.4.1` to `21.0.1`
### 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 ## 3.1.0 - 2026-04-29
### Added ### Added

4
apps/client/src/app/app.component.ts

@ -28,6 +28,7 @@ import {
RouterOutlet RouterOutlet
} from '@angular/router'; } from '@angular/router';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { Chart } from 'chart.js';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { openOutline } from 'ionicons/icons'; import { openOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
@ -255,6 +256,9 @@ export class GfAppComponent implements OnInit {
this.toggleTheme(isDarkTheme); this.toggleTheme(isDarkTheme);
// Default chart styles
Chart.defaults.font.family = getCssVariable('--font-family-sans-serif');
window.matchMedia('(prefers-color-scheme: dark)').addListener((event) => { window.matchMedia('(prefers-color-scheme: dark)').addListener((event) => {
if (!this.user?.settings.colorScheme) { if (!this.user?.settings.colorScheme) {
this.toggleTheme(event.matches); this.toggleTheme(event.matches);

6
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, assetClass: this.assetProfileForm.controls.assetClass.value ?? undefined,
assetSubClass: assetSubClass:
this.assetProfileForm.controls.assetSubClass.value ?? undefined, 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, currency: this.assetProfileForm.controls.currency.value ?? undefined,
isActive: isBoolean(this.assetProfileForm.controls.isActive.value) isActive: isBoolean(this.assetProfileForm.controls.isActive.value)
? this.assetProfileForm.controls.isActive.value ? this.assetProfileForm.controls.isActive.value
: undefined, : undefined,
name: this.assetProfileForm.controls.name.value ?? undefined, name: this.assetProfileForm.controls.name.value || undefined,
url: this.assetProfileForm.controls.url.value ?? undefined url: this.assetProfileForm.controls.url.value || undefined
}; };
try { try {

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

@ -16,7 +16,9 @@ import { CommonModule, NgStyle } from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
DestroyRef, DestroyRef,
inject,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -42,33 +44,40 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
templateUrl: './fire-page.html' templateUrl: './fire-page.html'
}) })
export class GfFirePageComponent implements OnInit { export class GfFirePageComponent implements OnInit {
public deviceType: string; protected readonly deviceType = computed(
public fireWealth: FireWealth; () => this.deviceDetectorService.deviceInfo().deviceType
public hasImpersonationId: boolean; );
public hasPermissionToUpdateUserSettings: boolean;
public isLoading = false; protected fireWealth: FireWealth;
public projectedTotalAmount: number; protected hasImpersonationId: boolean;
public retirementDate: Date; protected hasPermissionToUpdateUserSettings: boolean;
public safeWithdrawalRateControl = new FormControl<number>(undefined); protected isLoading = false;
public safeWithdrawalRateOptions = [0.025, 0.03, 0.035, 0.04, 0.045]; protected retirementDate: Date;
public user: User; protected readonly safeWithdrawalRateControl = new FormControl<
public withdrawalRatePerMonth: Big; number | undefined
public withdrawalRatePerMonthProjected: Big; >(undefined);
public withdrawalRatePerYear: Big; protected readonly safeWithdrawalRateOptions = [
public withdrawalRatePerYearProjected: Big; 0.025, 0.03, 0.035, 0.04, 0.045
] as const;
public constructor( protected user: User;
private changeDetectorRef: ChangeDetectorRef, protected withdrawalRatePerMonth: Big;
private dataService: DataService, protected withdrawalRatePerMonthProjected: Big;
private destroyRef: DestroyRef, protected withdrawalRatePerYear: Big;
private deviceService: DeviceDetectorService, protected withdrawalRatePerYearProjected: Big;
private impersonationStorageService: ImpersonationStorageService,
private userService: UserService 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() { public ngOnInit() {
this.isLoading = true; this.isLoading = true;
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.dataService this.dataService
.fetchPortfolioDetails() .fetchPortfolioDetails()
@ -76,7 +85,7 @@ export class GfFirePageComponent implements OnInit {
.subscribe(({ summary }) => { .subscribe(({ summary }) => {
this.fireWealth = { this.fireWealth = {
today: { today: {
valueInBaseCurrency: summary.fireWealth valueInBaseCurrency: summary?.fireWealth
? summary.fireWealth.today.valueInBaseCurrency ? summary.fireWealth.today.valueInBaseCurrency
: 0 : 0
} }
@ -104,7 +113,7 @@ export class GfFirePageComponent implements OnInit {
this.safeWithdrawalRateControl.valueChanges this.safeWithdrawalRateControl.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value) => { .subscribe((value) => {
this.onSafeWithdrawalRateChange(Number(value)); this.updateSafeWithdrawalRate(Number(value));
}); });
this.userService.stateChanged this.userService.stateChanged
@ -133,7 +142,7 @@ export class GfFirePageComponent implements OnInit {
}); });
} }
public onAnnualInterestRateChange(annualInterestRate: number) { protected onAnnualInterestRateChange(annualInterestRate: number) {
this.dataService this.dataService
.putUserSetting({ annualInterestRate }) .putUserSetting({ annualInterestRate })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -149,7 +158,7 @@ export class GfFirePageComponent implements OnInit {
}); });
} }
public onCalculationComplete({ protected onCalculationComplete({
projectedTotalAmount, projectedTotalAmount,
retirementDate retirementDate
}: FireCalculationCompleteEvent) { }: FireCalculationCompleteEvent) {
@ -161,11 +170,11 @@ export class GfFirePageComponent implements OnInit {
this.isLoading = false; this.isLoading = false;
} }
public onRetirementDateChange(retirementDate: Date) { protected onProjectedTotalAmountChange(projectedTotalAmount: number) {
this.dataService this.dataService
.putUserSetting({ .putUserSetting({
retirementDate: retirementDate.toISOString(), projectedTotalAmount,
projectedTotalAmount: null retirementDate: null
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
@ -180,9 +189,12 @@ export class GfFirePageComponent implements OnInit {
}); });
} }
public onSafeWithdrawalRateChange(safeWithdrawalRate: number) { protected onRetirementDateChange(retirementDate: Date) {
this.dataService this.dataService
.putUserSetting({ safeWithdrawalRate }) .putUserSetting({
projectedTotalAmount: null,
retirementDate: retirementDate.toISOString()
})
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
@ -191,15 +203,12 @@ export class GfFirePageComponent implements OnInit {
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
this.calculateWithdrawalRates();
this.calculateWithdrawalRatesProjected();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
}); });
} }
public onSavingsRateChange(savingsRate: number) { protected onSavingsRateChange(savingsRate: number) {
this.dataService this.dataService
.putUserSetting({ savingsRate }) .putUserSetting({ savingsRate })
.pipe(takeUntilDestroyed(this.destroyRef)) .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() { private calculateWithdrawalRates() {
if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) { if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) {
this.withdrawalRatePerYear = new Big( this.withdrawalRatePerYear = new Big(
@ -258,4 +248,23 @@ export class GfFirePageComponent implements OnInit {
this.withdrawalRatePerYearProjected.div(12); 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();
});
});
}
} }

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

@ -13,7 +13,7 @@
[annualInterestRate]="user?.settings?.annualInterestRate" [annualInterestRate]="user?.settings?.annualInterestRate"
[colorScheme]="user?.settings?.colorScheme" [colorScheme]="user?.settings?.colorScheme"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType()"
[fireWealth]="fireWealth?.today.valueInBaseCurrency" [fireWealth]="fireWealth?.today.valueInBaseCurrency"
[hasPermissionToUpdateUserSettings]=" [hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings !hasImpersonationId && hasPermissionToUpdateUserSettings

22
apps/client/src/styles/theme.scss

@ -139,20 +139,28 @@ $_tertiary: map.merge(map.get($_palettes, tertiary), $_rest);
@include mat.elevation-classes(); @include mat.elevation-classes();
@include mat.table-density(-1); @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 { .theme-light {
$gf-theme-default: mat.define-theme( $gf-theme-default: mat.define-theme(
( (
color: ( color: (
primary: $_primary, primary: $_primary,
theme-type: light, tertiary: $_tertiary,
tertiary: $_tertiary theme-type: light
), ),
density: ( density: (
scale: -3 scale: -3
), ),
// typography: $gf-typography typography: $gf-typography
) )
); );
@ -229,13 +237,13 @@ $_tertiary: map.merge(map.get($_palettes, tertiary), $_rest);
( (
color: ( color: (
primary: $_primary, primary: $_primary,
theme-type: dark, tertiary: $_tertiary,
tertiary: $_tertiary theme-type: dark
), ),
density: ( density: (
scale: -3 scale: -3
), ),
// typography: $gf-typography typography: $gf-typography
) )
); );

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

@ -96,13 +96,21 @@ export class UpdateUserSettingDto {
@IsOptional() @IsOptional()
locale?: string; 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() @IsNumber()
@IsOptional() @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() @IsISO8601()
@IsOptional() @IsOptional()
retirementDate?: string; retirementDate?: string | null;
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()

26
libs/ui/src/lib/value/value.component.html

@ -38,21 +38,6 @@
<div class="mr-1 text-danger">-</div> <div class="mr-1 text-danger">-</div>
} }
} }
@if (isPercent) {
<div
class="mb-0 value"
[ngClass]="{
'font-weight-bold h2': size === 'large',
h4: size === 'medium'
}"
>
@if (value === null) {
<span class="text-monospace text-muted">*****</span>%
} @else {
{{ formattedValue }}%
}
</div>
} @else {
<div <div
class="mb-0 value" class="mb-0 value"
[ngClass]="{ [ngClass]="{
@ -66,19 +51,12 @@
{{ formattedValue }} {{ formattedValue }}
} }
</div> </div>
} @if (isPercent || unit) {
@if (unit) {
@if (size === 'medium') {
<small class="ml-1">
{{ unit }}
</small>
} @else {
<div class="ml-1"> <div class="ml-1">
{{ unit }} {{ isPercent ? '%' : unit }}
</div> </div>
} }
} }
}
@if (isString) { @if (isString) {
<div <div
class="mb-0 text-truncate value" class="mb-0 text-truncate value"

4
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.1.0", "version": "3.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.1.0", "version": "3.2.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.1.0", "version": "3.2.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",

Loading…
Cancel
Save