diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 2303c3913..2c09faa81 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -284,13 +284,14 @@ export class PortfolioController { @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, + @Query('quantity') filterByQuantity?: string, @Query('query') filterBySearchQuery?: string, - @Query('pastInvestments') pastInvestments: boolean = false, @Query('tags') filterByTags?: string ): Promise { const filters = this.apiService.buildFiltersFromQueryParams({ filterByAccounts, filterByAssetClasses, + filterByQuantity, filterBySearchQuery, filterByTags }); @@ -298,7 +299,6 @@ export class PortfolioController { const { holdings } = await this.portfolioService.getDetails({ filters, impersonationId, - pastInvestments, userId: 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 d021886cf..b781af475 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -336,7 +336,6 @@ export class PortfolioService { dateRange = 'max', filters, impersonationId, - pastInvestments, userId, withExcludedAccounts = false, withLiabilities = false, @@ -345,7 +344,6 @@ export class PortfolioService { dateRange?: DateRange; filters?: Filter[]; impersonationId: string; - pastInvestments?: boolean; userId: string; withExcludedAccounts?: boolean; withLiabilities?: boolean; @@ -401,8 +399,17 @@ export class PortfolioService { ); const isFilteredByAccount = - filters?.some((filter) => { - return filter.type === 'ACCOUNT'; + filters?.some(({ type }) => { + return type === 'ACCOUNT'; + }) ?? false; + + const isFilteredByCash = filters?.some(({ id, type }) => { + return id === 'CASH' && type === 'ASSET_CLASS'; + }); + + const isFilteredByQuantityEqualsZero = + filters?.some(({ id, type }) => { + return id === '0' && type === 'QUANTITY'; }) ?? false; let filteredValueInBaseCurrency = isFilteredByAccount @@ -454,7 +461,6 @@ export class PortfolioService { grossPerformancePercentageWithCurrencyEffect, investment, marketPrice, - marketPriceInBaseCurrency, netPerformance, netPerformancePercentage, netPerformancePercentageWithCurrencyEffect, @@ -465,11 +471,16 @@ export class PortfolioService { transactionCount, valueInBaseCurrency } of currentPositions.positions) { - if (quantity.eq(0) && pastInvestments === false) { - // Ignore positions without any quantity if past investments are not requested - continue; - } else if (!quantity.eq(0) && pastInvestments === true) { - continue; + if (isFilteredByQuantityEqualsZero === true) { + if (!quantity.eq(0)) { + // Ignore positions with a quantity + continue; + } + } else { + if (quantity.eq(0)) { + // Ignore positions without any quantity + continue; + } } const symbolProfile = symbolProfileMap[symbol]; @@ -584,10 +595,6 @@ export class PortfolioService { }; } - const isFilteredByCash = filters?.some((filter) => { - return filter.type === 'ASSET_CLASS' && filter.id === 'CASH'; - }); - if (filters?.length === 0 || isFilteredByAccount || isFilteredByCash) { const cashPositions = await this.getCashPositions({ cashDetails, diff --git a/apps/api/src/services/api/api.service.ts b/apps/api/src/services/api/api.service.ts index a469254f7..d4513e2c0 100644 --- a/apps/api/src/services/api/api.service.ts +++ b/apps/api/src/services/api/api.service.ts @@ -10,18 +10,21 @@ export class ApiService { filterByAccounts, filterByAssetClasses, filterByAssetSubClasses, + filterByQuantity, filterBySearchQuery, filterByTags }: { filterByAccounts?: string; filterByAssetClasses?: string; filterByAssetSubClasses?: string; + filterByQuantity?: string; filterBySearchQuery?: string; filterByTags?: string; }): Filter[] { const accountIds = filterByAccounts?.split(',') ?? []; const assetClasses = filterByAssetClasses?.split(',') ?? []; const assetSubClasses = filterByAssetSubClasses?.split(',') ?? []; + const quantity = filterByQuantity; const searchQuery = filterBySearchQuery?.toLowerCase(); const tagIds = filterByTags?.split(',') ?? []; @@ -52,6 +55,13 @@ export class ApiService { }) ]; + if (quantity) { + filters.push({ + id: quantity, + type: 'QUANTITY' + }); + } + if (searchQuery) { filters.push({ id: searchQuery, diff --git a/apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts b/apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts index 6635393f5..bfcb76597 100644 --- a/apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts +++ b/apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts @@ -5,6 +5,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso import { UserService } from '@ghostfolio/client/services/user/user.service'; import { PortfolioPosition, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { HoldingMode, ToggleOption } from '@ghostfolio/common/types'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; @@ -24,6 +25,11 @@ export class HoldingsPageComponent implements OnDestroy, OnInit { public hasImpersonationId: boolean; public hasPermissionToCreateOrder: boolean; public holdings: PortfolioPosition[]; + public mode: HoldingMode = 'ACTIVE'; + public modeOptions: ToggleOption[] = [ + { label: $localize`Active`, value: 'ACTIVE' }, + { label: $localize`Closed`, value: 'CLOSED' } + ]; public user: User; private unsubscribeSubject = new Subject(); @@ -90,14 +96,34 @@ export class HoldingsPageComponent implements OnDestroy, OnInit { }); } + public onChangeMode(aMode: HoldingMode) { + this.mode = aMode; + + this.holdings = undefined; + + this.fetchHoldings() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ holdings }) => { + this.holdings = holdings; + + this.changeDetectorRef.markForCheck(); + }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); } private fetchHoldings() { + const filters = this.userService.getFilters(); + + if (this.mode === 'CLOSED') { + filters.push({ id: '0', type: 'QUANTITY' }); + } + return this.dataService.fetchPortfolioHoldings({ - filters: this.userService.getFilters() + filters }); } diff --git a/apps/client/src/app/pages/portfolio/holdings/holdings-page.html b/apps/client/src/app/pages/portfolio/holdings/holdings-page.html index bb8769c5a..a5ffbca8e 100644 --- a/apps/client/src/app/pages/portfolio/holdings/holdings-page.html +++ b/apps/client/src/app/pages/portfolio/holdings/holdings-page.html @@ -6,6 +6,15 @@
+
+ +
{ @@ -106,6 +107,10 @@ export class DataService { params = params.append('presetId', filtersByPresetId[0].id); } + if (filtersBySearchQuantity) { + params = params.append('quantity', filtersBySearchQuantity[0].id); + } + if (filtersBySearchQuery) { params = params.append('query', filtersBySearchQuery[0].id); } diff --git a/libs/common/src/lib/interfaces/filter.interface.ts b/libs/common/src/lib/interfaces/filter.interface.ts index 356b3add7..c66bb6b0e 100644 --- a/libs/common/src/lib/interfaces/filter.interface.ts +++ b/libs/common/src/lib/interfaces/filter.interface.ts @@ -6,6 +6,7 @@ export interface Filter { | 'ASSET_CLASS' | 'ASSET_SUB_CLASS' | 'PRESET_ID' + | 'QUANTITY' | 'SEARCH_QUERY' | 'SYMBOL' | 'TAG'; diff --git a/libs/common/src/lib/types/holding-mode.type.ts b/libs/common/src/lib/types/holding-mode.type.ts new file mode 100644 index 000000000..31d8220b0 --- /dev/null +++ b/libs/common/src/lib/types/holding-mode.type.ts @@ -0,0 +1 @@ +export type HoldingMode = 'ACTIVE' | 'CLOSED'; diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts index f1ea770d2..4c82ffef9 100644 --- a/libs/common/src/lib/types/index.ts +++ b/libs/common/src/lib/types/index.ts @@ -7,6 +7,7 @@ import type { ColorScheme } from './color-scheme.type'; import type { DateRange } from './date-range.type'; import type { Granularity } from './granularity.type'; import type { GroupBy } from './group-by.type'; +import type { HoldingMode } from './holding-mode.type'; import type { MarketAdvanced } from './market-advanced.type'; import type { MarketDataPreset } from './market-data-preset.type'; import type { MarketState } from './market-state.type'; @@ -28,6 +29,7 @@ export type { DateRange, Granularity, GroupBy, + HoldingMode, Market, MarketAdvanced, MarketDataPreset,