Browse Source

Merge branch 'main' into task/remove-deprecated-fee-ratio-x-ray-rule

pull/6364/head
Thomas Kaul 2 days ago
committed by GitHub
parent
commit
f421e241e5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 27
      CHANGELOG.md
  2. 2
      apps/api/src/models/rule.ts
  3. 12
      apps/api/src/services/data-provider/data-provider.service.ts
  4. 2
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  5. 12
      apps/client/src/app/pages/about/changelog/changelog-page.component.ts
  6. 12
      apps/client/src/app/pages/about/license/license-page.component.ts
  7. 12
      apps/client/src/app/pages/about/oss-friends/oss-friends-page.component.ts
  8. 12
      apps/client/src/app/pages/admin/admin-page.component.ts
  9. 12
      apps/client/src/app/pages/demo/demo-page.component.ts
  10. 24
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  11. 8
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  12. 8
      apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts
  13. 8
      apps/client/src/app/pages/resources/resources-page.component.ts
  14. 8
      apps/client/src/locales/messages.zh.xlf
  15. 24
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts
  16. 2
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html
  17. 102
      libs/ui/src/lib/assistant/assistant.component.ts
  18. 2
      libs/ui/src/lib/assistant/assistant.html
  19. 90
      libs/ui/src/lib/currency-selector/currency-selector.component.ts
  20. 128
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
  21. 8
      libs/ui/src/lib/portfolio-filter-form/interfaces/portfolio-filter-form-value.interface.ts
  22. 227
      package-lock.json
  23. 18
      package.json

27
CHANGELOG.md

@ -11,6 +11,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed the deprecated static portfolio analysis rule: _Fees_ (Fee Ratio) - Removed the deprecated static portfolio analysis rule: _Fees_ (Fee Ratio)
### Fixed
- Fixed an exception by adding a fallback for missing market price values on the _X-ray_ page
## 2.243.0 - 2026-02-23
### Changed
- Improved the language localization for Chinese (`zh`)
- Upgraded `nestjs` from version `11.1.8` to `11.1.14`
### Fixed
- Fixed an issue when creating activities of type `FEE`, `INTEREST` or `LIABILITY`
## 2.242.0 - 2026-02-22
### Changed
- Changed the account field to optional in the create or update activity dialog
### Fixed
- Fixed a validation issue for valuables used in the create and import activity logic
- Fixed the page size for presets in the historical market data table of the admin control panel
## 2.241.0 - 2026-02-21 ## 2.241.0 - 2026-02-21
### Changed ### Changed
@ -25,7 +51,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed an issue with `balanceInBaseCurrency` of the accounts in the value redaction interceptor for the impersonation mode - Fixed an issue with `balanceInBaseCurrency` of the accounts in the value redaction interceptor for the impersonation mode
- Fixed an issue with `comment` of the accounts in the value redaction interceptor for the impersonation mode - Fixed an issue with `comment` of the accounts in the value redaction interceptor for the impersonation mode
- Fixed an issue with `dividendInBaseCurrency` of the accounts in the value redaction interceptor for the impersonation mode - Fixed an issue with `dividendInBaseCurrency` of the accounts in the value redaction interceptor for the impersonation mode
- Fixed an issue with `dividendInBaseCurrency` of the accounts in the value redaction interceptor for the impersonation mode
- Fixed an issue with `interestInBaseCurrency` of the accounts in the value redaction interceptor for the impersonation mode - Fixed an issue with `interestInBaseCurrency` of the accounts in the value redaction interceptor for the impersonation mode
- Fixed an issue with `value` of the accounts in the value redaction interceptor for the impersonation mode - Fixed an issue with `value` of the accounts in the value redaction interceptor for the impersonation mode

2
apps/api/src/models/rule.ts

@ -57,7 +57,7 @@ export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
previousValue + previousValue +
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
new Big(currentValue.quantity) new Big(currentValue.quantity)
.mul(currentValue.marketPrice) .mul(currentValue.marketPrice ?? 0)
.toNumber(), .toNumber(),
currentValue.currency, currentValue.currency,
baseCurrency baseCurrency

12
apps/api/src/services/data-provider/data-provider.service.ts

