diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8cf2d1a731..46f30da1b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,11 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a loading indicator to the platform management of the admin control panel
- Added a loading indicator to the tag management of the admin control panel
+- Added a _Refresh_ action to the user detail dialog menu in the admin control panel
- Added the total column to the table of the tag management in the admin control panel
### Changed
- Extended the emergency fund dialog in the portfolio summary to include the currency suffix
+- Extended the user detail dialog in the users section of the admin control panel to include the last request
- Migrated the client to the code generated by the new `prisma-client` generator of _Prisma_
- Migrated from `lodash` to `lodash-es`
@@ -24,10 +26,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the colors of the investment chart component (dark mode)
- Fixed the colors of the charts if the appearance is set to light and the operating system uses dark mode
- Fixed the allocation column of the top holdings of ETFs on the allocations page after a change of the user settings
+- Fixed the allocation column of the top holdings of funds other than ETFs on the allocations page
- Fixed the value column of the top holdings of ETFs on the allocations page in the _Presenter View_
- Fixed the projected sustainable retirement income on the _FIRE_ page for a projected total amount of zero (experimental)
+- Fixed the sustainable retirement income on the _FIRE_ page for redacted values
- Fixed the emergency fund in the portfolio summary for redacted values
- Fixed the redaction of `valueInBaseCurrency` in the holdings of ETFs in a restricted view
+- Fixed the redaction of `comment`, `scraperConfiguration` and `symbolMapping` in the asset profile relation of the activities in a restricted view
+- Resolved the data source transformation in the asset profile relation of the activities
## 3.75.0 - 2026-09-28
diff --git a/apps/api/src/helper/object.helper.spec.ts b/apps/api/src/helper/object.helper.spec.ts
index ca487ed8f7..9776232a0b 100644
--- a/apps/api/src/helper/object.helper.spec.ts
+++ b/apps/api/src/helper/object.helper.spec.ts
@@ -3082,6 +3082,20 @@ describe('redactAttributes', () => {
feeInBaseCurrency: 18.2,
id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b',
quantity: 50,
+ // The relation of the database is part of the response next to the
+ // asset profile
+ SymbolProfile: {
+ comment: 'Reviewed by the administrator',
+ name: 'Apple Inc',
+ scraperConfiguration: {
+ headers: { Authorization: 'Bearer TOKEN' },
+ mode: 'lazy',
+ selector: '.price',
+ url: 'https://example.org/get_price'
+ },
+ symbol: 'AAPL',
+ symbolMapping: { YAHOO: 'AAPL' }
+ },
type: 'BUY',
unitPrice: 220.79,
value: 11039.5,
@@ -3111,6 +3125,13 @@ describe('redactAttributes', () => {
feeInBaseCurrency: null,
id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b',
quantity: null,
+ SymbolProfile: {
+ comment: null,
+ name: 'Apple Inc',
+ scraperConfiguration: null,
+ symbol: 'AAPL',
+ symbolMapping: null
+ },
type: 'BUY',
// A price per unit stays visible, like the average price and the market
// price of a holding
@@ -3169,6 +3190,18 @@ describe('redactAttributes', () => {
currency: 'USD',
fee: 19.9,
quantity: 50,
+ SymbolProfile: {
+ comment: 'Reviewed by the administrator',
+ name: 'Apple Inc',
+ scraperConfiguration: {
+ headers: { Authorization: 'Bearer TOKEN' },
+ mode: 'lazy',
+ selector: '.price',
+ url: 'https://example.org/get_price'
+ },
+ symbol: 'AAPL',
+ symbolMapping: { YAHOO: 'AAPL' }
+ },
type: 'BUY',
unitPrice: 220.79,
value: 11039.5,
@@ -3194,6 +3227,13 @@ describe('redactAttributes', () => {
currency: 'USD',
fee: null,
quantity: null,
+ SymbolProfile: {
+ comment: null,
+ name: 'Apple Inc',
+ scraperConfiguration: null,
+ symbol: 'AAPL',
+ symbolMapping: null
+ },
type: 'BUY',
unitPrice: 220.79,
value: null,
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 31bfb620ed..339b91a2f7 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
@@ -83,6 +83,7 @@ export class TransformDataSourceInResponseInterceptor<
'["filters.dataSource"]',
'activities[*].assetProfile.dataSource',
'activities[*].dataSource',
+ 'activities[*].SymbolProfile.dataSource',
'assetProfile.dataSource',
'benchmarks[*].dataSource',
'errors[*].dataSource',
@@ -93,6 +94,7 @@ export class TransformDataSourceInResponseInterceptor<
'items[*].dataSource',
'latestActivities[*].assetProfile.dataSource',
'settings["filters.dataSource"]',
+ 'SymbolProfile.dataSource',
'watchlist[*].dataSource'
]
});
diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts
index 733200c912..72b9194890 100644
--- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts
+++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts
@@ -48,12 +48,7 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
-import {
- addMilliseconds,
- differenceInSeconds,
- formatDistanceToNowStrict,
- parseISO
-} from 'date-fns';
+import { addMilliseconds, formatDistanceToNowStrict } from 'date-fns';
import { addIcons } from 'ionicons';
import {
closeCircleOutline,
@@ -187,21 +182,6 @@ export class GfAdminOverviewComponent implements OnInit {
this.fetchAdminData();
}
- protected formatDistanceToNow(aDateString: string) {
- if (aDateString) {
- const distanceString = formatDistanceToNowStrict(parseISO(aDateString), {
- addSuffix: true
- });
-
- return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) <
- 60
- ? 'just now'
- : distanceString;
- }
-
- return '';
- }
-
protected formatStringValue(aStringValue: StringValue) {
return formatDistanceToNowStrict(
addMilliseconds(new Date(), ms(aStringValue)),
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 0060b38ee0..8969583d35 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,12 +5,12 @@ 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 { formatRelativeTime } from '@ghostfolio/client/util/date.util';
import { DEFAULT_LOCALE, DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import {
canDeleteUser,
getCountryName,
- getDateFnsLocale,
getDateFormatString,
getEmojiFlag
} from '@ghostfolio/common/helper';
@@ -49,11 +49,6 @@ import {
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
-import {
- differenceInSeconds,
- formatDistanceToNowStrict,
- parseISO
-} from 'date-fns';
import { addIcons } from 'ionicons';
import {
contractOutline,
@@ -93,6 +88,7 @@ export class GfAdminUsersComponent implements OnInit {
protected defaultDateFormat: string;
protected displayedColumns: string[] = [];
protected readonly canDeleteUser = canDeleteUser;
+ protected readonly formatRelativeTime = formatRelativeTime;
protected readonly getCountryName = getCountryName;
protected readonly getEmojiFlag = getEmojiFlag;
protected hasPermissionForSubscription: boolean;
@@ -209,22 +205,6 @@ export class GfAdminUsersComponent implements OnInit {
});
}
- protected formatDistanceToNow(aDateString: string) {
- if (aDateString) {
- const distanceString = formatDistanceToNowStrict(parseISO(aDateString), {
- addSuffix: true,
- locale: getDateFnsLocale(this.user?.settings?.language)
- });
-
- return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) <
- 60
- ? $localize`just now`
- : distanceString;
- }
-
- return '';
- }
-
protected onChangePage(page: PageEvent) {
this.fetchUsers({
pageIndex: page.pageIndex
@@ -332,6 +312,7 @@ export class GfAdminUsersComponent implements OnInit {
currentUserId: this.user?.id,
deviceType: this.deviceType(),
hasPermissionForSubscription: this.hasPermissionForSubscription,
+ language: this.user?.settings?.language,
locale: this.user?.settings?.locale ?? DEFAULT_LOCALE,
userId: aUserId
} satisfies UserDetailDialogParams,
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 1be6174ace..e0ec70c551 100644
--- a/apps/client/src/app/components/admin-users/admin-users.html
+++ b/apps/client/src/app/components/admin-users/admin-users.html
@@ -32,7 +32,10 @@
class="ml-1"
i18n-title
title="Expires {{
- formatDistanceToNow(element.subscription.expiresAt)
+ formatRelativeTime({
+ date: element.subscription.expiresAt,
+ language: user?.settings?.language
+ })
}} ({{
element.subscription.expiresAt | date: defaultDateFormat
}})"
@@ -79,7 +82,12 @@
class="mat-mdc-cell px-1 py-2 text-nowrap"
mat-cell
>
- {{ formatDistanceToNow(element.createdAt) }}
+ {{
+ formatRelativeTime({
+ date: element.createdAt,
+ language: user?.settings?.language
+ })
+ }}
@@ -175,17 +183,21 @@
- {{ formatDistanceToNow(element.lastActivity) }}
+ {{
+ formatRelativeTime({
+ date: element.lastActivity,
+ language: user?.settings?.language
+ })
+ }}
|
}
diff --git a/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts
index 41b2b210f1..2b2f85fbde 100644
--- a/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts
+++ b/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts
@@ -2,6 +2,7 @@ export interface UserDetailDialogParams {
currentUserId: string;
deviceType: string;
hasPermissionForSubscription: boolean;
+ language?: string;
locale: string;
userId: string;
}
diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
index 926fb8d8cd..db8c94abcd 100644
--- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
+++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
@@ -1,3 +1,4 @@
+import { formatRelativeTime } from '@ghostfolio/client/util/date.util';
import {
canDeleteUser,
getCountryName,
@@ -55,6 +56,7 @@ import {
export class GfUserDetailDialogComponent implements OnInit {
protected readonly baseCurrency: string;
protected readonly canDeleteUser = canDeleteUser;
+ protected readonly formatRelativeTime = formatRelativeTime;
protected readonly getCountryName = getCountryName;
protected isLoading = true;
@@ -91,25 +93,7 @@ export class GfUserDetailDialogComponent implements OnInit {
}
public ngOnInit() {
- this.adminService
- .fetchUserById(this.data.userId)
- .pipe(
- takeUntilDestroyed(this.destroyRef),
- catchError(() => {
- this.dialogRef.close();
-
- return EMPTY;
- })
- )
- .subscribe((user) => {
- this.user = user;
-
- this.subscriptionsDataSource.data = this.user.subscriptions ?? [];
-
- this.isLoading = false;
-
- this.changeDetectorRef.markForCheck();
- });
+ this.initialize();
}
protected deleteUser() {
@@ -141,6 +125,30 @@ export class GfUserDetailDialogComponent implements OnInit {
: $localize`Coupon`;
}
+ protected initialize() {
+ this.isLoading = true;
+
+ this.adminService
+ .fetchUserById(this.data.userId)
+ .pipe(
+ takeUntilDestroyed(this.destroyRef),
+ catchError(() => {
+ this.dialogRef.close();
+
+ return EMPTY;
+ })
+ )
+ .subscribe((user) => {
+ this.user = user;
+
+ this.subscriptionsDataSource.data = this.user.subscriptions ?? [];
+
+ this.isLoading = false;
+
+ this.changeDetectorRef.markForCheck();
+ });
+ }
+
protected onClose() {
this.dialogRef.close();
}
diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html
index efc4fe5b43..c54318f856 100644
--- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html
+++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html
@@ -13,6 +13,10 @@
class="no-max-width"
xPosition="before"
>
+
+