diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe8f9e28..839d41c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added tabs to the portfolio page + ### Changed +- Merged the _FIRE_ calculator and the _X-ray_ section to a single page - Tightened the validation rule of the base currency environment variable (`BASE_CURRENCY`) ## 1.209.0 - 05.11.2022 diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 2596f5e39..7ce976185 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -429,16 +429,19 @@ export class PortfolioController { public async getReport( @Headers('impersonation-id') impersonationId: string ): Promise { + const report = await this.portfolioService.getReport(impersonationId); + if ( this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.request.user.subscription.type === 'Basic' ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); + for (const rule in report.rules) { + if (report.rules[rule]) { + report.rules[rule] = []; + } + } } - return await this.portfolioService.getReport(impersonationId); + return report; } } diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index 71e46416b..d09beffc3 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -145,48 +145,6 @@ const routes: Routes = [ (m) => m.PortfolioPageModule ) }, - { - path: 'portfolio/activities', - loadChildren: () => - import('./pages/portfolio/activities/activities-page.module').then( - (m) => m.ActivitiesPageModule - ) - }, - { - path: 'portfolio/allocations', - loadChildren: () => - import('./pages/portfolio/allocations/allocations-page.module').then( - (m) => m.AllocationsPageModule - ) - }, - { - path: 'portfolio/analysis', - loadChildren: () => - import('./pages/portfolio/analysis/analysis-page.module').then( - (m) => m.AnalysisPageModule - ) - }, - { - path: 'portfolio/fire', - loadChildren: () => - import('./pages/portfolio/fire/fire-page.module').then( - (m) => m.FirePageModule - ) - }, - { - path: 'portfolio/holdings', - loadChildren: () => - import('./pages/portfolio/holdings/holdings-page.module').then( - (m) => m.HoldingsPageModule - ) - }, - { - path: 'portfolio/report', - loadChildren: () => - import('./pages/portfolio/report/report-page.module').then( - (m) => m.ReportPageModule - ) - }, { path: 'pricing', loadChildren: () => diff --git a/apps/client/src/app/components/rules/rules.component.html b/apps/client/src/app/components/rules/rules.component.html index a049e6134..1c7cedfc7 100644 --- a/apps/client/src/app/components/rules/rules.component.html +++ b/apps/client/src/app/components/rules/rules.component.html @@ -10,7 +10,7 @@ > - + diff --git a/apps/client/src/app/components/rules/rules.component.ts b/apps/client/src/app/components/rules/rules.component.ts index e5429bdcc..e1f15d23f 100644 --- a/apps/client/src/app/components/rules/rules.component.ts +++ b/apps/client/src/app/components/rules/rules.component.ts @@ -9,7 +9,7 @@ import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; }) export class RulesComponent { @Input() hasPermissionToCreateOrder: boolean; - @Input() rules: PortfolioReportRule; + @Input() rules: PortfolioReportRule[]; public constructor() {} } diff --git a/apps/client/src/app/components/rules/rules.module.ts b/apps/client/src/app/components/rules/rules.module.ts index c5a3731bb..ebf4eaeaf 100644 --- a/apps/client/src/app/components/rules/rules.module.ts +++ b/apps/client/src/app/components/rules/rules.module.ts @@ -21,4 +21,4 @@ import { RulesComponent } from './rules.component'; ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) -export class RulesModule {} +export class GfRulesModule {} diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index 18b543c09..ef0a21d4e 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { MatSnackBar } from '@angular/material/snack-bar'; import { ActivatedRoute, Router } from '@angular/router'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; @@ -10,7 +9,6 @@ import { PositionDetailDialog } from '@ghostfolio/client/components/position/pos import { DataService } from '@ghostfolio/client/services/data.service'; import { IcsService } from '@ghostfolio/client/services/ics/ics.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; -import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { downloadAsFile } from '@ghostfolio/common/helper'; import { User } from '@ghostfolio/common/interfaces'; 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 032cf3b31..fca1814d0 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,7 +1,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { User } from '@ghostfolio/common/interfaces'; +import { PortfolioReportRule, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import Big from 'big.js'; import { DeviceDetectorService } from 'ngx-device-detector'; @@ -15,8 +15,12 @@ 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 feeRules: PortfolioReportRule[]; public fireWealth: Big; + public hasPermissionToCreateOrder: boolean; public hasPermissionToUpdateUserSettings: boolean; public isLoading = false; public user: User; @@ -53,12 +57,30 @@ export class FirePageComponent implements OnDestroy, OnInit { this.changeDetectorRef.markForCheck(); }); + this.dataService + .fetchPortfolioReport() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((portfolioReport) => { + this.accountClusterRiskRules = + portfolioReport.rules['accountClusterRisk'] || null; + this.currencyClusterRiskRules = + portfolioReport.rules['currencyClusterRisk'] || null; + this.feeRules = portfolioReport.rules['fees'] || null; + + this.changeDetectorRef.markForCheck(); + }); + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { if (state?.user) { this.user = state.user; + this.hasPermissionToCreateOrder = hasPermission( + this.user.permissions, + permissions.createOrder + ); + this.hasPermissionToUpdateUserSettings = hasPermission( this.user.permissions, permissions.updateUserSettings 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 f57ead453..006dcb800 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -79,3 +79,61 @@ + +
+
+
+

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

