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 }); } + 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()" > @@ -55,7 +55,7 @@ @@ -64,7 +64,7 @@ @@ -73,7 +73,7 @@ +
+ +
+
+ +
+ + +
+ +
+
+ +
+
+ +
+
Countries
+ +
+
+
Sectors
+ +
+
+
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 }} - +
- {{ formattedDate }} + {{ formattedValue | titlecase }}
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(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(this.value), DEFAULT_DATE_FORMAT ); } - } catch {} + } catch { + this.formattedValue = this.value; + } } }