Browse Source

Merge remote-tracking branch 'origin/main' into feature/enable-strict-null-checks-in-ui

pull/6264/head
KenTandrian 2 weeks ago
parent
commit
95ed5a4ee3
  1. 8
      CHANGELOG.md
  2. 2
      apps/api/src/app/activities/activities.controller.ts
  3. 2
      apps/api/src/app/admin/admin.controller.ts
  4. 6
      apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts
  5. 10
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  6. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  7. 2
      apps/api/src/app/portfolio/portfolio.controller.ts
  8. 4
      apps/api/src/app/portfolio/portfolio.service.ts
  9. 34
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  10. 30
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  11. 13
      apps/client/src/app/components/rule/rule.component.ts
  12. 12
      apps/client/src/app/pages/landing/landing-page.component.ts
  13. 14
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  14. 20
      libs/common/src/lib/calculation-helper.ts
  15. 3
      libs/ui/src/lib/services/admin.service.ts
  16. 4
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
  17. 123
      package-lock.json
  18. 4
      package.json

8
CHANGELOG.md

@ -5,19 +5,25 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## 2.250.0 - 2026-03-17
### Added
- Added support for specific calendar year date ranges (`2025`, `2024`, `2023`, etc.) on the portfolio activities page
### Changed ### Changed
- Consolidated the sign-out logic within the user service to unify cookie, state and token clearance - Consolidated the sign-out logic within the user service to unify cookie, state and token clearance
- Improved the language localization for Polish (`pl`) - Improved the language localization for Polish (`pl`)
- Upgraded `@ionic/angular` from version `8.7.3` to `8.8.1` - Upgraded `@ionic/angular` from version `8.7.3` to `8.8.1`
- Upgraded `replace-in-file` from version `8.3.0` to `8.4.0`
- Upgraded `svgmap` from version `2.14.0` to `2.19.2` - Upgraded `svgmap` from version `2.14.0` to `2.19.2`
- Pinned the _Node.js_ version in the _Build code_ _GitHub Action_ to ensure environment consistency for tests - Pinned the _Node.js_ version in the _Build code_ _GitHub Action_ to ensure environment consistency for tests
### Fixed ### Fixed
- Fixed an issue with the detection of the thousand separator for the `de-CH` locale - Fixed an issue with the detection of the thousand separator for the `de-CH` locale
- Fixed an issue in the _Storybook_ stories of the symbol autocomplete component caused by a circular dependency
## 2.249.0 - 2026-03-10 ## 2.249.0 - 2026-03-10

2
apps/api/src/app/activities/activities.controller.ts