+
+

+ Currency Cluster Risks +

+ +
+
+

+ Account Cluster Risks +

+ +
+
+

+ Fees +

+ +
+
+
+
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 18fd4a99a..7518a69d5 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,5 +1,6 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { GfRulesModule } from '@ghostfolio/client/components/rules/rules.module'; import { GfFireCalculatorModule } from '@ghostfolio/ui/fire-calculator'; import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; import { GfValueModule } from '@ghostfolio/ui/value'; @@ -15,6 +16,7 @@ import { FirePageComponent } from './fire-page.component'; FirePageRoutingModule, GfFireCalculatorModule, GfPremiumIndicatorModule, + GfRulesModule, GfValueModule, 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 c0c5c55fb..4fc013ee5 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 @@ -7,9 +7,45 @@ import { PortfolioPageComponent } from './portfolio-page.component'; const routes: Routes = [ { canActivate: [AuthGuard], + children: [ + { path: '', redirectTo: 'analysis', pathMatch: 'full' }, + { + path: 'analysis', + loadChildren: () => + import('./analysis/analysis-page.module').then( + (m) => m.AnalysisPageModule + ) + }, + { + path: 'holdings', + loadChildren: () => + import('./holdings/holdings-page.module').then( + (m) => m.HoldingsPageModule + ) + }, + { + path: 'activities', + loadChildren: () => + import('./activities/activities-page.module').then( + (m) => m.ActivitiesPageModule + ) + }, + { + path: 'allocations', + loadChildren: () => + import('./allocations/allocations-page.module').then( + (m) => m.AllocationsPageModule + ) + }, + { + path: 'fire', + loadChildren: () => + import('./fire/fire-page.module').then((m) => m.FirePageModule) + } + ], component: PortfolioPageComponent, path: '', - title: 'Portfolio' + title: $localize`Portfolio` } ]; 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 9c87f7bd1..b2c3933b0 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page.component.ts +++ b/apps/client/src/app/pages/portfolio/portfolio-page.component.ts @@ -1,19 +1,30 @@ -import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + HostBinding, + OnDestroy, + OnInit +} from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { User } from '@ghostfolio/common/interfaces'; +import { InfoItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - host: { class: 'page' }, selector: 'gf-portfolio-page', styleUrls: ['./portfolio-page.scss'], templateUrl: './portfolio-page.html' }) export class PortfolioPageComponent implements OnDestroy, OnInit { - public hasPermissionForSubscription: boolean; + @HostBinding('class.with-info-message') get getHasMessage() { + return this.hasMessage; + } + + public hasMessage: boolean; + public info: InfoItem; + public tabs: { iconName: string; path: string }[] = []; public user: User; private unsubscribeSubject = new Subject(); @@ -23,26 +34,34 @@ export class PortfolioPageComponent implements OnDestroy, OnInit { private dataService: DataService, private userService: UserService ) { - const { globalPermissions } = this.dataService.fetchInfo(); - - this.hasPermissionForSubscription = hasPermission( - globalPermissions, - permissions.enableSubscription - ); - } + this.info = this.dataService.fetchInfo(); - public ngOnInit() { this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { if (state?.user) { + this.tabs = [ + { iconName: 'analytics-outline', path: 'analysis' }, + { iconName: 'wallet-outline', path: 'holdings' }, + { iconName: 'swap-horizontal-outline', path: 'activities' }, + { iconName: 'pie-chart-outline', path: 'allocations' }, + { iconName: 'calculator-outline', path: 'fire' } + ]; this.user = state.user; + this.hasMessage = + hasPermission( + this.user?.permissions, + permissions.createUserAccount + ) || !!this.info.systemMessage; + this.changeDetectorRef.markForCheck(); } }); } + public ngOnInit() {} + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.html b/apps/client/src/app/pages/portfolio/portfolio-page.html index 14d4de50e..ec3c3bfed 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page.html +++ b/apps/client/src/app/pages/portfolio/portfolio-page.html @@ -1,110 +1,15 @@ -
-

