Browse Source

Introduce filter by quantity

pull/3146/head
Thomas Kaul 1 year ago
parent
commit
d9ce8f1a67
  1. 4
      apps/api/src/app/portfolio/portfolio.controller.ts
  2. 35
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 10
      apps/api/src/services/api/api.service.ts
  4. 28
      apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts
  5. 9
      apps/client/src/app/pages/portfolio/holdings/holdings-page.html
  6. 2
      apps/client/src/app/pages/portfolio/holdings/holdings-page.module.ts
  7. 5
      apps/client/src/app/services/data.service.ts
  8. 1
      libs/common/src/lib/interfaces/filter.interface.ts
  9. 1
      libs/common/src/lib/types/holding-mode.type.ts
  10. 2
      libs/common/src/lib/types/index.ts

4
apps/api/src/app/portfolio/portfolio.controller.ts

@ -284,13 +284,14 @@ export class PortfolioController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string, @Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string, @Query('assetClasses') filterByAssetClasses?: string,
@Query('quantity') filterByQuantity?: string,
@Query('query') filterBySearchQuery?: string, @Query('query') filterBySearchQuery?: string,
@Query('pastInvestments') pastInvestments: boolean = false,
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string
): Promise<PortfolioHoldingsResponse> { ): Promise<PortfolioHoldingsResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
filterByQuantity,
filterBySearchQuery, filterBySearchQuery,
filterByTags filterByTags
}); });
@ -298,7 +299,6 @@ export class PortfolioController {
const { holdings } = await this.portfolioService.getDetails({ const { holdings } = await this.portfolioService.getDetails({
filters, filters,
impersonationId, impersonationId,
pastInvestments,
userId: this.request.user.id userId: this.request.user.id
}); });

35
apps/api/src/app/portfolio/portfolio.service.ts

@ -336,7 +336,6 @@ export class PortfolioService {
dateRange = 'max', dateRange = 'max',
filters, filters,
impersonationId, impersonationId,
pastInvestments,
userId, userId,
withExcludedAccounts = false, withExcludedAccounts = false,
withLiabilities = false, withLiabilities = false,
@ -345,7 +344,6 @@ export class PortfolioService {
dateRange?: DateRange; dateRange?: DateRange;
filters?: Filter[]; filters?: Filter[];
impersonationId: string; impersonationId: string;
pastInvestments?: boolean;
userId: string; userId: string;
withExcludedAccounts?: boolean; withExcludedAccounts?: boolean;
withLiabilities?: boolean; withLiabilities?: boolean;
@ -401,8 +399,17 @@ export class PortfolioService {
); );
const isFilteredByAccount = const isFilteredByAccount =
filters?.some((filter) => { filters?.some(({ type }) => {
return filter.type === 'ACCOUNT'; 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; }) ?? false;
let filteredValueInBaseCurrency = isFilteredByAccount let filteredValueInBaseCurrency = isFilteredByAccount
@ -454,7 +461,6 @@ export class PortfolioService {
grossPerformancePercentageWithCurrencyEffect, grossPerformancePercentageWithCurrencyEffect,
investment, investment,
marketPrice, marketPrice,
marketPriceInBaseCurrency,
netPerformance, netPerformance,
netPerformancePercentage, netPerformancePercentage,
netPerformancePercentageWithCurrencyEffect, netPerformancePercentageWithCurrencyEffect,
@ -465,11 +471,16 @@ export class PortfolioService {
transactionCount, transactionCount,
valueInBaseCurrency valueInBaseCurrency
} of currentPositions.positions) { } of currentPositions.positions) {
if (quantity.eq(0) && pastInvestments === false) { if (isFilteredByQuantityEqualsZero === true) {
// Ignore positions without any quantity if past investments are not requested if (!quantity.eq(0)) {
continue; // Ignore positions with a quantity
} else if (!quantity.eq(0) && pastInvestments === true) { continue;
continue; }
} else {
if (quantity.eq(0)) {
// Ignore positions without any quantity
continue;
}
} }
const symbolProfile = symbolProfileMap[symbol]; 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) { if (filters?.length === 0 || isFilteredByAccount || isFilteredByCash) {
const cashPositions = await this.getCashPositions({ const cashPositions = await this.getCashPositions({
cashDetails, cashDetails,

10
apps/api/src/services/api/api.service.ts

@ -10,18 +10,21 @@ export class ApiService {
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
filterByAssetSubClasses, filterByAssetSubClasses,
filterByQuantity,
filterBySearchQuery, filterBySearchQuery,
filterByTags filterByTags
}: { }: {
filterByAccounts?: string; filterByAccounts?: string;
filterByAssetClasses?: string; filterByAssetClasses?: string;
filterByAssetSubClasses?: string; filterByAssetSubClasses?: string;
filterByQuantity?: string;
filterBySearchQuery?: string; filterBySearchQuery?: string;
filterByTags?: string; filterByTags?: string;
}): Filter[] { }): Filter[] {
const accountIds = filterByAccounts?.split(',') ?? []; const accountIds = filterByAccounts?.split(',') ?? [];
const assetClasses = filterByAssetClasses?.split(',') ?? []; const assetClasses = filterByAssetClasses?.split(',') ?? [];
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? []; const assetSubClasses = filterByAssetSubClasses?.split(',') ?? [];
const quantity = filterByQuantity;
const searchQuery = filterBySearchQuery?.toLowerCase(); const searchQuery = filterBySearchQuery?.toLowerCase();
const tagIds = filterByTags?.split(',') ?? []; const tagIds = filterByTags?.split(',') ?? [];
@ -52,6 +55,13 @@ export class ApiService {
}) })
]; ];
if (quantity) {
filters.push({
id: quantity,
type: 'QUANTITY'
});
}
if (searchQuery) { if (searchQuery) {
filters.push({ filters.push({
id: searchQuery, id: searchQuery,

28
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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces'; import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { HoldingMode, ToggleOption } from '@ghostfolio/common/types';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@ -24,6 +25,11 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean; public hasPermissionToCreateOrder: boolean;
public holdings: PortfolioPosition[]; public holdings: PortfolioPosition[];
public mode: HoldingMode = 'ACTIVE';
public modeOptions: ToggleOption[] = [
{ label: $localize`Active`, value: 'ACTIVE' },
{ label: $localize`Closed`, value: 'CLOSED' }
];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -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() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private fetchHoldings() { private fetchHoldings() {
const filters = this.userService.getFilters();
if (this.mode === 'CLOSED') {
filters.push({ id: '0', type: 'QUANTITY' });
}
return this.dataService.fetchPortfolioHoldings({ return this.dataService.fetchPortfolioHoldings({
filters: this.userService.getFilters() filters
}); });
} }

9
apps/client/src/app/pages/portfolio/holdings/holdings-page.html

@ -6,6 +6,15 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg"> <div class="col-lg">
<div class="d-flex justify-content-end">
<gf-toggle
class="d-none d-lg-block"
[defaultValue]="mode"
[isLoading]="false"
[options]="modeOptions"
(change)="onChangeMode($event.value)"
/>
</div>
<gf-holdings-table <gf-holdings-table
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"

2
apps/client/src/app/pages/portfolio/holdings/holdings-page.module.ts

@ -1,3 +1,4 @@
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfHoldingsTableModule } from '@ghostfolio/ui/holdings-table/holdings-table.module'; import { GfHoldingsTableModule } from '@ghostfolio/ui/holdings-table/holdings-table.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@ -12,6 +13,7 @@ import { HoldingsPageComponent } from './holdings-page.component';
imports: [ imports: [
CommonModule, CommonModule,
GfHoldingsTableModule, GfHoldingsTableModule,
GfToggleModule,
HoldingsPageRoutingModule, HoldingsPageRoutingModule,
MatButtonModule MatButtonModule
], ],

5
apps/client/src/app/services/data.service.ts

@ -63,6 +63,7 @@ export class DataService {
ASSET_CLASS: filtersByAssetClass, ASSET_CLASS: filtersByAssetClass,
ASSET_SUB_CLASS: filtersByAssetSubClass, ASSET_SUB_CLASS: filtersByAssetSubClass,
PRESET_ID: filtersByPresetId, PRESET_ID: filtersByPresetId,
QUANTITY: filtersBySearchQuantity,
SEARCH_QUERY: filtersBySearchQuery, SEARCH_QUERY: filtersBySearchQuery,
TAG: filtersByTag TAG: filtersByTag
} = groupBy(filters, (filter) => { } = groupBy(filters, (filter) => {
@ -106,6 +107,10 @@ export class DataService {
params = params.append('presetId', filtersByPresetId[0].id); params = params.append('presetId', filtersByPresetId[0].id);
} }
if (filtersBySearchQuantity) {
params = params.append('quantity', filtersBySearchQuantity[0].id);
}
if (filtersBySearchQuery) { if (filtersBySearchQuery) {
params = params.append('query', filtersBySearchQuery[0].id); params = params.append('query', filtersBySearchQuery[0].id);
} }

1
libs/common/src/lib/interfaces/filter.interface.ts

@ -6,6 +6,7 @@ export interface Filter {
| 'ASSET_CLASS' | 'ASSET_CLASS'
| 'ASSET_SUB_CLASS' | 'ASSET_SUB_CLASS'
| 'PRESET_ID' | 'PRESET_ID'
| 'QUANTITY'
| 'SEARCH_QUERY' | 'SEARCH_QUERY'
| 'SYMBOL' | 'SYMBOL'
| 'TAG'; | 'TAG';

1
libs/common/src/lib/types/holding-mode.type.ts

@ -0,0 +1 @@
export type HoldingMode = 'ACTIVE' | 'CLOSED';

2
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 { DateRange } from './date-range.type';
import type { Granularity } from './granularity.type'; import type { Granularity } from './granularity.type';
import type { GroupBy } from './group-by.type'; import type { GroupBy } from './group-by.type';
import type { HoldingMode } from './holding-mode.type';
import type { MarketAdvanced } from './market-advanced.type'; import type { MarketAdvanced } from './market-advanced.type';
import type { MarketDataPreset } from './market-data-preset.type'; import type { MarketDataPreset } from './market-data-preset.type';
import type { MarketState } from './market-state.type'; import type { MarketState } from './market-state.type';
@ -28,6 +29,7 @@ export type {
DateRange, DateRange,
Granularity, Granularity,
GroupBy, GroupBy,
HoldingMode,
Market, Market,
MarketAdvanced, MarketAdvanced,
MarketDataPreset, MarketDataPreset,

Loading…
Cancel
Save