diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page-routing.module.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page-routing.module.ts
new file mode 100644
index 000000000..8b2182fe4
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page-routing.module.ts
@@ -0,0 +1,15 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
+
+import { AllocationsPageComponent } from './allocations-page.component';
+
+const routes: Routes = [
+ { path: '', component: AllocationsPageComponent, canActivate: [AuthGuard] }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class AllocationsPageRoutingModule {}
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
new file mode 100644
index 000000000..6aaf81148
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
@@ -0,0 +1,221 @@
+import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
+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 { UNKNOWN_KEY } from '@ghostfolio/common/config';
+import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
+import { DeviceDetectorService } from 'ngx-device-detector';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+
+@Component({
+ selector: 'gf-allocations-page',
+ templateUrl: './allocations-page.html',
+ styleUrls: ['./allocations-page.scss']
+})
+export class AllocationsPageComponent implements OnDestroy, OnInit {
+ public accounts: {
+ [symbol: string]: Pick
& { value: number };
+ };
+ public continents: {
+ [code: string]: { name: string; value: number };
+ };
+ public countries: {
+ [code: string]: { name: string; value: number };
+ };
+ public deviceType: string;
+ public hasImpersonationId: boolean;
+ public period = 'current';
+ public periodOptions: ToggleOption[] = [
+ { label: 'Initial', value: 'original' },
+ { label: 'Current', value: 'current' }
+ ];
+ public portfolioPositions: { [symbol: string]: PortfolioPosition };
+ public positions: { [symbol: string]: any };
+ public positionsArray: PortfolioPosition[];
+ public sectors: {
+ [name: string]: { name: string; value: number };
+ };
+ public user: User;
+
+ private unsubscribeSubject = new Subject();
+
+ /**
+ * @constructor
+ */
+ public constructor(
+ private changeDetectorRef: ChangeDetectorRef,
+ private dataService: DataService,
+ private deviceService: DeviceDetectorService,
+ private impersonationStorageService: ImpersonationStorageService,
+ private userService: UserService
+ ) {}
+
+ /**
+ * Initializes the controller
+ */
+ public ngOnInit() {
+ this.deviceType = this.deviceService.getDeviceInfo().deviceType;
+
+ this.impersonationStorageService
+ .onChangeHasImpersonation()
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((aId) => {
+ this.hasImpersonationId = !!aId;
+ });
+
+ this.dataService
+ .fetchPortfolioPositions({})
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((response = {}) => {
+ this.portfolioPositions = response;
+ this.initializeAnalysisData(this.portfolioPositions, this.period);
+
+ this.changeDetectorRef.markForCheck();
+ });
+
+ this.userService.stateChanged
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((state) => {
+ if (state?.user) {
+ this.user = state.user;
+
+ this.changeDetectorRef.markForCheck();
+ }
+ });
+ }
+
+ public initializeAnalysisData(
+ aPortfolioPositions: {
+ [symbol: string]: PortfolioPosition;
+ },
+ aPeriod: string
+ ) {
+ this.accounts = {};
+ this.continents = {
+ [UNKNOWN_KEY]: {
+ name: UNKNOWN_KEY,
+ value: 0
+ }
+ };
+ this.countries = {
+ [UNKNOWN_KEY]: {
+ name: UNKNOWN_KEY,
+ value: 0
+ }
+ };
+ this.positions = {};
+ this.positionsArray = [];
+ this.sectors = {
+ [UNKNOWN_KEY]: {
+ name: UNKNOWN_KEY,
+ value: 0
+ }
+ };
+
+ for (const [symbol, position] of Object.entries(aPortfolioPositions)) {
+ this.positions[symbol] = {
+ currency: position.currency,
+ exchange: position.exchange,
+ type: position.type,
+ value:
+ aPeriod === 'original'
+ ? position.allocationInvestment
+ : position.allocationCurrent
+ };
+ this.positionsArray.push(position);
+
+ for (const [account, { current, original }] of Object.entries(
+ position.accounts
+ )) {
+ if (this.accounts[account]?.value) {
+ this.accounts[account].value +=
+ aPeriod === 'original' ? original : current;
+ } else {
+ this.accounts[account] = {
+ name: account,
+ value: aPeriod === 'original' ? original : current
+ };
+ }
+ }
+
+ if (position.countries.length > 0) {
+ for (const country of position.countries) {
+ const { code, continent, name, weight } = country;
+
+ if (this.continents[continent]?.value) {
+ this.continents[continent].value += weight * position.value;
+ } else {
+ this.continents[continent] = {
+ name: continent,
+ value:
+ weight *
+ (aPeriod === 'original'
+ ? this.portfolioPositions[symbol].investment
+ : this.portfolioPositions[symbol].value)
+ };
+ }
+
+ if (this.countries[code]?.value) {
+ this.countries[code].value += weight * position.value;
+ } else {
+ this.countries[code] = {
+ name,
+ value:
+ weight *
+ (aPeriod === 'original'
+ ? this.portfolioPositions[symbol].investment
+ : this.portfolioPositions[symbol].value)
+ };
+ }
+ }
+ } else {
+ this.continents[UNKNOWN_KEY].value +=
+ aPeriod === 'original'
+ ? this.portfolioPositions[symbol].investment
+ : this.portfolioPositions[symbol].value;
+
+ this.countries[UNKNOWN_KEY].value +=
+ aPeriod === 'original'
+ ? this.portfolioPositions[symbol].investment
+ : this.portfolioPositions[symbol].value;
+ }
+
+ if (position.sectors.length > 0) {
+ for (const sector of position.sectors) {
+ const { name, weight } = sector;
+
+ if (this.sectors[name]?.value) {
+ this.sectors[name].value += weight * position.value;
+ } else {
+ this.sectors[name] = {
+ name,
+ value:
+ weight *
+ (aPeriod === 'original'
+ ? this.portfolioPositions[symbol].investment
+ : this.portfolioPositions[symbol].value)
+ };
+ }
+ }
+ } else {
+ this.sectors[UNKNOWN_KEY].value +=
+ aPeriod === 'original'
+ ? this.portfolioPositions[symbol].investment
+ : this.portfolioPositions[symbol].value;
+ }
+ }
+ }
+
+ public onChangePeriod(aValue: string) {
+ this.period = aValue;
+
+ this.initializeAnalysisData(this.portfolioPositions, this.period);
+ }
+
+ public ngOnDestroy() {
+ this.unsubscribeSubject.next();
+ this.unsubscribeSubject.complete();
+ }
+}
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html
new file mode 100644
index 000000000..05f34d318
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+ By Type
+
+
+
+
+
+
+
+
+
+
+ By Account
+
+
+
+
+
+
+
+
+
+
+ By Currency
+
+
+
+
+
+
+
+
+
+
+ By Exchange
+
+
+
+
+
+
+
+
+
+
+ By Sector
+
+
+
+
+
+
+
+
+
+
+ By Continent
+
+
+
+
+
+
+
+
+
+
+ By Country
+
+
+
+
+
+
+
+
+
+
+
+
+ Regions
+
+
+
+
+
+
+
+
+
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts
new file mode 100644
index 000000000..d5d9fe0b5
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts
@@ -0,0 +1,27 @@
+import { CommonModule } from '@angular/common';
+import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
+import { MatCardModule } from '@angular/material/card';
+import { PortfolioProportionChartModule } from '@ghostfolio/client/components/portfolio-proportion-chart/portfolio-proportion-chart.module';
+import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
+import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
+import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
+
+import { AllocationsPageRoutingModule } from './allocations-page-routing.module';
+import { AllocationsPageComponent } from './allocations-page.component';
+
+@NgModule({
+ declarations: [AllocationsPageComponent],
+ exports: [],
+ imports: [
+ AllocationsPageRoutingModule,
+ CommonModule,
+ GfPositionsTableModule,
+ GfToggleModule,
+ GfWorldMapChartModule,
+ MatCardModule,
+ PortfolioProportionChartModule
+ ],
+ providers: [],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+})
+export class AllocationsPageModule {}
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.scss b/apps/client/src/app/pages/portfolio/allocations/allocations-page.scss
new file mode 100644
index 000000000..b41677eb6
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.scss
@@ -0,0 +1,40 @@
+:host {
+ .proportion-charts {
+ .mat-card {
+ .mat-card-content {
+ padding: 1rem 2rem;
+ }
+ }
+ }
+
+ .world-map-chart {
+ .mat-card {
+ .mat-card-content {
+ aspect-ratio: 16 / 9;
+ }
+ }
+ }
+
+ .mat-card {
+ .mat-card-header {
+ ::ng-deep {
+ .mat-card-header-text {
+ flex: 1 1 auto;
+ }
+ }
+
+ gf-toggle {
+ font-size: 90%;
+ }
+ }
+
+ a {
+ color: rgba(var(--palette-primary-500), 1);
+ font-weight: 500;
+
+ &:hover {
+ color: rgba(var(--palette-primary-300), 1);
+ }
+ }
+ }
+}
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
index 433bf63b1..187683930 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
@@ -3,12 +3,7 @@ import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/to
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 { UNKNOWN_KEY } from '@ghostfolio/common/config';
-import {
- PortfolioItem,
- PortfolioPosition,
- User
-} from '@ghostfolio/common/interfaces';
+import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
@@ -39,7 +34,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public investments: InvestmentItem[];
public portfolioPositions: { [symbol: string]: PortfolioPosition };
public positions: { [symbol: string]: any };
- public positionsArray: PortfolioPosition[];
public sectors: {
[name: string]: { name: string; value: number };
};
@@ -80,16 +74,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
});
- this.dataService
- .fetchPortfolioPositions({})
- .pipe(takeUntil(this.unsubscribeSubject))
- .subscribe((response = {}) => {
- this.portfolioPositions = response;
- this.initializeAnalysisData(this.portfolioPositions, this.period);
-
- this.changeDetectorRef.markForCheck();
- });
-
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
@@ -101,134 +85,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
});
}
- public initializeAnalysisData(
- aPortfolioPositions: {
- [symbol: string]: PortfolioPosition;
- },
- aPeriod: string
- ) {
- this.accounts = {};
- this.continents = {
- [UNKNOWN_KEY]: {
- name: UNKNOWN_KEY,
- value: 0
- }
- };
- this.countries = {
- [UNKNOWN_KEY]: {
- name: UNKNOWN_KEY,
- value: 0
- }
- };
- this.positions = {};
- this.positionsArray = [];
- this.sectors = {
- [UNKNOWN_KEY]: {
- name: UNKNOWN_KEY,
- value: 0
- }
- };
-
- for (const [symbol, position] of Object.entries(aPortfolioPositions)) {
- this.positions[symbol] = {
- currency: position.currency,
- exchange: position.exchange,
- type: position.type,
- value:
- aPeriod === 'original'
- ? position.allocationInvestment
- : position.allocationCurrent
- };
- this.positionsArray.push(position);
-
- for (const [account, { current, original }] of Object.entries(
- position.accounts
- )) {
- if (this.accounts[account]?.value) {
- this.accounts[account].value +=
- aPeriod === 'original' ? original : current;
- } else {
- this.accounts[account] = {
- name: account,
- value: aPeriod === 'original' ? original : current
- };
- }
- }
-
- if (position.countries.length > 0) {
- for (const country of position.countries) {
- const { code, continent, name, weight } = country;
-
- if (this.continents[continent]?.value) {
- this.continents[continent].value += weight * position.value;
- } else {
- this.continents[continent] = {
- name: continent,
- value:
- weight *
- (aPeriod === 'original'
- ? this.portfolioPositions[symbol].investment
- : this.portfolioPositions[symbol].value)
- };
- }
-
- if (this.countries[code]?.value) {
- this.countries[code].value += weight * position.value;
- } else {
- this.countries[code] = {
- name,
- value:
- weight *
- (aPeriod === 'original'
- ? this.portfolioPositions[symbol].investment
- : this.portfolioPositions[symbol].value)
- };
- }
- }
- } else {
- this.continents[UNKNOWN_KEY].value +=
- aPeriod === 'original'
- ? this.portfolioPositions[symbol].investment
- : this.portfolioPositions[symbol].value;
-
- this.countries[UNKNOWN_KEY].value +=
- aPeriod === 'original'
- ? this.portfolioPositions[symbol].investment
- : this.portfolioPositions[symbol].value;
- }
-
- if (position.sectors.length > 0) {
- for (const sector of position.sectors) {
- const { name, weight } = sector;
-
- if (this.sectors[name]?.value) {
- this.sectors[name].value += weight * position.value;
- } else {
- this.sectors[name] = {
- name,
- value:
- weight *
- (aPeriod === 'original'
- ? this.portfolioPositions[symbol].investment
- : this.portfolioPositions[symbol].value)
- };
- }
- }
- } else {
- this.sectors[UNKNOWN_KEY].value +=
- aPeriod === 'original'
- ? this.portfolioPositions[symbol].investment
- : this.portfolioPositions[symbol].value;
- }
- }
- }
-
- public onChangePeriod(aValue: string) {
- this.period = aValue;
-
- this.initializeAnalysisData(this.portfolioPositions, this.period);
- }
-
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
index 37137839b..0ed865686 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
@@ -1,199 +1,7 @@
-
-
-
Analysis
-
-
Positions
-
-
-
-
-
-
-
-
- By Type
-
-
-
-
-
-
-
-
-
-
- By Account
-
-
-
-
-
-
-
-
-
-
- By Currency
-
-
-
-
-
-
-
-
-
-
- By Exchange
-
-
-
-
-
-
-
-
-
-
- By Sector
-
-
-
-
-
-
-
-
-
-
- By Continent
-
-
-
-
-
-
-
-
-
-
- By Country
-
-
-
-
-
-
-
-
-
-
-
-
- Regions
-
-
-
-
-
-
-
-
+
Analysis
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts
index d8974b3ca..53e9bb283 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts
@@ -2,10 +2,6 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
-import { PortfolioProportionChartModule } from '@ghostfolio/client/components/portfolio-proportion-chart/portfolio-proportion-chart.module';
-import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
-import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
-import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
import { AnalysisPageRoutingModule } from './analysis-page-routing.module';
import { AnalysisPageComponent } from './analysis-page.component';
@@ -17,11 +13,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
AnalysisPageRoutingModule,
CommonModule,
GfInvestmentChartModule,
- GfPositionsTableModule,
- GfToggleModule,
- GfWorldMapChartModule,
- MatCardModule,
- PortfolioProportionChartModule
+ MatCardModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.scss b/apps/client/src/app/pages/portfolio/analysis/analysis-page.scss
index 09e4ea30d..ab302d8e3 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.scss
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.scss
@@ -11,22 +11,6 @@
}
}
- .proportion-charts {
- .mat-card {
- .mat-card-content {
- padding: 1rem 2rem;
- }
- }
- }
-
- .world-map-chart {
- .mat-card {
- .mat-card-content {
- aspect-ratio: 16 / 9;
- }
- }
- }
-
.mat-card {
.mat-card-header {
::ng-deep {
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 84b44117a..668d3e351 100644
--- a/apps/client/src/app/pages/portfolio/portfolio-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/portfolio-page.component.ts
@@ -1,18 +1,44 @@
-import { Component, OnInit } from '@angular/core';
+import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { UserService } from '@ghostfolio/client/services/user/user.service';
+import { User } from '@ghostfolio/common/interfaces';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'gf-portfolio-page',
templateUrl: './portfolio-page.html',
styleUrls: ['./portfolio-page.scss']
})
-export class PortfolioPageComponent implements OnInit {
+export class PortfolioPageComponent implements OnDestroy, OnInit {
+ public user: User;
+
+ private unsubscribeSubject = new Subject();
+
/**
* @constructor
*/
- public constructor() {}
+ public constructor(
+ private changeDetectorRef: ChangeDetectorRef,
+ private userService: UserService
+ ) {}
/**
* Initializes the controller
*/
- public ngOnInit() {}
+ public ngOnInit() {
+ this.userService.stateChanged
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((state) => {
+ if (state?.user) {
+ this.user = state.user;
+
+ this.changeDetectorRef.markForCheck();
+ }
+ });
+ }
+
+ 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 1aca66097..1a2578be4 100644
--- a/apps/client/src/app/pages/portfolio/portfolio-page.html
+++ b/apps/client/src/app/pages/portfolio/portfolio-page.html
@@ -17,7 +17,10 @@
-
+
Allocations
Check the allocations of your portfolio.
@@ -35,7 +38,10 @@
-
+
Analysis
Ghostfolio Analysis visualizes your portfolio.
@@ -51,7 +57,10 @@
-
+
X-ray
diff --git a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts
index a35e00152..42930fb27 100644
--- a/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts
+++ b/apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts
@@ -9,6 +9,7 @@ import { FormControl, Validators } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
+import { DataService } from '@ghostfolio/client/services/data.service';
import { Currency } from '@prisma/client';
import { Observable, Subject } from 'rxjs';
import {
@@ -19,7 +20,6 @@ import {
takeUntil
} from 'rxjs/operators';
-import { DataService } from '@ghostfolio/client/services/data.service';
import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
@Component({
diff --git a/apps/client/src/app/pages/zen/zen-page.html b/apps/client/src/app/pages/zen/zen-page.html
index a3f06be4e..200825c1a 100644
--- a/apps/client/src/app/pages/zen/zen-page.html
+++ b/apps/client/src/app/pages/zen/zen-page.html
@@ -60,7 +60,7 @@