Feature/restructure portfolio page (#1429)
* Restructure portfolio page * Update changelog
This commit is contained in:
parent
3b4da72ea3
commit
43426c9b01
@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added tabs to the portfolio page
|
||||||
|
|
||||||
### Changed
|
### 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`)
|
- Tightened the validation rule of the base currency environment variable (`BASE_CURRENCY`)
|
||||||
|
|
||||||
## 1.209.0 - 05.11.2022
|
## 1.209.0 - 05.11.2022
|
||||||
|
@ -429,16 +429,19 @@ export class PortfolioController {
|
|||||||
public async getReport(
|
public async getReport(
|
||||||
@Headers('impersonation-id') impersonationId: string
|
@Headers('impersonation-id') impersonationId: string
|
||||||
): Promise<PortfolioReport> {
|
): Promise<PortfolioReport> {
|
||||||
|
const report = await this.portfolioService.getReport(impersonationId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||||
this.request.user.subscription.type === 'Basic'
|
this.request.user.subscription.type === 'Basic'
|
||||||
) {
|
) {
|
||||||
throw new HttpException(
|
for (const rule in report.rules) {
|
||||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
if (report.rules[rule]) {
|
||||||
StatusCodes.FORBIDDEN
|
report.rules[rule] = [];
|
||||||
);
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.portfolioService.getReport(impersonationId);
|
return report;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,48 +145,6 @@ const routes: Routes = [
|
|||||||
(m) => m.PortfolioPageModule
|
(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',
|
path: 'pricing',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
></gf-no-transactions-info-indicator>
|
></gf-no-transactions-info-indicator>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<gf-rule *ngIf="rules === undefined" [isLoading]="true"></gf-rule>
|
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true"></gf-rule>
|
||||||
<ng-container *ngIf="rules !== null && rules !== undefined">
|
<ng-container *ngIf="rules !== null && rules !== undefined">
|
||||||
<gf-rule *ngFor="let rule of rules" [rule]="rule"></gf-rule>
|
<gf-rule *ngFor="let rule of rules" [rule]="rule"></gf-rule>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -9,7 +9,7 @@ import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
|||||||
})
|
})
|
||||||
export class RulesComponent {
|
export class RulesComponent {
|
||||||
@Input() hasPermissionToCreateOrder: boolean;
|
@Input() hasPermissionToCreateOrder: boolean;
|
||||||
@Input() rules: PortfolioReportRule;
|
@Input() rules: PortfolioReportRule[];
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
}
|
}
|
||||||
|
@ -21,4 +21,4 @@ import { RulesComponent } from './rules.component';
|
|||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class RulesModule {}
|
export class GfRulesModule {}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
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 { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.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 { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
@ -15,8 +15,12 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './fire-page.html'
|
templateUrl: './fire-page.html'
|
||||||
})
|
})
|
||||||
export class FirePageComponent implements OnDestroy, OnInit {
|
export class FirePageComponent implements OnDestroy, OnInit {
|
||||||
|
public accountClusterRiskRules: PortfolioReportRule[];
|
||||||
|
public currencyClusterRiskRules: PortfolioReportRule[];
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public feeRules: PortfolioReportRule[];
|
||||||
public fireWealth: Big;
|
public fireWealth: Big;
|
||||||
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public user: User;
|
public user: User;
|
||||||
@ -53,12 +57,30 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
this.changeDetectorRef.markForCheck();
|
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
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.hasPermissionToCreateOrder = hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.createOrder
|
||||||
|
);
|
||||||
|
|
||||||
this.hasPermissionToUpdateUserSettings = hasPermission(
|
this.hasPermissionToUpdateUserSettings = hasPermission(
|
||||||
this.user.permissions,
|
this.user.permissions,
|
||||||
permissions.updateUserSettings
|
permissions.updateUserSettings
|
||||||
|
@ -79,3 +79,61 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3 class="align-items-center d-flex justify-content-center mb-3">
|
||||||
|
X-ray
|
||||||
|
</h3>
|
||||||
|
<p class="mb-4">
|
||||||
|
Ghostfolio X-ray uses static analysis to identify potential issues and
|
||||||
|
risks in your portfolio.
|
||||||
|
<span class="d-none"
|
||||||
|
>It will be highly configurable in the future: activate / deactivate
|
||||||
|
rules and customize the thresholds to match your personal investment
|
||||||
|
style.</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="align-items-center d-flex m-0">
|
||||||
|
<span>Currency Cluster Risks</span
|
||||||
|
><gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
></gf-premium-indicator>
|
||||||
|
</h4>
|
||||||
|
<gf-rules
|
||||||
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
|
[rules]="currencyClusterRiskRules"
|
||||||
|
></gf-rules>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="align-items-center d-flex m-0">
|
||||||
|
<span>Account Cluster Risks</span
|
||||||
|
><gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
></gf-premium-indicator>
|
||||||
|
</h4>
|
||||||
|
<gf-rules
|
||||||
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
|
[rules]="accountClusterRiskRules"
|
||||||
|
></gf-rules>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="align-items-center d-flex m-0">
|
||||||
|
<span>Fees</span
|
||||||
|
><gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
></gf-premium-indicator>
|
||||||
|
</h4>
|
||||||
|
<gf-rules
|
||||||
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
|
[rules]="feeRules"
|
||||||
|
></gf-rules>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
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 { GfFireCalculatorModule } from '@ghostfolio/ui/fire-calculator';
|
||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
@ -15,6 +16,7 @@ import { FirePageComponent } from './fire-page.component';
|
|||||||
FirePageRoutingModule,
|
FirePageRoutingModule,
|
||||||
GfFireCalculatorModule,
|
GfFireCalculatorModule,
|
||||||
GfPremiumIndicatorModule,
|
GfPremiumIndicatorModule,
|
||||||
|
GfRulesModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
NgxSkeletonLoaderModule
|
NgxSkeletonLoaderModule
|
||||||
],
|
],
|
||||||
|
@ -7,9 +7,45 @@ import { PortfolioPageComponent } from './portfolio-page.component';
|
|||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
canActivate: [AuthGuard],
|
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,
|
component: PortfolioPageComponent,
|
||||||
path: '',
|
path: '',
|
||||||
title: 'Portfolio'
|
title: $localize`Portfolio`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -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 { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
|
||||||
selector: 'gf-portfolio-page',
|
selector: 'gf-portfolio-page',
|
||||||
styleUrls: ['./portfolio-page.scss'],
|
styleUrls: ['./portfolio-page.scss'],
|
||||||
templateUrl: './portfolio-page.html'
|
templateUrl: './portfolio-page.html'
|
||||||
})
|
})
|
||||||
export class PortfolioPageComponent implements OnDestroy, OnInit {
|
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;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -23,26 +34,34 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
|
|||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
const { globalPermissions } = this.dataService.fetchInfo();
|
this.info = this.dataService.fetchInfo();
|
||||||
|
|
||||||
this.hasPermissionForSubscription = hasPermission(
|
|
||||||
globalPermissions,
|
|
||||||
permissions.enableSubscription
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnInit() {
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
if (state?.user) {
|
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.user = state.user;
|
||||||
|
|
||||||
|
this.hasMessage =
|
||||||
|
hasPermission(
|
||||||
|
this.user?.permissions,
|
||||||
|
permissions.createUserAccount
|
||||||
|
) || !!this.info.systemMessage;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnInit() {}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -1,110 +1,15 @@
|
|||||||
<div class="container">
|
<router-outlet></router-outlet>
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Portfolio</h3>
|
|
||||||
<div class="row">
|
<nav mat-align-tabs="center" mat-tab-nav-bar>
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
<a
|
||||||
<mat-card class="d-flex flex-column h-100">
|
#rla="routerLinkActive"
|
||||||
<h4 i18n>Holdings</h4>
|
*ngFor="let tab of tabs"
|
||||||
<div class="flex-grow-1" i18n>
|
class="px-3"
|
||||||
Get an overview of your current holdings.
|
mat-tab-link
|
||||||
</div>
|
routerLinkActive
|
||||||
<div class="mt-2 text-right">
|
[active]="rla.isActive"
|
||||||
<a
|
[routerLink]="tab.path"
|
||||||
color="primary"
|
>
|
||||||
mat-button
|
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||||
[routerLink]="['/portfolio', 'holdings']"
|
</a>
|
||||||
>
|
</nav>
|
||||||
<span i18n>Open Holdings</span>
|
|
||||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
|
||||||
<mat-card class="d-flex flex-column h-100">
|
|
||||||
<h4 i18n>Activities</h4>
|
|
||||||
<div class="flex-grow-1" i18n>
|
|
||||||
Manage your activities: stocks, ETFs, cryptocurrencies, dividend, and
|
|
||||||
valuables.
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-right">
|
|
||||||
<a
|
|
||||||
color="primary"
|
|
||||||
mat-button
|
|
||||||
[routerLink]="['/portfolio', 'activities']"
|
|
||||||
>
|
|
||||||
<span i18n>Open Activities</span>
|
|
||||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
|
||||||
<mat-card class="d-flex flex-column h-100">
|
|
||||||
<h4 i18n>Allocations</h4>
|
|
||||||
<div class="flex-grow-1" i18n>
|
|
||||||
Check the allocations of your portfolio by account, asset class,
|
|
||||||
currency, sector and region.
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-right">
|
|
||||||
<a
|
|
||||||
color="primary"
|
|
||||||
mat-button
|
|
||||||
[routerLink]="['/portfolio', 'allocations']"
|
|
||||||
>
|
|
||||||
<span i18n>Open Allocations</span>
|
|
||||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
|
||||||
<mat-card class="d-flex flex-column h-100">
|
|
||||||
<h4 i18n>Analysis</h4>
|
|
||||||
<div class="flex-grow-1" i18n>
|
|
||||||
Ghostfolio Analysis visualizes your portfolio and shows your top and
|
|
||||||
bottom performers.
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-right">
|
|
||||||
<a
|
|
||||||
color="primary"
|
|
||||||
mat-button
|
|
||||||
[routerLink]="['/portfolio', 'analysis']"
|
|
||||||
>
|
|
||||||
<span i18n>Open Analysis</span>
|
|
||||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
|
||||||
<mat-card class="d-flex flex-column h-100">
|
|
||||||
<h4>X-ray</h4>
|
|
||||||
<div class="flex-grow-1" i18n>
|
|
||||||
Ghostfolio X-ray uses static analysis to identify potential issues and
|
|
||||||
risks in your portfolio.
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-right">
|
|
||||||
<a color="primary" mat-button [routerLink]="['/portfolio', 'report']">
|
|
||||||
<span i18n>Open X-ray</span>
|
|
||||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
|
||||||
<mat-card class="d-flex flex-column h-100">
|
|
||||||
<h4>FIRE</h4>
|
|
||||||
<div class="flex-grow-1" i18n>
|
|
||||||
Ghostfolio FIRE calculates metrics for the
|
|
||||||
<i>Financial Independence, Retire Early</i> lifestyle.
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-right">
|
|
||||||
<a color="primary" mat-button [routerLink]="['/portfolio', 'fire']">
|
|
||||||
<span i18n>Open FIRE</span>
|
|
||||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { PortfolioPageRoutingModule } from './portfolio-page-routing.module';
|
import { PortfolioPageRoutingModule } from './portfolio-page-routing.module';
|
||||||
@ -11,8 +10,7 @@ import { PortfolioPageComponent } from './portfolio-page.component';
|
|||||||
declarations: [PortfolioPageComponent],
|
declarations: [PortfolioPageComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
MatButtonModule,
|
MatTabsModule,
|
||||||
MatCardModule,
|
|
||||||
PortfolioPageRoutingModule,
|
PortfolioPageRoutingModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
|
@ -1,10 +1,46 @@
|
|||||||
|
@import '~apps/client/src/styles/ghostfolio-style';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
color: rgb(var(--dark-primary-text));
|
color: rgb(var(--dark-primary-text));
|
||||||
display: block;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 5rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
.mat-card {
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
.mat-button-disabled {
|
padding-bottom: constant(safe-area-inset-bottom);
|
||||||
pointer-events: none;
|
|
||||||
|
::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 599px) {
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {}
|
|
@ -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<void>();
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<h3 class="align-items-center d-flex justify-content-center mb-3">
|
|
||||||
X-ray
|
|
||||||
</h3>
|
|
||||||
<p class="mb-4">
|
|
||||||
Ghostfolio X-ray uses static analysis to identify potential issues and
|
|
||||||
risks in your portfolio.
|
|
||||||
<span class="d-none"
|
|
||||||
>It will be highly configurable in the future: activate / deactivate
|
|
||||||
rules and customize the thresholds to match your personal investment
|
|
||||||
style.</span
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="align-items-center d-flex m-0">
|
|
||||||
<span>Currency Cluster Risks</span
|
|
||||||
><gf-premium-indicator
|
|
||||||
*ngIf="user?.subscription?.type === 'Basic'"
|
|
||||||
class="ml-1"
|
|
||||||
></gf-premium-indicator>
|
|
||||||
</h4>
|
|
||||||
<gf-rules
|
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
|
||||||
[rules]="currencyClusterRiskRules"
|
|
||||||
></gf-rules>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="align-items-center d-flex m-0">
|
|
||||||
<span>Account Cluster Risks</span
|
|
||||||
><gf-premium-indicator
|
|
||||||
*ngIf="user?.subscription?.type === 'Basic'"
|
|
||||||
class="ml-1"
|
|
||||||
></gf-premium-indicator>
|
|
||||||
</h4>
|
|
||||||
<gf-rules
|
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
|
||||||
[rules]="accountClusterRiskRules"
|
|
||||||
></gf-rules>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="align-items-center d-flex m-0">
|
|
||||||
<span>Fees</span
|
|
||||||
><gf-premium-indicator
|
|
||||||
*ngIf="user?.subscription?.type === 'Basic'"
|
|
||||||
class="ml-1"
|
|
||||||
></gf-premium-indicator>
|
|
||||||
</h4>
|
|
||||||
<gf-rules
|
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
|
||||||
[rules]="feeRules"
|
|
||||||
></gf-rules>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -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 {}
|
|
@ -1,8 +0,0 @@
|
|||||||
:host {
|
|
||||||
color: rgb(var(--dark-primary-text));
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
|
||||||
color: rgb(var(--light-primary-text));
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user