mirror of https://github.com/ghostfolio/ghostfolio
Mohan
2 months ago
committed by
GitHub
12 changed files with 333 additions and 226 deletions
@ -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…
Reference in new issue