Compare commits

...

5 Commits

Author SHA1 Message Date
Thomas Kaul f14f98f766
Release 3.7.0 (#6980) 7 days ago
Thomas Kaul 8a93f1f301
Bugfix/asset class chart on allocations page (#6979) 7 days ago
Thomas Kaul 9996d8a8d2
Bugfix/base currency in impersonation mode (#6964) 7 days ago
Thomas Kaul 8acd77717c
Task/upgrade Nx to version 22.7.5 (#6962) 7 days ago
Kenrick Tandrian 60be4de9a5
Task/improve type safety in analysis page component (#6965) 7 days ago
  1. 4
      CHANGELOG.md
  2. 11
      apps/api/src/app/account/account.controller.ts
  3. 4
      apps/api/src/app/account/account.module.ts
  4. 11
      apps/api/src/app/portfolio/portfolio.controller.ts
  5. 7
      apps/api/src/app/portfolio/portfolio.service.ts
  6. 31
      apps/api/src/app/user/user.service.ts
  7. 145
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  8. 8
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  9. 10
      libs/ui/src/lib/services/data.service.ts
  10. 682
      package-lock.json
  11. 24
      package.json

4
CHANGELOG.md

@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 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 ## 3.7.0 - 2026-06-02
### Added ### Added
@ -15,9 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Extended the countries mapping in the data enhancer for asset profile data via _Trackinsight_ - Extended the countries mapping in the data enhancer for asset profile data via _Trackinsight_
- Removed the deprecated attributes (`assetClass`, `assetClassLabel`, `assetSubClass`, `assetSubClassLabel`, `countries`, `currency`, `dataSource`, `holdings`, `name`, `sectors`, `symbol` and `url`) from the holdings of the portfolio details endpoint response - Removed the deprecated attributes (`assetClass`, `assetClassLabel`, `assetSubClass`, `assetSubClassLabel`, `countries`, `currency`, `dataSource`, `holdings`, `name`, `sectors`, `symbol` and `url`) from the holdings of the portfolio details endpoint response
- Upgraded `Nx` from version `22.7.2` to `22.7.5`
### Fixed ### Fixed
- Resolved an issue in the impersonation mode where the values did not match the owner’s currency
- Fixed the environment variable expansion in the `.env` file when debugging via _Visual Studio Code_ - Fixed the environment variable expansion in the `.env` file when debugging via _Visual Studio Code_
## 3.6.0 - 2026-05-28 ## 3.6.0 - 2026-05-28

11
apps/api/src/app/account/account.controller.ts

@ -1,5 +1,6 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
@ -50,7 +51,8 @@ export class AccountController {
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly impersonationService: ImpersonationService, private readonly impersonationService: ImpersonationService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
) {} ) {}
@Delete(':id') @Delete(':id')
@ -137,11 +139,14 @@ export class AccountController {
): Promise<AccountBalancesResponse> { ): Promise<AccountBalancesResponse> {
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId); await this.impersonationService.validateImpersonationId(impersonationId);
const userId = impersonationUserId || this.request.user.id;
const { settings } = await this.userService.user({ id: userId });
return this.accountBalanceService.getAccountBalances({ return this.accountBalanceService.getAccountBalances({
userId,
filters: [{ id, type: 'ACCOUNT' }], filters: [{ id, type: 'ACCOUNT' }],
userCurrency: this.request.user.settings.settings.baseCurrency, userCurrency: settings.settings.baseCurrency
userId: impersonationUserId || this.request.user.id
}); });
} }

4
apps/api/src/app/account/account.module.ts

@ -1,5 +1,6 @@
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module'; import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module'; import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module'; import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
@ -23,7 +24,8 @@ import { AccountService } from './account.service';
ImpersonationModule, ImpersonationModule,
PortfolioModule, PortfolioModule,
PrismaModule, PrismaModule,
RedactValuesInResponseModule RedactValuesInResponseModule,
UserModule
], ],
providers: [AccountService] providers: [AccountService]
}) })

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

