Feature/separate FIRE and X-ray pages (#4037)
* Separate FIRE / X-ray page * Update changelog
This commit is contained in:
parent
15856264f8
commit
92b025bff3
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Extended the assistant by a holding selector
|
- Extended the assistant by a holding selector
|
||||||
|
- Separated the _FIRE_ / _X-ray_ page
|
||||||
- Improved the language localization for Italian (`it`)
|
- Improved the language localization for Italian (`it`)
|
||||||
- Upgraded `ngx-skeleton-loader` from version `7.0.0` to `9.0.0`
|
- Upgraded `ngx-skeleton-loader` from version `7.0.0` to `9.0.0`
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ const routes: Routes = [
|
|||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
component: FirePageComponent,
|
component: FirePageComponent,
|
||||||
path: '',
|
path: '',
|
||||||
title: $localize`FIRE`
|
title: 'FIRE'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
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 {
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
PortfolioReport,
|
|
||||||
PortfolioReportRule,
|
|
||||||
User
|
|
||||||
} from '@ghostfolio/common/interfaces';
|
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
@ -21,18 +16,11 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './fire-page.html'
|
templateUrl: './fire-page.html'
|
||||||
})
|
})
|
||||||
export class FirePageComponent implements OnDestroy, OnInit {
|
export class FirePageComponent implements OnDestroy, OnInit {
|
||||||
public accountClusterRiskRules: PortfolioReportRule[];
|
|
||||||
public currencyClusterRiskRules: PortfolioReportRule[];
|
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public economicMarketClusterRiskRules: PortfolioReportRule[];
|
|
||||||
public emergencyFundRules: PortfolioReportRule[];
|
|
||||||
public feeRules: PortfolioReportRule[];
|
|
||||||
public fireWealth: Big;
|
public fireWealth: Big;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
public inactiveRules: PortfolioReportRule[];
|
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public isLoadingPortfolioReport = false;
|
|
||||||
public user: User;
|
public user: User;
|
||||||
public withdrawalRatePerMonth: Big;
|
public withdrawalRatePerMonth: Big;
|
||||||
public withdrawalRatePerYear: Big;
|
public withdrawalRatePerYear: Big;
|
||||||
@ -95,8 +83,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.initializePortfolioReport();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onAnnualInterestRateChange(annualInterestRate: number) {
|
public onAnnualInterestRateChange(annualInterestRate: number) {
|
||||||
@ -133,21 +119,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onRulesUpdated(event: UpdateUserSettingDto) {
|
|
||||||
this.dataService
|
|
||||||
.putUserSetting(event)
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.userService
|
|
||||||
.get(true)
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe();
|
|
||||||
|
|
||||||
this.initializePortfolioReport();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public onSavingsRateChange(savingsRate: number) {
|
public onSavingsRateChange(savingsRate: number) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.putUserSetting({ savingsRate })
|
.putUserSetting({ savingsRate })
|
||||||
@ -187,66 +158,4 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializePortfolioReport() {
|
|
||||||
this.isLoadingPortfolioReport = true;
|
|
||||||
|
|
||||||
this.dataService
|
|
||||||
.fetchPortfolioReport()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((portfolioReport) => {
|
|
||||||
this.inactiveRules = this.mergeInactiveRules(portfolioReport);
|
|
||||||
|
|
||||||
this.accountClusterRiskRules =
|
|
||||||
portfolioReport.rules['accountClusterRisk']?.filter(
|
|
||||||
({ isActive }) => {
|
|
||||||
return isActive;
|
|
||||||
}
|
|
||||||
) ?? null;
|
|
||||||
|
|
||||||
this.currencyClusterRiskRules =
|
|
||||||
portfolioReport.rules['currencyClusterRisk']?.filter(
|
|
||||||
({ isActive }) => {
|
|
||||||
return isActive;
|
|
||||||
}
|
|
||||||
) ?? null;
|
|
||||||
|
|
||||||
this.economicMarketClusterRiskRules =
|
|
||||||
portfolioReport.rules['economicMarketClusterRisk']?.filter(
|
|
||||||
({ isActive }) => {
|
|
||||||
return isActive;
|
|
||||||
}
|
|
||||||
) ?? null;
|
|
||||||
|
|
||||||
this.emergencyFundRules =
|
|
||||||
portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => {
|
|
||||||
return isActive;
|
|
||||||
}) ?? null;
|
|
||||||
|
|
||||||
this.feeRules =
|
|
||||||
portfolioReport.rules['fees']?.filter(({ isActive }) => {
|
|
||||||
return isActive;
|
|
||||||
}) ?? null;
|
|
||||||
|
|
||||||
this.isLoadingPortfolioReport = false;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] {
|
|
||||||
let inactiveRules: PortfolioReportRule[] = [];
|
|
||||||
|
|
||||||
for (const category in report.rules) {
|
|
||||||
const rulesArray = report.rules[category];
|
|
||||||
|
|
||||||
inactiveRules = inactiveRules.concat(
|
|
||||||
rulesArray.filter(({ isActive }) => {
|
|
||||||
return !isActive;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return inactiveRules;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -101,133 +101,3 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container mt-5">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<h2 class="h3 mb-3 text-center">X-ray</h2>
|
|
||||||
<p class="mb-4">
|
|
||||||
<span i18n
|
|
||||||
>Ghostfolio X-ray uses static analysis to identify potential issues
|
|
||||||
and risks in your portfolio.</span
|
|
||||||
>
|
|
||||||
<span class="d-none"
|
|
||||||
>It will be highly configurable in the future: activate / deactivate
|
|
||||||
rules and customize the thresholds to match your personal investment
|
|
||||||
style.</span
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="align-items-center d-flex m-0">
|
|
||||||
<span i18n>Emergency Fund</span>
|
|
||||||
@if (user?.subscription?.type === 'Basic') {
|
|
||||||
<gf-premium-indicator class="ml-1" />
|
|
||||||
}
|
|
||||||
</h4>
|
|
||||||
<gf-rules
|
|
||||||
[hasPermissionToUpdateUserSettings]="
|
|
||||||
!hasImpersonationId &&
|
|
||||||
hasPermissionToUpdateUserSettings &&
|
|
||||||
user?.settings?.isExperimentalFeatures
|
|
||||||
"
|
|
||||||
[isLoading]="isLoadingPortfolioReport"
|
|
||||||
[rules]="emergencyFundRules"
|
|
||||||
[settings]="user?.settings?.xRayRules"
|
|
||||||
(rulesUpdated)="onRulesUpdated($event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="align-items-center d-flex m-0">
|
|
||||||
<span i18n>Currency Cluster Risks</span>
|
|
||||||
@if (user?.subscription?.type === 'Basic') {
|
|
||||||
<gf-premium-indicator class="ml-1" />
|
|
||||||
}
|
|
||||||
</h4>
|
|
||||||
<gf-rules
|
|
||||||
[hasPermissionToUpdateUserSettings]="
|
|
||||||
!hasImpersonationId &&
|
|
||||||
hasPermissionToUpdateUserSettings &&
|
|
||||||
user?.settings?.isExperimentalFeatures
|
|
||||||
"
|
|
||||||
[isLoading]="isLoadingPortfolioReport"
|
|
||||||
[rules]="currencyClusterRiskRules"
|
|
||||||
[settings]="user?.settings?.xRayRules"
|
|
||||||
(rulesUpdated)="onRulesUpdated($event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="align-items-center d-flex m-0">
|
|
||||||
<span i18n>Account Cluster Risks</span>
|
|
||||||
@if (user?.subscription?.type === 'Basic') {
|
|
||||||
<gf-premium-indicator class="ml-1" />
|
|
||||||
}
|
|
||||||
</h4>
|
|
||||||
<gf-rules
|
|
||||||
[hasPermissionToUpdateUserSettings]="
|
|
||||||
!hasImpersonationId &&
|
|
||||||
hasPermissionToUpdateUserSettings &&
|
|
||||||
user?.settings?.isExperimentalFeatures
|
|
||||||
"
|
|
||||||
[isLoading]="isLoadingPortfolioReport"
|
|
||||||
[rules]="accountClusterRiskRules"
|
|
||||||
[settings]="user?.settings?.xRayRules"
|
|
||||||
(rulesUpdated)="onRulesUpdated($event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="align-items-center d-flex m-0">
|
|
||||||
<span i18n>Economic Market Cluster Risks</span>
|
|
||||||
@if (user?.subscription?.type === 'Basic') {
|
|
||||||
<gf-premium-indicator class="ml-1" />
|
|
||||||
}
|
|
||||||
</h4>
|
|
||||||
<gf-rules
|
|
||||||
[hasPermissionToUpdateUserSettings]="
|
|
||||||
!hasImpersonationId &&
|
|
||||||
hasPermissionToUpdateUserSettings &&
|
|
||||||
user?.settings?.isExperimentalFeatures
|
|
||||||
"
|
|
||||||
[isLoading]="isLoadingPortfolioReport"
|
|
||||||
[rules]="economicMarketClusterRiskRules"
|
|
||||||
[settings]="user?.settings?.xRayRules"
|
|
||||||
(rulesUpdated)="onRulesUpdated($event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="align-items-center d-flex m-0">
|
|
||||||
<span i18n>Fees</span>
|
|
||||||
@if (user?.subscription?.type === 'Basic') {
|
|
||||||
<gf-premium-indicator class="ml-1" />
|
|
||||||
}
|
|
||||||
</h4>
|
|
||||||
<gf-rules
|
|
||||||
[hasPermissionToUpdateUserSettings]="
|
|
||||||
!hasImpersonationId &&
|
|
||||||
hasPermissionToUpdateUserSettings &&
|
|
||||||
user?.settings?.isExperimentalFeatures
|
|
||||||
"
|
|
||||||
[isLoading]="isLoadingPortfolioReport"
|
|
||||||
[rules]="feeRules"
|
|
||||||
[settings]="user?.settings?.xRayRules"
|
|
||||||
(rulesUpdated)="onRulesUpdated($event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
@if (inactiveRules?.length > 0) {
|
|
||||||
<div>
|
|
||||||
<h4 class="m-0" i18n>Inactive</h4>
|
|
||||||
<gf-rules
|
|
||||||
[hasPermissionToUpdateUserSettings]="
|
|
||||||
!hasImpersonationId &&
|
|
||||||
hasPermissionToUpdateUserSettings &&
|
|
||||||
user?.settings?.isExperimentalFeatures
|
|
||||||
"
|
|
||||||
[isLoading]="isLoadingPortfolioReport"
|
|
||||||
[rules]="inactiveRules"
|
|
||||||
[settings]="user?.settings?.xRayRules"
|
|
||||||
(rulesUpdated)="onRulesUpdated($event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { GfRulesModule } from '@ghostfolio/client/components/rules/rules.module';
|
|
||||||
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 { GfValueComponent } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
@ -17,7 +16,6 @@ import { FirePageComponent } from './fire-page.component';
|
|||||||
FirePageRoutingModule,
|
FirePageRoutingModule,
|
||||||
GfFireCalculatorComponent,
|
GfFireCalculatorComponent,
|
||||||
GfPremiumIndicatorComponent,
|
GfPremiumIndicatorComponent,
|
||||||
GfRulesModule,
|
|
||||||
GfValueComponent,
|
GfValueComponent,
|
||||||
NgxSkeletonLoaderModule
|
NgxSkeletonLoaderModule
|
||||||
],
|
],
|
||||||
|
@ -34,6 +34,11 @@ const routes: Routes = [
|
|||||||
path: 'fire',
|
path: 'fire',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./fire/fire-page.module').then((m) => m.FirePageModule)
|
import('./fire/fire-page.module').then((m) => m.FirePageModule)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'x-ray',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./x-ray/x-ray-page.module').then((m) => m.XRayPageModule)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
component: PortfolioPageComponent,
|
component: PortfolioPageComponent,
|
||||||
|
@ -46,8 +46,13 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
iconName: 'calculator-outline',
|
iconName: 'calculator-outline',
|
||||||
label: 'FIRE / X-ray',
|
label: 'FIRE ',
|
||||||
path: ['/portfolio', 'fire']
|
path: ['/portfolio', 'fire']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'scan-outline',
|
||||||
|
label: 'X-ray',
|
||||||
|
path: ['/portfolio', 'x-ray']
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { XRayPageComponent } from './x-ray-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
component: XRayPageComponent,
|
||||||
|
path: '',
|
||||||
|
title: 'X-ray'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class XRayPageRoutingModule {}
|
@ -0,0 +1,123 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h2 class="d-none d-sm-block h3 mb-3 text-center">X-ray</h2>
|
||||||
|
<p class="mb-4" i18n>
|
||||||
|
Ghostfolio X-ray uses static analysis to uncover potential issues and
|
||||||
|
risks in your portfolio. Adjust the rules below and set custom
|
||||||
|
thresholds to align with your personal investment strategy.
|
||||||
|
</p>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="align-items-center d-flex m-0">
|
||||||
|
<span i18n>Emergency Fund</span>
|
||||||
|
@if (user?.subscription?.type === 'Basic') {
|
||||||
|
<gf-premium-indicator class="ml-1" />
|
||||||
|
}
|
||||||
|
</h4>
|
||||||
|
<gf-rules
|
||||||
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
!hasImpersonationId &&
|
||||||
|
hasPermissionToUpdateUserSettings &&
|
||||||
|
user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
|
[isLoading]="isLoadingPortfolioReport"
|
||||||
|
[rules]="emergencyFundRules"
|
||||||
|
[settings]="user?.settings?.xRayRules"
|
||||||
|
(rulesUpdated)="onRulesUpdated($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="align-items-center d-flex m-0">
|
||||||
|
<span i18n>Currency Cluster Risks</span>
|
||||||
|
@if (user?.subscription?.type === 'Basic') {
|
||||||
|
<gf-premium-indicator class="ml-1" />
|
||||||
|
}
|
||||||
|
</h4>
|
||||||
|
<gf-rules
|
||||||
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
!hasImpersonationId &&
|
||||||
|
hasPermissionToUpdateUserSettings &&
|
||||||
|
user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
|
[isLoading]="isLoadingPortfolioReport"
|
||||||
|
[rules]="currencyClusterRiskRules"
|
||||||
|
[settings]="user?.settings?.xRayRules"
|
||||||
|
(rulesUpdated)="onRulesUpdated($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="align-items-center d-flex m-0">
|
||||||
|
<span i18n>Account Cluster Risks</span>
|
||||||
|
@if (user?.subscription?.type === 'Basic') {
|
||||||
|
<gf-premium-indicator class="ml-1" />
|
||||||
|
}
|
||||||
|
</h4>
|
||||||
|
<gf-rules
|
||||||
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
!hasImpersonationId &&
|
||||||
|
hasPermissionToUpdateUserSettings &&
|
||||||
|
user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
|
[isLoading]="isLoadingPortfolioReport"
|
||||||
|
[rules]="accountClusterRiskRules"
|
||||||
|
[settings]="user?.settings?.xRayRules"
|
||||||
|
(rulesUpdated)="onRulesUpdated($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="align-items-center d-flex m-0">
|
||||||
|
<span i18n>Economic Market Cluster Risks</span>
|
||||||
|
@if (user?.subscription?.type === 'Basic') {
|
||||||
|
<gf-premium-indicator class="ml-1" />
|
||||||
|
}
|
||||||
|
</h4>
|
||||||
|
<gf-rules
|
||||||
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
!hasImpersonationId &&
|
||||||
|
hasPermissionToUpdateUserSettings &&
|
||||||
|
user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
|
[isLoading]="isLoadingPortfolioReport"
|
||||||
|
[rules]="economicMarketClusterRiskRules"
|
||||||
|
[settings]="user?.settings?.xRayRules"
|
||||||
|
(rulesUpdated)="onRulesUpdated($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="align-items-center d-flex m-0">
|
||||||
|
<span i18n>Fees</span>
|
||||||
|
@if (user?.subscription?.type === 'Basic') {
|
||||||
|
<gf-premium-indicator class="ml-1" />
|
||||||
|
}
|
||||||
|
</h4>
|
||||||
|
<gf-rules
|
||||||
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
!hasImpersonationId &&
|
||||||
|
hasPermissionToUpdateUserSettings &&
|
||||||
|
user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
|
[isLoading]="isLoadingPortfolioReport"
|
||||||
|
[rules]="feeRules"
|
||||||
|
[settings]="user?.settings?.xRayRules"
|
||||||
|
(rulesUpdated)="onRulesUpdated($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@if (inactiveRules?.length > 0) {
|
||||||
|
<div>
|
||||||
|
<h4 class="m-0" i18n>Inactive</h4>
|
||||||
|
<gf-rules
|
||||||
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
!hasImpersonationId &&
|
||||||
|
hasPermissionToUpdateUserSettings &&
|
||||||
|
user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
|
[isLoading]="isLoadingPortfolioReport"
|
||||||
|
[rules]="inactiveRules"
|
||||||
|
[settings]="user?.settings?.xRayRules"
|
||||||
|
(rulesUpdated)="onRulesUpdated($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import {
|
||||||
|
PortfolioReportRule,
|
||||||
|
PortfolioReport
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import { User } from '@ghostfolio/common/interfaces/user.interface';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
|
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||||
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-x-ray-page',
|
||||||
|
styleUrl: './x-ray-page.component.scss',
|
||||||
|
templateUrl: './x-ray-page.component.html'
|
||||||
|
})
|
||||||
|
export class XRayPageComponent {
|
||||||
|
public accountClusterRiskRules: PortfolioReportRule[];
|
||||||
|
public currencyClusterRiskRules: PortfolioReportRule[];
|
||||||
|
public economicMarketClusterRiskRules: PortfolioReportRule[];
|
||||||
|
public emergencyFundRules: PortfolioReportRule[];
|
||||||
|
public feeRules: PortfolioReportRule[];
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
|
public inactiveRules: PortfolioReportRule[];
|
||||||
|
public isLoadingPortfolioReport = false;
|
||||||
|
public user: User;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private userService: UserService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((impersonationId) => {
|
||||||
|
this.hasImpersonationId = !!impersonationId;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userService.stateChanged
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((state) => {
|
||||||
|
if (state?.user) {
|
||||||
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.hasPermissionToUpdateUserSettings =
|
||||||
|
this.user.subscription?.type === 'Basic'
|
||||||
|
? false
|
||||||
|
: hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.updateUserSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initializePortfolioReport();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onRulesUpdated(event: UpdateUserSettingDto) {
|
||||||
|
this.dataService
|
||||||
|
.putUserSetting(event)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.userService
|
||||||
|
.get(true)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
this.initializePortfolioReport();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializePortfolioReport() {
|
||||||
|
this.isLoadingPortfolioReport = true;
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchPortfolioReport()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((portfolioReport) => {
|
||||||
|
this.inactiveRules = this.mergeInactiveRules(portfolioReport);
|
||||||
|
|
||||||
|
this.accountClusterRiskRules =
|
||||||
|
portfolioReport.rules['accountClusterRisk']?.filter(
|
||||||
|
({ isActive }) => {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
this.currencyClusterRiskRules =
|
||||||
|
portfolioReport.rules['currencyClusterRisk']?.filter(
|
||||||
|
({ isActive }) => {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
this.economicMarketClusterRiskRules =
|
||||||
|
portfolioReport.rules['economicMarketClusterRisk']?.filter(
|
||||||
|
({ isActive }) => {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
this.emergencyFundRules =
|
||||||
|
portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => {
|
||||||
|
return isActive;
|
||||||
|
}) ?? null;
|
||||||
|
|
||||||
|
this.feeRules =
|
||||||
|
portfolioReport.rules['fees']?.filter(({ isActive }) => {
|
||||||
|
return isActive;
|
||||||
|
}) ?? null;
|
||||||
|
|
||||||
|
this.isLoadingPortfolioReport = false;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] {
|
||||||
|
let inactiveRules: PortfolioReportRule[] = [];
|
||||||
|
|
||||||
|
for (const category in report.rules) {
|
||||||
|
const rulesArray = report.rules[category];
|
||||||
|
|
||||||
|
inactiveRules = inactiveRules.concat(
|
||||||
|
rulesArray.filter(({ isActive }) => {
|
||||||
|
return !isActive;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inactiveRules;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { GfRulesModule } from '@ghostfolio/client/components/rules/rules.module';
|
||||||
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
import { XRayPageRoutingModule } from './x-ray-page-routing.module';
|
||||||
|
import { XRayPageComponent } from './x-ray-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [XRayPageComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfPremiumIndicatorComponent,
|
||||||
|
GfRulesModule,
|
||||||
|
NgxSkeletonLoaderModule,
|
||||||
|
XRayPageRoutingModule
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class XRayPageModule {}
|
Loading…
x
Reference in New Issue
Block a user