Browse Source

Merge branch 'main' into task/add-loading-indicator-to-platform-and-tag-management-of-admin-panel

pull/7982/head
Thomas Kaul 2 weeks ago
committed by GitHub
parent
commit
db4f418459
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 40
      apps/api/src/helper/object.helper.spec.ts
  3. 2
      apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
  4. 22
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  5. 25
      apps/client/src/app/components/admin-users/admin-users.component.ts
  6. 22
      apps/client/src/app/components/admin-users/admin-users.html
  7. 1
      apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts
  8. 46
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
  9. 21
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html
  10. 17
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  11. 6
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  12. 30
      apps/client/src/app/pages/portfolio/fire/fire-page.html
  13. 28
      apps/client/src/app/util/date.util.ts
  14. 6
      libs/common/src/lib/config.ts

6
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 platform management of the admin control panel
- Added a loading indicator to the tag 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 - Added the total column to the table of the tag management in the admin control panel
### Changed ### Changed
- Extended the emergency fund dialog in the portfolio summary to include the currency suffix - 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 the client to the code generated by the new `prisma-client` generator of _Prisma_
- Migrated from `lodash` to `lodash-es` - 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 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 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 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 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 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 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 `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 ## 3.75.0 - 2026-09-28

40
apps/api/src/helper/object.helper.spec.ts

@ -3082,6 +3082,20 @@ describe('redactAttributes', () => {
feeInBaseCurrency: 18.2, feeInBaseCurrency: 18.2,
id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b', id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b',
quantity: 50, 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', type: 'BUY',
unitPrice: 220.79, unitPrice: 220.79,
value: 11039.5, value: 11039.5,
@ -3111,6 +3125,13 @@ describe('redactAttributes', () => {
feeInBaseCurrency: null, feeInBaseCurrency: null,
id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b', id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b',
quantity: null, quantity: null,
SymbolProfile: {
comment: null,
name: 'Apple Inc',
scraperConfiguration: null,
symbol: 'AAPL',
symbolMapping: null
},
type: 'BUY', type: 'BUY',
// A price per unit stays visible, like the average price and the market // A price per unit stays visible, like the average price and the market
// price of a holding // price of a holding
@ -3169,6 +3190,18 @@ describe('redactAttributes', () => {
currency: 'USD', currency: 'USD',
fee: 19.9, fee: 19.9,
quantity: 50, 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', type: 'BUY',
unitPrice: 220.79, unitPrice: 220.79,
value: 11039.5, value: 11039.5,
@ -3194,6 +3227,13 @@ describe('redactAttributes', () => {
currency: 'USD', currency: 'USD',
fee: null, fee: null,
quantity: null, quantity: null,
SymbolProfile: {
comment: null,
name: 'Apple Inc',
scraperConfiguration: null,
symbol: 'AAPL',
symbolMapping: null
},
type: 'BUY', type: 'BUY',
unitPrice: 220.79, unitPrice: 220.79,
value: null, value: null,

2
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"]', '["filters.dataSource"]',
'activities[*].assetProfile.dataSource', 'activities[*].assetProfile.dataSource',
'activities[*].dataSource', 'activities[*].dataSource',
'activities[*].SymbolProfile.dataSource',
'assetProfile.dataSource', 'assetProfile.dataSource',
'benchmarks[*].dataSource', 'benchmarks[*].dataSource',
'errors[*].dataSource', 'errors[*].dataSource',
@ -93,6 +94,7 @@ export class TransformDataSourceInResponseInterceptor<
'items[*].dataSource', 'items[*].dataSource',
'latestActivities[*].assetProfile.dataSource', 'latestActivities[*].assetProfile.dataSource',
'settings["filters.dataSource"]', 'settings["filters.dataSource"]',
'SymbolProfile.dataSource',
'watchlist[*].dataSource' 'watchlist[*].dataSource'
] ]
}); });

22
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 { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { import { addMilliseconds, formatDistanceToNowStrict } from 'date-fns';
addMilliseconds,
differenceInSeconds,
formatDistanceToNowStrict,
parseISO
} from 'date-fns';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
closeCircleOutline, closeCircleOutline,
@ -187,21 +182,6 @@ export class GfAdminOverviewComponent implements OnInit {
this.fetchAdminData(); 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) { protected formatStringValue(aStringValue: StringValue) {
return formatDistanceToNowStrict( return formatDistanceToNowStrict(
addMilliseconds(new Date(), ms(aStringValue)), addMilliseconds(new Date(), ms(aStringValue)),

25
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 { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { formatRelativeTime } from '@ghostfolio/client/util/date.util';
import { DEFAULT_LOCALE, DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_LOCALE, DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { import {
canDeleteUser, canDeleteUser,
getCountryName, getCountryName,
getDateFnsLocale,
getDateFormatString, getDateFormatString,
getEmojiFlag getEmojiFlag
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
@ -49,11 +49,6 @@ import {
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import {
differenceInSeconds,
formatDistanceToNowStrict,
parseISO
} from 'date-fns';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
contractOutline, contractOutline,
@ -93,6 +88,7 @@ export class GfAdminUsersComponent implements OnInit {
protected defaultDateFormat: string; protected defaultDateFormat: string;
protected displayedColumns: string[] = []; protected displayedColumns: string[] = [];
protected readonly canDeleteUser = canDeleteUser; protected readonly canDeleteUser = canDeleteUser;
protected readonly formatRelativeTime = formatRelativeTime;
protected readonly getCountryName = getCountryName; protected readonly getCountryName = getCountryName;
protected readonly getEmojiFlag = getEmojiFlag; protected readonly getEmojiFlag = getEmojiFlag;
protected hasPermissionForSubscription: boolean; 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) { protected onChangePage(page: PageEvent) {
this.fetchUsers({ this.fetchUsers({
pageIndex: page.pageIndex pageIndex: page.pageIndex
@ -332,6 +312,7 @@ export class GfAdminUsersComponent implements OnInit {
currentUserId: this.user?.id, currentUserId: this.user?.id,
deviceType: this.deviceType(), deviceType: this.deviceType(),
hasPermissionForSubscription: this.hasPermissionForSubscription, hasPermissionForSubscription: this.hasPermissionForSubscription,
language: this.user?.settings?.language,
locale: this.user?.settings?.locale ?? DEFAULT_LOCALE, locale: this.user?.settings?.locale ?? DEFAULT_LOCALE,
userId: aUserId userId: aUserId
} satisfies UserDetailDialogParams, } satisfies UserDetailDialogParams,

22
apps/client/src/app/components/admin-users/admin-users.html

@ -32,7 +32,10 @@
class="ml-1" class="ml-1"
i18n-title i18n-title
title="Expires {{ title="Expires {{
formatDistanceToNow(element.subscription.expiresAt) formatRelativeTime({
date: element.subscription.expiresAt,
language: user?.settings?.language
})
}} ({{ }} ({{
element.subscription.expiresAt | date: defaultDateFormat element.subscription.expiresAt | date: defaultDateFormat
}})" }})"
@ -79,7 +82,12 @@
class="mat-mdc-cell px-1 py-2 text-nowrap" class="mat-mdc-cell px-1 py-2 text-nowrap"
mat-cell mat-cell
> >
{{ formatDistanceToNow(element.createdAt) }} {{
formatRelativeTime({
date: element.createdAt,
language: user?.settings?.language
})
}}
</td> </td>
</ng-container> </ng-container>
@ -175,17 +183,21 @@
<th <th
*matHeaderCellDef *matHeaderCellDef
class="mat-mdc-header-cell px-1 py-2" class="mat-mdc-header-cell px-1 py-2"
i18n
mat-header-cell mat-header-cell
> >
Last Request <ng-container i18n>Last Request</ng-container>
</th> </th>
<td <td
*matCellDef="let element" *matCellDef="let element"
class="mat-mdc-cell px-1 py-2 text-nowrap" class="mat-mdc-cell px-1 py-2 text-nowrap"
mat-cell mat-cell
> >
{{ formatDistanceToNow(element.lastActivity) }} {{
formatRelativeTime({
date: element.lastActivity,
language: user?.settings?.language
})
}}
</td> </td>
</ng-container> </ng-container>
} }

1
apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts

@ -2,6 +2,7 @@ export interface UserDetailDialogParams {
currentUserId: string; currentUserId: string;
deviceType: string; deviceType: string;
hasPermissionForSubscription: boolean; hasPermissionForSubscription: boolean;
language?: string;
locale: string; locale: string;
userId: string; userId: string;
} }

46
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 { import {
canDeleteUser, canDeleteUser,
getCountryName, getCountryName,
@ -55,6 +56,7 @@ import {
export class GfUserDetailDialogComponent implements OnInit { export class GfUserDetailDialogComponent implements OnInit {
protected readonly baseCurrency: string; protected readonly baseCurrency: string;
protected readonly canDeleteUser = canDeleteUser; protected readonly canDeleteUser = canDeleteUser;
protected readonly formatRelativeTime = formatRelativeTime;
protected readonly getCountryName = getCountryName; protected readonly getCountryName = getCountryName;
protected isLoading = true; protected isLoading = true;
@ -91,25 +93,7 @@ export class GfUserDetailDialogComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.adminService this.initialize();
.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 deleteUser() { protected deleteUser() {
@ -141,6 +125,30 @@ export class GfUserDetailDialogComponent implements OnInit {
: $localize`Coupon`; : $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() { protected onClose() {
this.dialogRef.close(); this.dialogRef.close();
} }

21
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

@ -13,6 +13,10 @@
class="no-max-width" class="no-max-width"
xPosition="before" xPosition="before"
> >
<button mat-menu-item type="button" (click)="initialize()">
<ng-container i18n>Refresh</ng-container>
</button>
<hr class="m-0" />
<button <button
mat-menu-item mat-menu-item
type="button" type="button"
@ -150,6 +154,23 @@
</div> </div>
</div> </div>
<div class="mb-3 row">
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[isLoading]="isLoading"
[value]="
formatRelativeTime({
date: user?.lastActivity,
language: data.language
}) || '-'
"
>Last Request</gf-value
>
</div>
</div>
@if (subscriptionsDataSource.data.length > 0) { @if (subscriptionsDataSource.data.length > 0) {
<div class="row"> <div class="row">
<div class="col"> <div class="col">

17
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -130,7 +130,7 @@ export class GfAllocationsPageComponent implements OnInit {
value: number; value: number;
}; };
}; };
protected topHoldings: HoldingWithParents[]; protected topHoldings: HoldingWithParents[] | undefined;
protected readonly UNKNOWN_KEY = UNKNOWN_KEY; protected readonly UNKNOWN_KEY = UNKNOWN_KEY;
protected user: User; protected user: User;
@ -360,6 +360,7 @@ export class GfAllocationsPageComponent implements OnInit {
value: 0 value: 0
} }
}; };
this.topHoldings = undefined;
} }
private initializeAllocationsData() { private initializeAllocationsData() {
@ -387,7 +388,7 @@ export class GfAllocationsPageComponent implements OnInit {
} = {}; } = {};
let totalValueExcludingCashPositions = 0; let totalValueExcludingCashPositions = 0;
let totalValueInEtf = 0; let totalValueInFunds = 0;
for (const position of this.portfolioDetails.holdings) { for (const position of this.portfolioDetails.holdings) {
const assetProfileIdentifier = getAssetProfileIdentifier( const assetProfileIdentifier = getAssetProfileIdentifier(
@ -463,6 +464,8 @@ export class GfAllocationsPageComponent implements OnInit {
} }
if (position.assetProfile.holdings.length > 0) { if (position.assetProfile.holdings.length > 0) {
totalValueInFunds += this.holdings[assetProfileIdentifier].value;
for (const { for (const {
allocationInPercentage, allocationInPercentage,
name, name,
@ -513,10 +516,6 @@ export class GfAllocationsPageComponent implements OnInit {
} }
} }
if (this.holdings[assetProfileIdentifier].assetSubClass === 'ETF') {
totalValueInEtf += this.holdings[assetProfileIdentifier].value;
}
const symbol = position.assetProfile.symbol; const symbol = position.assetProfile.symbol;
const value = this.extractValue(position); const value = this.extractValue(position);
@ -593,7 +592,7 @@ export class GfAllocationsPageComponent implements OnInit {
return { return {
name, name,
allocationInPercentage: allocationInPercentage:
totalValueInEtf > 0 ? value / totalValueInEtf : 0, totalValueInFunds > 0 ? value / totalValueInFunds : 0,
parents: this.portfolioDetails.holdings parents: this.portfolioDetails.holdings
.map((holding) => { .map((holding) => {
if (holding.assetProfile.holdings.length > 0) { if (holding.assetProfile.holdings.length > 0) {
@ -610,7 +609,9 @@ export class GfAllocationsPageComponent implements OnInit {
isNumber(currentParentHolding.valueInBaseCurrency) isNumber(currentParentHolding.valueInBaseCurrency)
? { ? {
allocationInPercentage: allocationInPercentage:
currentParentHolding.valueInBaseCurrency / value, value > 0
? currentParentHolding.valueInBaseCurrency / value
: 0,
name: holding.assetProfile.name ?? '', name: holding.assetProfile.name ?? '',
position: holding, position: holding,
symbol: holding.assetProfile.symbol, symbol: holding.assetProfile.symbol,

6
apps/client/src/app/pages/portfolio/fire/fire-page.component.ts

@ -8,6 +8,7 @@ import {
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { GfFireCalculatorComponent } from '@ghostfolio/ui/fire-calculator'; import { GfFireCalculatorComponent } from '@ghostfolio/ui/fire-calculator';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
@ -53,6 +54,7 @@ export class GfFirePageComponent implements OnInit {
protected fireWealth: FireWealth; protected fireWealth: FireWealth;
protected hasImpersonationId: boolean; protected hasImpersonationId: boolean;
protected hasPermissionToShowValues: boolean;
protected hasPermissionToUpdateUserSettings: boolean; protected hasPermissionToUpdateUserSettings: boolean;
protected isLoading = false; protected isLoading = false;
protected retirementDate: Date; protected retirementDate: Date;
@ -142,6 +144,10 @@ export class GfFirePageComponent implements OnInit {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.hasPermissionToShowValues =
hasScope(this.user.scopes, scopes.portfolioReadValues) &&
!this.user.settings.isRestrictedView;
this.hasPermissionToUpdateUserSettings = this.hasPermissionToUpdateUserSettings =
this.user.subscription?.type === SubscriptionType.Basic this.user.subscription?.type === SubscriptionType.Basic
? false ? false

30
apps/client/src/app/pages/portfolio/fire/fire-page.html

@ -73,7 +73,11 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerYear?.toNumber()" [value]="
hasPermissionToShowValues
? withdrawalRatePerYear?.toNumber()
: null
"
/> />
per year</span per year</span
> >
@ -84,7 +88,11 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerMonth?.toNumber()" [value]="
hasPermissionToShowValues
? withdrawalRatePerMonth?.toNumber()
: null
"
/> />
per month</span per month</span
>, based on your total assets of >, based on your total assets of
@ -94,7 +102,11 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="fireWealth?.today.valueInBaseCurrency" [value]="
hasPermissionToShowValues
? fireWealth?.today.valueInBaseCurrency
: null
"
/></span> /></span>
and a safe withdrawal rate (SWR) of</ng-container and a safe withdrawal rate (SWR) of</ng-container
> >
@ -137,7 +149,11 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerYearProjected?.toNumber()" [value]="
hasPermissionToShowValues
? withdrawalRatePerYearProjected?.toNumber()
: null
"
/> />
per year</span per year</span
> >
@ -148,7 +164,11 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerMonthProjected?.toNumber()" [value]="
hasPermissionToShowValues
? withdrawalRatePerMonthProjected?.toNumber()
: null
"
/> />
per month</span per month</span
>, assuming a >, assuming a

28
apps/client/src/app/util/date.util.ts

@ -0,0 +1,28 @@
import { getDateFnsLocale } from '@ghostfolio/common/helper';
import {
differenceInSeconds,
formatDistanceToNowStrict,
parseISO
} from 'date-fns';
export function formatRelativeTime({
date,
language
}: {
date: string;
language?: string;
}) {
if (date) {
const distanceString = formatDistanceToNowStrict(parseISO(date), {
addSuffix: true,
locale: getDateFnsLocale(language)
});
return Math.abs(differenceInSeconds(parseISO(date), new Date())) < 60
? $localize`just now`
: distanceString;
}
return '';
}

6
libs/common/src/lib/config.ts

@ -138,6 +138,9 @@ export const DEFAULT_REDACTED_PATHS = [
'activities[*].feeInAssetProfileCurrency', 'activities[*].feeInAssetProfileCurrency',
'activities[*].feeInBaseCurrency', 'activities[*].feeInBaseCurrency',
'activities[*].quantity', 'activities[*].quantity',
'activities[*].SymbolProfile.comment',
'activities[*].SymbolProfile.scraperConfiguration',
'activities[*].SymbolProfile.symbolMapping',
'activities[*].value', 'activities[*].value',
'activities[*].valueInBaseCurrency', 'activities[*].valueInBaseCurrency',
'assetProfile.comment', 'assetProfile.comment',
@ -180,6 +183,9 @@ export const DEFAULT_REDACTED_PATHS = [
'settings.emergencyFund', 'settings.emergencyFund',
'settings.projectedTotalAmount', 'settings.projectedTotalAmount',
'settings.savingsRate', 'settings.savingsRate',
'SymbolProfile.comment',
'SymbolProfile.scraperConfiguration',
'SymbolProfile.symbolMapping',
'totalBalanceInBaseCurrency', 'totalBalanceInBaseCurrency',
'totalDividendInBaseCurrency', 'totalDividendInBaseCurrency',
'totalInterestInBaseCurrency', 'totalInterestInBaseCurrency',

Loading…
Cancel
Save