@ -244,11 +244,15 @@ export class DataProviderService implements OnModuleInit {
}); });
if (!assetProfiles[assetProfileIdentifier]) { if (!assetProfiles[assetProfileIdentifier]) {
if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) { if (
(dataSource === DataSource.MANUAL && type === 'BUY') ||
['FEE', 'INTEREST', 'LIABILITY'].includes(type)
) {
const assetProfileInImport = assetProfilesWithMarketDataDto?.find( const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
(profile) => { (assetProfile) => {
return ( return (
profile.dataSource === dataSource && profile.symbol === symbol assetProfile.dataSource === dataSource &&
assetProfile.symbol === symbol
); );
} }
); );
@ -257,7 +261,7 @@ export class DataProviderService implements OnModuleInit {
currency, currency,
dataSource, dataSource,
symbol, symbol,
name: assetProfileInImport?.name name: assetProfileInImport?.name ?? symbol
}; };
continue; continue;

2
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -379,7 +379,7 @@ export class GfAdminMarketDataComponent
this.pageSize = this.pageSize =
this.activeFilters.length === 1 && this.activeFilters.length === 1 &&
this.activeFilters[0].type === 'PRESET_ID' this.activeFilters[0].type === 'PRESET_ID'
? undefined ? Number.MAX_SAFE_INTEGER
: DEFAULT_PAGE_SIZE; : DEFAULT_PAGE_SIZE;
if (pageIndex === 0 && this.paginator) { if (pageIndex === 0 && this.paginator) {

12
apps/client/src/app/pages/about/changelog/changelog-page.component.ts

@ -1,7 +1,6 @@
import { Component, OnDestroy } from '@angular/core'; import { Component } from '@angular/core';
import { MarkdownModule } from 'ngx-markdown'; import { MarkdownModule } from 'ngx-markdown';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
@Component({ @Component({
imports: [MarkdownModule, NgxSkeletonLoaderModule], imports: [MarkdownModule, NgxSkeletonLoaderModule],
@ -9,17 +8,10 @@ import { Subject } from 'rxjs';
styleUrls: ['./changelog-page.scss'], styleUrls: ['./changelog-page.scss'],
templateUrl: './changelog-page.html' templateUrl: './changelog-page.html'
}) })
export class GfChangelogPageComponent implements OnDestroy { export class GfChangelogPageComponent {
public isLoading = true; public isLoading = true;
private unsubscribeSubject = new Subject<void>();
public onLoad() { public onLoad() {
this.isLoading = false; this.isLoading = false;
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

12
apps/client/src/app/pages/about/license/license-page.component.ts

@ -1,6 +1,5 @@
import { Component, OnDestroy } from '@angular/core'; import { Component } from '@angular/core';
import { MarkdownModule } from 'ngx-markdown'; import { MarkdownModule } from 'ngx-markdown';
import { Subject } from 'rxjs';
@Component({ @Component({
imports: [MarkdownModule], imports: [MarkdownModule],
@ -8,11 +7,4 @@ import { Subject } from 'rxjs';
styleUrls: ['./license-page.scss'], styleUrls: ['./license-page.scss'],
templateUrl: './license-page.html' templateUrl: './license-page.html'
}) })
export class GfLicensePageComponent implements OnDestroy { export class GfLicensePageComponent {}
private unsubscribeSubject = new Subject<void>();
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

12
apps/client/src/app/pages/about/oss-friends/oss-friends-page.component.ts

@ -1,10 +1,9 @@
import { Component, OnDestroy } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { arrowForwardOutline } from 'ionicons/icons'; import { arrowForwardOutline } from 'ionicons/icons';
import { Subject } from 'rxjs';
const ossFriends = require('../../../../assets/oss-friends.json'); const ossFriends = require('../../../../assets/oss-friends.json');
@ -14,17 +13,10 @@ const ossFriends = require('../../../../assets/oss-friends.json');
styleUrls: ['./oss-friends-page.scss'], styleUrls: ['./oss-friends-page.scss'],
templateUrl: './oss-friends-page.html' templateUrl: './oss-friends-page.html'
}) })
export class GfOpenSourceSoftwareFriendsPageComponent implements OnDestroy { export class GfOpenSourceSoftwareFriendsPageComponent {
public ossFriends = ossFriends.data; public ossFriends = ossFriends.data;
private unsubscribeSubject = new Subject<void>();
public constructor() { public constructor() {
addIcons({ arrowForwardOutline }); addIcons({ arrowForwardOutline });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

12
apps/client/src/app/pages/admin/admin-page.component.ts

@ -1,7 +1,7 @@
import { TabConfiguration } from '@ghostfolio/common/interfaces'; import { TabConfiguration } from '@ghostfolio/common/interfaces';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
@ -14,7 +14,6 @@ import {
settingsOutline settingsOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
@ -23,12 +22,10 @@ import { Subject } from 'rxjs';
styleUrls: ['./admin-page.scss'], styleUrls: ['./admin-page.scss'],
templateUrl: './admin-page.html' templateUrl: './admin-page.html'
}) })
export class AdminPageComponent implements OnDestroy, OnInit { export class AdminPageComponent implements OnInit {
public deviceType: string; public deviceType: string;
public tabs: TabConfiguration[] = []; public tabs: TabConfiguration[] = [];
private unsubscribeSubject = new Subject<void>();
public constructor(private deviceService: DeviceDetectorService) { public constructor(private deviceService: DeviceDetectorService) {
addIcons({ addIcons({
flashOutline, flashOutline,
@ -74,9 +71,4 @@ export class AdminPageComponent implements OnDestroy, OnInit {
} }
]; ];
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

12
apps/client/src/app/pages/demo/demo-page.component.ts

@ -3,9 +3,8 @@ import { InfoItem } from '@ghostfolio/common/interfaces';
import { NotificationService } from '@ghostfolio/ui/notifications'; import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { Component, OnDestroy } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Subject } from 'rxjs';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,11 +12,9 @@ import { Subject } from 'rxjs';
standalone: true, standalone: true,
templateUrl: './demo-page.html' templateUrl: './demo-page.html'
}) })
export class GfDemoPageComponent implements OnDestroy { export class GfDemoPageComponent {
public info: InfoItem; public info: InfoItem;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private dataService: DataService, private dataService: DataService,
private notificationService: NotificationService, private notificationService: NotificationService,
@ -40,9 +37,4 @@ export class GfDemoPageComponent implements OnDestroy {
this.router.navigate(['/']); this.router.navigate(['/']);
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

24
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -188,8 +188,7 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
!this.data.activity?.accountId && !this.data.activity?.accountId &&
this.mode === 'create' this.mode === 'create'
? this.data.accounts[0].id ? this.data.accounts[0].id
: this.data.activity?.accountId, : this.data.activity?.accountId
Validators.required
], ],
assetClass: [this.data.activity?.SymbolProfile?.assetClass], assetClass: [this.data.activity?.SymbolProfile?.assetClass],
assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass], assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass],
@ -365,11 +364,6 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
(this.activityForm.get('dataSource').value === 'MANUAL' && (this.activityForm.get('dataSource').value === 'MANUAL' &&
type === 'BUY') type === 'BUY')
) { ) {
this.activityForm
.get('accountId')
.removeValidators(Validators.required);
this.activityForm.get('accountId').updateValueAndValidity();
const currency = const currency =
this.data.accounts.find(({ id }) => { this.data.accounts.find(({ id }) => {
return id === this.activityForm.get('accountId').value; return id === this.activityForm.get('accountId').value;
@ -397,11 +391,6 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
this.activityForm.get('updateAccountBalance').disable(); this.activityForm.get('updateAccountBalance').disable();
this.activityForm.get('updateAccountBalance').setValue(false); this.activityForm.get('updateAccountBalance').setValue(false);
} else if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) { } else if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) {
this.activityForm
.get('accountId')
.removeValidators(Validators.required);
this.activityForm.get('accountId').updateValueAndValidity();
const currency = const currency =
this.data.accounts.find(({ id }) => { this.data.accounts.find(({ id }) => {
return id === this.activityForm.get('accountId').value; return id === this.activityForm.get('accountId').value;
@ -447,8 +436,6 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
this.activityForm.get('updateAccountBalance').setValue(false); this.activityForm.get('updateAccountBalance').setValue(false);
} }
} else { } else {
this.activityForm.get('accountId').setValidators(Validators.required);
this.activityForm.get('accountId').updateValueAndValidity();
this.activityForm this.activityForm
.get('dataSource') .get('dataSource')
.setValidators(Validators.required); .setValidators(Validators.required);
@ -514,11 +501,12 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
comment: this.activityForm.get('comment').value || null, comment: this.activityForm.get('comment').value || null,
currency: this.activityForm.get('currency').value, currency: this.activityForm.get('currency').value,
customCurrency: this.activityForm.get('currencyOfUnitPrice').value, customCurrency: this.activityForm.get('currencyOfUnitPrice').value,
dataSource: ['FEE', 'INTEREST', 'LIABILITY', 'VALUABLE'].includes(
this.activityForm.get('type').value
)
? 'MANUAL'
: this.activityForm.get('dataSource').value,
date: this.activityForm.get('date').value, date: this.activityForm.get('date').value,
dataSource:
this.activityForm.get('type').value === 'VALUABLE'
? 'MANUAL'
: this.activityForm.get('dataSource').value,
fee: this.activityForm.get('fee').value, fee: this.activityForm.get('fee').value,
quantity: this.activityForm.get('quantity').value, quantity: this.activityForm.get('quantity').value,
symbol: symbol:

8
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -84,12 +84,8 @@
> >
<mat-label i18n>Account</mat-label> <mat-label i18n>Account</mat-label>
<mat-select formControlName="accountId"> <mat-select formControlName="accountId">
@if ( <mat-option [value]="null" />
!activityForm.get('accountId').hasValidator(Validators.required) ||
(!activityForm.get('accountId').value && mode === 'update')
) {
<mat-option [value]="null" />
}
@for (account of data.accounts; track account) { @for (account of data.accounts; track account) {
<mat-option [value]="account.id"> <mat-option [value]="account.id">
<div class="d-flex"> <div class="d-flex">

8
apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts

@ -9,6 +9,7 @@ import {
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
Inject, Inject,
OnDestroy,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@ -52,7 +53,7 @@ import { UserAccountRegistrationDialogParams } from './interfaces/interfaces';
styleUrls: ['./user-account-registration-dialog.scss'], styleUrls: ['./user-account-registration-dialog.scss'],
templateUrl: 'user-account-registration-dialog.html' templateUrl: 'user-account-registration-dialog.html'
}) })
export class GfUserAccountRegistrationDialogComponent { export class GfUserAccountRegistrationDialogComponent implements OnDestroy {
@ViewChild(MatStepper) stepper!: MatStepper; @ViewChild(MatStepper) stepper!: MatStepper;
public accessToken: string; public accessToken: string;
@ -95,4 +96,9 @@ export class GfUserAccountRegistrationDialogComponent {
public onChangeDislaimerChecked() { public onChangeDislaimerChecked() {
this.isDisclaimerChecked = !this.isDisclaimerChecked; this.isDisclaimerChecked = !this.isDisclaimerChecked;
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

8
apps/client/src/app/pages/resources/resources-page.component.ts

@ -13,7 +13,6 @@ import {
readerOutline readerOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
@ -47,8 +46,6 @@ export class ResourcesPageComponent implements OnInit {
} }
]; ];
private unsubscribeSubject = new Subject<void>();
public constructor(private deviceService: DeviceDetectorService) { public constructor(private deviceService: DeviceDetectorService) {
addIcons({ bookOutline, libraryOutline, newspaperOutline, readerOutline }); addIcons({ bookOutline, libraryOutline, newspaperOutline, readerOutline });
} }
@ -56,9 +53,4 @@ export class ResourcesPageComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

8
apps/client/src/locales/messages.zh.xlf

@ -7891,7 +7891,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.feeRatioInitialInvestment" datatype="html"> <trans-unit id="rule.feeRatioInitialInvestment" datatype="html">
<source>Fee Ratio (legacy)</source> <source>Fee Ratio (legacy)</source>
<target state="new">费率</target> <target state="translated">费率(旧版)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">152</context> <context context-type="linenumber">152</context>
@ -7915,7 +7915,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.feeRatioTotalInvestmentVolume" datatype="html"> <trans-unit id="rule.feeRatioTotalInvestmentVolume" datatype="html">
<source>Fee Ratio</source> <source>Fee Ratio</source>
<target state="new">Fee Ratio</target> <target state="translated">费率</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">161</context> <context context-type="linenumber">161</context>
@ -7923,7 +7923,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.feeRatioTotalInvestmentVolume.false" datatype="html"> <trans-unit id="rule.feeRatioTotalInvestmentVolume.false" datatype="html">
<source>The fees do exceed ${thresholdMax}% of your total investment volume (${feeRatio}%)</source> <source>The fees do exceed ${thresholdMax}% of your total investment volume (${feeRatio}%)</source>
<target state="new">The fees do exceed ${thresholdMax}% of your total investment volume (${feeRatio}%)</target> <target state="translated">费用已超过您总投资金额的 ${thresholdMax}%(${feeRatio}%)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">163</context> <context context-type="linenumber">163</context>
@ -7931,7 +7931,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.feeRatioTotalInvestmentVolume.true" datatype="html"> <trans-unit id="rule.feeRatioTotalInvestmentVolume.true" datatype="html">
<source>The fees do not exceed ${thresholdMax}% of your total investment volume (${feeRatio}%)</source> <source>The fees do not exceed ${thresholdMax}% of your total investment volume (${feeRatio}%)</source>
<target state="new">The fees do not exceed ${thresholdMax}% of your total investment volume (${feeRatio}%)</target> <target state="translated">费用未超过您总投资金额的 ${thresholdMax}%(${feeRatio}%)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">167</context> <context context-type="linenumber">167</context>

24
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts

@ -7,12 +7,12 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
EventEmitter,
HostBinding, HostBinding,
Input, Input,
OnChanges, OnChanges,
Output, ViewChild,
ViewChild inject,
output
} from '@angular/core'; } from '@angular/core';
import { Params, RouterModule } from '@angular/router'; import { Params, RouterModule } from '@angular/router';
@ -33,21 +33,23 @@ export class GfAssistantListItemComponent
implements FocusableOption, OnChanges implements FocusableOption, OnChanges
{ {
@HostBinding('attr.tabindex') tabindex = -1; @HostBinding('attr.tabindex') tabindex = -1;
@HostBinding('class.has-focus') get getHasFocus() {
return this.hasFocus;
}
@Input() item: SearchResultItem; @Input() item: SearchResultItem;
@Output() clicked = new EventEmitter<void>(); @ViewChild('link') public linkElement: ElementRef<HTMLAnchorElement>;
@ViewChild('link') public linkElement: ElementRef;
public hasFocus = false; public hasFocus = false;
public queryParams: Params; public queryParams: Params;
public routerLink: string[]; public routerLink: string[];
public constructor(private changeDetectorRef: ChangeDetectorRef) {} protected readonly clicked = output<void>();
private readonly changeDetectorRef = inject(ChangeDetectorRef);
@HostBinding('class.has-focus')
public get getHasFocus() {
return this.hasFocus;
}
public ngOnChanges() { public ngOnChanges() {
if (this.item?.mode === SearchMode.ACCOUNT) { if (this.item?.mode === SearchMode.ACCOUNT) {
@ -65,7 +67,7 @@ export class GfAssistantListItemComponent
}; };
this.routerLink = this.routerLink =
internalRoutes.adminControl.subRoutes.marketData.routerLink; internalRoutes.adminControl.subRoutes?.marketData.routerLink ?? [];
} else if (this.item?.mode === SearchMode.HOLDING) { } else if (this.item?.mode === SearchMode.HOLDING) {
this.queryParams = { this.queryParams = {
dataSource: this.item.dataSource, dataSource: this.item.dataSource,

2
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html

@ -8,7 +8,7 @@
@if (item && isAsset(item)) { @if (item && isAsset(item)) {
<br /> <br />
<small class="text-muted" <small class="text-muted"
>{{ item?.symbol | gfSymbol }} · {{ item?.currency }} >{{ item?.symbol ?? '' | gfSymbol }} · {{ item?.currency }}
@if (item?.assetSubClassString) { @if (item?.assetSubClassString) {
· {{ item.assetSubClassString }} · {{ item.assetSubClassString }}
} }

102
libs/ui/src/lib/assistant/assistant.component.ts

@ -12,16 +12,15 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
EventEmitter,
HostListener, HostListener,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output,
QueryList, QueryList,
ViewChild, ViewChild,
ViewChildren ViewChildren,
output
} from '@angular/core'; } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@ -86,37 +85,7 @@ import {
templateUrl: './assistant.html' templateUrl: './assistant.html'
}) })
export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
@HostListener('document:keydown', ['$event']) onKeydown( public static readonly SEARCH_RESULTS_DEFAULT_LIMIT = 5;
event: KeyboardEvent
) {
if (!this.isOpen) {
return;
}
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
for (const item of this.assistantListItems) {
item.removeFocus();
}
this.keyManager.onKeydown(event);
const currentAssistantListItem = this.getCurrentAssistantListItem();
if (currentAssistantListItem?.linkElement) {
currentAssistantListItem.linkElement.nativeElement?.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
} else if (event.key === 'Enter') {
const currentAssistantListItem = this.getCurrentAssistantListItem();
if (currentAssistantListItem?.linkElement) {
currentAssistantListItem.linkElement.nativeElement?.click();
event.stopPropagation();
}
}
}
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToAccessAdminControl: boolean; @Input() hasPermissionToAccessAdminControl: boolean;
@ -124,21 +93,16 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
@Input() hasPermissionToChangeFilters: boolean; @Input() hasPermissionToChangeFilters: boolean;
@Input() user: User; @Input() user: User;
@Output() closed = new EventEmitter<void>();
@Output() dateRangeChanged = new EventEmitter<DateRange>();
@Output() filtersChanged = new EventEmitter<Filter[]>();
@ViewChild('menuTrigger') menuTriggerElement: MatMenuTrigger; @ViewChild('menuTrigger') menuTriggerElement: MatMenuTrigger;
@ViewChild('search', { static: true }) searchElement: ElementRef; @ViewChild('search', { static: true })
searchElement: ElementRef<HTMLInputElement>;
@ViewChildren(GfAssistantListItemComponent) @ViewChildren(GfAssistantListItemComponent)
assistantListItems: QueryList<GfAssistantListItemComponent>; assistantListItems: QueryList<GfAssistantListItemComponent>;
public static readonly SEARCH_RESULTS_DEFAULT_LIMIT = 5;
public accounts: AccountWithPlatform[] = []; public accounts: AccountWithPlatform[] = [];
public assetClasses: Filter[] = []; public assetClasses: Filter[] = [];
public dateRangeFormControl = new FormControl<string>(undefined); public dateRangeFormControl = new FormControl<string | null>(null);
public dateRangeOptions: DateRangeOption[] = []; public dateRangeOptions: DateRangeOption[] = [];
public holdings: PortfolioPosition[] = []; public holdings: PortfolioPosition[] = [];
public isLoading = { public isLoading = {
@ -166,6 +130,10 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}; };
public tags: Filter[] = []; public tags: Filter[] = [];
protected readonly closed = output<void>();
protected readonly dateRangeChanged = output<DateRange>();
protected readonly filtersChanged = output<Filter[]>();
private readonly PRESELECTION_DELAY = 100; private readonly PRESELECTION_DELAY = 100;
private filterTypes: Filter['type'][] = [ private filterTypes: Filter['type'][] = [
@ -188,6 +156,37 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
addIcons({ closeCircleOutline, closeOutline, searchOutline }); addIcons({ closeCircleOutline, closeOutline, searchOutline });
} }
@HostListener('document:keydown', ['$event'])
public onKeydown(event: KeyboardEvent) {
if (!this.isOpen) {
return;
}
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
for (const item of this.assistantListItems) {
item.removeFocus();
}
this.keyManager.onKeydown(event);
const currentAssistantListItem = this.getCurrentAssistantListItem();
if (currentAssistantListItem?.linkElement) {
currentAssistantListItem.linkElement.nativeElement?.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
} else if (event.key === 'Enter') {
const currentAssistantListItem = this.getCurrentAssistantListItem();
if (currentAssistantListItem?.linkElement) {
currentAssistantListItem.linkElement.nativeElement?.click();
event.stopPropagation();
}
}
}
public ngOnInit() { public ngOnInit() {
this.assetClasses = Object.keys(AssetClass).map((assetClass) => { this.assetClasses = Object.keys(AssetClass).map((assetClass) => {
return { return {
@ -482,7 +481,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
this.holdings = holdings this.holdings = holdings
.filter(({ assetSubClass }) => { .filter(({ assetSubClass }) => {
return !['CASH'].includes(assetSubClass); return assetSubClass && !['CASH'].includes(assetSubClass);
}) })
.sort((a, b) => { .sort((a, b) => {
return a.name?.localeCompare(b.name); return a.name?.localeCompare(b.name);
@ -499,23 +498,23 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
this.filtersChanged.emit([ this.filtersChanged.emit([
{ {
id: filterValue?.account, id: filterValue?.account ?? '',
type: 'ACCOUNT' type: 'ACCOUNT'
}, },
{ {
id: filterValue?.assetClass, id: filterValue?.assetClass ?? '',
type: 'ASSET_CLASS' type: 'ASSET_CLASS'
}, },
{ {
id: filterValue?.holding?.dataSource, id: filterValue?.holding?.dataSource ?? '',
type: 'DATA_SOURCE' type: 'DATA_SOURCE'
}, },
{ {
id: filterValue?.holding?.symbol, id: filterValue?.holding?.symbol ?? '',
type: 'SYMBOL' type: 'SYMBOL'
}, },
{ {
id: filterValue?.tag, id: filterValue?.tag ?? '',
type: 'TAG' type: 'TAG'
} }
]); ]);
@ -541,7 +540,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
this.filterTypes.map((type) => { this.filterTypes.map((type) => {
return { return {
type, type,
id: null id: ''
}; };
}) })
); );
@ -673,7 +672,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
dataSource, dataSource,
name, name,
symbol, symbol,
assetSubClassString: translate(assetSubClass), assetSubClassString: translate(assetSubClass ?? ''),
mode: SearchMode.ASSET_PROFILE as const mode: SearchMode.ASSET_PROFILE as const
}; };
} }
@ -705,7 +704,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
dataSource, dataSource,
name, name,
symbol, symbol,
assetSubClassString: translate(assetSubClass), assetSubClassString: translate(assetSubClass ?? ''),
mode: SearchMode.HOLDING as const mode: SearchMode.HOLDING as const
}; };
} }
@ -755,6 +754,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
const symbol = this.user?.settings?.['filters.symbol']; const symbol = this.user?.settings?.['filters.symbol'];
const selectedHolding = this.holdings.find((holding) => { const selectedHolding = this.holdings.find((holding) => {
return ( return (
!!(dataSource && symbol) &&
getAssetProfileIdentifier({ getAssetProfileIdentifier({
dataSource: holding.dataSource, dataSource: holding.dataSource,
symbol: holding.symbol symbol: holding.symbol

2
libs/ui/src/lib/assistant/assistant.html

@ -186,7 +186,7 @@
<div class="p-3"> <div class="p-3">
<gf-portfolio-filter-form <gf-portfolio-filter-form
#portfolioFilterForm #portfolioFilterForm
[accounts]="user?.accounts" [accounts]="user?.accounts ?? []"
[assetClasses]="assetClasses" [assetClasses]="assetClasses"
[formControl]="portfolioFilterFormControl" [formControl]="portfolioFilterFormControl"
[holdings]="holdings" [holdings]="holdings"

90
libs/ui/src/lib/currency-selector/currency-selector.component.ts

@ -4,13 +4,16 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef,
DoCheck, DoCheck,
ElementRef, ElementRef,
Input,
OnDestroy,
OnInit, OnInit,
ViewChild ViewChild,
inject,
input,
viewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { import {
FormControl, FormControl,
FormGroupDirective, FormGroupDirective,
@ -21,15 +24,14 @@ import {
import { import {
MatAutocomplete, MatAutocomplete,
MatAutocompleteModule, MatAutocompleteModule,
MatAutocompleteSelectedEvent MatOption
} from '@angular/material/autocomplete'; } from '@angular/material/autocomplete';
import { import {
MatFormFieldControl, MatFormFieldControl,
MatFormFieldModule MatFormFieldModule
} from '@angular/material/form-field'; } from '@angular/material/form-field';
import { MatInput, MatInputModule } from '@angular/material/input'; import { MatInput, MatInputModule } from '@angular/material/input';
import { Subject } from 'rxjs'; import { map, startWith } from 'rxjs/operators';
import { map, startWith, takeUntil } from 'rxjs/operators';
import { AbstractMatFormField } from '../shared/abstract-mat-form-field'; import { AbstractMatFormField } from '../shared/abstract-mat-form-field';
@ -58,21 +60,19 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field';
templateUrl: 'currency-selector.component.html' templateUrl: 'currency-selector.component.html'
}) })
export class GfCurrencySelectorComponent export class GfCurrencySelectorComponent
extends AbstractMatFormField<string> extends AbstractMatFormField<string | null>
implements DoCheck, OnDestroy, OnInit implements DoCheck, OnInit
{ {
@Input() private currencies: string[] = [];
@Input() private formControlName: string;
@ViewChild(MatInput) private input: MatInput;
@ViewChild('currencyAutocomplete') @ViewChild('currencyAutocomplete')
public currencyAutocomplete: MatAutocomplete; public currencyAutocomplete: MatAutocomplete;
public control = new FormControl(); public readonly control = new FormControl<string | null>(null);
public readonly currencies = input.required<string[]>();
public filteredCurrencies: string[] = []; public filteredCurrencies: string[] = [];
public readonly formControlName = input.required<string>();
private unsubscribeSubject = new Subject<void>(); private readonly destroyRef = inject(DestroyRef);
private readonly input = viewChild.required(MatInput);
public constructor( public constructor(
public readonly _elementRef: ElementRef, public readonly _elementRef: ElementRef,
@ -86,6 +86,19 @@ export class GfCurrencySelectorComponent
this.controlType = 'currency-selector'; this.controlType = 'currency-selector';
} }
public get empty() {
return this.input().empty;
}
public set value(value: string | null) {
this.control.setValue(value);
super.value = value;
}
public focus() {
this.input().focus();
}
public ngOnInit() { public ngOnInit() {
if (this.disabled) { if (this.disabled) {
this.control.disable(); this.control.disable();
@ -94,17 +107,18 @@ export class GfCurrencySelectorComponent
const formGroup = this.formGroupDirective.form; const formGroup = this.formGroupDirective.form;
if (formGroup) { if (formGroup) {
const control = formGroup.get(this.formControlName); const control = formGroup.get(this.formControlName());
if (control) { if (control) {
this.value = this.currencies.find((value) => { this.value =
return value === control.value; this.currencies().find((value) => {
}); return value === control.value;
}) ?? null;
} }
} }
this.control.valueChanges this.control.valueChanges
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
if (super.value) { if (super.value) {
super.value = null; super.value = null;
@ -113,10 +127,10 @@ export class GfCurrencySelectorComponent
this.control.valueChanges this.control.valueChanges
.pipe( .pipe(
takeUntil(this.unsubscribeSubject), takeUntilDestroyed(this.destroyRef),
startWith(''), startWith(''),
map((value) => { map((value) => {
return value ? this.filter(value) : this.currencies.slice(); return value ? this.filter(value) : this.currencies().slice();
}) })
) )
.subscribe((values) => { .subscribe((values) => {
@ -124,42 +138,22 @@ export class GfCurrencySelectorComponent
}); });
} }
public get empty() {
return this.input?.empty;
}
public focus() {
this.input.focus();
}
public ngDoCheck() { public ngDoCheck() {
if (this.ngControl) { if (this.ngControl) {
this.validateRequired(); this.validateRequired();
this.errorState = this.ngControl.invalid && this.ngControl.touched; this.errorState = !!(this.ngControl.invalid && this.ngControl.touched);
this.stateChanges.next(); this.stateChanges.next();
} }
} }
public onUpdateCurrency(event: MatAutocompleteSelectedEvent) { public onUpdateCurrency({ option }: { option: MatOption<string> }) {
super.value = event.option.value; super.value = option.value;
}
public set value(value: string) {
this.control.setValue(value);
super.value = value;
}
public ngOnDestroy() {
super.ngOnDestroy();
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
} }
private filter(value: string) { private filter(value: string) {
const filterValue = value?.toLowerCase(); const filterValue = value.toLowerCase();
return this.currencies.filter((currency) => { return this.currencies().filter((currency) => {
return currency.toLowerCase().startsWith(filterValue); return currency.toLowerCase().startsWith(filterValue);
}); });
} }
@ -168,7 +162,7 @@ export class GfCurrencySelectorComponent
const requiredCheck = super.required ? !super.value : false; const requiredCheck = super.required ? !super.value : false;
if (requiredCheck) { if (requiredCheck) {
this.ngControl.control.setErrors({ invalidData: true }); this.ngControl.control?.setErrors({ invalidData: true });
} }
} }
} }

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

@ -13,13 +13,13 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
EventEmitter,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
Output, ViewChild,
ViewChild output
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { import {
FormBuilder, FormBuilder,
FormControl, FormControl,
@ -55,9 +55,9 @@ import {
startOfMonth, startOfMonth,
sub sub
} from 'date-fns'; } from 'date-fns';
import { isNumber } from 'lodash'; import { isNil, isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, debounceTime, takeUntil } from 'rxjs'; import { debounceTime } from 'rxjs';
import { FireCalculatorService } from './fire-calculator.service'; import { FireCalculatorService } from './fire-calculator.service';
@ -90,32 +90,31 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@Input() retirementDate: Date; @Input() retirementDate: Date;
@Input() savingsRate = 0; @Input() savingsRate = 0;
@Output() annualInterestRateChanged = new EventEmitter<number>();
@Output() calculationCompleted =
new EventEmitter<FireCalculationCompleteEvent>();
@Output() projectedTotalAmountChanged = new EventEmitter<number>();
@Output() retirementDateChanged = new EventEmitter<Date>();
@Output() savingsRateChanged = new EventEmitter<number>();
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public calculatorForm = this.formBuilder.group({ public calculatorForm = this.formBuilder.group({
annualInterestRate: new FormControl<number>(undefined), annualInterestRate: new FormControl<number | null>(null),
paymentPerPeriod: new FormControl<number>(undefined), paymentPerPeriod: new FormControl<number | null>(null),
principalInvestmentAmount: new FormControl<number>(undefined), principalInvestmentAmount: new FormControl<number | null>(null),
projectedTotalAmount: new FormControl<number>(undefined), projectedTotalAmount: new FormControl<number | null>(null),
retirementDate: new FormControl<Date>(undefined) retirementDate: new FormControl<Date | null>(null)
}); });
public chart: Chart<'bar'>; public chart: Chart<'bar'>;
public isLoading = true; public isLoading = true;
public minDate = addDays(new Date(), 1); public minDate = addDays(new Date(), 1);
public periodsToRetire = 0; public periodsToRetire = 0;
protected readonly annualInterestRateChanged = output<number>();
protected readonly calculationCompleted =
output<FireCalculationCompleteEvent>();
protected readonly projectedTotalAmountChanged = output<number>();
protected readonly retirementDateChanged = output<Date>();
protected readonly savingsRateChanged = output<number>();
private readonly CONTRIBUTION_PERIOD = 12; private readonly CONTRIBUTION_PERIOD = 12;
private readonly DEFAULT_RETIREMENT_DATE = startOfMonth( private readonly DEFAULT_RETIREMENT_DATE = startOfMonth(
addYears(new Date(), 10) addYears(new Date(), 10)
); );
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@ -131,46 +130,56 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
); );
this.calculatorForm.valueChanges this.calculatorForm.valueChanges
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed())
.subscribe(() => { .subscribe(() => {
this.initialize(); this.initialize();
}); });
this.calculatorForm.valueChanges this.calculatorForm.valueChanges
.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) .pipe(debounceTime(500), takeUntilDestroyed())
.subscribe(() => { .subscribe(() => {
const { projectedTotalAmount, retirementDate } = const { projectedTotalAmount, retirementDate } =
this.calculatorForm.getRawValue(); this.calculatorForm.getRawValue();
this.calculationCompleted.emit({ if (projectedTotalAmount !== null && retirementDate !== null) {
projectedTotalAmount, this.calculationCompleted.emit({
retirementDate projectedTotalAmount,
}); retirementDate
});
}
}); });
this.calculatorForm this.calculatorForm
.get('annualInterestRate') .get('annualInterestRate')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) ?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed())
.subscribe((annualInterestRate) => { .subscribe((annualInterestRate) => {
this.annualInterestRateChanged.emit(annualInterestRate); if (annualInterestRate !== null) {
this.annualInterestRateChanged.emit(annualInterestRate);
}
}); });
this.calculatorForm this.calculatorForm
.get('paymentPerPeriod') .get('paymentPerPeriod')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) ?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed())
.subscribe((savingsRate) => { .subscribe((savingsRate) => {
this.savingsRateChanged.emit(savingsRate); if (savingsRate !== null) {
this.savingsRateChanged.emit(savingsRate);
}
}); });
this.calculatorForm this.calculatorForm
.get('projectedTotalAmount') .get('projectedTotalAmount')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) ?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed())
.subscribe((projectedTotalAmount) => { .subscribe((projectedTotalAmount) => {
this.projectedTotalAmountChanged.emit(projectedTotalAmount); if (projectedTotalAmount !== null) {
this.projectedTotalAmountChanged.emit(projectedTotalAmount);
}
}); });
this.calculatorForm this.calculatorForm
.get('retirementDate') .get('retirementDate')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) ?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed())
.subscribe((retirementDate) => { .subscribe((retirementDate) => {
this.retirementDateChanged.emit(retirementDate); if (retirementDate !== null) {
this.retirementDateChanged.emit(retirementDate);
}
}); });
} }
@ -196,11 +205,11 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.calculatorForm.patchValue( this.calculatorForm.patchValue(
{ {
annualInterestRate: annualInterestRate:
this.calculatorForm.get('annualInterestRate').value, this.calculatorForm.get('annualInterestRate')?.value,
paymentPerPeriod: this.getPMT(), paymentPerPeriod: this.getPMT(),
principalInvestmentAmount: this.calculatorForm.get( principalInvestmentAmount: this.calculatorForm.get(
'principalInvestmentAmount' 'principalInvestmentAmount'
).value, )?.value,
projectedTotalAmount: projectedTotalAmount:
Math.round(this.getProjectedTotalAmount()) || 0, Math.round(this.getProjectedTotalAmount()) || 0,
retirementDate: retirementDate:
@ -210,7 +219,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
emitEvent: false emitEvent: false
} }
); );
this.calculatorForm.get('principalInvestmentAmount').disable(); this.calculatorForm.get('principalInvestmentAmount')?.disable();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -219,42 +228,43 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
if (this.hasPermissionToUpdateUserSettings === true) { if (this.hasPermissionToUpdateUserSettings === true) {
this.calculatorForm this.calculatorForm
.get('annualInterestRate') .get('annualInterestRate')
.enable({ emitEvent: false }); ?.enable({ emitEvent: false });
this.calculatorForm.get('paymentPerPeriod').enable({ emitEvent: false }); this.calculatorForm.get('paymentPerPeriod')?.enable({ emitEvent: false });
this.calculatorForm this.calculatorForm
.get('projectedTotalAmount') .get('projectedTotalAmount')
.enable({ emitEvent: false }); ?.enable({ emitEvent: false });
} else { } else {
this.calculatorForm this.calculatorForm
.get('annualInterestRate') .get('annualInterestRate')
.disable({ emitEvent: false }); ?.disable({ emitEvent: false });
this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false }); this.calculatorForm
.get('paymentPerPeriod')
?.disable({ emitEvent: false });
this.calculatorForm this.calculatorForm
.get('projectedTotalAmount') .get('projectedTotalAmount')
.disable({ emitEvent: false }); ?.disable({ emitEvent: false });
} }
this.calculatorForm.get('retirementDate').disable({ emitEvent: false }); this.calculatorForm.get('retirementDate')?.disable({ emitEvent: false });
} }
public setMonthAndYear( public setMonthAndYear(
normalizedMonthAndYear: Date, normalizedMonthAndYear: Date,
datepicker: MatDatepicker<Date> datepicker: MatDatepicker<Date>
) { ) {
const retirementDate = this.calculatorForm.get('retirementDate').value; const retirementDate =
this.calculatorForm.get('retirementDate')?.value ??
this.DEFAULT_RETIREMENT_DATE;
const newRetirementDate = setMonth( const newRetirementDate = setMonth(
setYear(retirementDate, normalizedMonthAndYear.getFullYear()), setYear(retirementDate, normalizedMonthAndYear.getFullYear()),
normalizedMonthAndYear.getMonth() normalizedMonthAndYear.getMonth()
); );
this.calculatorForm.get('retirementDate').setValue(newRetirementDate); this.calculatorForm.get('retirementDate')?.setValue(newRetirementDate);
datepicker.close(); datepicker.close();
} }
public ngOnDestroy() { public ngOnDestroy() {
this.chart?.destroy(); this.chart?.destroy();
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
} }
private initialize() { private initialize() {
@ -288,8 +298,6 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
return `Total: ${new Intl.NumberFormat(this.locale, { return `Total: ${new Intl.NumberFormat(this.locale, {
currency: this.currency, currency: this.currency,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Only supported from ES2020 or later
currencyDisplay: 'code', currencyDisplay: 'code',
style: 'currency' style: 'currency'
}).format(totalAmount)}`; }).format(totalAmount)}`;
@ -426,12 +434,16 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
} }
private getPeriodsToRetire(): number { private getPeriodsToRetire(): number {
if (this.calculatorForm.get('projectedTotalAmount').value) { const projectedTotalAmount = this.calculatorForm.get(
'projectedTotalAmount'
)?.value;
if (projectedTotalAmount) {
let periods = this.fireCalculatorService.calculatePeriodsToRetire({ let periods = this.fireCalculatorService.calculatePeriodsToRetire({
P: this.getP(), P: this.getP(),
PMT: this.getPMT(), PMT: this.getPMT(),
r: this.getR(), r: this.getR(),
totalAmount: this.calculatorForm.get('projectedTotalAmount').value totalAmount: projectedTotalAmount
}); });
if (periods === Infinity) { if (periods === Infinity) {
@ -453,12 +465,16 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
} }
private getPMT() { private getPMT() {
return this.calculatorForm.get('paymentPerPeriod').value; return this.calculatorForm.get('paymentPerPeriod')?.value ?? 0;
} }
private getProjectedTotalAmount() { private getProjectedTotalAmount() {
if (this.calculatorForm.get('projectedTotalAmount').value) { const projectedTotalAmount = this.calculatorForm.get(
return this.calculatorForm.get('projectedTotalAmount').value; 'projectedTotalAmount'
)?.value;
if (!isNil(projectedTotalAmount)) {
return projectedTotalAmount;
} }
const { totalAmount } = const { totalAmount } =
@ -473,12 +489,12 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
} }
private getR() { private getR() {
return this.calculatorForm.get('annualInterestRate').value / 100; return (this.calculatorForm.get('annualInterestRate')?.value ?? 0) / 100;
} }
private getRetirementDate(): Date { private getRetirementDate(): Date {
if (this.periodsToRetire === Number.MAX_SAFE_INTEGER) { if (this.periodsToRetire === Number.MAX_SAFE_INTEGER) {
return undefined; return this.DEFAULT_RETIREMENT_DATE;
} }
const monthsToRetire = this.periodsToRetire % 12; const monthsToRetire = this.periodsToRetire % 12;

8
libs/ui/src/lib/portfolio-filter-form/interfaces/portfolio-filter-form-value.interface.ts

@ -1,8 +1,8 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { PortfolioPosition } from '@ghostfolio/common/interfaces';
export interface PortfolioFilterFormValue { export interface PortfolioFilterFormValue {
account: string; account: string | null;
assetClass: string; assetClass: string | null;
holding: PortfolioPosition; holding: PortfolioPosition | null;
tag: string; tag: string | null;
} }

227
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.241.0", "version": "2.243.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.241.0", "version": "2.243.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -27,15 +27,15 @@
"@ionic/angular": "8.7.8", "@ionic/angular": "8.7.8",
"@keyv/redis": "4.4.0", "@keyv/redis": "4.4.0",
"@nestjs/bull": "11.0.4", "@nestjs/bull": "11.0.4",
"@nestjs/cache-manager": "3.0.1", "@nestjs/cache-manager": "3.1.0",
"@nestjs/common": "11.1.8", "@nestjs/common": "11.1.14",
"@nestjs/config": "4.0.2", "@nestjs/config": "4.0.3",
"@nestjs/core": "11.1.8", "@nestjs/core": "11.1.14",
"@nestjs/event-emitter": "3.0.1", "@nestjs/event-emitter": "3.0.1",
"@nestjs/jwt": "11.0.1", "@nestjs/jwt": "11.0.2",
"@nestjs/passport": "11.0.5", "@nestjs/passport": "11.0.5",
"@nestjs/platform-express": "11.1.8", "@nestjs/platform-express": "11.1.14",
"@nestjs/schedule": "6.0.1", "@nestjs/schedule": "6.1.1",
"@nestjs/serve-static": "5.0.4", "@nestjs/serve-static": "5.0.4",
"@openrouter/ai-sdk-provider": "0.7.2", "@openrouter/ai-sdk-provider": "0.7.2",
"@prisma/client": "6.19.0", "@prisma/client": "6.19.0",
@ -107,7 +107,7 @@
"@eslint/eslintrc": "3.3.1", "@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.35.0", "@eslint/js": "9.35.0",
"@nestjs/schematics": "11.0.9", "@nestjs/schematics": "11.0.9",
"@nestjs/testing": "11.1.8", "@nestjs/testing": "11.1.14",
"@nx/angular": "22.4.5", "@nx/angular": "22.4.5",
"@nx/eslint-plugin": "22.4.5", "@nx/eslint-plugin": "22.4.5",
"@nx/jest": "22.4.5", "@nx/jest": "22.4.5",
@ -3617,6 +3617,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@borewit/text-codec": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz",
"integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/@braintree/sanitize-url": { "node_modules/@braintree/sanitize-url": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz",
@ -7531,9 +7541,9 @@
} }
}, },
"node_modules/@nestjs/cache-manager": { "node_modules/@nestjs/cache-manager": {
"version": "3.0.1", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.1.0.tgz",
"integrity": "sha512-4UxTnR0fsmKL5YDalU2eLFVnL+OBebWUpX+hEduKGncrVKH4PPNoiRn1kXyOCjmzb0UvWgqubpssNouc8e0MCw==", "integrity": "sha512-pEIqYZrBcE8UdkJmZRduurvoUfdU+3kRPeO1R2muiMbZnRuqlki5klFFNllO9LyYWzrx98bd1j0PSPKSJk1Wbw==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
@ -7544,12 +7554,12 @@
} }
}, },
"node_modules/@nestjs/common": { "node_modules/@nestjs/common": {
"version": "11.1.8", "version": "11.1.14",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.8.tgz", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.14.tgz",
"integrity": "sha512-bbsOqwld/GdBfiRNc4nnjyWWENDEicq4SH+R5AuYatvf++vf1x5JIsHB1i1KtfZMD3eRte0D4K9WXuAYil6XAg==", "integrity": "sha512-IN/tlqd7Nl9gl6f0jsWEuOrQDaCI9vHzxv0fisHysfBQzfQIkqlv5A7w4Qge02BUQyczXT9HHPgHtWHCxhjRng==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"file-type": "21.0.0", "file-type": "21.3.0",
"iterare": "1.2.1", "iterare": "1.2.1",
"load-esm": "1.0.3", "load-esm": "1.0.3",
"tslib": "2.8.1", "tslib": "2.8.1",
@ -7575,57 +7585,24 @@
} }
}, },
"node_modules/@nestjs/config": { "node_modules/@nestjs/config": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.3.tgz",
"integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", "integrity": "sha512-FQ3M3Ohqfl+nHAn5tp7++wUQw0f2nAk+SFKe8EpNRnIifPqvfJP6JQxPKtFLMOHbyer4X646prFG4zSRYEssQQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dotenv": "16.4.7", "dotenv": "17.2.3",
"dotenv-expand": "12.0.1", "dotenv-expand": "12.0.3",
"lodash": "4.17.21" "lodash": "4.17.23"
}, },
"peerDependencies": { "peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/common": "^10.0.0 || ^11.0.0",
"rxjs": "^7.1.0" "rxjs": "^7.1.0"
} }
}, },
"node_modules/@nestjs/config/node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@nestjs/config/node_modules/dotenv-expand": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz",
"integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==",
"license": "BSD-2-Clause",
"dependencies": {
"dotenv": "^16.4.5"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@nestjs/config/node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/@nestjs/core": { "node_modules/@nestjs/core": {
"version": "11.1.8", "version": "11.1.14",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.8.tgz", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.14.tgz",
"integrity": "sha512-7riWfmTmMhCJHZ5ZiaG+crj4t85IPCq/wLRuOUSigBYyFT2JZj0lVHtAdf4Davp9ouNI8GINBDt9h9b5Gz9nTw==", "integrity": "sha512-7OXPPMoDr6z+5NkoQKu4hOhfjz/YYqM3bNilPqv1WVFWrzSmuNXxvhbX69YMmNmRYascPXiwESqf5jJdjKXEww==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -7677,13 +7654,13 @@
} }
}, },
"node_modules/@nestjs/jwt": { "node_modules/@nestjs/jwt": {
"version": "11.0.1", "version": "11.0.2",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz",
"integrity": "sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==", "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/jsonwebtoken": "9.0.10", "@types/jsonwebtoken": "9.0.10",
"jsonwebtoken": "9.0.2" "jsonwebtoken": "9.0.3"
}, },
"peerDependencies": { "peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
@ -7700,13 +7677,13 @@
} }
}, },
"node_modules/@nestjs/platform-express": { "node_modules/@nestjs/platform-express": {
"version": "11.1.8", "version": "11.1.14",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.8.tgz", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.14.tgz",
"integrity": "sha512-rL6pZH9BW7BnL5X2eWbJMtt86uloAKjFgyY5+L2UkizgfEp7rgAs0+Z1z0BcW2Pgu5+q8O7RKPNyHJ/9ZNz/ZQ==", "integrity": "sha512-Fs+/j+mBSBSXErOQJ/YdUn/HqJGSJ4pGfiJyYOyz04l42uNVnqEakvu1kXLbxMabR6vd6/h9d6Bi4tso9p7o4Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cors": "2.8.5", "cors": "2.8.6",
"express": "5.1.0", "express": "5.2.1",
"multer": "2.0.2", "multer": "2.0.2",
"path-to-regexp": "8.3.0", "path-to-regexp": "8.3.0",
"tslib": "2.8.1" "tslib": "2.8.1"
@ -7721,12 +7698,12 @@
} }
}, },
"node_modules/@nestjs/schedule": { "node_modules/@nestjs/schedule": {
"version": "6.0.1", "version": "6.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.1.1.tgz",
"integrity": "sha512-v3yO6cSPAoBSSyH67HWnXHzuhPhSNZhRmLY38JvCt2sqY8sPMOODpcU1D79iUMFf7k16DaMEbL4Mgx61ZhiC8Q==", "integrity": "sha512-kQl1RRgi02GJ0uaUGCrXHCcwISsCsJDciCKe38ykJZgnAeeoeVWs8luWtBo4AqAAXm4nS5K8RlV0smHUJ4+2FA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cron": "4.3.3" "cron": "4.4.0"
}, },
"peerDependencies": { "peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/common": "^10.0.0 || ^11.0.0",
@ -7910,9 +7887,9 @@
} }
}, },
"node_modules/@nestjs/testing": { "node_modules/@nestjs/testing": {
"version": "11.1.8", "version": "11.1.14",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.8.tgz", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.14.tgz",
"integrity": "sha512-E6K+0UTKztcPxJzLnQa7S34lFjZbrj3Z1r7c5y5WDrL1m5HD1H4AeyBhicHgdaFmxjLAva2bq0sYKy/S7cdeYA==", "integrity": "sha512-cQxX0ronsTbpfHz8/LYOVWXxoTxv6VoxrnuZoQaVX7QV2PSMqxWE7/9jSQR0GcqAFUEmFP34c6EJqfkjfX/k4Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -12408,14 +12385,13 @@
} }
}, },
"node_modules/@tokenizer/inflate": { "node_modules/@tokenizer/inflate": {
"version": "0.2.7", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
"integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "^4.4.0", "debug": "^4.4.3",
"fflate": "^0.8.2", "token-types": "^6.1.1"
"token-types": "^6.0.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -16984,9 +16960,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/cors": { "node_modules/cors": {
"version": "2.8.5", "version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"object-assign": "^4", "object-assign": "^4",
@ -16994,6 +16970,10 @@
}, },
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/corser": { "node_modules/corser": {
@ -17073,9 +17053,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/cron": { "node_modules/cron": {
"version": "4.3.3", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/cron/-/cron-4.3.3.tgz", "resolved": "https://registry.npmjs.org/cron/-/cron-4.4.0.tgz",
"integrity": "sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==", "integrity": "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/luxon": "~3.7.0", "@types/luxon": "~3.7.0",
@ -17083,6 +17063,10 @@
}, },
"engines": { "engines": {
"node": ">=18.x" "node": ">=18.x"
},
"funding": {
"type": "ko-fi",
"url": "https://ko-fi.com/intcreator"
} }
}, },
"node_modules/cron-parser": { "node_modules/cron-parser": {
@ -19852,18 +19836,19 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/express": { "node_modules/express": {
"version": "5.1.0", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.1",
"content-disposition": "^1.0.0", "content-disposition": "^1.0.0",
"content-type": "^1.0.5", "content-type": "^1.0.5",
"cookie": "^0.7.1", "cookie": "^0.7.1",
"cookie-signature": "^1.2.1", "cookie-signature": "^1.2.1",
"debug": "^4.4.0", "debug": "^4.4.0",
"depd": "^2.0.0",
"encodeurl": "^2.0.0", "encodeurl": "^2.0.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"etag": "^1.8.1", "etag": "^1.8.1",
@ -20129,12 +20114,6 @@
"filenamify-url": "2.1.2" "filenamify-url": "2.1.2"
} }
}, },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/figures": { "node_modules/figures": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@ -20175,14 +20154,14 @@
} }
}, },
"node_modules/file-type": { "node_modules/file-type": {
"version": "21.0.0", "version": "21.3.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.0.tgz",
"integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", "integrity": "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tokenizer/inflate": "^0.2.7", "@tokenizer/inflate": "^0.4.1",
"strtok3": "^10.2.2", "strtok3": "^10.3.4",
"token-types": "^6.0.0", "token-types": "^6.1.1",
"uint8array-extras": "^1.4.0" "uint8array-extras": "^1.4.0"
}, },
"engines": { "engines": {
@ -24592,12 +24571,12 @@
} }
}, },
"node_modules/jsonwebtoken": { "node_modules/jsonwebtoken": {
"version": "9.0.2", "version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"jws": "^3.2.2", "jws": "^4.0.1",
"lodash.includes": "^4.3.0", "lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3", "lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4", "lodash.isinteger": "^4.0.4",
@ -24613,27 +24592,6 @@
"npm": ">=6" "npm": ">=6"
} }
}, },
"node_modules/jsonwebtoken/node_modules/jwa": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jsonwebtoken/node_modules/jws": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.2",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jsonwebtoken/node_modules/ms": { "node_modules/jsonwebtoken/node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -32900,11 +32858,12 @@
} }
}, },
"node_modules/token-types": { "node_modules/token-types": {
"version": "6.0.4", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.4.tgz", "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
"integrity": "sha512-MD9MjpVNhVyH4fyd5rKphjvt/1qj+PtQUz65aFqAZA6XniWAuSFRjLk3e2VALEFlh9OwBpXUN7rfeqSnT/Fmkw==", "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@borewit/text-codec": "^0.2.1",
"@tokenizer/token": "^0.3.0", "@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1" "ieee754": "^1.2.1"
}, },
@ -33690,9 +33649,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/uint8array-extras": { "node_modules/uint8array-extras": {
"version": "1.4.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
"integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"

