From c0f19d56ec6d1e57ef396f36f856e2e5524fade1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 28 Jun 2022 21:08:34 +0200 Subject: [PATCH] Feature/add account detail dialog (#1047) * Add account detail dialog * Update changelog --- CHANGELOG.md | 4 + .../api/src/app/account/account.controller.ts | 51 ++++++-- apps/api/src/app/order/order.controller.ts | 33 +++++- .../src/app/portfolio/portfolio.service.ts | 29 ++++- apps/client/src/app/app.module.ts | 4 + .../account-detail-dialog.component.scss | 3 + .../account-detail-dialog.component.ts | 112 ++++++++++++++++++ .../account-detail-dialog.html | 65 ++++++++++ .../account-detail-dialog.module.ts | 29 +++++ .../interfaces/interfaces.ts | 5 + .../accounts-table.component.html | 9 +- .../accounts-table.component.ts | 9 +- .../src/app/pages/account/account-page.html | 2 +- .../pages/accounts/accounts-page.component.ts | 33 +++++- .../pages/accounts/accounts-page.module.ts | 4 +- .../create-or-update-account-dialog.html | 11 ++ .../allocations/allocations-page.component.ts | 36 +++++- .../allocations/allocations-page.html | 4 +- .../transactions-page.component.ts | 14 +-- apps/client/src/app/services/data.service.ts | 79 ++++++++++-- .../src/lib/types/account-with-value.type.ts | 3 +- .../portfolio-proportion-chart.component.ts | 10 +- 22 files changed, 497 insertions(+), 52 deletions(-) create mode 100644 apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.scss create mode 100644 apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts create mode 100644 apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html create mode 100644 apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts create mode 100644 apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe5c93e3..5caba02c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added an account detail dialog + ### Changed - Improved the label of the (symbol) search diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 819fc5a0d..524e36f5a 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -7,7 +7,10 @@ import { import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { Accounts } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import type { RequestWithUser } from '@ghostfolio/common/types'; +import type { + AccountWithValue, + RequestWithUser +} from '@ghostfolio/common/types'; import { Body, Controller, @@ -123,13 +126,45 @@ export class AccountController { @Get(':id') @UseGuards(AuthGuard('jwt')) - public async getAccountById(@Param('id') id: string): Promise { - return this.accountService.account({ - id_userId: { - id, - userId: this.request.user.id - } - }); + public async getAccountById( + @Headers('impersonation-id') impersonationId, + @Param('id') id: string + ): Promise { + const impersonationUserId = + await this.impersonationService.validateImpersonationId( + impersonationId, + this.request.user.id + ); + + let accountsWithAggregations = + await this.portfolioService.getAccountsWithAggregations( + impersonationUserId || this.request.user.id, + [{ id, type: 'ACCOUNT' }] + ); + + if ( + impersonationUserId || + this.userService.isRestrictedView(this.request.user) + ) { + accountsWithAggregations = { + ...nullifyValuesInObject(accountsWithAggregations, [ + 'totalBalanceInBaseCurrency', + 'totalValueInBaseCurrency' + ]), + accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [ + 'balance', + 'balanceInBaseCurrency', + 'convertedBalance', + 'fee', + 'quantity', + 'unitPrice', + 'value', + 'valueInBaseCurrency' + ]) + }; + } + + return accountsWithAggregations.accounts[0]; } @Post() diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index e61c57ef7..1d62e79a3 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -4,6 +4,7 @@ import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; +import { Filter } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { @@ -17,6 +18,7 @@ import { Param, Post, Put, + Query, UseGuards, UseInterceptors } from '@nestjs/common'; @@ -66,8 +68,36 @@ export class OrderController { @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getAllOrders( - @Headers('impersonation-id') impersonationId + @Headers('impersonation-id') impersonationId, + @Query('accounts') filterByAccounts?: string, + @Query('assetClasses') filterByAssetClasses?: string, + @Query('tags') filterByTags?: string ): Promise { + const accountIds = filterByAccounts?.split(',') ?? []; + const assetClasses = filterByAssetClasses?.split(',') ?? []; + const tagIds = filterByTags?.split(',') ?? []; + + const filters: Filter[] = [ + ...accountIds.map((accountId) => { + return { + id: accountId, + type: 'ACCOUNT' + }; + }), + ...assetClasses.map((assetClass) => { + return { + id: assetClass, + type: 'ASSET_CLASS' + }; + }), + ...tagIds.map((tagId) => { + return { + id: tagId, + type: 'TAG' + }; + }) + ]; + const impersonationUserId = await this.impersonationService.validateImpersonationId( impersonationId, @@ -76,6 +106,7 @@ export class OrderController { const userCurrency = this.request.user.Settings.currency; let activities = await this.orderService.getOrders({ + filters, userCurrency, includeDrafts: true, userId: impersonationUserId || this.request.user.id diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b7f9248b4..ab32c8486 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -50,6 +50,7 @@ import { REQUEST } from '@nestjs/core'; import { AssetClass, DataSource, + Prisma, Tag, Type as TypeOfOrder } from '@prisma/client'; @@ -100,14 +101,23 @@ export class PortfolioService { this.baseCurrency = this.configurationService.get('BASE_CURRENCY'); } - public async getAccounts(aUserId: string): Promise { + public async getAccounts( + aUserId: string, + aFilters?: Filter[] + ): Promise { + const where: Prisma.AccountWhereInput = { userId: aUserId }; + + if (aFilters?.[0].id && aFilters?.[0].type === 'ACCOUNT') { + where.id = aFilters[0].id; + } + const [accounts, details] = await Promise.all([ this.accountService.accounts({ + where, include: { Order: true, Platform: true }, - orderBy: { name: 'asc' }, - where: { userId: aUserId } + orderBy: { name: 'asc' } }), - this.getDetails(aUserId, aUserId) + this.getDetails(aUserId, aUserId, undefined, aFilters) ]); const userCurrency = this.request.user.Settings.currency; @@ -145,8 +155,11 @@ export class PortfolioService { }); } - public async getAccountsWithAggregations(aUserId: string): Promise { - const accounts = await this.getAccounts(aUserId); + public async getAccountsWithAggregations( + aUserId: string, + aFilters?: Filter[] + ): Promise { + const accounts = await this.getAccounts(aUserId, aFilters); let totalBalanceInBaseCurrency = new Big(0); let totalValueInBaseCurrency = new Big(0); let transactionCount = 0; @@ -1290,6 +1303,10 @@ export class PortfolioService { if (filters.length === 0) { currentAccounts = await this.accountService.getAccounts(userId); + } else if (filters.length === 1 && filters[0].type === 'ACCOUNT') { + currentAccounts = await this.accountService.accounts({ + where: { id: filters[0].id } + }); } else { const accountIds = uniq( orders.map(({ accountId }) => { diff --git a/apps/client/src/app/app.module.ts b/apps/client/src/app/app.module.ts index 9dca3244c..695612864 100644 --- a/apps/client/src/app/app.module.ts +++ b/apps/client/src/app/app.module.ts @@ -1,6 +1,8 @@ import { Platform } from '@angular/cdk/platform'; import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatChipsModule } from '@angular/material/chips'; import { DateAdapter, MAT_DATE_FORMATS, @@ -38,6 +40,8 @@ export function NgxStripeFactory(): string { GfHeaderModule, HttpClientModule, MarkdownModule.forRoot(), + MatAutocompleteModule, + MatChipsModule, MaterialCssVarsModule.forRoot({ darkThemeClass: 'is-dark-theme', isAutoContrast: true, diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.scss b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.scss new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts new file mode 100644 index 000000000..ca2f229e7 --- /dev/null +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -0,0 +1,112 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Inject, + OnDestroy, + OnInit +} from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { downloadAsFile } from '@ghostfolio/common/helper'; +import { User } from '@ghostfolio/common/interfaces'; +import { OrderWithAccount } from '@ghostfolio/common/types'; +import { AccountType } from '@prisma/client'; +import { format, parseISO } from 'date-fns'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { AccountDetailDialogParams } from './interfaces/interfaces'; + +@Component({ + host: { class: 'd-flex flex-column h-100' }, + selector: 'gf-account-detail-dialog', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: 'account-detail-dialog.html', + styleUrls: ['./account-detail-dialog.component.scss'] +}) +export class AccountDetailDialog implements OnDestroy, OnInit { + public accountType: AccountType; + public name: string; + public orders: OrderWithAccount[]; + public platformName: string; + public user: User; + public valueInBaseCurrency: number; + + private unsubscribeSubject = new Subject(); + + public constructor( + private changeDetectorRef: ChangeDetectorRef, + @Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams, + private dataService: DataService, + public dialogRef: MatDialogRef, + private userService: UserService + ) { + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + + this.changeDetectorRef.markForCheck(); + } + }); + } + + public ngOnInit(): void { + this.dataService + .fetchAccount(this.data.accountId) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => { + this.accountType = accountType; + this.name = name; + this.platformName = Platform?.name; + this.valueInBaseCurrency = valueInBaseCurrency; + + this.changeDetectorRef.markForCheck(); + }); + + this.dataService + .fetchActivities({ + filters: [{ id: this.data.accountId, type: 'ACCOUNT' }] + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ activities }) => { + this.orders = activities; + + this.changeDetectorRef.markForCheck(); + }); + } + + public onClose(): void { + this.dialogRef.close(); + } + + public onExport() { + this.dataService + .fetchExport( + this.orders.map((order) => { + return order.id; + }) + ) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data) => { + downloadAsFile({ + content: data, + fileName: `ghostfolio-export-${this.name + .replace(/\s+/g, '-') + .toLowerCase()}-${format( + parseISO(data.meta.date), + 'yyyyMMddHHmm' + )}.json`, + format: 'json' + }); + }); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html new file mode 100644 index 000000000..150f74e74 --- /dev/null +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html @@ -0,0 +1,65 @@ + + +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
Activities
+ +
+
+
+
+ + diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts new file mode 100644 index 000000000..8c5b7abcd --- /dev/null +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts @@ -0,0 +1,29 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; +import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; +import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; +import { GfValueModule } from '@ghostfolio/ui/value'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + +import { AccountDetailDialog } from './account-detail-dialog.component'; + +@NgModule({ + declarations: [AccountDetailDialog], + exports: [], + imports: [ + CommonModule, + GfActivitiesTableModule, + GfDialogFooterModule, + GfDialogHeaderModule, + GfValueModule, + MatButtonModule, + MatDialogModule, + NgxSkeletonLoaderModule + ], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfAccountDetailDialogModule {} diff --git a/apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts new file mode 100644 index 000000000..016fc3b7d --- /dev/null +++ b/apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts @@ -0,0 +1,5 @@ +export interface AccountDetailDialogParams { + accountId: string; + deviceType: string; + hasImpersonationId: boolean; +} diff --git a/apps/client/src/app/components/accounts-table/accounts-table.component.html b/apps/client/src/app/components/accounts-table/accounts-table.component.html index 08c6c3de9..d576e5447 100644 --- a/apps/client/src/app/components/accounts-table/accounts-table.component.html +++ b/apps/client/src/app/components/accounts-table/accounts-table.component.html @@ -65,7 +65,7 @@ # - Transactions + Activities {{ @@ -212,7 +212,12 @@ - + (); - public constructor() {} + public constructor(private router: Router) {} public ngOnInit() {} @@ -75,6 +76,12 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit { } } + public onOpenAccountDetailDialog(accountId: string) { + this.router.navigate([], { + queryParams: { accountId, accountDetailDialog: true } + }); + } + public onUpdateAccount(aAccount: AccountModel) { this.accountToUpdate.emit(aAccount); } diff --git a/apps/client/src/app/pages/account/account-page.html b/apps/client/src/app/pages/account/account-page.html index 5a312fc09..68800ce30 100644 --- a/apps/client/src/app/pages/account/account-page.html +++ b/apps/client/src/app/pages/account/account-page.html @@ -171,7 +171,7 @@
-
ID
+
User ID
{{ user?.id }}
diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index 826e2e622..81c02c2fd 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -3,6 +3,8 @@ import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; +import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component'; +import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; 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'; @@ -48,12 +50,17 @@ export class AccountsPageComponent implements OnDestroy, OnInit { this.route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { - if (params['createDialog'] && this.hasPermissionToCreateAccount) { + if (params['accountId'] && params['accountDetailDialog']) { + this.openAccountDetailDialog(params['accountId']); + } else if ( + params['createDialog'] && + this.hasPermissionToCreateAccount + ) { this.openCreateAccountDialog(); } else if (params['editDialog']) { if (this.accounts) { const account = this.accounts.find((account) => { - return account.id === params['transactionId']; + return account.id === params['accountId']; }); this.openUpdateAccountDialog(account); @@ -139,7 +146,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit { public onUpdateAccount(aAccount: AccountModel) { this.router.navigate([], { - queryParams: { editDialog: true, transactionId: aAccount.id } + queryParams: { accountId: aAccount.id, editDialog: true } }); } @@ -197,6 +204,26 @@ export class AccountsPageComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } + private openAccountDetailDialog(aAccountId: string) { + const dialogRef = this.dialog.open(AccountDetailDialog, { + autoFocus: false, + data: { + accountId: aAccountId, + deviceType: this.deviceType, + hasImpersonationId: this.hasImpersonationId + }, + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); + } + private openCreateAccountDialog(): void { const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, { data: { diff --git a/apps/client/src/app/pages/accounts/accounts-page.module.ts b/apps/client/src/app/pages/accounts/accounts-page.module.ts index b9de21ff8..9edb43ba7 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.module.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.module.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; +import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module'; import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module'; import { AccountsPageRoutingModule } from './accounts-page-routing.module'; @@ -10,16 +11,15 @@ import { GfCreateOrUpdateAccountDialogModule } from './create-or-update-account- @NgModule({ declarations: [AccountsPageComponent], - exports: [], imports: [ AccountsPageRoutingModule, CommonModule, + GfAccountDetailDialogModule, GfAccountsTableModule, GfCreateOrUpdateAccountDialogModule, MatButtonModule, RouterModule ], - providers: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class AccountsPageModule {} diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html index ad7ee3988..eaef9d607 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -50,6 +50,17 @@ +
+ + Account ID + + +
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 index ee4b755f4..9c60b4e13 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -1,6 +1,8 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; +import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component'; +import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; @@ -99,7 +101,9 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.routeQueryParams = route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { - if ( + if (params['accountId'] && params['accountDetailDialog']) { + this.openAccountDetailDialog(params['accountId']); + } else if ( params['dataSource'] && params['positionDetailDialog'] && params['symbol'] @@ -379,13 +383,21 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.markets.otherMarkets.value / marketsTotal; } + public onAccountChartClicked({ symbol }: UniqueAsset) { + if (symbol) { + this.router.navigate([], { + queryParams: { accountId: symbol, accountDetailDialog: true } + }); + } + } + public onChangePeriod(aValue: string) { this.period = aValue; this.initializeAnalysisData(this.period); } - public onProportionChartClicked({ dataSource, symbol }: UniqueAsset) { + public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) { if (dataSource && symbol) { this.router.navigate([], { queryParams: { dataSource, symbol, positionDetailDialog: true } @@ -398,6 +410,26 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } + private openAccountDetailDialog(aAccountId: string) { + const dialogRef = this.dialog.open(AccountDetailDialog, { + autoFocus: false, + data: { + accountId: aAccountId, + deviceType: this.deviceType, + hasImpersonationId: this.hasImpersonationId + }, + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); + } + private openPositionDialog({ dataSource, symbol diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html index 416af40c0..869148b5c 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -24,11 +24,13 @@ @@ -116,7 +118,7 @@ [locale]="user?.settings?.locale" [positions]="symbols" [showLabels]="deviceType !== 'mobile'" - (proportionChartClicked)="onProportionChartClicked($event)" + (proportionChartClicked)="onSymbolChartClicked($event)" > diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 66ffee9df..80c0701b5 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -111,12 +111,12 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { } }); - this.fetchOrders(); + this.fetchActivities(); } - public fetchOrders() { + public fetchActivities() { this.dataService - .fetchOrders() + .fetchActivities({}) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ activities }) => { this.activities = activities; @@ -139,7 +139,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe({ next: () => { - this.fetchOrders(); + this.fetchActivities(); } }); } @@ -298,7 +298,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe({ next: () => { - this.fetchOrders(); + this.fetchActivities(); } }); } @@ -332,7 +332,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { } private handleImportSuccess() { - this.fetchOrders(); + this.fetchActivities(); this.snackBar.open('✅ Import has been completed', undefined, { duration: 3000 @@ -376,7 +376,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { if (transaction) { this.dataService.postOrder(transaction).subscribe({ next: () => { - this.fetchOrders(); + this.fetchActivities(); } }); } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index f6b92ac2a..250fd1f7f 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -33,7 +33,7 @@ import { User } from '@ghostfolio/common/interfaces'; import { filterGlobalPermissions } from '@ghostfolio/common/permissions'; -import { DateRange } from '@ghostfolio/common/types'; +import { AccountWithValue, DateRange } from '@ghostfolio/common/types'; import { DataSource, Order as OrderModel } from '@prisma/client'; import { parseISO } from 'date-fns'; import { cloneDeep, groupBy } from 'lodash'; @@ -59,10 +59,75 @@ export class DataService { }); } + public fetchAccount(aAccountId: string) { + return this.http.get(`/api/v1/account/${aAccountId}`); + } + public fetchAccounts() { return this.http.get('/api/v1/account'); } + public fetchActivities({ + filters + }: { + filters?: Filter[]; + }): Observable { + let params = new HttpParams(); + + if (filters?.length > 0) { + const { + ACCOUNT: filtersByAccount, + ASSET_CLASS: filtersByAssetClass, + TAG: filtersByTag + } = groupBy(filters, (filter) => { + return filter.type; + }); + + if (filtersByAccount) { + params = params.append( + 'accounts', + filtersByAccount + .map(({ id }) => { + return id; + }) + .join(',') + ); + } + + if (filtersByAssetClass) { + params = params.append( + 'assetClasses', + filtersByAssetClass + .map(({ id }) => { + return id; + }) + .join(',') + ); + } + + if (filtersByTag) { + params = params.append( + 'tags', + filtersByTag + .map(({ id }) => { + return id; + }) + .join(',') + ); + } + } + + return this.http.get('/api/v1/order', { params }).pipe( + map(({ activities }) => { + for (const activity of activities) { + activity.createdAt = parseISO(activity.createdAt); + activity.date = parseISO(activity.date); + } + return { activities }; + }) + ); + } + public fetchAdminData() { return this.http.get('/api/v1/admin'); } @@ -179,18 +244,6 @@ export class DataService { ); } - public fetchOrders(): Observable { - return this.http.get('/api/v1/order').pipe( - map(({ activities }) => { - for (const activity of activities) { - activity.createdAt = parseISO(activity.createdAt); - activity.date = parseISO(activity.date); - } - return { activities }; - }) - ); - } - public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) { let params = new HttpParams(); diff --git a/libs/common/src/lib/types/account-with-value.type.ts b/libs/common/src/lib/types/account-with-value.type.ts index 7c0cca747..bc7577d91 100644 --- a/libs/common/src/lib/types/account-with-value.type.ts +++ b/libs/common/src/lib/types/account-with-value.type.ts @@ -1,7 +1,8 @@ -import { Account as AccountModel } from '@prisma/client'; +import { Account as AccountModel, Platform } from '@prisma/client'; export type AccountWithValue = AccountModel & { balanceInBaseCurrency: number; + Platform?: Platform; transactionCount: number; value: number; valueInBaseCurrency: number; diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index 11795788e..f8e43fa5e 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -276,12 +276,14 @@ export class PortfolioProportionChartComponent padding: this.showLabels === true ? 100 : 0 }, onClick: (event, activeElements) => { - const dataIndex = activeElements[0].index; - const symbol: string = event.chart.data.labels[dataIndex]; + try { + const dataIndex = activeElements[0].index; + const symbol: string = event.chart.data.labels[dataIndex]; - const dataSource = this.positions[symbol]?.dataSource; + const dataSource = this.positions[symbol]?.dataSource; - this.proportionChartClicked.emit({ dataSource, symbol }); + this.proportionChartClicked.emit({ dataSource, symbol }); + } catch {} }, onHover: (event, chartElement) => { if (this.cursor) {