From 92b025bff3a7f770d920cb58fbe5bbc662ee5ac2 Mon Sep 17 00:00:00 2001 From: Mohan <41165473+mohanbyte@users.noreply.github.com> Date: Tue, 12 Nov 2024 01:37:02 +0530 Subject: [PATCH] Feature/separate FIRE and X-ray pages (#4037) * Separate FIRE / X-ray page * Update changelog --- CHANGELOG.md | 1 + .../fire/fire-page-routing.module.ts | 2 +- .../portfolio/fire/fire-page.component.ts | 93 +---------- .../app/pages/portfolio/fire/fire-page.html | 130 --------------- .../pages/portfolio/fire/fire-page.module.ts | 2 - .../portfolio-page-routing.module.ts | 5 + .../portfolio/portfolio-page.component.ts | 7 +- .../x-ray/x-ray-page-routing.module.ts | 21 +++ .../portfolio/x-ray/x-ray-page.component.html | 123 ++++++++++++++ .../portfolio/x-ray/x-ray-page.component.scss | 3 + .../portfolio/x-ray/x-ray-page.component.ts | 150 ++++++++++++++++++ .../portfolio/x-ray/x-ray-page.module.ts | 22 +++ 12 files changed, 333 insertions(+), 226 deletions(-) create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray-page-routing.module.ts create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.scss create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray-page.module.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2700af210..8f3628ca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Extended the assistant by a holding selector +- Separated the _FIRE_ / _X-ray_ page - Improved the language localization for Italian (`it`) - Upgraded `ngx-skeleton-loader` from version `7.0.0` to `9.0.0` diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts b/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts index 885dc5509..96260adda 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts @@ -10,7 +10,7 @@ const routes: Routes = [ canActivate: [AuthGuard], component: FirePageComponent, path: '', - title: $localize`FIRE` + title: 'FIRE' } ]; diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index d20c66912..897b9824e 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -1,12 +1,7 @@ -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 { - PortfolioReport, - PortfolioReportRule, - User -} from '@ghostfolio/common/interfaces'; +import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; @@ -21,18 +16,11 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './fire-page.html' }) export class FirePageComponent implements OnDestroy, OnInit { - public accountClusterRiskRules: PortfolioReportRule[]; - public currencyClusterRiskRules: PortfolioReportRule[]; public deviceType: string; - public economicMarketClusterRiskRules: PortfolioReportRule[]; - public emergencyFundRules: PortfolioReportRule[]; - public feeRules: PortfolioReportRule[]; public fireWealth: Big; public hasImpersonationId: boolean; public hasPermissionToUpdateUserSettings: boolean; - public inactiveRules: PortfolioReportRule[]; public isLoading = false; - public isLoadingPortfolioReport = false; public user: User; public withdrawalRatePerMonth: Big; public withdrawalRatePerYear: Big; @@ -95,8 +83,6 @@ export class FirePageComponent implements OnDestroy, OnInit { this.changeDetectorRef.markForCheck(); } }); - - this.initializePortfolioReport(); } 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) { this.dataService .putUserSetting({ savingsRate }) @@ -187,66 +158,4 @@ export class FirePageComponent implements OnDestroy, OnInit { 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/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/fire/fire-page.module.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts index 60e3127d9..a606ae1b4 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts @@ -1,4 +1,3 @@ -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'; @@ -17,7 +16,6 @@ import { FirePageComponent } from './fire-page.component'; FirePageRoutingModule, GfFireCalculatorComponent, GfPremiumIndicatorComponent, - GfRulesModule, GfValueComponent, NgxSkeletonLoaderModule ], 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..20de6f8fa 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-page.module').then((m) => m.XRayPageModule) } ], 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..7f40bf1d7 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: 'scan-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-page-routing.module.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page-routing.module.ts new file mode 100644 index 000000000..091cbc49f --- /dev/null +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page-routing.module.ts @@ -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 {} diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html new file mode 100644 index 000000000..cd03b49bb --- /dev/null +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html @@ -0,0 +1,123 @@ +
+
+
+

X-ray

+

+ 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. +

+
+

+ 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-page.component.scss b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.scss new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts new file mode 100644 index 000000000..36f42fc3e --- /dev/null +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.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-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(); + + 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-page.module.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.module.ts new file mode 100644 index 000000000..bff4f4dc9 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.module.ts @@ -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 {}