diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 0210284f8..ed5bbe0cd 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -4,6 +4,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; +import { Filter } from '@ghostfolio/common/interfaces'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; import { @@ -16,6 +17,7 @@ import { } from '@prisma/client'; import Big from 'big.js'; import { endOfToday, isAfter } from 'date-fns'; +import { groupBy } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { Activity } from './interfaces/activities.interface'; @@ -166,31 +168,44 @@ export class OrderService { } public async getOrders({ + filters, includeDrafts = false, - tags, types, userCurrency, userId }: { + filters?: Filter[]; includeDrafts?: boolean; - tags?: string[]; types?: TypeOfOrder[]; userCurrency: string; userId: string; }): Promise { const where: Prisma.OrderWhereInput = { userId }; + const { account: filtersByAccount, tag: filtersByTag } = groupBy( + filters, + (filter) => { + return filter.type; + } + ); + + if (filtersByAccount?.length > 0) { + where.accountId = { + in: filtersByAccount.map(({ id }) => { + return id; + }) + }; + } + if (includeDrafts === false) { where.isDraft = false; } - if (tags?.length > 0) { + if (filtersByTag?.length > 0) { where.tags = { some: { - OR: tags.map((tag) => { - return { - id: tag - }; + OR: filtersByTag.map(({ id }) => { + return { id }; }) } }; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 9f9b20c56..37252b8fc 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -11,6 +11,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { baseCurrency } from '@ghostfolio/common/config'; import { parseDate } from '@ghostfolio/common/helper'; import { + Filter, PortfolioChart, PortfolioDetails, PortfolioInvestments, @@ -19,7 +20,7 @@ import { PortfolioReport, PortfolioSummary } from '@ghostfolio/common/interfaces'; -import type { RequestWithUser } from '@ghostfolio/common/types'; +import type { DateRange, RequestWithUser } from '@ghostfolio/common/types'; import { Controller, Get, @@ -105,17 +106,36 @@ export class PortfolioController { @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getDetails( @Headers('impersonation-id') impersonationId: string, - @Query('range') range, - @Query('tags') tags?: string + @Query('accounts') filterByAccounts?: string, + @Query('range') range?: DateRange, + @Query('tags') filterByTags?: string ): Promise { let hasError = false; + const accountIds = filterByAccounts?.split(',') ?? []; + const tagIds = filterByTags?.split(',') ?? []; + + const filters: Filter[] = [ + ...accountIds.map((accountId) => { + return { + id: accountId, + type: 'account' + }; + }), + ...tagIds.map((tagId) => { + return { + id: tagId, + type: 'tag' + }; + }) + ]; + const { accounts, holdings, hasErrors } = await this.portfolioService.getDetails( impersonationId, this.request.user.id, range, - tags?.split(',') + filters ); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { @@ -163,7 +183,7 @@ export class PortfolioController { return { hasError, - accounts: tags ? {} : accounts, + accounts: filters ? {} : accounts, holdings: isBasicUser ? {} : holdings }; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index ecc0ea20e..795f516ee 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -29,6 +29,7 @@ import { import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Accounts, + Filter, PortfolioDetails, PortfolioPerformanceResponse, PortfolioReport, @@ -309,7 +310,7 @@ export class PortfolioService { aImpersonationId: string, aUserId: string, aDateRange: DateRange = 'max', - tags?: string[] + aFilters?: Filter[] ): Promise { const userId = await this.getUserId(aImpersonationId, aUserId); const user = await this.userService.user({ id: userId }); @@ -324,8 +325,8 @@ export class PortfolioService { const { orders, portfolioOrders, transactionPoints } = await this.getTransactionPoints({ - tags, - userId + userId, + filters: aFilters }); const portfolioCalculator = new PortfolioCalculator({ @@ -448,7 +449,7 @@ export class PortfolioService { value: totalValue }); - if (tags === undefined) { + if (aFilters === undefined) { for (const symbol of Object.keys(cashPositions)) { holdings[symbol] = cashPositions[symbol]; } @@ -1195,12 +1196,12 @@ export class PortfolioService { } private async getTransactionPoints({ + filters, includeDrafts = false, - tags, userId }: { + filters?: Filter[]; includeDrafts?: boolean; - tags?: string[]; userId: string; }): Promise<{ transactionPoints: TransactionPoint[]; @@ -1210,8 +1211,8 @@ export class PortfolioService { const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const orders = await this.orderService.getOrders({ + filters, includeDrafts, - tags, userCurrency, userId, types: ['BUY', 'SELL'] 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 82334be68..1c20dc518 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 @@ -151,14 +151,26 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; - this.allFilters = this.user.tags.map((tag) => { + const accountFilters: Filter[] = this.user.accounts.map( + ({ id, name }) => { + return { + id: id, + label: name, + type: 'account' + }; + } + ); + + const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => { return { - id: tag.id, - label: tag.name, + id, + label: name, type: 'tag' }; }); + this.allFilters = [...accountFilters, ...tagFilters]; + this.changeDetectorRef.markForCheck(); } }); 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 c7fd9854f..07418a4af 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -5,7 +5,6 @@ diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 6c7d2d022..7679c154f 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -34,7 +34,7 @@ import { permissions } from '@ghostfolio/common/permissions'; import { DateRange } from '@ghostfolio/common/types'; import { DataSource, Order as OrderModel } from '@prisma/client'; import { parseISO } from 'date-fns'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, groupBy } from 'lodash'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -187,17 +187,34 @@ export class DataService { let params = new HttpParams(); if (filters?.length > 0) { - params = params.append( - 'tags', - filters - .filter((filter) => { - return filter.type === 'tag'; - }) - .map((filter) => { - return filter.id; - }) - .join(',') + const { account: filtersByAccount, tag: filtersByTag } = groupBy( + filters, + (filter) => { + return filter.type; + } ); + + if (filtersByAccount) { + params = params.append( + 'accounts', + filtersByAccount + .map(({ id }) => { + return id; + }) + .join(',') + ); + } + + if (filtersByTag) { + params = params.append( + 'tags', + filtersByTag + .map(({ id }) => { + return id; + }) + .join(',') + ); + } } return this.http.get('/api/v1/portfolio/details', { diff --git a/libs/common/src/lib/interfaces/filter.interface.ts b/libs/common/src/lib/interfaces/filter.interface.ts index 4aee7bf91..e6d5bb108 100644 --- a/libs/common/src/lib/interfaces/filter.interface.ts +++ b/libs/common/src/lib/interfaces/filter.interface.ts @@ -1,5 +1,5 @@ export interface Filter { id: string; - label: string; - type: 'tag'; + label?: string; + type: 'account' | 'tag'; } diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.ts b/libs/ui/src/lib/activities-filter/activities-filter.component.ts index d2589e513..b114317e5 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.ts +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.ts @@ -63,6 +63,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { .toLowerCase() .startsWith(currentFilter?.toLowerCase()); }) + .sort((a, b) => a.label.localeCompare(b.label)) ); } }); @@ -109,12 +110,14 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { private updateFilter() { this.filters$.next( - this.allFilters.filter((filter) => { - // Filter selected filters - return !this.selectedFilters.some((selectedFilter) => { - return selectedFilter.id === filter.id; - }); - }) + this.allFilters + .filter((filter) => { + // Filter selected filters + return !this.selectedFilters.some((selectedFilter) => { + return selectedFilter.id === filter.id; + }); + }) + .sort((a, b) => a.label.localeCompare(b.label)) ); // Emit an array with a new reference