@ -126,7 +126,7 @@ export class ActivitiesController {
let startDate: Date; let startDate: Date;
if (dateRange) { if (dateRange) {
({ endDate, startDate } = getIntervalFromDateRange(dateRange)); ({ endDate, startDate } = getIntervalFromDateRange({ dateRange }));
} }
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({

2
apps/api/src/app/admin/admin.controller.ts

@ -172,7 +172,7 @@ export class AdminController {
let date: Date; let date: Date;
if (dateRange) { if (dateRange) {
const { startDate } = getIntervalFromDateRange(dateRange); const { startDate } = getIntervalFromDateRange({ dateRange });
date = startDate; date = startDate;
} }

6
apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts

@ -126,10 +126,10 @@ export class BenchmarksController {
@Query('tags') filterByTags?: string, @Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false' @Query('withExcludedAccounts') withExcludedAccountsParam = 'false'
): Promise<BenchmarkMarketDataDetailsResponse> { ): Promise<BenchmarkMarketDataDetailsResponse> {
const { endDate, startDate } = getIntervalFromDateRange( const { endDate, startDate } = getIntervalFromDateRange({
dateRange, dateRange,
new Date(startDateString) startDate: new Date(startDateString)
); });
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,

10
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -158,10 +158,10 @@ export abstract class PortfolioCalculator {
this.redisCacheService = redisCacheService; this.redisCacheService = redisCacheService;
this.userId = userId; this.userId = userId;
const { endDate, startDate } = getIntervalFromDateRange( const { endDate, startDate } = getIntervalFromDateRange({
'max', dateRange: 'max',
subDays(dateOfFirstActivity, 1) startDate: subDays(dateOfFirstActivity, 1)
); });
this.endDate = endOfDay(endDate); this.endDate = endOfDay(endDate);
this.startDate = startOfDay(startDate); this.startDate = startOfDay(startDate);
@ -885,7 +885,7 @@ export abstract class PortfolioCalculator {
// Make sure some key dates are present // Make sure some key dates are present
for (const dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) { for (const dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) {
const { endDate: dateRangeEnd, startDate: dateRangeStart } = const { endDate: dateRangeEnd, startDate: dateRangeStart } =
getIntervalFromDateRange(dateRange); getIntervalFromDateRange({ dateRange });
if ( if (
!isBefore(dateRangeStart, startDate) && !isBefore(dateRangeStart, startDate) &&

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

@ -860,7 +860,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
return format(date, 'yyyy'); return format(date, 'yyyy');
}) })
] as DateRange[]) { ] as DateRange[]) {
const dateInterval = getIntervalFromDateRange(dateRange); const dateInterval = getIntervalFromDateRange({ dateRange });
const endDate = dateInterval.endDate; const endDate = dateInterval.endDate;
let startDate = dateInterval.startDate; let startDate = dateInterval.startDate;

2
apps/api/src/app/portfolio/portfolio.controller.ts

@ -320,7 +320,7 @@ export class PortfolioController {
await this.impersonationService.validateImpersonationId(impersonationId); await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.settings.settings.baseCurrency; const userCurrency = this.request.user.settings.settings.baseCurrency;
const { endDate, startDate } = getIntervalFromDateRange(dateRange); const { endDate, startDate } = getIntervalFromDateRange({ dateRange });
const { activities } = await this.activitiesService.getActivities({ const { activities } = await this.activitiesService.getActivities({
endDate, endDate,

4
apps/api/src/app/portfolio/portfolio.service.ts

@ -403,7 +403,7 @@ export class PortfolioService {
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user); const userCurrency = this.getUserCurrency(user);
const { endDate, startDate } = getIntervalFromDateRange(dateRange); const { endDate, startDate } = getIntervalFromDateRange({ dateRange });
const { activities } = const { activities } =
await this.activitiesService.getActivitiesForPortfolioCalculator({ await this.activitiesService.getActivitiesForPortfolioCalculator({
@ -1047,7 +1047,7 @@ export class PortfolioService {
const { errors, hasErrors, historicalData } = const { errors, hasErrors, historicalData } =
await portfolioCalculator.getSnapshot(); await portfolioCalculator.getSnapshot();
const { endDate, startDate } = getIntervalFromDateRange(dateRange); const { endDate, startDate } = getIntervalFromDateRange({ dateRange });
const { chart } = await portfolioCalculator.getPerformance({ const { chart } = await portfolioCalculator.getPerformance({
end: endDate, end: endDate,

34
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -26,11 +26,12 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef,
Inject, Inject,
OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
@ -49,8 +50,7 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { forkJoin, Subject } from 'rxjs'; import { forkJoin } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AccountDetailDialogParams } from './interfaces/interfaces'; import { AccountDetailDialogParams } from './interfaces/interfaces';
@ -77,7 +77,7 @@ import { AccountDetailDialogParams } from './interfaces/interfaces';
styleUrls: ['./account-detail-dialog.component.scss'], styleUrls: ['./account-detail-dialog.component.scss'],
templateUrl: 'account-detail-dialog.html' templateUrl: 'account-detail-dialog.html'
}) })
export class GfAccountDetailDialogComponent implements OnDestroy, OnInit { export class GfAccountDetailDialogComponent implements OnInit {
public accountBalances: AccountBalancesResponse['balances']; public accountBalances: AccountBalancesResponse['balances'];
public activities: OrderWithAccount[]; public activities: OrderWithAccount[];
public activitiesCount: number; public activitiesCount: number;
@ -104,18 +104,17 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
public user: User; public user: User;
public valueInBaseCurrency: number; public valueInBaseCurrency: number;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams, @Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
public dialogRef: MatDialogRef<GfAccountDetailDialogComponent>, public dialogRef: MatDialogRef<GfAccountDetailDialogComponent>,
private router: Router, private router: Router,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -154,7 +153,7 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
public onAddAccountBalance(accountBalance: CreateAccountBalanceDto) { public onAddAccountBalance(accountBalance: CreateAccountBalanceDto) {
this.dataService this.dataService
.postAccountBalance(accountBalance) .postAccountBalance(accountBalance)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.initialize(); this.initialize();
}); });
@ -163,7 +162,7 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
public onDeleteAccountBalance(aId: string) { public onDeleteAccountBalance(aId: string) {
this.dataService this.dataService
.deleteAccountBalance(aId) .deleteAccountBalance(aId)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.initialize(); this.initialize();
}); });
@ -176,7 +175,7 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchExport({ activityIds }) .fetchExport({ activityIds })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data) => { .subscribe((data) => {
downloadAsFile({ downloadAsFile({
content: data, content: data,
@ -212,7 +211,7 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
private fetchAccount() { private fetchAccount() {
this.dataService this.dataService
.fetchAccount(this.data.accountId) .fetchAccount(this.data.accountId)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe( .subscribe(
({ ({
activitiesCount, activitiesCount,
@ -287,7 +286,7 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
sortColumn: this.sortColumn, sortColumn: this.sortColumn,
sortDirection: this.sortDirection sortDirection: this.sortDirection
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ activities, count }) => { .subscribe(({ activities, count }) => {
this.dataSource = new MatTableDataSource(activities); this.dataSource = new MatTableDataSource(activities);
this.totalItems = count; this.totalItems = count;
@ -304,7 +303,7 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
forkJoin({ forkJoin({
accountBalances: this.dataService accountBalances: this.dataService
.fetchAccountBalances(this.data.accountId) .fetchAccountBalances(this.data.accountId)
.pipe(takeUntil(this.unsubscribeSubject)), .pipe(takeUntilDestroyed(this.destroyRef)),
portfolioPerformance: this.dataService portfolioPerformance: this.dataService
.fetchPortfolioPerformance({ .fetchPortfolioPerformance({
filters: [ filters: [
@ -317,7 +316,7 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
withExcludedAccounts: true, withExcludedAccounts: true,
withItems: true withItems: true
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
}).subscribe({ }).subscribe({
error: () => { error: () => {
this.isLoadingChart = false; this.isLoadingChart = false;
@ -360,7 +359,7 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
} }
] ]
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
this.holdings = holdings; this.holdings = holdings;
@ -374,9 +373,4 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
this.fetchChart(); this.fetchChart();
this.fetchPortfolioHoldings(); this.fetchPortfolioHoldings();
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

30
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -22,7 +22,13 @@ import { AdminService, DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectorRef,
Component,
DestroyRef,
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
@ -50,8 +56,6 @@ import {
trashOutline trashOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import ms, { StringValue } from 'ms'; import ms, { StringValue } from 'ms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -72,7 +76,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./admin-overview.scss'], styleUrls: ['./admin-overview.scss'],
templateUrl: './admin-overview.html' templateUrl: './admin-overview.html'
}) })
export class GfAdminOverviewComponent implements OnDestroy, OnInit { export class GfAdminOverviewComponent implements OnInit {
public activitiesCount: number; public activitiesCount: number;
public couponDuration: StringValue = '14 days'; public couponDuration: StringValue = '14 days';
public coupons: Coupon[]; public coupons: Coupon[];
@ -88,13 +92,12 @@ export class GfAdminOverviewComponent implements OnDestroy, OnInit {
public user: User; public user: User;
public version: string; public version: string;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private cacheService: CacheService, private cacheService: CacheService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private notificationService: NotificationService, private notificationService: NotificationService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private userService: UserService private userService: UserService
@ -102,7 +105,7 @@ export class GfAdminOverviewComponent implements OnDestroy, OnInit {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -219,7 +222,7 @@ export class GfAdminOverviewComponent implements OnDestroy, OnInit {
confirmFn: () => { confirmFn: () => {
this.cacheService this.cacheService
.flush() .flush()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
@ -268,7 +271,7 @@ export class GfAdminOverviewComponent implements OnDestroy, OnInit {
public onSyncDemoUserAccount() { public onSyncDemoUserAccount() {
this.adminService this.adminService
.syncDemoUserAccount() .syncDemoUserAccount()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.snackBar.open( this.snackBar.open(
'✅ ' + $localize`Demo user account has been synced.`, '✅ ' + $localize`Demo user account has been synced.`,
@ -280,15 +283,10 @@ export class GfAdminOverviewComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private fetchAdminData() { private fetchAdminData() {
this.adminService this.adminService
.fetchAdminData() .fetchAdminData()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ activitiesCount, settings, userCount, version }) => { .subscribe(({ activitiesCount, settings, userCount, version }) => {
this.activitiesCount = activitiesCount; this.activitiesCount = activitiesCount;
this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? []; this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? [];
@ -320,7 +318,7 @@ export class GfAdminOverviewComponent implements OnDestroy, OnInit {
.putAdminSetting(key, { .putAdminSetting(key, {
value: value || value === false ? JSON.stringify(value) : undefined value: value || value === false ? JSON.stringify(value) : undefined
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();

13
apps/client/src/app/components/rule/rule.component.ts

@ -9,11 +9,13 @@ import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
DestroyRef,
EventEmitter, EventEmitter,
Input, Input,
OnInit, OnInit,
Output Output
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
@ -29,7 +31,6 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, takeUntil } from 'rxjs';
import { RuleSettingsDialogParams } from './rule-settings-dialog/interfaces/interfaces'; import { RuleSettingsDialogParams } from './rule-settings-dialog/interfaces/interfaces';
import { GfRuleSettingsDialogComponent } from './rule-settings-dialog/rule-settings-dialog.component'; import { GfRuleSettingsDialogComponent } from './rule-settings-dialog/rule-settings-dialog.component';
@ -58,9 +59,8 @@ export class GfRuleComponent implements OnInit {
@Output() ruleUpdated = new EventEmitter<UpdateUserSettingDto>(); @Output() ruleUpdated = new EventEmitter<UpdateUserSettingDto>();
private deviceType: string; private deviceType: string;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog private dialog: MatDialog
) { ) {
@ -94,7 +94,7 @@ export class GfRuleComponent implements OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((settings: RuleSettings) => { .subscribe((settings: RuleSettings) => {
if (settings) { if (settings) {
this.ruleUpdated.emit({ this.ruleUpdated.emit({
@ -115,9 +115,4 @@ export class GfRuleComponent implements OnInit {
this.ruleUpdated.emit(settings); this.ruleUpdated.emit(settings);
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

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

@ -9,7 +9,7 @@ import { GfValueComponent } from '@ghostfolio/ui/value';
import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart'; import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnInit } 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 { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -21,7 +21,6 @@ import {
starOutline starOutline
} 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' }, host: { class: 'page' },
@ -40,7 +39,7 @@ import { Subject } from 'rxjs';
styleUrls: ['./landing-page.scss'], styleUrls: ['./landing-page.scss'],
templateUrl: './landing-page.html' templateUrl: './landing-page.html'
}) })
export class GfLandingPageComponent implements OnDestroy, OnInit { export class GfLandingPageComponent implements OnInit {
public countriesOfSubscribersMap: { public countriesOfSubscribersMap: {
[code: string]: { value: number }; [code: string]: { value: number };
} = {}; } = {};
@ -107,8 +106,6 @@ export class GfLandingPageComponent implements OnDestroy, OnInit {
} }
]; ];
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService private deviceService: DeviceDetectorService
@ -155,9 +152,4 @@ export class GfLandingPageComponent implements OnDestroy, 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();
}
} }

14
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -10,6 +10,7 @@ import {
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
@ -129,8 +130,13 @@ export class GfActivitiesPageComponent implements OnDestroy, OnInit {
} }
public fetchActivities() { public fetchActivities() {
const dateRange = this.user?.settings?.dateRange;
const range = this.isCalendarYear(dateRange) ? dateRange : undefined;
this.dataService this.dataService
.fetchActivities({ .fetchActivities({
range,
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
skip: this.pageIndex * this.pageSize, skip: this.pageIndex * this.pageSize,
sortColumn: this.sortColumn, sortColumn: this.sortColumn,
@ -352,6 +358,14 @@ export class GfActivitiesPageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private isCalendarYear(dateRange: DateRange) {
if (!dateRange) {
return false;
}
return /^\d{4}$/.test(dateRange);
}
private openCreateActivityDialog(aActivity?: Activity) { private openCreateActivityDialog(aActivity?: Activity) {
this.userService this.userService
.get() .get()

20
libs/common/src/lib/calculation-helper.ts

@ -36,14 +36,16 @@ export function getAnnualizedPerformancePercent({
return new Big(0); return new Big(0);
} }
export function getIntervalFromDateRange( export function getIntervalFromDateRange(params: {
aDateRange: DateRange, dateRange: DateRange;
portfolioStart = new Date(0) endDate?: Date;
) { startDate?: Date;
let endDate = endOfDay(new Date()); }) {
let startDate = portfolioStart; const { dateRange } = params;
let endDate = params.endDate ?? endOfDay(new Date());
let startDate = params.startDate ?? new Date(0);
switch (aDateRange) { switch (dateRange) {
case '1d': case '1d':
startDate = max([startDate, subDays(resetHours(new Date()), 1)]); startDate = max([startDate, subDays(resetHours(new Date()), 1)]);
break; break;
@ -75,8 +77,8 @@ export function getIntervalFromDateRange(
break; break;
default: default:
// '2024', '2023', '2022', etc. // '2024', '2023', '2022', etc.
endDate = endOfYear(new Date(aDateRange)); endDate = endOfYear(new Date(dateRange));
startDate = max([startDate, new Date(aDateRange)]); startDate = max([startDate, new Date(dateRange)]);
} }
return { endDate, startDate }; return { endDate, startDate };

3
libs/ui/src/lib/services/admin.service.ts

@ -22,7 +22,6 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment'; import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment';
import { DataService } from '@ghostfolio/ui/services';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
@ -31,6 +30,8 @@ import { DataSource, MarketData, Platform } from '@prisma/client';
import { JobStatus } from 'bull'; import { JobStatus } from 'bull';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { DataService } from './data.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })

4
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -162,7 +162,9 @@ export class GfTreemapChartComponent
private initialize() { private initialize() {
this.isLoading = true; this.isLoading = true;
const { endDate, startDate } = getIntervalFromDateRange(this.dateRange); const { endDate, startDate } = getIntervalFromDateRange({
dateRange: this.dateRange
});
const netPerformancePercentsWithCurrencyEffect = this.holdings.map( const netPerformancePercentsWithCurrencyEffect = this.holdings.map(
({ dateOfFirstActivity, netPerformancePercentWithCurrencyEffect }) => { ({ dateOfFirstActivity, netPerformancePercentWithCurrencyEffect }) => {

123
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.249.0", "version": "2.250.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.249.0", "version": "2.250.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -153,7 +153,7 @@
"prisma": "6.19.0", "prisma": "6.19.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "8.3.0", "replace-in-file": "8.4.0",
"shx": "0.4.0", "shx": "0.4.0",
"storybook": "10.1.10", "storybook": "10.1.10",
"ts-jest": "29.4.0", "ts-jest": "29.4.0",
@ -26007,11 +26007,11 @@
} }
}, },
"node_modules/minipass": { "node_modules/minipass": {
"version": "7.1.2", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"dev": true, "dev": true,
"license": "ISC", "license": "BlueOak-1.0.0",
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
@ -29871,15 +29871,15 @@
} }
}, },
"node_modules/replace-in-file": { "node_modules/replace-in-file": {
"version": "8.3.0", "version": "8.4.0",
"resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-8.3.0.tgz", "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-8.4.0.tgz",
"integrity": "sha512-4VhddQiMCPIuypiwHDTM+XHjZoVu9h7ngBbSCnwGRcwdHwxltjt/m//Ep3GDwqaOx1fDSrKFQ+n7uo4uVcEz9Q==", "integrity": "sha512-D28k8jy2LtUGbCzCnR3znajaTWIjJ/Uee3UdodzcHRxE7zn6NmYW/dcSqyivnsYU3W+MxdX6SbF28NvJ0GRoLA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chalk": "^5.3.0", "chalk": "^5.6.2",
"glob": "^10.4.2", "glob": "^13.0.0",
"yargs": "^17.7.2" "yargs": "^18.0.0"
}, },
"bin": { "bin": {
"replace-in-file": "bin/cli.js" "replace-in-file": "bin/cli.js"
@ -29888,10 +29888,33 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/replace-in-file/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/replace-in-file/node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/replace-in-file/node_modules/chalk": { "node_modules/replace-in-file/node_modules/chalk": {
"version": "5.4.1", "version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -29901,23 +29924,65 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/replace-in-file/node_modules/yargs": { "node_modules/replace-in-file/node_modules/glob": {
"version": "17.7.2", "version": "13.0.6",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"dev": true, "dev": true,
"license": "MIT", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"cliui": "^8.0.1", "minimatch": "^10.2.2",
"escalade": "^3.1.1", "minipass": "^7.1.3",
"get-caller-file": "^2.0.5", "path-scurry": "^2.0.2"
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
}, },
"engines": { "engines": {
"node": ">=12" "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/replace-in-file/node_modules/lru-cache": {
"version": "11.2.7",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/replace-in-file/node_modules/minimatch": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/replace-in-file/node_modules/path-scurry": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/require-directory": { "node_modules/require-directory": {

4
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.249.0", "version": "2.250.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",
@ -198,7 +198,7 @@
"prisma": "6.19.0", "prisma": "6.19.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "8.3.0", "replace-in-file": "8.4.0",
"shx": "0.4.0", "shx": "0.4.0",
"storybook": "10.1.10", "storybook": "10.1.10",
"ts-jest": "29.4.0", "ts-jest": "29.4.0",

Loading…
Cancel
Save