Browse Source

Bugfix/percentage values in impersonation mode with unrestricted access (#7522)

* Fix percentage values in impersonation mode with unrestricted access

* Fix savings rate in impersonation mode

* Improve savingsRate on FIRE page

* Update changelog
pull/7427/head^2
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
4386d22e53
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 18
      apps/api/src/app/portfolio/portfolio.controller.ts
  3. 5
      apps/api/src/app/portfolio/portfolio.service.ts
  4. 2
      apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts
  5. 10
      apps/client/src/app/app.component.ts
  6. 11
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  7. 4
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  8. 2
      apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts
  9. 4
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  10. 2
      apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts
  11. 12
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  12. 23
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  13. 34
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  14. 12
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  15. 2
      apps/client/src/app/pages/portfolio/fire/fire-page.html
  16. 1
      libs/common/src/lib/interfaces/responses/portfolio-investments.interface.ts
  17. 12
      libs/common/src/lib/permissions.ts

6
CHANGELOG.md

@ -20,6 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Migrated the abstract _Material_ form field from a component to a directive - Migrated the abstract _Material_ form field from a component to a directive
- Removed the redundant `balance` attribute of the account in favor of the account balances - Removed the redundant `balance` attribute of the account in favor of the account balances
### Fixed
- Fixed the values of the charts and tables in impersonation mode with an unrestricted access to show absolute values instead of percentages
- Fixed the savings rate of the investment timeline chart and the streaks on the analysis page in impersonation mode to be based on the impersonated user
- Fixed the savings rate of the _FIRE_ calculator in impersonation mode to not be based on the impersonating user
## 3.43.0 - 2026-08-06 ## 3.43.0 - 2026-08-06
### Added ### Added

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

@ -136,7 +136,7 @@ export class PortfolioController {
if ( if (
hasReadRestrictedAccessPermission({ hasReadRestrictedAccessPermission({
impersonationId, impersonationId,
user: this.request.user accesses: this.request.user?.accessesGet
}) || }) ||
isRestrictedView(this.request.user) isRestrictedView(this.request.user)
) { ) {
@ -180,7 +180,7 @@ export class PortfolioController {
hasDetails === false || hasDetails === false ||
hasReadRestrictedAccessPermission({ hasReadRestrictedAccessPermission({
impersonationId, impersonationId,
user: this.request.user accesses: this.request.user?.accessesGet
}) || }) ||
isRestrictedView(this.request.user) isRestrictedView(this.request.user)
) { ) {
@ -374,7 +374,7 @@ export class PortfolioController {
if ( if (
hasReadRestrictedAccessPermission({ hasReadRestrictedAccessPermission({
impersonationId, impersonationId,
user: this.request.user accesses: this.request.user?.accessesGet
}) || }) ||
isRestrictedView(this.request.user) isRestrictedView(this.request.user)
) { ) {
@ -491,19 +491,19 @@ export class PortfolioController {
filterByTags: tags filterByTags: tags
}); });
let { investments, streaks } = await this.portfolioService.getInvestments({ let { investments, savingsRate, streaks } =
await this.portfolioService.getInvestments({
filters, filters,
groupBy, groupBy,
impersonationId, impersonationId,
dateRange: range, dateRange: range,
savingsRate: this.request.user?.settings?.settings.savingsRate,
userId: this.request.user.id userId: this.request.user.id
}); });
if ( if (
hasReadRestrictedAccessPermission({ hasReadRestrictedAccessPermission({
impersonationId, impersonationId,
user: this.request.user accesses: this.request.user?.accessesGet
}) || }) ||
isRestrictedView(this.request.user) isRestrictedView(this.request.user)
) { ) {
@ -521,6 +521,8 @@ export class PortfolioController {
'currentStreak', 'currentStreak',
'longestStreak' 'longestStreak'
]); ]);
savingsRate = null;
} }
if ( if (
@ -537,7 +539,7 @@ export class PortfolioController {
]); ]);
} }
return { investments, streaks }; return { investments, savingsRate, streaks };
} }
@Get('performance') @Get('performance')
@ -578,7 +580,7 @@ export class PortfolioController {
if ( if (
hasReadRestrictedAccessPermission({ hasReadRestrictedAccessPermission({
impersonationId, impersonationId,
user: this.request.user accesses: this.request.user?.accessesGet
}) || }) ||
isRestrictedView(this.request.user) || isRestrictedView(this.request.user) ||
this.request.user.settings.settings.viewMode === 'ZEN' this.request.user.settings.settings.viewMode === 'ZEN'

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

@ -413,19 +413,18 @@ export class PortfolioService {
filters, filters,
groupBy, groupBy,
impersonationId, impersonationId,
savingsRate,
userId userId
}: { }: {
dateRange: DateRange; dateRange: DateRange;
filters?: Filter[]; filters?: Filter[];
groupBy?: GroupBy; groupBy?: GroupBy;
impersonationId: string; impersonationId: string;
savingsRate: number;
userId: string; userId: string;
}): Promise<PortfolioInvestmentsResponse> { }): Promise<PortfolioInvestmentsResponse> {
userId = await this.getUserId(impersonationId, userId); userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user); const userCurrency = this.getUserCurrency(user);
const savingsRate = (user.settings?.settings as UserSettings)?.savingsRate;
const { endDate, startDate } = getIntervalFromDateRange({ dateRange }); const { endDate, startDate } = getIntervalFromDateRange({ dateRange });
@ -438,6 +437,7 @@ export class PortfolioService {
if (activities.length === 0) { if (activities.length === 0) {
return { return {
savingsRate,
investments: [], investments: [],
streaks: { currentStreak: 0, longestStreak: 0 } streaks: { currentStreak: 0, longestStreak: 0 }
}; };
@ -484,6 +484,7 @@ export class PortfolioService {
return { return {
investments, investments,
savingsRate,
streaks streaks
}; };
} }

2
apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts

@ -38,7 +38,7 @@ export class RedactValuesInResponseInterceptor<T> implements NestInterceptor<
if ( if (
hasReadRestrictedAccessPermission({ hasReadRestrictedAccessPermission({
impersonationId, impersonationId,
user accesses: user?.accessesGet
}) || }) ||
isRestrictedView(user) isRestrictedView(user)
) { ) {

10
apps/client/src/app/app.component.ts

@ -57,12 +57,12 @@ export class GfAppComponent implements OnInit {
public currentRoute: string; public currentRoute: string;
public currentSubRoute: string; public currentSubRoute: string;
public deviceType: string; public deviceType: string;
public hasImpersonationId: boolean;
public hasInfoMessage: boolean; public hasInfoMessage: boolean;
public hasPermissionToChangeDateRange: boolean; public hasPermissionToChangeDateRange: boolean;
public hasPermissionToChangeFilters: boolean; public hasPermissionToChangeFilters: boolean;
public hasPromotion = false; public hasPromotion = false;
public hasTabs = false; public hasTabs = false;
public impersonationId: string | null;
public info: InfoItem; public info: InfoItem;
public pageTitle: string; public pageTitle: string;
public routerLinkRegister = publicRoutes.register.routerLink; public routerLinkRegister = publicRoutes.register.routerLink;
@ -116,7 +116,7 @@ export class GfAppComponent implements OnInit {
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.impersonationId = impersonationId;
}); });
this.router.events this.router.events
@ -291,13 +291,12 @@ export class GfAppComponent implements OnInit {
baseCurrency: this.user?.settings?.baseCurrency, baseCurrency: this.user?.settings?.baseCurrency,
colorScheme: this.user?.settings?.colorScheme, colorScheme: this.user?.settings?.colorScheme,
deviceType: this.deviceType, deviceType: this.deviceType,
hasImpersonationId: this.hasImpersonationId,
hasPermissionToAccessAdminControl: hasPermission( hasPermissionToAccessAdminControl: hasPermission(
this.user?.permissions, this.user?.permissions,
permissions.accessAdminControl permissions.accessAdminControl
), ),
hasPermissionToCreateActivity: hasPermissionToCreateActivity:
!this.hasImpersonationId && !this.impersonationId &&
hasPermission( hasPermission(
this.user?.permissions, this.user?.permissions,
permissions.createActivity permissions.createActivity
@ -308,12 +307,13 @@ export class GfAppComponent implements OnInit {
permissions.reportDataGlitch permissions.reportDataGlitch
), ),
hasPermissionToUpdateActivity: hasPermissionToUpdateActivity:
!this.hasImpersonationId && !this.impersonationId &&
hasPermission( hasPermission(
this.user?.permissions, this.user?.permissions,
permissions.updateActivity permissions.updateActivity
) && ) &&
!this.user?.settings?.isRestrictedView, !this.user?.settings?.isRestrictedView,
impersonationId: this.impersonationId,
locale: this.user?.settings?.locale locale: this.user?.settings?.locale
}, },
height: this.deviceType === 'mobile' ? '98vh' : '80vh', height: this.deviceType === 'mobile' ? '98vh' : '80vh',

11
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -14,7 +14,11 @@ import {
PortfolioPosition, PortfolioPosition,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import {
hasPermission,
hasReadRestrictedAccessPermission,
permissions
} from '@ghostfolio/common/permissions';
import { GfAccountBalancesComponent } from '@ghostfolio/ui/account-balances'; import { GfAccountBalancesComponent } from '@ghostfolio/ui/account-balances';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer'; import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer';
@ -225,7 +229,10 @@ export class GfAccountDetailDialogComponent implements OnInit {
protected showValuesInPercentage() { protected showValuesInPercentage() {
return ( return (
this.data.hasImpersonationId || this.user?.settings?.isRestrictedView hasReadRestrictedAccessPermission({
accesses: this.user?.access,
impersonationId: this.data.impersonationId
}) || this.user?.settings?.isRestrictedView
); );
} }

4
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -158,8 +158,8 @@
[pageSize]="pageSize" [pageSize]="pageSize"
[showAccountColumn]="false" [showAccountColumn]="false"
[showActions]=" [showActions]="
!data.hasImpersonationId &&
data.hasPermissionToCreateActivity && data.hasPermissionToCreateActivity &&
!data.impersonationId &&
user?.settings?.isExperimentalFeatures && user?.settings?.isExperimentalFeatures &&
!user?.settings?.isRestrictedView !user?.settings?.isRestrictedView
" "
@ -183,8 +183,8 @@
[currentBalance]="balance" [currentBalance]="balance"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]=" [showActions]="
!data.hasImpersonationId &&
hasPermissionToDeleteAccountBalance && hasPermissionToDeleteAccountBalance &&
!data.impersonationId &&
!user.settings.isRestrictedView !user.settings.isRestrictedView
" "
(accountBalanceCreated)="onAddAccountBalance($event)" (accountBalanceCreated)="onAddAccountBalance($event)"

2
apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts

@ -1,8 +1,8 @@
export interface AccountDetailDialogParams { export interface AccountDetailDialogParams {
accountId: string; accountId: string;
deviceType: string; deviceType: string;
hasImpersonationId: boolean;
hasPermissionToCreateActivity: boolean; hasPermissionToCreateActivity: boolean;
impersonationId: string | null;
} }
export interface AccountDetailDialogResult { export interface AccountDetailDialogResult {

4
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html

@ -382,7 +382,7 @@
[hasPermissionToCreateActivity]="false" [hasPermissionToCreateActivity]="false"
[hasPermissionToDeleteActivity]="false" [hasPermissionToDeleteActivity]="false"
[hasPermissionToExportActivities]=" [hasPermissionToExportActivities]="
!data.hasImpersonationId && !user?.settings?.isRestrictedView !data.impersonationId && !user?.settings?.isRestrictedView
" "
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
@ -390,8 +390,8 @@
[pageIndex]="pageIndex" [pageIndex]="pageIndex"
[pageSize]="pageSize" [pageSize]="pageSize"
[showActions]=" [showActions]="
!data.hasImpersonationId &&
data.hasPermissionToCreateActivity && data.hasPermissionToCreateActivity &&
!data.impersonationId &&
user?.settings?.isExperimentalFeatures && user?.settings?.isExperimentalFeatures &&
!user?.settings?.isRestrictedView !user?.settings?.isRestrictedView
" "

2
apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts

@ -7,11 +7,11 @@ export interface HoldingDetailDialogParams {
colorScheme: ColorScheme; colorScheme: ColorScheme;
dataSource: DataSource; dataSource: DataSource;
deviceType: string; deviceType: string;
hasImpersonationId: boolean;
hasPermissionToAccessAdminControl: boolean; hasPermissionToAccessAdminControl: boolean;
hasPermissionToCreateActivity: boolean; hasPermissionToCreateActivity: boolean;
hasPermissionToReportDataGlitch: boolean; hasPermissionToReportDataGlitch: boolean;
hasPermissionToUpdateActivity: boolean; hasPermissionToUpdateActivity: boolean;
impersonationId: string | null;
locale: string; locale: string;
symbol: string; symbol: string;
} }

12
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -51,9 +51,9 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba
export class GfAccountsPageComponent implements OnInit { export class GfAccountsPageComponent implements OnInit {
protected accounts: AccountWithValue[]; protected accounts: AccountWithValue[];
protected activitiesCount = 0; protected activitiesCount = 0;
protected hasImpersonationId: boolean;
protected hasPermissionToCreateAccount: boolean; protected hasPermissionToCreateAccount: boolean;
protected hasPermissionToUpdateAccount: boolean; protected hasPermissionToUpdateAccount: boolean;
protected impersonationId: string | null;
protected totalBalanceInBaseCurrency = 0; protected totalBalanceInBaseCurrency = 0;
protected totalValueInBaseCurrency = 0; protected totalValueInBaseCurrency = 0;
protected user: User; protected user: User;
@ -104,12 +104,16 @@ export class GfAccountsPageComponent implements OnInit {
}); });
} }
protected get hasImpersonationId() {
return !!this.impersonationId;
}
public ngOnInit() { public ngOnInit() {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.impersonationId = impersonationId;
}); });
this.userService.stateChanged this.userService.stateChanged
@ -252,11 +256,11 @@ export class GfAccountsPageComponent implements OnInit {
data: { data: {
accountId: aAccountId, accountId: aAccountId,
deviceType: this.deviceType(), deviceType: this.deviceType(),
hasImpersonationId: this.hasImpersonationId,
hasPermissionToCreateActivity: hasPermissionToCreateActivity:
!this.hasImpersonationId && !this.hasImpersonationId &&
hasPermission(this.user?.permissions, permissions.createActivity) && hasPermission(this.user?.permissions, permissions.createActivity) &&
!this.user?.settings?.isRestrictedView !this.user?.settings?.isRestrictedView,
impersonationId: this.impersonationId
}, },
height: this.deviceType() === 'mobile' ? '98vh' : '80vh', height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType() === 'mobile' ? '100vw' : '50rem' width: this.deviceType() === 'mobile' ? '100vw' : '50rem'

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

@ -17,7 +17,11 @@ import {
PortfolioPosition, PortfolioPosition,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import {
hasPermission,
hasReadRestrictedAccessPermission,
permissions
} from '@ghostfolio/common/permissions';
import { MarketAdvanced } from '@ghostfolio/common/types'; import { MarketAdvanced } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart'; import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
@ -85,7 +89,6 @@ export class GfAllocationsPageComponent implements OnInit {
protected readonly deviceType = computed( protected readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType () => this.deviceDetectorService.deviceInfo().deviceType
); );
protected hasImpersonationId: boolean;
protected holdings: { protected holdings: {
[symbol: string]: Pick< [symbol: string]: Pick<
PortfolioPosition['assetProfile'], PortfolioPosition['assetProfile'],
@ -97,6 +100,7 @@ export class GfAllocationsPageComponent implements OnInit {
| 'name' | 'name'
> & { etfProvider: string; value: number }; > & { etfProvider: string; value: number };
}; };
protected impersonationId: string | null;
protected isLoading = false; protected isLoading = false;
protected markets: PortfolioDetails['markets']; protected markets: PortfolioDetails['markets'];
protected marketsAdvanced: { protected marketsAdvanced: {
@ -169,7 +173,7 @@ export class GfAllocationsPageComponent implements OnInit {
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.impersonationId = impersonationId;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -224,7 +228,12 @@ export class GfAllocationsPageComponent implements OnInit {
} }
protected showValuesInPercentage() { protected showValuesInPercentage() {
return this.hasImpersonationId || this.user?.settings?.isRestrictedView; return (
hasReadRestrictedAccessPermission({
accesses: this.user?.access,
impersonationId: this.impersonationId
}) || this.user?.settings?.isRestrictedView
);
} }
private extractCurrency({ private extractCurrency({
@ -618,11 +627,11 @@ export class GfAllocationsPageComponent implements OnInit {
data: { data: {
accountId: aAccountId, accountId: aAccountId,
deviceType: this.deviceType(), deviceType: this.deviceType(),
hasImpersonationId: this.hasImpersonationId,
hasPermissionToCreateActivity: hasPermissionToCreateActivity:
!this.hasImpersonationId && !this.impersonationId &&
hasPermission(this.user?.permissions, permissions.createActivity) && hasPermission(this.user?.permissions, permissions.createActivity) &&
!this.user?.settings?.isRestrictedView !this.user?.settings?.isRestrictedView,
impersonationId: this.impersonationId
}, },
height: this.deviceType() === 'mobile' ? '98vh' : '80vh', height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType() === 'mobile' ? '100vw' : '50rem' width: this.deviceType() === 'mobile' ? '100vw' : '50rem'

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

@ -16,7 +16,11 @@ import {
ToggleOption, ToggleOption,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import {
hasPermission,
hasReadRestrictedAccessPermission,
permissions
} from '@ghostfolio/common/permissions';
import type { AiPromptMode, GroupBy } from '@ghostfolio/common/types'; import type { AiPromptMode, GroupBy } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -79,8 +83,8 @@ export class GfAnalysisPageComponent implements OnInit {
protected bottom3: PortfolioPosition[]; protected bottom3: PortfolioPosition[];
protected dividendsByGroup: InvestmentItem[]; protected dividendsByGroup: InvestmentItem[];
protected readonly dividendTimelineDataLabel = $localize`Dividend`; protected readonly dividendTimelineDataLabel = $localize`Dividend`;
protected hasImpersonationId: boolean;
protected hasPermissionToReadAiPrompt: boolean; protected hasPermissionToReadAiPrompt: boolean;
protected impersonationId: string | null;
protected investments: InvestmentItem[]; protected investments: InvestmentItem[];
protected readonly investmentTimelineDataLabel = $localize`Invested Capital`; protected readonly investmentTimelineDataLabel = $localize`Invested Capital`;
protected investmentsByGroup: InvestmentItem[]; protected investmentsByGroup: InvestmentItem[];
@ -100,6 +104,7 @@ export class GfAnalysisPageComponent implements OnInit {
protected performanceDataItemsInPercentage: HistoricalDataItem[]; protected performanceDataItemsInPercentage: HistoricalDataItem[];
protected readonly portfolioEvolutionDataLabel = $localize`Investment`; protected readonly portfolioEvolutionDataLabel = $localize`Investment`;
protected precision = 2; protected precision = 2;
protected savingsRatePerMonth: number | undefined;
protected streaks: PortfolioInvestmentsResponse['streaks']; protected streaks: PortfolioInvestmentsResponse['streaks'];
protected top3: PortfolioPosition[]; protected top3: PortfolioPosition[];
protected unitCurrentStreak: string; protected unitCurrentStreak: string;
@ -131,18 +136,13 @@ export class GfAnalysisPageComponent implements OnInit {
} }
get savingsRate() { get savingsRate() {
const savingsRatePerMonth = if (!this.savingsRatePerMonth) {
this.hasImpersonationId || this.user.settings.isRestrictedView
? undefined
: this.user?.settings?.savingsRate;
if (savingsRatePerMonth === undefined) {
return undefined; return undefined;
} }
return this.mode() === 'year' return this.mode() === 'year'
? savingsRatePerMonth * 12 ? this.savingsRatePerMonth * 12
: savingsRatePerMonth; : this.savingsRatePerMonth;
} }
public ngOnInit() { public ngOnInit() {
@ -150,7 +150,7 @@ export class GfAnalysisPageComponent implements OnInit {
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.impersonationId = impersonationId;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -241,6 +241,15 @@ export class GfAnalysisPageComponent implements OnInit {
}); });
} }
protected showValuesInPercentage() {
return (
hasReadRestrictedAccessPermission({
accesses: this.user?.access,
impersonationId: this.impersonationId
}) || this.user?.settings?.isRestrictedView
);
}
private fetchDividendsAndInvestments() { private fetchDividendsAndInvestments() {
this.isLoadingDividendTimelineChart = true; this.isLoadingDividendTimelineChart = true;
this.isLoadingInvestmentTimelineChart = true; this.isLoadingInvestmentTimelineChart = true;
@ -267,8 +276,9 @@ export class GfAnalysisPageComponent implements OnInit {
range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ investments, streaks }) => { .subscribe(({ investments, savingsRate, streaks }) => {
this.investmentsByGroup = investments; this.investmentsByGroup = investments;
this.savingsRatePerMonth = savingsRate;
this.streaks = streaks; this.streaks = streaks;
this.unitCurrentStreak = this.unitCurrentStreak =
this.mode() === 'year' this.mode() === 'year'

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

@ -398,9 +398,7 @@
[benchmarkDataLabel]="portfolioEvolutionDataLabel" [benchmarkDataLabel]="portfolioEvolutionDataLabel"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[historicalDataItems]="performanceDataItems" [historicalDataItems]="performanceDataItems"
[isInPercentage]=" [isInPercentage]="showValuesInPercentage()"
hasImpersonationId || user.settings.isRestrictedView
"
[isLoading]="isLoadingInvestmentChart" [isLoading]="isLoadingInvestmentChart"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
/> />
@ -456,9 +454,7 @@
[benchmarkDataLabel]="investmentTimelineDataLabel" [benchmarkDataLabel]="investmentTimelineDataLabel"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[groupBy]="mode()" [groupBy]="mode()"
[isInPercentage]=" [isInPercentage]="showValuesInPercentage()"
hasImpersonationId || user.settings.isRestrictedView
"
[isLoading]="isLoadingInvestmentTimelineChart" [isLoading]="isLoadingInvestmentTimelineChart"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[savingsRate]="savingsRate" [savingsRate]="savingsRate"
@ -493,9 +489,7 @@
[benchmarkDataLabel]="dividendTimelineDataLabel" [benchmarkDataLabel]="dividendTimelineDataLabel"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[groupBy]="mode()" [groupBy]="mode()"
[isInPercentage]=" [isInPercentage]="showValuesInPercentage()"
hasImpersonationId || user.settings.isRestrictedView
"
[isLoading]="isLoadingDividendTimelineChart" [isLoading]="isLoadingDividendTimelineChart"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
/> />

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

@ -21,7 +21,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[projectedTotalAmount]="user?.settings?.projectedTotalAmount" [projectedTotalAmount]="user?.settings?.projectedTotalAmount"
[retirementDate]="user?.settings?.retirementDate" [retirementDate]="user?.settings?.retirementDate"
[savingsRate]="user?.settings?.savingsRate" [savingsRate]="hasImpersonationId ? 0 : user?.settings?.savingsRate"
[style.opacity]=" [style.opacity]="
user?.subscription?.type === 'Basic' ? '0.67' : 'initial' user?.subscription?.type === 'Basic' ? '0.67' : 'initial'
" "

1
libs/common/src/lib/interfaces/responses/portfolio-investments.interface.ts

@ -2,5 +2,6 @@ import { InvestmentItem } from '../investment-item.interface';
export interface PortfolioInvestmentsResponse { export interface PortfolioInvestmentsResponse {
investments: InvestmentItem[]; investments: InvestmentItem[];
savingsRate?: number;
streaks: { currentStreak: number; longestStreak: number }; streaks: { currentStreak: number; longestStreak: number };
} }

12
libs/common/src/lib/permissions.ts

@ -1,6 +1,6 @@
import { UserWithSettings } from '@ghostfolio/common/types'; import { UserWithSettings } from '@ghostfolio/common/types';
import { Role } from '@prisma/client'; import { Access, Role } from '@prisma/client';
export const permissions = { export const permissions = {
accessAdminControl: 'accessAdminControl', accessAdminControl: 'accessAdminControl',
@ -198,17 +198,17 @@ export function hasPermission(
} }
export function hasReadRestrictedAccessPermission({ export function hasReadRestrictedAccessPermission({
impersonationId, accesses = [],
user impersonationId
}: { }: {
impersonationId: string; accesses?: Pick<Access, 'id' | 'permissions'>[];
user: UserWithSettings; impersonationId: string | null;
}) { }) {
if (!impersonationId) { if (!impersonationId) {
return false; return false;
} }
const access = user?.accessesGet?.find(({ id }) => { const access = accesses.find(({ id }) => {
return id === impersonationId; return id === impersonationId;
}); });

Loading…
Cancel
Save