Portfolio

-
-
- -

Holdings

-
- Get an overview of your current holdings. -
- -
-
-
- -

Activities

-
- Manage your activities: stocks, ETFs, cryptocurrencies, dividend, and - valuables. -
- -
-
-
- -

Allocations

-
- Check the allocations of your portfolio by account, asset class, - currency, sector and region. -
- -
-
-
- -

Analysis

-
- Ghostfolio Analysis visualizes your portfolio and shows your top and - bottom performers. -
- -
-
-
- -

X-ray

-
- Ghostfolio X-ray uses static analysis to identify potential issues and - risks in your portfolio. -
- -
-
-
- -

FIRE

-
- Ghostfolio FIRE calculates metrics for the - Financial Independence, Retire Early lifestyle. -
- -
-
-
-
+ + + diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.module.ts b/apps/client/src/app/pages/portfolio/portfolio-page.module.ts index 7c3977f0f..0291179eb 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page.module.ts +++ b/apps/client/src/app/pages/portfolio/portfolio-page.module.ts @@ -1,7 +1,6 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; +import { MatTabsModule } from '@angular/material/tabs'; import { RouterModule } from '@angular/router'; import { PortfolioPageRoutingModule } from './portfolio-page-routing.module'; @@ -11,8 +10,7 @@ import { PortfolioPageComponent } from './portfolio-page.component'; declarations: [PortfolioPageComponent], imports: [ CommonModule, - MatButtonModule, - MatCardModule, + MatTabsModule, PortfolioPageRoutingModule, RouterModule ], diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.scss b/apps/client/src/app/pages/portfolio/portfolio-page.scss index 761afaa8c..b5471d5ce 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page.scss +++ b/apps/client/src/app/pages/portfolio/portfolio-page.scss @@ -1,10 +1,46 @@ +@import '~apps/client/src/styles/ghostfolio-style'; + :host { color: rgb(var(--dark-primary-text)); - display: block; + display: flex; + flex-direction: column; + height: calc(100vh - 5rem); + overflow-y: auto; + + padding-bottom: env(safe-area-inset-bottom); + padding-bottom: constant(safe-area-inset-bottom); + + ::ng-deep { + gf-activities-page, + gf-allocations-page, + gf-analysis-page, + gf-holdings-page, + gf-fire-page { + flex: 1 1 auto; + overflow-y: auto; + } + + .mat-tab-header { + border-bottom: 0; + + .mat-ink-bar { + visibility: hidden !important; + } + + .mat-tab-label-active { + color: rgba(var(--palette-primary-500), 1); + opacity: 1; + } + + .mat-tab-link { + &:hover { + opacity: 0.75; + } - .mat-card { - .mat-button-disabled { - pointer-events: none; + @media (max-width: 599px) { + min-width: unset; + } + } } } } diff --git a/apps/client/src/app/pages/portfolio/report/report-page-routing.module.ts b/apps/client/src/app/pages/portfolio/report/report-page-routing.module.ts deleted file mode 100644 index 9189debb0..000000000 --- a/apps/client/src/app/pages/portfolio/report/report-page-routing.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; - -import { ReportPageComponent } from './report-page.component'; - -const routes: Routes = [ - { - canActivate: [AuthGuard], - component: ReportPageComponent, - path: '', - title: 'X-ray' - } -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) -export class ReportPageRoutingModule {} diff --git a/apps/client/src/app/pages/portfolio/report/report-page.component.ts b/apps/client/src/app/pages/portfolio/report/report-page.component.ts deleted file mode 100644 index 4007b2408..000000000 --- a/apps/client/src/app/pages/portfolio/report/report-page.component.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; -import { DataService } from '@ghostfolio/client/services/data.service'; -import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { PortfolioReportRule, User } from '@ghostfolio/common/interfaces'; -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; - -@Component({ - host: { class: 'page' }, - selector: 'gf-report-page', - styleUrls: ['./report-page.scss'], - templateUrl: './report-page.html' -}) -export class ReportPageComponent implements OnDestroy, OnInit { - public accountClusterRiskRules: PortfolioReportRule[]; - public currencyClusterRiskRules: PortfolioReportRule[]; - public feeRules: PortfolioReportRule[]; - public hasPermissionToCreateOrder: boolean; - public user: User; - - private unsubscribeSubject = new Subject(); - - public constructor( - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private userService: UserService - ) {} - - public ngOnInit() { - this.dataService - .fetchPortfolioReport() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((portfolioReport) => { - this.accountClusterRiskRules = - portfolioReport.rules['accountClusterRisk'] || null; - this.currencyClusterRiskRules = - portfolioReport.rules['currencyClusterRisk'] || null; - this.feeRules = portfolioReport.rules['fees'] || null; - - this.changeDetectorRef.markForCheck(); - }); - - this.userService.stateChanged - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((state) => { - if (state?.user) { - this.user = state.user; - - this.hasPermissionToCreateOrder = hasPermission( - this.user.permissions, - permissions.createOrder - ); - - this.changeDetectorRef.markForCheck(); - } - }); - } - - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } -} diff --git a/apps/client/src/app/pages/portfolio/report/report-page.html b/apps/client/src/app/pages/portfolio/report/report-page.html deleted file mode 100644 index e2e1d71a3..000000000 --- a/apps/client/src/app/pages/portfolio/report/report-page.html +++ /dev/null @@ -1,57 +0,0 @@ -
-
-
-

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

-
-

- Currency Cluster Risks -

- -
-
-

- Account Cluster Risks -

- -
-
-

- Fees -

- -
-
-
-
diff --git a/apps/client/src/app/pages/portfolio/report/report-page.module.ts b/apps/client/src/app/pages/portfolio/report/report-page.module.ts deleted file mode 100644 index 056b0d642..000000000 --- a/apps/client/src/app/pages/portfolio/report/report-page.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; -import { RulesModule } from '@ghostfolio/client/components/rules/rules.module'; -import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; - -import { ReportPageRoutingModule } from './report-page-routing.module'; -import { ReportPageComponent } from './report-page.component'; - -@NgModule({ - declarations: [ReportPageComponent], - imports: [ - CommonModule, - GfPremiumIndicatorModule, - ReportPageRoutingModule, - RulesModule - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] -}) -export class ReportPageModule {} diff --git a/apps/client/src/app/pages/portfolio/report/report-page.scss b/apps/client/src/app/pages/portfolio/report/report-page.scss deleted file mode 100644 index 39eb6792e..000000000 --- a/apps/client/src/app/pages/portfolio/report/report-page.scss +++ /dev/null @@ -1,8 +0,0 @@ -:host { - color: rgb(var(--dark-primary-text)); - display: block; -} - -:host-context(.is-dark-theme) { - color: rgb(var(--light-primary-text)); -}