From fa66cd5bce09079783fc7143ba478cc5ff66f71f Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 12 Feb 2022 11:22:03 +0100
Subject: [PATCH] Feature/add countries and sectors to position detail dialog
 (#692)

* Add asset and asset sub class

* Add countries and sectors to position detail dialog

* Update changelog
---
 CHANGELOG.md                                  |  5 ++
 .../portfolio-position-detail.interface.ts    |  8 +-
 .../src/app/portfolio/portfolio.controller.ts |  1 +
 .../app/portfolio/portfolio.service-new.ts    | 22 ++----
 .../src/app/portfolio/portfolio.service.ts    | 22 ++----
 ...orm-data-source-in-response.interceptor.ts | 13 ++++
 .../position-detail-dialog.component.ts       | 47 ++++++++----
 .../position-detail-dialog.html               | 76 +++++++++++++++++--
 .../position-detail-dialog.module.ts          |  2 +
 libs/ui/src/lib/value/value.component.html    |  6 +-
 libs/ui/src/lib/value/value.component.ts      | 21 ++---
 11 files changed, 152 insertions(+), 71 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97df251e5..11d366e19 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## Unreleased
 
+### Added
+
+- Added the asset and asset sub class to the position detail dialog
+- Added the countries and sectors to the position detail dialog
+
 ### Changed
 
 - Upgraded `angular` from version `13.1.2` to `13.2.3`
diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts
index ddc5fbf37..99c7c6911 100644
--- a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts
+++ b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts
@@ -1,11 +1,8 @@
+import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
 import { OrderWithAccount } from '@ghostfolio/common/types';
-import { AssetClass, AssetSubClass } from '@prisma/client';
 
 export interface PortfolioPositionDetail {
-  assetClass?: AssetClass;
-  assetSubClass?: AssetSubClass;
   averagePrice: number;
-  currency: string;
   firstBuyDate: string;
   grossPerformance: number;
   grossPerformancePercent: number;
@@ -14,12 +11,11 @@ export interface PortfolioPositionDetail {
   marketPrice: number;
   maxPrice: number;
   minPrice: number;
-  name: string;
   netPerformance: number;
   netPerformancePercent: number;
   orders: OrderWithAccount[];
   quantity: number;
-  symbol: string;
+  SymbolProfile: EnhancedSymbolProfile;
   transactionCount: number;
   value: number;
 }
diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts
index db4786527..38a083c75 100644
--- a/apps/api/src/app/portfolio/portfolio.controller.ts
+++ b/apps/api/src/app/portfolio/portfolio.controller.ts
@@ -344,6 +344,7 @@ export class PortfolioController {
 
   @Get('position/:dataSource/:symbol')
   @UseInterceptors(TransformDataSourceInRequestInterceptor)
+  @UseInterceptors(TransformDataSourceInResponseInterceptor)
   @UseGuards(AuthGuard('jwt'))
   public async getPosition(
     @Headers('impersonation-id') impersonationId: string,
diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts
index 04803d14b..adeea5c91 100644
--- a/apps/api/src/app/portfolio/portfolio.service-new.ts
+++ b/apps/api/src/app/portfolio/portfolio.service-new.ts
@@ -417,7 +417,6 @@ export class PortfolioServiceNew {
     if (orders.length <= 0) {
       return {
         averagePrice: undefined,
-        currency: undefined,
         firstBuyDate: undefined,
         grossPerformance: undefined,
         grossPerformancePercent: undefined,
@@ -426,21 +425,20 @@ export class PortfolioServiceNew {
         marketPrice: undefined,
         maxPrice: undefined,
         minPrice: undefined,
-        name: undefined,
         netPerformance: undefined,
         netPerformancePercent: undefined,
         orders: [],
         quantity: undefined,
-        symbol: aSymbol,
+        SymbolProfile: undefined,
         transactionCount: undefined,
         value: undefined
       };
     }
 
-    const assetClass = orders[0].SymbolProfile?.assetClass;
-    const assetSubClass = orders[0].SymbolProfile?.assetSubClass;
     const positionCurrency = orders[0].currency;
-    const name = orders[0].SymbolProfile?.name ?? '';
+    const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
+      aSymbol
+    ]);
 
     const portfolioOrders: PortfolioOrder[] = orders
       .filter((order) => {
@@ -557,18 +555,15 @@ export class PortfolioServiceNew {
       }
 
       return {
-        assetClass,
-        assetSubClass,
-        currency,
         firstBuyDate,
         grossPerformance,
         investment,
         marketPrice,
         maxPrice,
         minPrice,
-        name,
         netPerformance,
         orders,
+        SymbolProfile,
         transactionCount,
         averagePrice: averagePrice.toNumber(),
         grossPerformancePercent:
@@ -576,7 +571,6 @@ export class PortfolioServiceNew {
         historicalData: historicalDataArray,
         netPerformancePercent: position.netPerformancePercentage?.toNumber(),
         quantity: quantity.toNumber(),
-        symbol: aSymbol,
         value: this.exchangeRateDataService.toCurrency(
           quantity.mul(marketPrice).toNumber(),
           currency,
@@ -621,15 +615,12 @@ export class PortfolioServiceNew {
       }
 
       return {
-        assetClass,
-        assetSubClass,
         marketPrice,
         maxPrice,
         minPrice,
-        name,
         orders,
+        SymbolProfile,
         averagePrice: 0,
-        currency: currentData[aSymbol]?.currency,
         firstBuyDate: undefined,
         grossPerformance: undefined,
         grossPerformancePercent: undefined,
@@ -638,7 +629,6 @@ export class PortfolioServiceNew {
         netPerformance: undefined,
         netPerformancePercent: undefined,
         quantity: 0,
-        symbol: aSymbol,
         transactionCount: undefined,
         value: 0
       };
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index 4b02e4d0a..0a164708c 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -405,7 +405,6 @@ export class PortfolioService {
     if (orders.length <= 0) {
       return {
         averagePrice: undefined,
-        currency: undefined,
         firstBuyDate: undefined,
         grossPerformance: undefined,
         grossPerformancePercent: undefined,
@@ -414,21 +413,20 @@ export class PortfolioService {
         marketPrice: undefined,
         maxPrice: undefined,
         minPrice: undefined,
-        name: undefined,
         netPerformance: undefined,
         netPerformancePercent: undefined,
         orders: [],
         quantity: undefined,
-        symbol: aSymbol,
+        SymbolProfile: undefined,
         transactionCount: undefined,
         value: undefined
       };
     }
 
-    const assetClass = orders[0].SymbolProfile?.assetClass;
-    const assetSubClass = orders[0].SymbolProfile?.assetSubClass;
     const positionCurrency = orders[0].currency;
-    const name = orders[0].SymbolProfile?.name ?? '';
+    const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
+      aSymbol
+    ]);
 
     const portfolioOrders: PortfolioOrder[] = orders
       .filter((order) => {
@@ -543,25 +541,21 @@ export class PortfolioService {
       }
 
       return {
-        assetClass,
-        assetSubClass,
-        currency,
         firstBuyDate,
         grossPerformance,
         investment,
         marketPrice,
         maxPrice,
         minPrice,
-        name,
         netPerformance,
         orders,
+        SymbolProfile,
         transactionCount,
         averagePrice: averagePrice.toNumber(),
         grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
         historicalData: historicalDataArray,
         netPerformancePercent: position.netPerformancePercentage.toNumber(),
         quantity: quantity.toNumber(),
-        symbol: aSymbol,
         value: this.exchangeRateDataService.toCurrency(
           quantity.mul(marketPrice).toNumber(),
           currency,
@@ -606,15 +600,12 @@ export class PortfolioService {
       }
 
       return {
-        assetClass,
-        assetSubClass,
         marketPrice,
         maxPrice,
         minPrice,
-        name,
         orders,
+        SymbolProfile,
         averagePrice: 0,
-        currency: currentData[aSymbol]?.currency,
         firstBuyDate: undefined,
         grossPerformance: undefined,
         grossPerformancePercent: undefined,
@@ -623,7 +614,6 @@ export class PortfolioService {
         netPerformance: undefined,
         netPerformancePercent: undefined,
         quantity: 0,
-        symbol: aSymbol,
         transactionCount: undefined,
         value: 0
       };
diff --git a/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts
index 59e6c0e20..720f02b67 100644
--- a/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts
+++ b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts
@@ -58,12 +58,25 @@ export class TransformDataSourceInResponseInterceptor<T>
             });
           }
 
+          if (data.orders) {
+            data.orders.map((order) => {
+              order.dataSource = encodeDataSource(order.dataSource);
+              return order;
+            });
+          }
+
           if (data.positions) {
             data.positions.map((position) => {
               position.dataSource = encodeDataSource(position.dataSource);
               return position;
             });
           }
+
+          if (data.SymbolProfile) {
+            data.SymbolProfile.dataSource = encodeDataSource(
+              data.SymbolProfile.dataSource
+            );
+          }
         }
 
         return data;
diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts
index 02563afba..db4cbf471 100644
--- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts
+++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts
@@ -11,7 +11,7 @@ import { DataService } from '@ghostfolio/client/services/data.service';
 import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
 import { OrderWithAccount } from '@ghostfolio/common/types';
 import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
-import { AssetSubClass } from '@prisma/client';
+import { SymbolProfile } from '@prisma/client';
 import { format, isSameMonth, isToday, parseISO } from 'date-fns';
 import { Subject } from 'rxjs';
 import { takeUntil } from 'rxjs/operators';
@@ -26,10 +26,11 @@ import { PositionDetailDialogParams } from './interfaces/interfaces';
   styleUrls: ['./position-detail-dialog.component.scss']
 })
 export class PositionDetailDialog implements OnDestroy, OnInit {
-  public assetSubClass: AssetSubClass;
   public averagePrice: number;
   public benchmarkDataItems: LineChartItem[];
-  public currency: string;
+  public countries: {
+    [code: string]: { name: string; value: number };
+  };
   public firstBuyDate: string;
   public grossPerformance: number;
   public grossPerformancePercent: number;
@@ -38,13 +39,15 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
   public marketPrice: number;
   public maxPrice: number;
   public minPrice: number;
-  public name: string;
   public netPerformance: number;
   public netPerformancePercent: number;
   public orders: OrderWithAccount[];
   public quantity: number;
   public quantityPrecision = 2;
-  public symbol: string;
+  public sectors: {
+    [name: string]: { name: string; value: number };
+  };
+  public SymbolProfile: SymbolProfile;
   public transactionCount: number;
   public value: number;
 
@@ -66,9 +69,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
       .pipe(takeUntil(this.unsubscribeSubject))
       .subscribe(
         ({
-          assetSubClass,
           averagePrice,
-          currency,
           firstBuyDate,
           grossPerformance,
           grossPerformancePercent,
@@ -77,19 +78,17 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
           marketPrice,
           maxPrice,
           minPrice,
-          name,
           netPerformance,
           netPerformancePercent,
           orders,
           quantity,
-          symbol,
+          SymbolProfile,
           transactionCount,
           value
         }) => {
-          this.assetSubClass = assetSubClass;
           this.averagePrice = averagePrice;
           this.benchmarkDataItems = [];
-          this.currency = currency;
+          this.countries = {};
           this.firstBuyDate = firstBuyDate;
           this.grossPerformance = grossPerformance;
           this.grossPerformancePercent = grossPerformancePercent;
@@ -110,15 +109,33 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
           this.marketPrice = marketPrice;
           this.maxPrice = maxPrice;
           this.minPrice = minPrice;
-          this.name = name;
           this.netPerformance = netPerformance;
           this.netPerformancePercent = netPerformancePercent;
           this.orders = orders;
           this.quantity = quantity;
-          this.symbol = symbol;
+          this.sectors = {};
+          this.SymbolProfile = SymbolProfile;
           this.transactionCount = transactionCount;
           this.value = value;
 
+          if (SymbolProfile?.countries?.length > 0) {
+            for (const country of SymbolProfile.countries) {
+              this.countries[country.code] = {
+                name: country.name,
+                value: country.weight
+              };
+            }
+          }
+
+          if (SymbolProfile?.sectors?.length > 0) {
+            for (const sector of SymbolProfile.sectors) {
+              this.sectors[sector.name] = {
+                name: sector.name,
+                value: sector.weight
+              };
+            }
+          }
+
           if (isToday(parseISO(this.firstBuyDate))) {
             // Add average price
             this.historicalDataItems.push({
@@ -166,7 +183,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
 
           if (Number.isInteger(this.quantity)) {
             this.quantityPrecision = 0;
-          } else if (assetSubClass === 'CRYPTOCURRENCY') {
+          } else if (this.SymbolProfile?.assetSubClass === 'CRYPTOCURRENCY') {
             if (this.quantity < 1) {
               this.quantityPrecision = 7;
             } else if (this.quantity < 1000) {
@@ -196,7 +213,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
       .subscribe((data) => {
         downloadAsFile(
           data,
-          `ghostfolio-export-${this.symbol}-${format(
+          `ghostfolio-export-${this.SymbolProfile?.symbol}-${format(
             parseISO(data.meta.date),
             'yyyyMMddHHmm'
           )}.json`,
diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html
index db8f78bc3..3a4026a14 100644
--- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html
+++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html
@@ -2,7 +2,7 @@
   mat-dialog-title
   position="center"
   [deviceType]="data.deviceType"
-  [title]="name ?? symbol"
+  [title]="SymbolProfile?.name ?? SymbolProfile?.symbol"
   (closeButtonClicked)="onClose()"
 ></gf-dialog-header>
 
@@ -55,7 +55,7 @@
         <gf-value
           label="Ø Buy Price"
           size="medium"
-          [currency]="currency"
+          [currency]="SymbolProfile?.currency"
           [locale]="data.locale"
           [value]="averagePrice"
         ></gf-value>
@@ -64,7 +64,7 @@
         <gf-value
           label="Market Price"
           size="medium"
-          [currency]="currency"
+          [currency]="SymbolProfile?.currency"
           [locale]="data.locale"
           [value]="marketPrice"
         ></gf-value>
@@ -73,7 +73,7 @@
         <gf-value
           label="Minimum Price"
           size="medium"
-          [currency]="currency"
+          [currency]="SymbolProfile?.currency"
           [locale]="data.locale"
           [ngClass]="{ 'text-danger': minPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
           [value]="minPrice"
@@ -83,7 +83,7 @@
         <gf-value
           label="Maximum Price"
           size="medium"
-          [currency]="currency"
+          [currency]="SymbolProfile?.currency"
           [locale]="data.locale"
           [ngClass]="{ 'text-success': maxPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
           [value]="maxPrice"
@@ -122,6 +122,72 @@
           [value]="transactionCount"
         ></gf-value>
       </div>
+      <div class="col-6 mb-3">
+        <gf-value
+          label="Asset Class"
+          size="medium"
+          [value]="SymbolProfile?.assetClass"
+        ></gf-value>
+      </div>
+      <div class="col-6 mb-3">
+        <gf-value
+          size="medium"
+          label="Asset Sub Class"
+          [locale]="data.locale"
+          [value]="SymbolProfile?.assetSubClass"
+        ></gf-value>
+      </div>
+      <ng-container
+        *ngIf="SymbolProfile?.countries?.length > 0 || SymbolProfile?.sectors?.length > 0"
+      >
+        <ng-container
+          *ngIf="SymbolProfile?.countries?.length === 1 && SymbolProfile?.sectors?.length === 1; else charts"
+        >
+          <div
+            *ngIf="SymbolProfile?.countries?.length === 1"
+            class="col-6 mb-3"
+          >
+            <gf-value
+              label="Country"
+              size="medium"
+              [locale]="data.locale"
+              [value]="SymbolProfile.countries[0].name"
+            ></gf-value>
+          </div>
+          <div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
+            <gf-value
+              label="Sector"
+              size="medium"
+              [locale]="data.locale"
+              [value]="SymbolProfile.sectors[0].name"
+            ></gf-value>
+          </div>
+        </ng-container>
+        <ng-template #charts>
+          <div class="col-6 mb-3">
+            <div class="h4 mb-0" i18n>Countries</div>
+            <gf-portfolio-proportion-chart
+              [baseCurrency]="user?.settings?.baseCurrency"
+              [isInPercent]="true"
+              [keys]="['name']"
+              [locale]="user?.settings?.locale"
+              [maxItems]="10"
+              [positions]="countries"
+            ></gf-portfolio-proportion-chart>
+          </div>
+          <div class="col-6 mb-3">
+            <div class="h4 mb-0" i18n>Sectors</div>
+            <gf-portfolio-proportion-chart
+              [baseCurrency]="user?.settings?.baseCurrency"
+              [isInPercent]="true"
+              [keys]="['name']"
+              [locale]="user?.settings?.locale"
+              [maxItems]="10"
+              [positions]="sectors"
+            ></gf-portfolio-proportion-chart>
+          </div>
+        </ng-template>
+      </ng-container>
     </div>
   </div>
 
diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts
index 4cd013fd4..72f80f065 100644
--- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts
+++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts
@@ -6,6 +6,7 @@ import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-foote
 import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
 import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
 import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
+import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
 import { GfValueModule } from '@ghostfolio/ui/value';
 import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
 
@@ -20,6 +21,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
     GfDialogFooterModule,
     GfDialogHeaderModule,
     GfLineChartModule,
+    GfPortfolioProportionChartModule,
     GfValueModule,
     MatButtonModule,
     MatDialogModule,
diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html
index 282a99ca7..e4f8b0f45 100644
--- a/libs/ui/src/lib/value/value.component.html
+++ b/libs/ui/src/lib/value/value.component.html
@@ -34,12 +34,12 @@
         {{ currency }}
       </div>
     </ng-container>
-    <ng-container *ngIf="isDate">
+    <ng-container *ngIf="isString">
       <div
-        class="mb-0"
+        class="mb-0 text-truncate"
         [ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
       >
-        {{ formattedDate }}
+        {{ formattedValue | titlecase }}
       </div>
     </ng-container>
   </div>
diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts
index 07fc2b136..29863e9bd 100644
--- a/libs/ui/src/lib/value/value.component.ts
+++ b/libs/ui/src/lib/value/value.component.ts
@@ -5,7 +5,7 @@ import {
   OnChanges
 } from '@angular/core';
 import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
-import { format, isDate } from 'date-fns';
+import { format, isDate, parseISO } from 'date-fns';
 import { isNumber } from 'lodash';
 
 @Component({
@@ -28,10 +28,9 @@ export class ValueComponent implements OnChanges {
   @Input() value: number | string = '';
 
   public absoluteValue = 0;
-  public formattedDate = '';
   public formattedValue = '';
-  public isDate = false;
   public isNumber = false;
+  public isString = false;
   public useAbsoluteValue = false;
 
   public constructor() {}
@@ -39,8 +38,8 @@ export class ValueComponent implements OnChanges {
   public ngOnChanges() {
     if (this.value || this.value === 0) {
       if (isNumber(this.value)) {
-        this.isDate = false;
         this.isNumber = true;
+        this.isString = false;
         this.absoluteValue = Math.abs(<number>this.value);
 
         if (this.colorizeSign) {
@@ -98,17 +97,19 @@ export class ValueComponent implements OnChanges {
           this.formattedValue = this.formattedValue.replace(/^-/, '');
         }
       } else {
-        try {
-          if (isDate(new Date(this.value))) {
-            this.isDate = true;
-            this.isNumber = false;
+        this.isNumber = false;
+        this.isString = true;
 
-            this.formattedDate = format(
+        try {
+          if (isDate(parseISO(this.value))) {
+            this.formattedValue = format(
               new Date(<string>this.value),
               DEFAULT_DATE_FORMAT
             );
           }
-        } catch {}
+        } catch {
+          this.formattedValue = this.value;
+        }
       }
     }