From 1659d8ca7e706fbba39de685c2a426a80008fe02 Mon Sep 17 00:00:00 2001 From: mohan Date: Sat, 9 Nov 2024 15:32:22 +0530 Subject: [PATCH] Feature: Moved x-ray module and component to portfolio --- .../app/pages/portfolio/fire/fire-page.html | 130 --------------- .../portfolio-page-routing.module.ts | 5 + .../portfolio/portfolio-page.component.ts | 7 +- .../portfolio/x-ray/x-ray.component.html | 129 +++++++++++++++ .../portfolio/x-ray/x-ray.component.scss | 3 + .../pages/portfolio/x-ray/x-ray.component.ts | 150 ++++++++++++++++++ .../app/pages/portfolio/x-ray/x-ray.module.ts | 26 +++ 7 files changed, 319 insertions(+), 131 deletions(-) create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray.component.html create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray.component.scss create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray.component.ts create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray.module.ts diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index 7a336b62f..77fd1640c 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -101,133 +101,3 @@ } - -
-
-
-

X-ray

-

- Ghostfolio X-ray uses static analysis to identify potential issues - and risks in your portfolio. - It will be highly configurable in the future: activate / deactivate - rules and customize the thresholds to match your personal investment - style. -

-
-

- Emergency Fund - @if (user?.subscription?.type === 'Basic') { - - } -

- -
-
-

- Currency Cluster Risks - @if (user?.subscription?.type === 'Basic') { - - } -

- -
-
-

- Account Cluster Risks - @if (user?.subscription?.type === 'Basic') { - - } -

- -
-
-

- Economic Market Cluster Risks - @if (user?.subscription?.type === 'Basic') { - - } -

- -
-
-

- Fees - @if (user?.subscription?.type === 'Basic') { - - } -

- -
- @if (inactiveRules?.length > 0) { -
-

Inactive

- -
- } -
-
-
diff --git a/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts b/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts index 6146c573c..ca03dedf3 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts +++ b/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts @@ -34,6 +34,11 @@ const routes: Routes = [ path: 'fire', loadChildren: () => import('./fire/fire-page.module').then((m) => m.FirePageModule) + }, + { + path: 'x-ray', + loadChildren: () => + import('./x-ray/x-ray.module').then((m) => m.XRayModule) } ], component: PortfolioPageComponent, diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.component.ts b/apps/client/src/app/pages/portfolio/portfolio-page.component.ts index 0c980e25b..a35119aea 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page.component.ts +++ b/apps/client/src/app/pages/portfolio/portfolio-page.component.ts @@ -46,8 +46,13 @@ export class PortfolioPageComponent implements OnDestroy, OnInit { }, { iconName: 'calculator-outline', - label: 'FIRE / X-ray', + label: 'FIRE ', path: ['/portfolio', 'fire'] + }, + { + iconName: 'calculator-outline', + label: 'X-ray', + path: ['/portfolio', 'x-ray'] } ]; this.user = state.user; diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray.component.html b/apps/client/src/app/pages/portfolio/x-ray/x-ray.component.html new file mode 100644 index 000000000..5f887d660 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray.component.html @@ -0,0 +1,129 @@ +
+
+
+

X-ray

+

+ Ghostfolio X-ray uses static analysis to identify potential issues + and risks in your portfolio. + It will be highly configurable in the future: activate / deactivate + rules and customize the thresholds to match your personal investment + style. +

+
+

+ Emergency Fund + @if (user?.subscription?.type === 'Basic') { + + } +

+ +
+
+

+ Currency Cluster Risks + @if (user?.subscription?.type === 'Basic') { + + } +

+ +
+
+

+ Account Cluster Risks + @if (user?.subscription?.type === 'Basic') { + + } +

+ +
+
+

+ Economic Market Cluster Risks + @if (user?.subscription?.type === 'Basic') { + + } +

+ +
+
+

+ Fees + @if (user?.subscription?.type === 'Basic') { + + } +

+ +
+ @if (inactiveRules?.length > 0) { +
+

Inactive

+ +
+ } +
+
+
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray.component.scss b/apps/client/src/app/pages/portfolio/x-ray/x-ray.component.scss new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray.component.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray.component.ts new file mode 100644 index 000000000..c356931c6 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray.component.ts @@ -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-xray-page', + templateUrl: './x-ray.component.html', + styleUrl: './x-ray.component.scss' +}) +export class XRayComponent { + 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(); + + 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; + } +} diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray.module.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray.module.ts new file mode 100644 index 000000000..3b6cfe769 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray.module.ts @@ -0,0 +1,26 @@ +import { GfRulesModule } from '@ghostfolio/client/components/rules/rules.module'; +import { GfFireCalculatorComponent } from '@ghostfolio/ui/fire-calculator'; +import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; +import { GfValueComponent } from '@ghostfolio/ui/value'; + +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + +import { XRayComponent } from './x-ray.component'; + +@NgModule({ + declarations: [XRayComponent], + imports: [ + CommonModule, + GfFireCalculatorComponent, + GfPremiumIndicatorComponent, + GfRulesModule, + GfValueComponent, + NgxSkeletonLoaderModule, + RouterModule.forChild([{ path: '', component: XRayComponent }]) + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class XRayModule {}