18
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.241.0", "version": "2.243.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",
@ -72,15 +72,15 @@
"@ionic/angular": "8.7.8", "@ionic/angular": "8.7.8",
"@keyv/redis": "4.4.0", "@keyv/redis": "4.4.0",
"@nestjs/bull": "11.0.4", "@nestjs/bull": "11.0.4",
"@nestjs/cache-manager": "3.0.1", "@nestjs/cache-manager": "3.1.0",
"@nestjs/common": "11.1.8", "@nestjs/common": "11.1.14",
"@nestjs/config": "4.0.2", "@nestjs/config": "4.0.3",
"@nestjs/core": "11.1.8", "@nestjs/core": "11.1.14",
"@nestjs/event-emitter": "3.0.1", "@nestjs/event-emitter": "3.0.1",
"@nestjs/jwt": "11.0.1", "@nestjs/jwt": "11.0.2",
"@nestjs/passport": "11.0.5", "@nestjs/passport": "11.0.5",
"@nestjs/platform-express": "11.1.8", "@nestjs/platform-express": "11.1.14",
"@nestjs/schedule": "6.0.1", "@nestjs/schedule": "6.1.1",
"@nestjs/serve-static": "5.0.4", "@nestjs/serve-static": "5.0.4",
"@openrouter/ai-sdk-provider": "0.7.2", "@openrouter/ai-sdk-provider": "0.7.2",
"@prisma/client": "6.19.0", "@prisma/client": "6.19.0",
@ -152,7 +152,7 @@
"@eslint/eslintrc": "3.3.1", "@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.35.0", "@eslint/js": "9.35.0",
"@nestjs/schematics": "11.0.9", "@nestjs/schematics": "11.0.9",
"@nestjs/testing": "11.1.8", "@nestjs/testing": "11.1.14",
"@nx/angular": "22.4.5", "@nx/angular": "22.4.5",
"@nx/eslint-plugin": "22.4.5", "@nx/eslint-plugin": "22.4.5",
"@nx/jest": "22.4.5", "@nx/jest": "22.4.5",

Loading…
Cancel
Save