@ -1,4 +1,5 @@
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { import {
@ -70,7 +71,8 @@ export class PortfolioController {
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly impersonationService: ImpersonationService, private readonly impersonationService: ImpersonationService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
) {} ) {}
@Get('details') @Get('details')
@ -340,7 +342,10 @@ export class PortfolioController {
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId); await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.settings.settings.baseCurrency; const userId = impersonationUserId || this.request.user.id;
const { settings } = await this.userService.user({ id: userId });
const userCurrency = settings.settings.baseCurrency;
const { endDate, startDate } = getIntervalFromDateRange({ dateRange }); const { endDate, startDate } = getIntervalFromDateRange({ dateRange });
@ -349,7 +354,7 @@ export class PortfolioController {
filters, filters,
startDate, startDate,
userCurrency, userCurrency,
userId: impersonationUserId || this.request.user.id, userId,
types: ['DIVIDEND'] types: ['DIVIDEND']
}); });

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

@ -164,7 +164,7 @@ export class PortfolioService {
}; };
} }
const [accounts, details] = await Promise.all([ const [accounts, details, user] = await Promise.all([
this.accountService.accounts({ this.accountService.accounts({
where, where,
include: { include: {
@ -178,10 +178,11 @@ export class PortfolioService {
withExcludedAccounts, withExcludedAccounts,
impersonationId: userId, impersonationId: userId,
userId: this.request.user.id userId: this.request.user.id
}) }),
this.userService.user({ id: userId })
]); ]);
const userCurrency = this.request.user.settings.settings.baseCurrency; const userCurrency = this.getUserCurrency(user);
return Promise.all( return Promise.all(
accounts.map(async (account) => { accounts.map(async (account) => {

31
apps/api/src/app/user/user.service.ts

@ -49,7 +49,7 @@ import { PerformanceCalculationType } from '@ghostfolio/common/types/performance
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { Prisma, Role, User } from '@prisma/client'; import { Prisma, Role, Settings, User } from '@prisma/client';
import { differenceInDays, subDays } from 'date-fns'; import { differenceInDays, subDays } from 'date-fns';
import { without } from 'lodash'; import { without } from 'lodash';
import { createHmac } from 'node:crypto'; import { createHmac } from 'node:crypto';
@ -109,7 +109,14 @@ export class UserService {
}): Promise<IUser> { }): Promise<IUser> {
const { id, permissions, settings, subscription } = user; const { id, permissions, settings, subscription } = user;
const userData = await Promise.all([ const [
access,
accounts,
activitiesCount,
firstActivity,
impersonationUserSettings,
tagsForUser
] = await Promise.all([
this.prismaService.access.findMany({ this.prismaService.access.findMany({
include: { include: {
user: true user: true
@ -134,16 +141,17 @@ export class UserService {
}, },
where: { userId: impersonationUserId || user.id } where: { userId: impersonationUserId || user.id }
}), }),
impersonationUserId
? this.prismaService.settings.findUnique({
where: { userId: impersonationUserId }
})
: Promise.resolve<Settings>(null),
this.tagService.getTagsForUser(impersonationUserId || user.id) this.tagService.getTagsForUser(impersonationUserId || user.id)
]); ]);
const access = userData[0]; const baseCurrency =
const accounts = userData[1]; (impersonationUserSettings?.settings as UserSettings)?.baseCurrency ??
const activitiesCount = userData[2]; (settings.settings as UserSettings)?.baseCurrency;
const firstActivity = userData[3];
let tags = userData[4].filter((tag) => {
return tag.id !== TAG_ID_EXCLUDE_FROM_ANALYSIS;
});
let systemMessage: SystemMessage; let systemMessage: SystemMessage;
@ -156,6 +164,10 @@ export class UserService {
systemMessage = systemMessageProperty; systemMessage = systemMessageProperty;
} }
let tags = tagsForUser.filter((tag) => {
return tag.id !== TAG_ID_EXCLUDE_FROM_ANALYSIS;
});
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
subscription.type === SubscriptionType.Basic subscription.type === SubscriptionType.Basic
@ -183,6 +195,7 @@ export class UserService {
dateOfFirstActivity: firstActivity?.date ?? new Date(), dateOfFirstActivity: firstActivity?.date ?? new Date(),
settings: { settings: {
...(settings.settings as UserSettings), ...(settings.settings as UserSettings),
baseCurrency,
locale: (settings.settings as UserSettings)?.locale ?? locale locale: (settings.settings as UserSettings)?.locale ?? locale
} }
}; };

145
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -2,7 +2,10 @@ import { GfBenchmarkComparatorComponent } from '@ghostfolio/client/components/be
import { GfInvestmentChartComponent } from '@ghostfolio/client/components/investment-chart/investment-chart.component'; import { GfInvestmentChartComponent } from '@ghostfolio/client/components/investment-chart/investment-chart.component';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config'; import {
DEFAULT_DATE_RANGE,
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
} from '@ghostfolio/common/config';
import { import {
HistoricalDataItem, HistoricalDataItem,
InvestmentItem, InvestmentItem,
@ -24,9 +27,12 @@ import { Clipboard } from '@angular/cdk/clipboard';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
DestroyRef, DestroyRef,
inject,
OnInit, OnInit,
ViewChild signal,
viewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@ -64,53 +70,57 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
templateUrl: './analysis-page.html' templateUrl: './analysis-page.html'
}) })
export class GfAnalysisPageComponent implements OnInit { export class GfAnalysisPageComponent implements OnInit {
@ViewChild(MatMenuTrigger) actionsMenuButton!: MatMenuTrigger; protected benchmark?: Partial<SymbolProfile>;
protected benchmarkDataItems: HistoricalDataItem[] = [];
public benchmark: Partial<SymbolProfile>; protected readonly benchmarks: Partial<SymbolProfile>[];
public benchmarkDataItems: HistoricalDataItem[] = []; protected bottom3: PortfolioPosition[];
public benchmarks: Partial<SymbolProfile>[]; protected dividendsByGroup: InvestmentItem[];
public bottom3: PortfolioPosition[]; protected readonly dividendTimelineDataLabel = $localize`Dividend`;
public deviceType: string; protected hasImpersonationId: boolean;
public dividendsByGroup: InvestmentItem[]; protected hasPermissionToReadAiPrompt: boolean;
public dividendTimelineDataLabel = $localize`Dividend`; protected investments: InvestmentItem[];
public firstOrderDate: Date; protected readonly investmentTimelineDataLabel = $localize`Investment`;
public hasImpersonationId: boolean; protected investmentsByGroup: InvestmentItem[];
public hasPermissionToReadAiPrompt: boolean; protected isLoadingAnalysisPrompt: boolean;
public investments: InvestmentItem[]; protected isLoadingBenchmarkComparator: boolean;
public investmentTimelineDataLabel = $localize`Investment`; protected isLoadingDividendTimelineChart: boolean;
public investmentsByGroup: InvestmentItem[]; protected isLoadingInvestmentChart: boolean;
public isLoadingAnalysisPrompt: boolean; protected isLoadingInvestmentTimelineChart: boolean;
public isLoadingBenchmarkComparator: boolean; protected isLoadingPortfolioPrompt: boolean;
public isLoadingDividendTimelineChart: boolean; protected readonly mode = signal<GroupBy>('month');
public isLoadingInvestmentChart: boolean; protected readonly modeOptions: ToggleOption[] = [
public isLoadingInvestmentTimelineChart: boolean;
public isLoadingPortfolioPrompt: boolean;
public mode: GroupBy = 'month';
public modeOptions: ToggleOption[] = [
{ label: $localize`Monthly`, value: 'month' }, { label: $localize`Monthly`, value: 'month' },
{ label: $localize`Yearly`, value: 'year' } { label: $localize`Yearly`, value: 'year' }
]; ];
public performance: PortfolioPerformance; protected performance: PortfolioPerformance;
public performanceDataItems: HistoricalDataItem[]; protected performanceDataItems: HistoricalDataItem[];
public performanceDataItemsInPercentage: HistoricalDataItem[]; protected performanceDataItemsInPercentage: HistoricalDataItem[];
public portfolioEvolutionDataLabel = $localize`Investment`; protected readonly portfolioEvolutionDataLabel = $localize`Investment`;
public precision = 2; protected precision = 2;
public streaks: PortfolioInvestmentsResponse['streaks']; protected streaks: PortfolioInvestmentsResponse['streaks'];
public top3: PortfolioPosition[]; protected top3: PortfolioPosition[];
public unitCurrentStreak: string; protected unitCurrentStreak: string;
public unitLongestStreak: string; protected unitLongestStreak: string;
public user: User; protected user: User;
public constructor( private readonly actionsMenuButton = viewChild.required(MatMenuTrigger);
private changeDetectorRef: ChangeDetectorRef, private readonly deviceType = computed(
private clipboard: Clipboard, () => this.deviceDetectorService.deviceInfo().deviceType
private dataService: DataService, );
private destroyRef: DestroyRef, private firstOrderDate: Date;
private deviceDetectorService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private readonly changeDetectorRef = inject(ChangeDetectorRef);
private snackBar: MatSnackBar, private readonly clipboard = inject(Clipboard);
private userService: UserService private readonly dataService = inject(DataService);
) { private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly impersonationStorageService = inject(
ImpersonationStorageService
);
private readonly snackBar = inject(MatSnackBar);
private readonly userService = inject(UserService);
public constructor() {
const { benchmarks } = this.dataService.fetchInfo(); const { benchmarks } = this.dataService.fetchInfo();
this.benchmarks = benchmarks; this.benchmarks = benchmarks;
@ -123,14 +133,16 @@ export class GfAnalysisPageComponent implements OnInit {
? undefined ? undefined
: this.user?.settings?.savingsRate; : this.user?.settings?.savingsRate;
return this.mode === 'year' if (savingsRatePerMonth === undefined) {
return undefined;
}
return this.mode() === 'year'
? savingsRatePerMonth * 12 ? savingsRatePerMonth * 12
: savingsRatePerMonth; : savingsRatePerMonth;
} }
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -158,7 +170,7 @@ export class GfAnalysisPageComponent implements OnInit {
}); });
} }
public onChangeBenchmark(symbolProfileId: string) { protected onChangeBenchmark(symbolProfileId: string) {
this.dataService this.dataService
.putUserSetting({ benchmark: symbolProfileId }) .putUserSetting({ benchmark: symbolProfileId })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -174,12 +186,12 @@ export class GfAnalysisPageComponent implements OnInit {
}); });
} }
public onChangeGroupBy(aMode: GroupBy) { protected onChangeGroupBy(aMode: GroupBy) {
this.mode = aMode; this.mode.set(aMode);
this.fetchDividendsAndInvestments(); this.fetchDividendsAndInvestments();
} }
public onCopyPromptToClipboard(mode: AiPromptMode) { protected onCopyPromptToClipboard(mode: AiPromptMode) {
if (mode === 'analysis') { if (mode === 'analysis') {
this.isLoadingAnalysisPrompt = true; this.isLoadingAnalysisPrompt = true;
} else if (mode === 'portfolio') { } else if (mode === 'portfolio') {
@ -210,7 +222,7 @@ export class GfAnalysisPageComponent implements OnInit {
window.open('https://duck.ai', '_blank'); window.open('https://duck.ai', '_blank');
}); });
this.actionsMenuButton.closeMenu(); this.actionsMenuButton().closeMenu();
if (mode === 'analysis') { if (mode === 'analysis') {
this.isLoadingAnalysisPrompt = false; this.isLoadingAnalysisPrompt = false;
@ -227,8 +239,8 @@ export class GfAnalysisPageComponent implements OnInit {
this.dataService this.dataService
.fetchDividends({ .fetchDividends({
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
groupBy: this.mode, groupBy: this.mode(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ dividends }) => { .subscribe(({ dividends }) => {
@ -242,15 +254,15 @@ export class GfAnalysisPageComponent implements OnInit {
this.dataService this.dataService
.fetchInvestments({ .fetchInvestments({
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
groupBy: this.mode, groupBy: this.mode(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ investments, streaks }) => { .subscribe(({ investments, streaks }) => {
this.investmentsByGroup = investments; this.investmentsByGroup = investments;
this.streaks = streaks; this.streaks = streaks;
this.unitCurrentStreak = this.unitCurrentStreak =
this.mode === 'year' this.mode() === 'year'
? this.streaks?.currentStreak === 1 ? this.streaks?.currentStreak === 1
? translate('YEAR') ? translate('YEAR')
: translate('YEARS') : translate('YEARS')
@ -258,7 +270,7 @@ export class GfAnalysisPageComponent implements OnInit {
? translate('MONTH') ? translate('MONTH')
: translate('MONTHS'); : translate('MONTHS');
this.unitLongestStreak = this.unitLongestStreak =
this.mode === 'year' this.mode() === 'year'
? this.streaks?.longestStreak === 1 ? this.streaks?.longestStreak === 1
? translate('YEAR') ? translate('YEAR')
: translate('YEARS') : translate('YEARS')
@ -278,7 +290,7 @@ export class GfAnalysisPageComponent implements OnInit {
this.dataService this.dataService
.fetchPortfolioPerformance({ .fetchPortfolioPerformance({
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ chart, firstOrderDate, performance }) => { .subscribe(({ chart, firstOrderDate, performance }) => {
@ -298,13 +310,16 @@ export class GfAnalysisPageComponent implements OnInit {
valueInPercentage, valueInPercentage,
valueWithCurrencyEffect valueWithCurrencyEffect
} }
] of chart.entries()) { ] of (chart ?? []).entries()) {
if (index > 0 || this.user?.settings?.dateRange === 'max') {
// Ignore first item where value is 0 // Ignore first item where value is 0
if (index > 0 || this.user?.settings?.dateRange === 'max') {
if (totalInvestmentValueWithCurrencyEffect !== undefined) {
this.investments.push({ this.investments.push({
date, date,
investment: totalInvestmentValueWithCurrencyEffect investment: totalInvestmentValueWithCurrencyEffect
}); });
}
this.performanceDataItems.push({ this.performanceDataItems.push({
date, date,
value: isNumber(valueWithCurrencyEffect) value: isNumber(valueWithCurrencyEffect)
@ -320,7 +335,7 @@ export class GfAnalysisPageComponent implements OnInit {
} }
if ( if (
this.deviceType === 'mobile' && this.deviceType() === 'mobile' &&
this.performance.currentValueInBaseCurrency >= this.performance.currentValueInBaseCurrency >=
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
) { ) {
@ -387,7 +402,7 @@ export class GfAnalysisPageComponent implements OnInit {
dataSource, dataSource,
symbol, symbol,
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange, range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE,
startDate: this.firstOrderDate startDate: this.firstOrderDate
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))

8
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -442,7 +442,7 @@
</div> </div>
<gf-toggle <gf-toggle
class="d-none d-lg-block" class="d-none d-lg-block"
[defaultValue]="mode" [defaultValue]="mode()"
[isLoading]="false" [isLoading]="false"
[options]="modeOptions" [options]="modeOptions"
(valueChange)="onChangeGroupBy($event.value)" (valueChange)="onChangeGroupBy($event.value)"
@ -476,7 +476,7 @@
[benchmarkDataItems]="investmentsByGroup" [benchmarkDataItems]="investmentsByGroup"
[benchmarkDataLabel]="investmentTimelineDataLabel" [benchmarkDataLabel]="investmentTimelineDataLabel"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[groupBy]="mode" [groupBy]="mode()"
[isInPercentage]=" [isInPercentage]="
hasImpersonationId || user.settings.isRestrictedView hasImpersonationId || user.settings.isRestrictedView
" "
@ -501,7 +501,7 @@
</div> </div>
<gf-toggle <gf-toggle
class="d-none d-lg-block" class="d-none d-lg-block"
[defaultValue]="mode" [defaultValue]="mode()"
[isLoading]="false" [isLoading]="false"
[options]="modeOptions" [options]="modeOptions"
(valueChange)="onChangeGroupBy($event.value)" (valueChange)="onChangeGroupBy($event.value)"
@ -513,7 +513,7 @@
[benchmarkDataItems]="dividendsByGroup" [benchmarkDataItems]="dividendsByGroup"
[benchmarkDataLabel]="dividendTimelineDataLabel" [benchmarkDataLabel]="dividendTimelineDataLabel"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[groupBy]="mode" [groupBy]="mode()"
[isInPercentage]=" [isInPercentage]="
hasImpersonationId || user.settings.isRestrictedView hasImpersonationId || user.settings.isRestrictedView
" "

10
libs/ui/src/lib/services/data.service.ts

@ -556,13 +556,11 @@ export class DataService {
map((response) => { map((response) => {
if (response.holdings) { if (response.holdings) {
for (const symbol of Object.keys(response.holdings)) { for (const symbol of Object.keys(response.holdings)) {
response.holdings[symbol].assetClassLabel = translate( response.holdings[symbol].assetProfile.assetClassLabel =
response.holdings[symbol].assetClass translate(response.holdings[symbol].assetProfile.assetClass);
);
response.holdings[symbol].assetSubClassLabel = translate( response.holdings[symbol].assetProfile.assetSubClassLabel =
response.holdings[symbol].assetSubClass translate(response.holdings[symbol].assetProfile.assetSubClass);
);
response.holdings[symbol].dateOfFirstActivity = response.holdings[ response.holdings[symbol].dateOfFirstActivity = response.holdings[
symbol symbol

682
package-lock.json

File diff suppressed because it is too large

24
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.6.0", "version": "3.7.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",
@ -158,16 +158,16 @@
"@eslint/js": "9.35.0", "@eslint/js": "9.35.0",
"@nestjs/schematics": "11.1.0", "@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.1.21", "@nestjs/testing": "11.1.21",
"@nx/angular": "22.7.2", "@nx/angular": "22.7.5",
"@nx/eslint-plugin": "22.7.2", "@nx/eslint-plugin": "22.7.5",
"@nx/jest": "22.7.2", "@nx/jest": "22.7.5",
"@nx/js": "22.7.2", "@nx/js": "22.7.5",
"@nx/module-federation": "22.7.2", "@nx/module-federation": "22.7.5",
"@nx/nest": "22.7.2", "@nx/nest": "22.7.5",
"@nx/node": "22.7.2", "@nx/node": "22.7.5",
"@nx/storybook": "22.7.2", "@nx/storybook": "22.7.5",
"@nx/web": "22.7.2", "@nx/web": "22.7.5",
"@nx/workspace": "22.7.2", "@nx/workspace": "22.7.5",
"@schematics/angular": "21.2.6", "@schematics/angular": "21.2.6",
"@storybook/addon-docs": "10.1.10", "@storybook/addon-docs": "10.1.10",
"@storybook/addon-themes": "10.1.10", "@storybook/addon-themes": "10.1.10",
@ -194,7 +194,7 @@
"jest": "30.2.0", "jest": "30.2.0",
"jest-environment-jsdom": "30.2.0", "jest-environment-jsdom": "30.2.0",
"jest-preset-angular": "16.0.0", "jest-preset-angular": "16.0.0",
"nx": "22.7.2", "nx": "22.7.5",
"prettier": "3.8.3", "prettier": "3.8.3",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.8.0", "prisma": "7.8.0",

Loading…
Cancel
Save