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 {}