apps/client/src/app/pages/about/overview/about-overview-page.html
94
From 10fffd68cb749ab52aed06beeb5567f3076978bd Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 15 Jun 2026 19:55:21 +0200
Subject: [PATCH 13/29] Task/improve name display in activities table (#7043)
* Do not wrap name
* Update changelog
---
CHANGELOG.md | 1 +
.../activities-table/activities-table.component.html | 12 +++++-------
2 files changed, 6 insertions(+), 7 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a799aa55..c981ab664 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the styling of the checkboxes to consistently use the primary color in their states
+- Improved the name display in the activities table
- Improved the user id display in the users table of the admin control panel
- Improved the language localization for German (`de`)
diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html
index 1e57e8c7e..0041f5a14 100644
--- a/libs/ui/src/lib/activities-table/activities-table.component.html
+++ b/libs/ui/src/lib/activities-table/activities-table.component.html
@@ -161,13 +161,11 @@
Name
-
-
- {{ element.SymbolProfile?.name }}
- @if (element.isDraft) {
- Draft
- }
-
+
+ {{ element.SymbolProfile?.name }}
+ @if (element.isDraft) {
+ Draft
+ }
@if (
element.SymbolProfile?.dataSource !== 'MANUAL' &&
From 9d06b3befa3e8e915dcc458caee7379259d64f04 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 15 Jun 2026 19:58:13 +0200
Subject: [PATCH 14/29] Bugfix/chart error on interaction by registering
annotation plugin early (#7049)
* Fix chart error on interaction
* Update changelog
---
CHANGELOG.md | 4 ++++
.../benchmark-comparator/benchmark-comparator.component.ts | 2 --
.../investment-chart/investment-chart.component.ts | 5 +----
apps/client/src/main.ts | 3 +++
libs/ui/src/lib/chart/chart.registry.ts | 7 ++++++-
5 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c981ab664..1ea25489d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the user id display in the users table of the admin control panel
- Improved the language localization for German (`de`)
+### Fixed
+
+- Fixed a chart error on interaction by registering the annotation plugin early
+
## 3.11.0 - 2026-06-14
### Added
diff --git a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
index 8d13fb91d..c474e29ab 100644
--- a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
+++ b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
@@ -44,7 +44,6 @@ import {
type TooltipOptions
} from 'chart.js';
import 'chartjs-adapter-date-fns';
-import annotationPlugin from 'chartjs-plugin-annotation';
import { addIcons } from 'ionicons';
import { arrowForwardOutline } from 'ionicons/icons';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@@ -86,7 +85,6 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
public constructor() {
Chart.register(
- annotationPlugin,
LinearScale,
LineController,
LineElement,
diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts
index e55aebdda..dc3152f12 100644
--- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts
+++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts
@@ -40,9 +40,7 @@ import {
type TooltipOptions
} from 'chart.js';
import 'chartjs-adapter-date-fns';
-import annotationPlugin, {
- type AnnotationOptions
-} from 'chartjs-plugin-annotation';
+import { type AnnotationOptions } from 'chartjs-plugin-annotation';
import { isAfter } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@@ -74,7 +72,6 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
public constructor() {
Chart.register(
- annotationPlugin,
BarController,
BarElement,
LinearScale,
diff --git a/apps/client/src/main.ts b/apps/client/src/main.ts
index f596de5f4..45901baec 100644
--- a/apps/client/src/main.ts
+++ b/apps/client/src/main.ts
@@ -1,5 +1,6 @@
import { InfoResponse } from '@ghostfolio/common/interfaces';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
+import { registerChartConfiguration } from '@ghostfolio/ui/chart';
import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment';
import { GfNotificationModule } from '@ghostfolio/ui/notifications';
@@ -58,6 +59,8 @@ import { environment } from './environments/environment';
enableProdMode();
}
+ registerChartConfiguration();
+
await bootstrapApplication(GfAppComponent, {
providers: [
authInterceptorProviders,
diff --git a/libs/ui/src/lib/chart/chart.registry.ts b/libs/ui/src/lib/chart/chart.registry.ts
index 465d6e716..a6a8b3e55 100644
--- a/libs/ui/src/lib/chart/chart.registry.ts
+++ b/libs/ui/src/lib/chart/chart.registry.ts
@@ -1,6 +1,7 @@
import { getTooltipPositionerMapTop } from '@ghostfolio/common/chart-helper';
-import { Tooltip, TooltipPositionerFunction, ChartType } from 'chart.js';
+import { Chart, Tooltip, TooltipPositionerFunction, ChartType } from 'chart.js';
+import annotationPlugin from 'chartjs-plugin-annotation';
interface VerticalHoverLinePluginOptions {
color?: string;
@@ -23,6 +24,10 @@ export function registerChartConfiguration() {
return;
}
+ // Register the annotation plugin early so that every chart is initialized
+ // with its state and does not crash on interaction
+ Chart.register(annotationPlugin);
+
Tooltip.positioners.top = function (_elements, eventPosition) {
return getTooltipPositionerMapTop(this.chart, eventPosition);
};
From f4d17c786ae94e36251049a3a537d9addd01b7ed Mon Sep 17 00:00:00 2001
From: Parman Mohammadalizadeh
Date: Mon, 15 Jun 2026 21:44:29 +0330
Subject: [PATCH 15/29] Bugfix/open account details dialog on allocations page
(#6988)
* Fix open account details dialog
* Update changelog
---------
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
---
CHANGELOG.md | 1 +
.../allocations/allocations-page.component.ts | 6 +++---
.../interfaces/interfaces.ts | 5 +++++
.../portfolio-proportion-chart.component.ts | 18 +++++++++---------
4 files changed, 18 insertions(+), 12 deletions(-)
create mode 100644 libs/ui/src/lib/portfolio-proportion-chart/interfaces/interfaces.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ea25489d..cbd9369b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed a chart error on interaction by registering the annotation plugin early
+- Fixed an issue on the allocations page where clicking an account in the _By Account_ chart did not open the detail dialog
## 3.11.0 - 2026-06-14
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
index d53977ae8..26601ad59 100644
--- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
@@ -185,10 +185,10 @@ export class GfAllocationsPageComponent implements OnInit {
this.initialize();
}
- public onAccountChartClicked({ symbol }: AssetProfileIdentifier) {
- if (symbol && symbol !== UNKNOWN_KEY) {
+ public onAccountChartClicked({ accountId }: { accountId: string }) {
+ if (accountId && accountId !== UNKNOWN_KEY) {
this.router.navigate([], {
- queryParams: { accountId: symbol, accountDetailDialog: true }
+ queryParams: { accountId, accountDetailDialog: true }
});
}
}
diff --git a/libs/ui/src/lib/portfolio-proportion-chart/interfaces/interfaces.ts b/libs/ui/src/lib/portfolio-proportion-chart/interfaces/interfaces.ts
new file mode 100644
index 000000000..25d12b049
--- /dev/null
+++ b/libs/ui/src/lib/portfolio-proportion-chart/interfaces/interfaces.ts
@@ -0,0 +1,5 @@
+import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
+
+export type PortfolioProportionChartClickEvent =
+ | AssetProfileIdentifier
+ | { accountId: string };
diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
index e2c41e956..bf6fd193e 100644
--- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
+++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
@@ -1,10 +1,7 @@
import { getTooltipOptions } from '@ghostfolio/common/chart-helper';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getLocale, getSum, getTextColor } from '@ghostfolio/common/helper';
-import {
- AssetProfileIdentifier,
- PortfolioPosition
-} from '@ghostfolio/common/interfaces';
+import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { ColorScheme } from '@ghostfolio/common/types';
import {
@@ -36,6 +33,8 @@ import Color from 'color';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color';
+import { PortfolioProportionChartClickEvent } from './interfaces/interfaces';
+
const {
blue,
cyan,
@@ -80,7 +79,8 @@ export class GfPortfolioProportionChartComponent
public chart: Chart<'doughnut'>;
public isLoading = true;
- protected readonly proportionChartClicked = output();
+ protected readonly proportionChartClicked =
+ output();
private readonly OTHER_KEY = 'OTHER';
@@ -355,11 +355,11 @@ export class GfPortfolioProportionChartComponent
const dataIndex = activeElements[0].index;
const symbol = chart.data.labels?.[dataIndex] as string;
- const dataSource = this.data[symbol].dataSource;
+ const dataSource = this.data[symbol]?.dataSource;
- if (dataSource) {
- this.proportionChartClicked.emit({ dataSource, symbol });
- }
+ this.proportionChartClicked.emit(
+ dataSource ? { dataSource, symbol } : { accountId: symbol }
+ );
} catch {}
},
onHover: (event, chartElement) => {
From 5bb8bf834b435662446a4dd85db06610af3991a2 Mon Sep 17 00:00:00 2001
From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com>
Date: Wed, 17 Jun 2026 01:16:44 +0700
Subject: [PATCH 16/29] Task/improve type safety in holding detail dialog
component (#7050)
Improve type safety
---
.../holding-detail-dialog.component.ts | 208 +++++++++---------
libs/common/src/lib/interfaces/index.ts | 6 +-
.../interfaces/line-chart-item.interface.ts | 6 +-
...historical-market-data-editor.component.ts | 6 +-
libs/ui/src/lib/services/data.service.ts | 25 ++-
5 files changed, 137 insertions(+), 114 deletions(-)
diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
index 416e9106d..08c8a7a1e 100644
--- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
+++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
@@ -16,6 +16,7 @@ import {
EnhancedSymbolProfile,
Filter,
LineChartItem,
+ NullableLineChartItem,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@@ -39,11 +40,16 @@ import {
ChangeDetectorRef,
Component,
DestroyRef,
- Inject,
- OnInit
+ OnInit,
+ inject
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import {
+ FormBuilder,
+ FormControl,
+ FormGroup,
+ ReactiveFormsModule
+} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import {
@@ -71,6 +77,7 @@ import {
swapVerticalOutline,
walletOutline
} from 'ionicons/icons';
+import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { switchMap } from 'rxjs/operators';
@@ -106,75 +113,78 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces';
templateUrl: 'holding-detail-dialog.html'
})
export class GfHoldingDetailDialogComponent implements OnInit {
- public activitiesCount: number;
- public accounts: Account[];
- public assetClass: string;
- public assetSubClass: string;
- public averagePrice: number;
- public averagePricePrecision = 2;
- public benchmarkDataItems: LineChartItem[];
- public benchmarkLabel = $localize`Average Unit Price`;
- public countries: {
+ protected accounts: Account[];
+ protected activitiesCount: number;
+ protected assetClass: string;
+ protected assetSubClass: string;
+ protected averagePrice: number;
+ protected averagePricePrecision = 2;
+ protected benchmarkDataItems: NullableLineChartItem[];
+ protected readonly benchmarkLabel = $localize`Average Unit Price`;
+ protected countries: {
[code: string]: { name: string; value: number };
};
- public dataProviderInfo: DataProviderInfo;
- public dataSource: MatTableDataSource;
- public dateOfFirstActivity: string;
- public dividendInBaseCurrency: number;
- public dividendInBaseCurrencyPrecision = 2;
- public dividendYieldPercentWithCurrencyEffect: number;
- public feeInBaseCurrency: number;
- public getCountryName = getCountryName;
- public hasPermissionToCreateOwnTag: boolean;
- public hasPermissionToReadMarketDataOfOwnAssetProfile: boolean;
- public historicalDataItems: LineChartItem[];
- public holdingForm: FormGroup;
- public investmentInBaseCurrencyWithCurrencyEffect: number;
- public investmentInBaseCurrencyWithCurrencyEffectPrecision = 2;
- public isUUID = isUUID;
- public marketDataItems: MarketData[] = [];
- public marketPrice: number;
- public marketPriceMax: number;
- public marketPriceMaxPrecision = 2;
- public marketPriceMin: number;
- public marketPriceMinPrecision = 2;
- public marketPricePrecision = 2;
- public netPerformance: number;
- public netPerformancePrecision = 2;
- public netPerformancePercent: number;
- public netPerformancePercentWithCurrencyEffect: number;
- public netPerformancePercentWithCurrencyEffectPrecision = 2;
- public netPerformanceWithCurrencyEffect: number;
- public netPerformanceWithCurrencyEffectPrecision = 2;
- public pageIndex = 0;
- public pageSize = DEFAULT_PAGE_SIZE;
- public quantity: number;
- public quantityPrecision = 2;
- public reportDataGlitchMail: string;
- public routerLinkAdminControlMarketData =
+ protected dataProviderInfo: DataProviderInfo;
+ protected dataSource: MatTableDataSource;
+ protected dateOfFirstActivity: Date;
+ protected dividendInBaseCurrency: number;
+ protected dividendInBaseCurrencyPrecision = 2;
+ protected dividendYieldPercentWithCurrencyEffect: number;
+ protected feeInBaseCurrency: number;
+ protected readonly getCountryName = getCountryName;
+ protected hasPermissionToCreateOwnTag: boolean;
+ protected hasPermissionToReadMarketDataOfOwnAssetProfile: boolean;
+ protected historicalDataItems: LineChartItem[];
+ protected holdingForm: FormGroup<{
+ tags: FormControl;
+ }>;
+ protected investmentInBaseCurrencyWithCurrencyEffect: number;
+ protected investmentInBaseCurrencyWithCurrencyEffectPrecision = 2;
+ protected readonly isUUID = isUUID;
+ protected marketDataItems: MarketData[] = [];
+ protected marketPrice: number;
+ protected marketPriceMax: number;
+ protected marketPriceMaxPrecision = 2;
+ protected marketPriceMin: number;
+ protected marketPriceMinPrecision = 2;
+ protected marketPricePrecision = 2;
+ protected netPerformancePercentWithCurrencyEffect: number;
+ protected netPerformancePercentWithCurrencyEffectPrecision = 2;
+ protected netPerformanceWithCurrencyEffect: number;
+ protected netPerformanceWithCurrencyEffectPrecision = 2;
+ protected pageIndex = 0;
+ protected readonly pageSize = DEFAULT_PAGE_SIZE;
+ protected quantity: number;
+ protected quantityPrecision = 2;
+ protected reportDataGlitchMail: string;
+ protected readonly routerLinkAdminControlMarketData =
internalRoutes.adminControl.subRoutes.marketData.routerLink;
- public sectors: {
+ protected sectors: {
[name: string]: { name: string; value: number };
};
- public sortColumn = 'date';
- public sortDirection: SortDirection = 'desc';
- public SymbolProfile: EnhancedSymbolProfile;
- public tags: Tag[];
- public tagsAvailable: Tag[];
- public translate = translate;
- public user: User;
- public value: number;
-
- public constructor(
- private changeDetectorRef: ChangeDetectorRef,
- private dataService: DataService,
- private destroyRef: DestroyRef,
- public dialogRef: MatDialogRef,
- @Inject(MAT_DIALOG_DATA) public data: HoldingDetailDialogParams,
- private formBuilder: FormBuilder,
- private router: Router,
- private userService: UserService
- ) {
+ protected sortColumn = 'date';
+ protected sortDirection: SortDirection = 'desc';
+ protected SymbolProfile: EnhancedSymbolProfile;
+ protected tagsAvailable: Tag[];
+ protected readonly translate = translate;
+ protected user: User;
+ protected value: number;
+
+ protected readonly data = inject(MAT_DIALOG_DATA);
+ protected readonly dialogRef = inject(
+ MatDialogRef
+ );
+
+ private tags: Tag[];
+
+ private readonly changeDetectorRef = inject(ChangeDetectorRef);
+ private readonly dataService = inject(DataService);
+ private readonly destroyRef = inject(DestroyRef);
+ private readonly formBuilder = inject(FormBuilder);
+ private readonly router = inject(Router);
+ private readonly userService = inject(UserService);
+
+ public constructor() {
addIcons({
arrowDownCircleOutline,
createOutline,
@@ -190,12 +200,11 @@ export class GfHoldingDetailDialogComponent implements OnInit {
const filters = this.getActivityFilters();
this.holdingForm = this.formBuilder.group({
- tags: [] as string[]
+ tags: new FormControl([])
});
- this.holdingForm
- .get('tags')
- .valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
+ this.holdingForm.controls.tags.valueChanges
+ .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((tags: Tag[]) => {
const newTag = tags.find(({ id }) => {
return id === undefined;
@@ -268,8 +277,6 @@ export class GfHoldingDetailDialogComponent implements OnInit {
marketPrice,
marketPriceMax,
marketPriceMin,
- netPerformance,
- netPerformancePercent,
netPerformancePercentWithCurrencyEffect,
netPerformanceWithCurrencyEffect,
quantity,
@@ -290,7 +297,11 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.benchmarkDataItems = [];
this.countries = {};
this.dataProviderInfo = dataProviderInfo;
- this.dateOfFirstActivity = dateOfFirstActivity;
+
+ if (dateOfFirstActivity) {
+ this.dateOfFirstActivity = dateOfFirstActivity;
+ }
+
this.dividendInBaseCurrency = dividendInBaseCurrency;
if (
@@ -318,12 +329,12 @@ export class GfHoldingDetailDialogComponent implements OnInit {
({ averagePrice, date, marketPrice }) => {
this.benchmarkDataItems.push({
date,
- value: averagePrice
+ value: isNumber(averagePrice) ? averagePrice : null
});
return {
date,
- value: marketPrice
+ value: marketPrice ?? 0
};
}
);
@@ -365,17 +376,6 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.marketPricePrecision = 0;
}
- this.netPerformance = netPerformance;
-
- if (
- this.data.deviceType === 'mobile' &&
- this.netPerformance >= NUMERICAL_PRECISION_THRESHOLD_5_FIGURES
- ) {
- this.netPerformancePrecision = 0;
- }
-
- this.netPerformancePercent = netPerformancePercent;
-
this.netPerformancePercentWithCurrencyEffect =
netPerformancePercentWithCurrencyEffect;
@@ -456,16 +456,16 @@ export class GfHoldingDetailDialogComponent implements OnInit {
}
}
- if (isToday(parseISO(this.dateOfFirstActivity))) {
+ if (isToday(this.dateOfFirstActivity)) {
// Add average price
this.historicalDataItems.push({
- date: this.dateOfFirstActivity,
+ date: this.dateOfFirstActivity.toISOString(),
value: this.averagePrice
});
// Add benchmark 1
this.benchmarkDataItems.push({
- date: this.dateOfFirstActivity,
+ date: this.dateOfFirstActivity.toISOString(),
value: averagePrice
});
@@ -496,7 +496,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
if (
this.benchmarkDataItems[0]?.value === undefined &&
- isSameMonth(parseISO(this.dateOfFirstActivity), new Date())
+ isSameMonth(this.dateOfFirstActivity, new Date())
) {
this.benchmarkDataItems[0].value = this.averagePrice;
}
@@ -526,7 +526,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.hasPermissionToCreateOwnTag =
hasPermission(this.user.permissions, permissions.createOwnTag) &&
- this.user?.settings?.isExperimentalFeatures;
+ (this.user?.settings?.isExperimentalFeatures ?? false);
this.tagsAvailable =
this.user?.tags?.map((tag) => {
@@ -541,13 +541,13 @@ export class GfHoldingDetailDialogComponent implements OnInit {
});
}
- public onChangePage(page: PageEvent) {
+ protected onChangePage(page: PageEvent) {
this.pageIndex = page.pageIndex;
this.fetchActivities();
}
- public onCloneActivity(aActivity: Activity) {
+ protected onCloneActivity(aActivity: Activity) {
this.router.navigate(
internalRoutes.portfolio.subRoutes.activities.routerLink,
{
@@ -558,22 +558,22 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.dialogRef.close();
}
- public onClose() {
+ protected onClose() {
this.dialogRef.close();
}
- public onCloseHolding() {
+ protected onCloseHolding() {
const today = new Date();
const activity: CreateOrderDto = {
- accountId: this.accounts.length === 1 ? this.accounts[0].id : null,
- comment: null,
- currency: this.SymbolProfile.currency,
- dataSource: this.SymbolProfile.dataSource,
+ accountId: this.accounts.length === 1 ? this.accounts[0].id : undefined,
+ comment: undefined,
+ currency: this.SymbolProfile?.currency ?? '',
+ dataSource: this.SymbolProfile?.dataSource,
date: today.toISOString(),
fee: 0,
quantity: this.quantity,
- symbol: this.SymbolProfile.symbol,
+ symbol: this.SymbolProfile?.symbol ?? '',
tags: this.tags.map(({ id }) => {
return id;
}),
@@ -593,7 +593,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
});
}
- public onExport() {
+ protected onExport() {
const activityIds = this.dataSource.data.map(({ id }) => {
return id;
});
@@ -613,13 +613,13 @@ export class GfHoldingDetailDialogComponent implements OnInit {
});
}
- public onMarketDataChanged(withRefresh = false) {
+ protected onMarketDataChanged(withRefresh = false) {
if (withRefresh) {
this.fetchMarketData();
}
}
- public onUpdateActivity(aActivity: Activity) {
+ protected onUpdateActivity(aActivity: Activity) {
this.router.navigate(
internalRoutes.portfolio.subRoutes.activities.routerLink,
{
diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts
index 536fc0feb..8b8ec875c 100644
--- a/libs/common/src/lib/interfaces/index.ts
+++ b/libs/common/src/lib/interfaces/index.ts
@@ -25,7 +25,10 @@ import type { HoldingWithParents } from './holding-with-parents.interface';
import type { Holding } from './holding.interface';
import type { InfoItem } from './info-item.interface';
import type { InvestmentItem } from './investment-item.interface';
-import type { LineChartItem } from './line-chart-item.interface';
+import type {
+ LineChartItem,
+ NullableLineChartItem
+} from './line-chart-item.interface';
import type { LookupItem } from './lookup-item.interface';
import type { MarketData } from './market-data.interface';
import type { PortfolioChart } from './portfolio-chart.interface';
@@ -158,6 +161,7 @@ export {
MarketData,
MarketDataDetailsResponse,
MarketDataOfMarketsResponse,
+ NullableLineChartItem,
OAuthResponse,
PlatformsResponse,
PortfolioChart,
diff --git a/libs/common/src/lib/interfaces/line-chart-item.interface.ts b/libs/common/src/lib/interfaces/line-chart-item.interface.ts
index e010ddfe6..43208f853 100644
--- a/libs/common/src/lib/interfaces/line-chart-item.interface.ts
+++ b/libs/common/src/lib/interfaces/line-chart-item.interface.ts
@@ -1,4 +1,6 @@
-export interface LineChartItem {
+export interface LineChartItem {
date: string;
- value: number;
+ value: T;
}
+
+export type NullableLineChartItem = LineChartItem;
diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts
index cde180dd9..f99c34b03 100644
--- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts
+++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts
@@ -68,7 +68,7 @@ export class GfHistoricalMarketDataEditorComponent
@Input() currency: string;
@Input() dataSource: DataSource;
- @Input() dateOfFirstActivity: string;
+ @Input() dateOfFirstActivity: Date;
@Input() symbol: string;
@Input() user: User;
@@ -124,7 +124,7 @@ export class GfHistoricalMarketDataEditorComponent
public ngOnChanges() {
if (this.dateOfFirstActivity) {
- let date = parseISO(this.dateOfFirstActivity);
+ let date = this.dateOfFirstActivity;
const missingMarketData: { date: Date; marketPrice?: number }[] = [];
@@ -174,7 +174,7 @@ export class GfHistoricalMarketDataEditorComponent
const dates = Object.keys(this.marketDataByMonth).sort();
const startDateString = first(dates);
const startDate = min([
- parseISO(this.dateOfFirstActivity),
+ this.dateOfFirstActivity,
...(startDateString ? [parseISO(startDateString)] : [])
]);
const endDateString = last(dates);
diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts
index 2ae07708d..a3d8bec98 100644
--- a/libs/ui/src/lib/services/data.service.ts
+++ b/libs/ui/src/lib/services/data.service.ts
@@ -445,10 +445,27 @@ export class DataService {
}: {
dataSource: DataSource;
symbol: string;
- }) {
- return this.http.get(
- `/api/v1/portfolio/holding/${dataSource}/${symbol}`
- );
+ }): Observable<
+ Omit & {
+ dateOfFirstActivity: Date | undefined;
+ }
+ > {
+ return this.http
+ .get(
+ `/api/v1/portfolio/holding/${dataSource}/${symbol}`
+ )
+ .pipe(
+ map((response) => {
+ const dateOfFirstActivity = response.dateOfFirstActivity
+ ? parseISO(response.dateOfFirstActivity)
+ : undefined;
+
+ return {
+ ...response,
+ dateOfFirstActivity
+ };
+ })
+ );
}
public fetchInfo(): InfoItem {
From ebc911b9d4b7c26e514de829c5f0296b7c45c3bc Mon Sep 17 00:00:00 2001
From: Jorge
Date: Wed, 17 Jun 2026 17:31:06 +0200
Subject: [PATCH 17/29] Bugfix/restrict maximum height of import activities
dialog (#7047)
* Restrict maximum height
* Update changelog
---
CHANGELOG.md | 1 +
.../import-activities-dialog/import-activities-dialog.scss | 2 --
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cbd9369b6..459ab1dcf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed a chart error on interaction by registering the annotation plugin early
- Fixed an issue on the allocations page where clicking an account in the _By Account_ chart did not open the detail dialog
+- Restricted the maximum height of the import activities dialog
## 3.11.0 - 2026-06-14
diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.scss b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.scss
index 64f488e36..0f3b181cc 100644
--- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.scss
+++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.scss
@@ -2,8 +2,6 @@
display: block;
.mat-mdc-dialog-content {
- max-height: unset;
-
a {
color: rgba(var(--palette-primary-500), 1);
}
From 51e9a3c807edacc0751291a5d7d0860cfb72460d Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Wed, 17 Jun 2026 17:31:57 +0200
Subject: [PATCH 18/29] Task/improve account name display in accounts table
(#7044)
* Do not wrap account name
* Update changelog
---
CHANGELOG.md | 1 +
libs/ui/src/lib/accounts-table/accounts-table.component.html | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 459ab1dcf..323f05c79 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the styling of the checkboxes to consistently use the primary color in their states
+- Improved the account name display in the accounts table
- Improved the name display in the activities table
- Improved the user id display in the users table of the admin control panel
- Improved the language localization for German (`de`)
diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html
index 1ba0ecc56..daedd5d66 100644
--- a/libs/ui/src/lib/accounts-table/accounts-table.component.html
+++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html
@@ -49,7 +49,7 @@
Name
|
-
+ |
@if (element.platform?.url) {
Date: Wed, 17 Jun 2026 19:01:32 +0200
Subject: [PATCH 19/29] Bugfix/dark mode styling of safe withdrawal rate
selector in FIRE section (#7053)
* Fix styling of safe withdrawal rate selector in dark mode
* Update changelog
---
CHANGELOG.md | 1 +
apps/client/src/app/pages/portfolio/fire/fire-page.scss | 4 ++++
2 files changed, 5 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 323f05c79..5d68aa0de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed a chart error on interaction by registering the annotation plugin early
- Fixed an issue on the allocations page where clicking an account in the _By Account_ chart did not open the detail dialog
- Restricted the maximum height of the import activities dialog
+- Fixed the dark mode styling of the safe withdrawal rate selector in the _FIRE_ section (experimental)
## 3.11.0 - 2026-06-14
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.scss b/apps/client/src/app/pages/portfolio/fire/fire-page.scss
index 3a0618ed6..6a5f037e2 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.scss
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.scss
@@ -29,5 +29,9 @@
@include select-arrow(variables.$light-primary-text);
color: rgb(var(--light-primary-text));
+
+ option {
+ color: rgb(var(--dark-primary-text));
+ }
}
}
From 1e41aa8ba0e9eb36b79a8b8f7d64d4f9eadc9bb6 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Wed, 17 Jun 2026 19:01:59 +0200
Subject: [PATCH 20/29] Task/upgrade svgmap to version 2.21.0 (#7036)
* Upgrade svgmap to version 2.21.0
* Update changelog
---
CHANGELOG.md | 1 +
package-lock.json | 8 ++++----
package.json | 2 +-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d68aa0de..0ea139fc0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the name display in the activities table
- Improved the user id display in the users table of the admin control panel
- Improved the language localization for German (`de`)
+- Upgraded `svgmap` from version `2.19.3` to `2.21.0`
### Fixed
diff --git a/package-lock.json b/package-lock.json
index 9a1d96138..81e002229 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -91,7 +91,7 @@
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"stripe": "21.0.1",
- "svgmap": "2.19.3",
+ "svgmap": "2.21.0",
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.0",
"undici": "7.24.4",
@@ -36530,9 +36530,9 @@
"license": "BSD-2-Clause"
},
"node_modules/svgmap": {
- "version": "2.19.3",
- "resolved": "https://registry.npmjs.org/svgmap/-/svgmap-2.19.3.tgz",
- "integrity": "sha512-LjKVzKgANVMRiHqFQVz57FqqX7tVm+EskubySCWb5kvizdeDBanscPA/c4tmK/48VCvYxrR1ecBbqStcD6HYfQ==",
+ "version": "2.21.0",
+ "resolved": "https://registry.npmjs.org/svgmap/-/svgmap-2.21.0.tgz",
+ "integrity": "sha512-KQNP4rkkYe3D4EgqSxX+QoRzl13oDaOt5Oqe/ClIJme/u/AaQXMU+SXgt7iL4rfqK+lRc+15dXp+y7+zQg5WiA==",
"license": "MIT",
"dependencies": {
"svg-pan-zoom": "^3.6.2"
diff --git a/package.json b/package.json
index 56d673d40..e8ae2286c 100644
--- a/package.json
+++ b/package.json
@@ -135,7 +135,7 @@
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"stripe": "21.0.1",
- "svgmap": "2.19.3",
+ "svgmap": "2.21.0",
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.0",
"undici": "7.24.4",
From 116357b797248117c10b5c94acb5dd5b4ca7cc8d Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Wed, 17 Jun 2026 20:26:26 +0200
Subject: [PATCH 21/29] Task/move referral partners to database (#7060)
* Move referral partners to database
---
apps/api/src/app/user/user.service.ts | 14 ++++++++++++++
.../app/pages/pricing/pricing-page.component.ts | 14 --------------
.../client/src/app/pages/pricing/pricing-page.html | 10 +++++-----
libs/common/src/lib/config.ts | 1 +
libs/common/src/lib/interfaces/index.ts | 2 ++
.../lib/interfaces/referral-partner.interface.ts | 3 +++
libs/common/src/lib/interfaces/user.interface.ts | 2 ++
7 files changed, 27 insertions(+), 19 deletions(-)
create mode 100644 libs/common/src/lib/interfaces/referral-partner.interface.ts
diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts
index 9d8d9da9d..6d6041c81 100644
--- a/apps/api/src/app/user/user.service.ts
+++ b/apps/api/src/app/user/user.service.ts
@@ -29,6 +29,7 @@ import {
DEFAULT_DATE_RANGE,
DEFAULT_LANGUAGE_CODE,
PROPERTY_IS_READ_ONLY_MODE,
+ PROPERTY_REFERRAL_PARTNERS,
PROPERTY_SYSTEM_MESSAGE,
TAG_ID_EXCLUDE_FROM_ANALYSIS,
locale as defaultLocale
@@ -36,6 +37,7 @@ import {
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
User as IUser,
+ ReferralPartner,
SystemMessage,
UserSettings
} from '@ghostfolio/common/interfaces';
@@ -153,6 +155,17 @@ export class UserService {
(impersonationUserSettings?.settings as UserSettings)?.baseCurrency ??
(settings.settings as UserSettings)?.baseCurrency;
+ let referralPartners: ReferralPartner[];
+
+ if (
+ this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
+ subscription.type === SubscriptionType.Basic
+ ) {
+ referralPartners = await this.propertyService.getByKey(
+ PROPERTY_REFERRAL_PARTNERS
+ );
+ }
+
let systemMessage: SystemMessage;
const systemMessageProperty =
@@ -179,6 +192,7 @@ export class UserService {
activitiesCount,
id,
permissions,
+ referralPartners,
subscription,
systemMessage,
tags,
diff --git a/apps/client/src/app/pages/pricing/pricing-page.component.ts b/apps/client/src/app/pages/pricing/pricing-page.component.ts
index a1fe0c0b5..c0e3848bb 100644
--- a/apps/client/src/app/pages/pricing/pricing-page.component.ts
+++ b/apps/client/src/app/pages/pricing/pricing-page.component.ts
@@ -68,20 +68,6 @@ export class GfPricingPageComponent implements OnInit {
'PROFESSIONAL_DATA_PROVIDER_TOOLTIP_PREMIUM'
);
- protected readonly referralBrokers = [
- 'Alpian',
- 'DEGIRO',
- 'finpension',
- 'frankly',
- 'Interactive Brokers',
- 'Mintos',
- 'Monefit SmartSaver',
- 'Revolut',
- 'Swissquote',
- 'VIAC',
- 'Zak'
- ] as const;
-
protected readonly routerLinkFeatures = publicRoutes.features.routerLink;
protected readonly routerLinkRegister = publicRoutes.register.routerLink;
protected user: User;
diff --git a/apps/client/src/app/pages/pricing/pricing-page.html b/apps/client/src/app/pages/pricing/pricing-page.html
index b951baa98..23693457c 100644
--- a/apps/client/src/app/pages/pricing/pricing-page.html
+++ b/apps/client/src/app/pages/pricing/pricing-page.html
@@ -305,23 +305,23 @@
- @if (user?.subscription?.type === 'Basic') {
+ @if (user?.referralPartners?.length) {
If you plan to open an account at
@for (
- broker of referralBrokers;
- track broker;
+ partner of user.referralPartners;
+ track partner.name;
let i = $index;
let last = $last
) {
- {{ broker }}
+ {{ partner.name }}
@if (last) {
,
} @else {
- @if (i === referralBrokers.length - 2) {
+ @if (i === user.referralPartners.length - 2) {
or
diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts
index 7e7cd2ba5..3e7fbe6df 100644
--- a/libs/common/src/lib/config.ts
+++ b/libs/common/src/lib/config.ts
@@ -253,6 +253,7 @@ export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED';
export const PROPERTY_OPENROUTER_MODEL = 'OPENROUTER_MODEL';
export const PROPERTY_OPENROUTER_MODEL_WEB_FETCH = 'OPENROUTER_MODEL_WEB_FETCH';
+export const PROPERTY_REFERRAL_PARTNERS = 'REFERRAL_PARTNERS';
export const PROPERTY_SLACK_COMMUNITY_USERS = 'SLACK_COMMUNITY_USERS';
export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG';
export const PROPERTY_SYSTEM_MESSAGE = 'SYSTEM_MESSAGE';
diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts
index 8b8ec875c..6c076514c 100644
--- a/libs/common/src/lib/interfaces/index.ts
+++ b/libs/common/src/lib/interfaces/index.ts
@@ -38,6 +38,7 @@ import type { PortfolioPosition } from './portfolio-position.interface';
import type { PortfolioReportRule } from './portfolio-report-rule.interface';
import type { PortfolioSummary } from './portfolio-summary.interface';
import type { Product } from './product';
+import type { ReferralPartner } from './referral-partner.interface';
import type { AccessTokenResponse } from './responses/access-token-response.interface';
import type { AccountBalancesResponse } from './responses/account-balances-response.interface';
import type { AccountResponse } from './responses/account-response.interface';
@@ -181,6 +182,7 @@ export {
PublicKeyCredentialRequestOptionsJSON,
PublicPortfolioResponse,
QuotesResponse,
+ ReferralPartner,
ResponseError,
RuleSettings,
ScraperConfiguration,
diff --git a/libs/common/src/lib/interfaces/referral-partner.interface.ts b/libs/common/src/lib/interfaces/referral-partner.interface.ts
new file mode 100644
index 000000000..435070fa5
--- /dev/null
+++ b/libs/common/src/lib/interfaces/referral-partner.interface.ts
@@ -0,0 +1,3 @@
+export interface ReferralPartner {
+ name: string;
+}
diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts
index e60f01915..619d4ee71 100644
--- a/libs/common/src/lib/interfaces/user.interface.ts
+++ b/libs/common/src/lib/interfaces/user.interface.ts
@@ -3,6 +3,7 @@ import { AccountWithPlatform } from '@ghostfolio/common/types';
import { Access, Tag } from '@prisma/client';
+import { ReferralPartner } from './referral-partner.interface';
import { SubscriptionOffer } from './subscription-offer.interface';
import { SystemMessage } from './system-message.interface';
import { UserSettings } from './user-settings.interface';
@@ -15,6 +16,7 @@ export interface User {
dateOfFirstActivity: Date;
id: string;
permissions: string[];
+ referralPartners?: ReferralPartner[];
settings: UserSettings;
systemMessage?: SystemMessage;
subscription: {
From bab02b3d68d71c9c14464b32b860d69099908e4f Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Wed, 17 Jun 2026 20:27:00 +0200
Subject: [PATCH 22/29] Task/deprecate SymbolProfile in favor of assetProfile
in get holding endpoint (#7054)
* Deprecate SymbolProfile in favor of assetProfile
* Update changelog
---
CHANGELOG.md | 1 +
apps/api/src/app/portfolio/portfolio.service.ts | 12 ++++++++++++
...nsform-data-source-in-response.interceptor.ts | 1 +
.../portfolio-holding-response.interface.ts | 16 ++++++++++++++++
4 files changed, 30 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ea139fc0..9abb2bc2b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the account name display in the accounts table
- Improved the name display in the activities table
- Improved the user id display in the users table of the admin control panel
+- Deprecated `SymbolProfile` in favor of `assetProfile` in the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol`
- Improved the language localization for German (`de`)
- Upgraded `svgmap` from version `2.19.3` to `2.21.0`
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index 24d760888..c15353521 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -957,6 +957,18 @@ export class PortfolioService {
marketPriceMin,
SymbolProfile,
tags,
+ assetProfile: {
+ assetClass: SymbolProfile.assetClass,
+ assetSubClass: SymbolProfile.assetSubClass,
+ countries: SymbolProfile.countries,
+ currency: SymbolProfile.currency,
+ dataSource: SymbolProfile.dataSource,
+ isin: SymbolProfile.isin,
+ name: SymbolProfile.name,
+ sectors: SymbolProfile.sectors,
+ symbol: SymbolProfile.symbol,
+ userId: SymbolProfile.userId
+ },
averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
diff --git a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
index 57643f76c..fb8ff5dc5 100644
--- a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
+++ b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
@@ -64,6 +64,7 @@ export class TransformDataSourceInResponseInterceptor<
paths: [
'activities[*].dataSource',
'activities[*].SymbolProfile.dataSource',
+ 'assetProfile.dataSource',
'benchmarks[*].dataSource',
'errors[*].dataSource',
'fearAndGreedIndex.CRYPTOCURRENCIES.dataSource',
diff --git a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
index 76bc7dc02..3b07666c9 100644
--- a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
+++ b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
@@ -9,6 +9,19 @@ import { Tag } from '@prisma/client';
export interface PortfolioHoldingResponse {
activitiesCount: number;
+ assetProfile: Pick<
+ EnhancedSymbolProfile,
+ | 'assetClass'
+ | 'assetSubClass'
+ | 'countries'
+ | 'currency'
+ | 'dataSource'
+ | 'isin'
+ | 'name'
+ | 'sectors'
+ | 'symbol'
+ | 'userId'
+ >;
averagePrice: number;
dataProviderInfo: DataProviderInfo;
dateOfFirstActivity: string;
@@ -31,7 +44,10 @@ export interface PortfolioHoldingResponse {
netPerformanceWithCurrencyEffect: number;
performances: Benchmark['performances'];
quantity: number;
+
+ /* @deprecated */
SymbolProfile: EnhancedSymbolProfile;
+
tags: Tag[];
value: number;
}
From ce286ab31495e19df62187100f1660196397363d Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Wed, 17 Jun 2026 20:37:53 +0200
Subject: [PATCH 23/29] Task/improve registration and last activity display in
admin users component (#7062)
* Improve display of registration and last activity
* Update changelog
---
CHANGELOG.md | 2 ++
apps/client/src/app/components/admin-users/admin-users.html | 4 ++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9abb2bc2b..e520ba51a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the styling of the checkboxes to consistently use the primary color in their states
- Improved the account name display in the accounts table
- Improved the name display in the activities table
+- Improved the last activity display in the users table of the admin control panel
+- Improved the registration display in the users table of the admin control panel
- Improved the user id display in the users table of the admin control panel
- Deprecated `SymbolProfile` in favor of `assetProfile` in the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol`
- Improved the language localization for German (`de`)
diff --git a/apps/client/src/app/components/admin-users/admin-users.html b/apps/client/src/app/components/admin-users/admin-users.html
index 0899c57d1..be9bb377d 100644
--- a/apps/client/src/app/components/admin-users/admin-users.html
+++ b/apps/client/src/app/components/admin-users/admin-users.html
@@ -77,7 +77,7 @@
{{ formatDistanceToNow(element.createdAt) }}
@@ -183,7 +183,7 @@
|
{{ formatDistanceToNow(element.lastActivity) }}
From efbfb6bf9c5c9e793b4f1d9ae57b05a0f8f8e88a Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Wed, 17 Jun 2026 20:40:04 +0200
Subject: [PATCH 24/29] Release 3.12.0 (#7063)
---
CHANGELOG.md | 2 +-
package-lock.json | 4 ++--
package.json | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e520ba51a..f6a2a9a94 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## Unreleased
+## 3.12.0 - 2026-06-17
### Changed
diff --git a/package-lock.json b/package-lock.json
index 81e002229..7afc46fd8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "3.11.0",
+ "version": "3.12.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "3.11.0",
+ "version": "3.12.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
diff --git a/package.json b/package.json
index e8ae2286c..f6f31747b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "3.11.0",
+ "version": "3.12.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
From fdd96bcd6bdc93e1fb4c8bea99b666de3d9b0ddb Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Thu, 18 Jun 2026 17:47:34 +0200
Subject: [PATCH 25/29] Bugfix/country name localization (#7065)
* Fix country name localization
* Update changelog
---
CHANGELOG.md | 6 ++++++
.../asset-profile-dialog.component.ts | 2 +-
.../asset-profile-dialog/asset-profile-dialog.html | 3 +--
.../holding-detail-dialog.component.ts | 5 +----
.../holding-detail-dialog.html | 3 +--
.../allocations/allocations-page.component.ts | 5 +----
.../product-page.component.ts | 8 ++------
libs/common/src/lib/helper.ts | 14 ++++++--------
.../world-map-chart/world-map-chart.component.ts | 2 +-
9 files changed, 20 insertions(+), 28 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6a2a9a94..f964c9cae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## Unreleased
+
+### Fixed
+
+- Fixed an issue with the localization of the country names
+
## 3.12.0 - 2026-06-17
### Changed
diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
index ce997cf27..829129b38 100644
--- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
+++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
@@ -373,7 +373,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
) {
for (const { code, weight } of this.assetProfile.countries) {
this.countries[code] = {
- name: getCountryName({ code, locale: this.data.locale }),
+ name: getCountryName({ code }),
value: weight
};
}
diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
index d9a5354e5..453b8cb6a 100644
--- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
+++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
@@ -271,8 +271,7 @@
[locale]="data.locale"
[value]="
getCountryName({
- code: assetProfile?.countries[0].code,
- locale: data.locale
+ code: assetProfile?.countries[0].code
})
"
>Country 0) {
for (const country of SymbolProfile.countries) {
this.countries[country.code] = {
- name: getCountryName({
- code: country.code,
- locale: this.data.locale
- }),
+ name: getCountryName({ code: country.code }),
value: country.weight
};
}
diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
index 609eec3c0..0d5691874 100644
--- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
+++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
@@ -274,8 +274,7 @@
[locale]="data.locale"
[value]="
getCountryName({
- code: SymbolProfile.countries[0].code,
- locale: data.locale
+ code: SymbolProfile.countries[0].code
})
"
>Country((names, code) => {
- names[code] = getCountryName({ code, locale: this.locale });
+ names[code] = getCountryName({ code });
return names;
}, {});
From cb34c4bc5a9023f4e7f7e7a9d00ea7a50bf0b2f3 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Fri, 19 Jun 2026 18:30:21 +0200
Subject: [PATCH 26/29] Task/extend personal finance tools (20260619) (#7071)
Extend personal finance tools
---
libs/common/src/lib/personal-finance-tools.ts | 79 +++++++++++++++++++
1 file changed, 79 insertions(+)
diff --git a/libs/common/src/lib/personal-finance-tools.ts b/libs/common/src/lib/personal-finance-tools.ts
index 8698c76f7..744081464 100644
--- a/libs/common/src/lib/personal-finance-tools.ts
+++ b/libs/common/src/lib/personal-finance-tools.ts
@@ -1,6 +1,17 @@
import { Product } from '@ghostfolio/common/interfaces';
export const personalFinanceTools: Product[] = [
+ {
+ hasFreePlan: true,
+ hasSelfHostingAbility: true,
+ key: 'acemoney',
+ name: 'AceMoney',
+ note: 'License is a perpetual license',
+ origin: 'US',
+ pricingPerYear: '$44.95',
+ slogan: 'Lifetime Personal Finance Control, One Single Payment.',
+ url: 'https://www.mechcad.net'
+ },
{
founded: 2023,
hasSelfHostingAbility: false,
@@ -361,6 +372,16 @@ export const personalFinanceTools: Product[] = [
slogan: 'Your financial superpower',
url: 'https://www.etops.com'
},
+ {
+ founded: 2015,
+ hasFreePlan: true,
+ hasSelfHostingAbility: false,
+ key: 'everydollar',
+ name: 'EveryDollar',
+ origin: 'US',
+ slogan: 'Plan, track, find more margin',
+ url: 'https://www.ramseysolutions.com/money/everydollar'
+ },
{
founded: 2020,
hasFreePlan: true,
@@ -497,6 +518,17 @@ export const personalFinanceTools: Product[] = [
slogan: 'Take control over your investments',
url: 'https://www.folishare.com'
},
+ {
+ founded: 1993,
+ hasSelfHostingAbility: true,
+ key: 'fund-manager',
+ name: 'Fund Manager',
+ note: 'License is a perpetual license',
+ origin: 'US',
+ pricingPerYear: '$99',
+ slogan: 'Powerful portfolio management software',
+ url: 'https://www.fundmanagersoftware.com'
+ },
{
hasFreePlan: true,
hasSelfHostingAbility: false,
@@ -528,6 +560,15 @@ export const personalFinanceTools: Product[] = [
slogan: 'Portfolio Tracker, Analysis & Community',
url: 'https://www.getquin.com'
},
+ {
+ hasFreePlan: true,
+ hasSelfHostingAbility: false,
+ key: 'goodbudget',
+ name: 'Goodbudget',
+ origin: 'US',
+ slogan: 'Budget with a why',
+ url: 'https://goodbudget.com'
+ },
{
hasFreePlan: true,
hasSelfHostingAbility: false,
@@ -665,6 +706,16 @@ export const personalFinanceTools: Product[] = [
slogan: 'Sustainability insights for wealth managers',
url: 'https://leafs.ch'
},
+ {
+ founded: 2020,
+ hasFreePlan: false,
+ hasSelfHostingAbility: false,
+ key: 'lunch-money',
+ name: 'Lunch Money',
+ origin: 'CA',
+ slogan: 'Delightfully simple personal finance and budgeting',
+ url: 'https://lunchmoney.app'
+ },
{
founded: 2018,
hasFreePlan: false,
@@ -725,6 +776,16 @@ export const personalFinanceTools: Product[] = [
slogan: 'The smartest way to track your crypto',
url: 'https://www.merlincrypto.com'
},
+ {
+ hasFreePlan: false,
+ hasSelfHostingAbility: false,
+ key: 'mezzi',
+ name: 'Mezzi',
+ origin: 'US',
+ pricingPerYear: '$48',
+ slogan: 'Self-manage your wealth. Get fiduciary advice.',
+ url: 'https://www.mezzi.com'
+ },
{
founded: 1991,
hasSelfHostingAbility: true,
@@ -776,6 +837,15 @@ export const personalFinanceTools: Product[] = [
slogan: 'Have total control of your financial life',
url: 'https://www.moneyspire.com'
},
+ {
+ hasFreePlan: true,
+ hasSelfHostingAbility: true,
+ key: 'moneywell',
+ name: 'MoneyWell',
+ origin: 'US',
+ slogan: 'Personal Budgeting Software for Mac and iOS',
+ url: 'https://moneywell.app'
+ },
{
key: 'moneywiz',
name: 'MoneyWiz',
@@ -1109,6 +1179,15 @@ export const personalFinanceTools: Product[] = [
slogan: 'Stock Portfolio Tracker',
url: 'https://simpleportfolio.app'
},
+ {
+ founded: 2020,
+ hasFreePlan: false,
+ hasSelfHostingAbility: false,
+ key: 'simplifi',
+ name: 'Simplifi by Quicken',
+ origin: 'US',
+ url: 'https://www.quicken.com/products/simplifi'
+ },
{
founded: 2014,
hasFreePlan: true,
From 2589a60f288fbeb4a2ed3d4d44dc58f56ce9fca3 Mon Sep 17 00:00:00 2001
From: David Requeno <108202767+DavidReque@users.noreply.github.com>
Date: Fri, 19 Jun 2026 10:32:32 -0600
Subject: [PATCH 27/29] Task/include selected profile count in market data
delete action (#7064)
* Include selected profile count
* Update changelog
---
CHANGELOG.md | 4 ++++
.../admin-market-data/admin-market-data.html | 13 ++++++++++++-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f964c9cae..0a2337d89 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### Changed
+
+- Added the selected asset profile count to the delete menu item of the historical market data table in the admin control panel
+
### Fixed
- Fixed an issue with the localization of the country names
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html
index 63d425513..e86e3e631 100644
--- a/apps/client/src/app/components/admin-market-data/admin-market-data.html
+++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html
@@ -239,7 +239,18 @@
[disabled]="!selection.hasValue()"
(click)="onDeleteAssetProfiles()"
>
- Delete Profiles
+ Delete
+ {{
+ selection.selected.length > 1
+ ? selection.selected.length
+ : ''
+ }}
+ {selection.selected.length, plural,
+ =1 {Profile}
+ other {Profiles}
+ }
From 77efbc5a437b5be665b83467f5fab5b9366b36d8 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Fri, 19 Jun 2026 18:35:29 +0200
Subject: [PATCH 28/29] Task/refactor default locale (#7066)
Refactor default locale
---
apps/api/src/app/user/user.service.ts | 6 +++---
.../admin-market-data/admin-market-data.component.ts | 8 ++++----
.../app/components/admin-users/admin-users.component.ts | 4 ++--
.../components/home-watchlist/home-watchlist.component.ts | 4 ++--
.../create-or-update-activity-dialog.component.ts | 5 ++---
libs/common/src/lib/config.ts | 3 +--
libs/common/src/lib/helper.ts | 8 ++++----
.../fire-calculator/fire-calculator.component.stories.ts | 4 ++--
8 files changed, 20 insertions(+), 22 deletions(-)
diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts
index 6d6041c81..0c159bc1c 100644
--- a/apps/api/src/app/user/user.service.ts
+++ b/apps/api/src/app/user/user.service.ts
@@ -28,11 +28,11 @@ import {
DEFAULT_CURRENCY,
DEFAULT_DATE_RANGE,
DEFAULT_LANGUAGE_CODE,
+ DEFAULT_LOCALE,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_REFERRAL_PARTNERS,
PROPERTY_SYSTEM_MESSAGE,
- TAG_ID_EXCLUDE_FROM_ANALYSIS,
- locale as defaultLocale
+ TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
@@ -102,7 +102,7 @@ export class UserService {
public async getUser({
impersonationUserId,
- locale = defaultLocale,
+ locale = DEFAULT_LOCALE,
user
}: {
impersonationUserId: string;
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
index 805adf89d..a4a9b6e19 100644
--- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
+++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
@@ -1,8 +1,8 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
DEFAULT_COLOR_SCHEME,
- DEFAULT_PAGE_SIZE,
- locale
+ DEFAULT_LOCALE,
+ DEFAULT_PAGE_SIZE
} from '@ghostfolio/common/config';
import {
canDeleteAssetProfile,
@@ -429,7 +429,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit {
colorScheme:
this.user?.settings.colorScheme ?? DEFAULT_COLOR_SCHEME,
deviceType: this.deviceType(),
- locale: this.user?.settings?.locale ?? locale
+ locale: this.user?.settings?.locale ?? DEFAULT_LOCALE
} satisfies AssetProfileDialogParams,
height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
@@ -464,7 +464,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit {
autoFocus: false,
data: {
deviceType: this.deviceType(),
- locale: this.user?.settings?.locale ?? locale
+ locale: this.user?.settings?.locale ?? DEFAULT_LOCALE
} satisfies CreateAssetProfileDialogParams,
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts
index f477776a5..15133b781 100644
--- a/apps/client/src/app/components/admin-users/admin-users.component.ts
+++ b/apps/client/src/app/components/admin-users/admin-users.component.ts
@@ -5,7 +5,7 @@ import {
import { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
-import { DEFAULT_PAGE_SIZE, locale } from '@ghostfolio/common/config';
+import { DEFAULT_LOCALE, DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import {
getDateFnsLocale,
@@ -317,7 +317,7 @@ export class GfAdminUsersComponent implements OnInit {
currentUserId: this.user?.id,
deviceType: this.deviceType(),
hasPermissionForSubscription: this.hasPermissionForSubscription,
- locale: this.user?.settings?.locale ?? locale,
+ locale: this.user?.settings?.locale ?? DEFAULT_LOCALE,
userId: aUserId
} satisfies UserDetailDialogParams,
height: this.deviceType() === 'mobile' ? '98vh' : '60vh',
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
index 22d829daa..0107f36a2 100644
--- a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
@@ -1,6 +1,6 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
-import { locale as defaultLocale } from '@ghostfolio/common/config';
+import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
import {
AssetProfileIdentifier,
Benchmark,
@@ -149,7 +149,7 @@ export class GfHomeWatchlistComponent implements OnInit {
autoFocus: false,
data: {
deviceType: this.deviceType(),
- locale: this.user?.settings?.locale ?? defaultLocale
+ locale: this.user?.settings?.locale ?? DEFAULT_LOCALE
},
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
index decb30682..ba17dbef3 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
@@ -1,6 +1,5 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
-import { ASSET_CLASS_MAPPING } from '@ghostfolio/common/config';
-import { locale as defaultLocale } from '@ghostfolio/common/config';
+import { ASSET_CLASS_MAPPING, DEFAULT_LOCALE } from '@ghostfolio/common/config';
import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos';
import { getDateFormatString } from '@ghostfolio/common/helper';
import {
@@ -120,7 +119,7 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.hasPermissionToCreateOwnTag =
this.data.user?.settings?.isExperimentalFeatures &&
hasPermission(this.data.user?.permissions, permissions.createOwnTag);
- this.locale = this.data.user.settings.locale ?? defaultLocale;
+ this.locale = this.data.user.settings.locale ?? DEFAULT_LOCALE;
this.mode = this.data.activity?.id ? 'update' : 'create';
this.dateAdapter.setLocale(this.locale);
diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts
index 3e7fbe6df..4924aaeea 100644
--- a/libs/common/src/lib/config.ts
+++ b/libs/common/src/lib/config.ts
@@ -13,8 +13,6 @@ export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPr
export const ghostfolioFearAndGreedIndexSymbolCryptocurrencies = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_CRYPTOCURRENCIES`;
export const ghostfolioFearAndGreedIndexSymbolStocks = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_STOCKS`;
-export const locale = 'en-US';
-
export const primaryColorHex = '#36cfcc';
export const primaryColorRgb = {
r: 54,
@@ -85,6 +83,7 @@ export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
export const DEFAULT_DATE_RANGE: DateRange = 'max';
export const DEFAULT_HOST = '0.0.0.0';
export const DEFAULT_LANGUAGE_CODE = 'en';
+export const DEFAULT_LOCALE = 'en-US';
export const DEFAULT_PAGE_SIZE = 50;
export const DEFAULT_PORT = 3333;
export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1;
diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts
index a29129f91..3ca95d9f6 100644
--- a/libs/common/src/lib/helper.ts
+++ b/libs/common/src/lib/helper.ts
@@ -36,12 +36,12 @@ import { get, isNil, isString } from 'lodash';
import {
DEFAULT_CURRENCY,
+ DEFAULT_LOCALE,
DERIVED_CURRENCIES,
ghostfolioFearAndGreedIndexSymbol,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks,
- ghostfolioScraperApiSymbolPrefix,
- locale
+ ghostfolioScraperApiSymbolPrefix
} from './config';
import {
AdminMarketDataItem,
@@ -261,7 +261,7 @@ export function getCurrencyFromSymbol(aSymbol = '') {
export function getCountryName({ code }: { code: string }): string {
try {
return (
- new Intl.DisplayNames([document.documentElement.lang || locale], {
+ new Intl.DisplayNames([document.documentElement.lang || DEFAULT_LOCALE], {
type: 'region'
}).of(code) ?? code
);
@@ -338,7 +338,7 @@ export function getEmojiFlag(aCountryCode: string) {
}
export function getLocale() {
- return navigator.language ?? locale;
+ return navigator.language ?? DEFAULT_LOCALE;
}
export function getLowercase(object: object, path: string) {
diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts
index 0872c2aac..ad80499d7 100644
--- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts
+++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts
@@ -1,4 +1,4 @@
-import { locale } from '@ghostfolio/common/config';
+import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -47,7 +47,7 @@ export const Simple: Story = {
annualInterestRate: 5,
currency: 'USD',
fireWealth: 50000,
- locale: locale,
+ locale: DEFAULT_LOCALE,
savingsRate: 1000
}
};
From 2fe863726198f0f1c6868bd320e2bc34d7a2db64 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Fri, 19 Jun 2026 18:44:29 +0200
Subject: [PATCH 29/29] Task/add selected asset profile count to deletion
confirmation dialog of historical market data table in admin control panel
(#7072)
* Add selected asset profile count
* Update changelog
---
CHANGELOG.md | 1 +
.../admin-market-data/admin-market-data.service.ts | 12 ++++++++++--
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a2337d89..85d661e84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Added the selected asset profile count to the delete menu item of the historical market data table in the admin control panel
+- Added the selected asset profile count to the deletion confirmation dialog of the historical market data table in the admin control panel
### Fixed
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
index 77e2151ef..28c8c2d9f 100644
--- a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
+++ b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
@@ -32,6 +32,8 @@ export class AdminMarketDataService {
public deleteAssetProfiles(
aAssetProfileIdentifiers: AssetProfileIdentifier[]
) {
+ const assetProfileCount = aAssetProfileIdentifiers.length;
+
this.notificationService.confirm({
confirmFn: () => {
const deleteRequests = aAssetProfileIdentifiers.map(
@@ -44,7 +46,10 @@ export class AdminMarketDataService {
.pipe(
catchError(() => {
this.notificationService.alert({
- title: $localize`Oops! Could not delete profiles.`
+ title:
+ assetProfileCount === 1
+ ? $localize`Oops! Could not delete the asset profile.`
+ : $localize`Oops! Could not delete the asset profiles.`
});
return EMPTY;
@@ -56,7 +61,10 @@ export class AdminMarketDataService {
.subscribe();
},
confirmType: ConfirmationDialogType.Warn,
- title: $localize`Do you really want to delete these profiles?`
+ title:
+ assetProfileCount === 1
+ ? $localize`Do you really want to delete this asset profile?`
+ : $localize`Do you really want to delete these ${assetProfileCount}:count: asset profiles?`
});
}
}
| | |