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 @@ - Last Request + Last Request - {{ 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" > + +