Browse Source
			
			
			Feature/support filtering by asset class on the allocations page (#926)
			
				* Support filtering by asset class
* Update changelog
			
			
				pull/927/head
			
			
		 
		
			
				
					
						 Thomas Kaul
					
					4 years ago
						Thomas Kaul
					
					4 years ago
					
						
							committed by
							
								 GitHub
								GitHub
							
						 
					
				 
				
			 
		 
		
			
				
					
					No known key found for this signature in database
					
						
							GPG Key ID: 4AEE18F83AFDEB23
						
					
				
			
		
		
		
	
		
			
				 8 changed files with 
96 additions and 
23 deletions
			 
			
		 
		
			
				- 
					
					
					 
					CHANGELOG.md
				
- 
					
					
					 
					apps/api/src/app/account/account.service.ts
				
- 
					
					
					 
					apps/api/src/app/order/order.service.ts
				
- 
					
					
					 
					apps/api/src/app/portfolio/portfolio.controller.ts
				
- 
					
					
					 
					apps/api/src/app/portfolio/portfolio.service.ts
				
- 
					
					
					 
					apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
				
- 
					
					
					 
					apps/client/src/app/services/data.service.ts
				
- 
					
					
					 
					libs/common/src/lib/interfaces/filter.interface.ts
				
				
				
					
						
							
								
									
	
		
			
				
					|  |  | @ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | 
			
		
	
		
			
				
					|  |  |  | ### Added | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | - Added groups to the activities filter component | 
			
		
	
		
			
				
					|  |  |  | - Added support for filtering by asset class on the allocations page | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | ## 1.148.0 - 14.05.2022 | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  | 
 | 
			
		
	
								
							
						
					 
					
				 
			 
		
			
			
			
			
			
			
				
				
					
						
							
								
									
	
		
			
				
					|  |  | @ -4,6 +4,7 @@ import { Filter } from '@ghostfolio/common/interfaces'; | 
			
		
	
		
			
				
					|  |  |  | import { Injectable } from '@nestjs/common'; | 
			
		
	
		
			
				
					|  |  |  | import { Account, Order, Platform, Prisma } from '@prisma/client'; | 
			
		
	
		
			
				
					|  |  |  | import Big from 'big.js'; | 
			
		
	
		
			
				
					|  |  |  | import { groupBy } from 'lodash'; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | import { CashDetails } from './interfaces/cash-details.interface'; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  | @ -116,15 +117,19 @@ export class AccountService { | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     const where: Prisma.AccountWhereInput = { userId }; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if (filters?.length > 0) { | 
			
		
	
		
			
				
					|  |  |  |     const { | 
			
		
	
		
			
				
					|  |  |  |       ACCOUNT: filtersByAccount, | 
			
		
	
		
			
				
					|  |  |  |       ASSET_CLASS: filtersByAssetClass, | 
			
		
	
		
			
				
					|  |  |  |       TAG: filtersByTag | 
			
		
	
		
			
				
					|  |  |  |     } = groupBy(filters, (filter) => { | 
			
		
	
		
			
				
					|  |  |  |       return filter.type; | 
			
		
	
		
			
				
					|  |  |  |     }); | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if (filtersByAccount?.length > 0) { | 
			
		
	
		
			
				
					|  |  |  |       where.id = { | 
			
		
	
		
			
				
					|  |  |  |         in: filters | 
			
		
	
		
			
				
					|  |  |  |           .filter(({ type }) => { | 
			
		
	
		
			
				
					|  |  |  |             return type === 'ACCOUNT'; | 
			
		
	
		
			
				
					|  |  |  |           }) | 
			
		
	
		
			
				
					|  |  |  |           .map(({ id }) => { | 
			
		
	
		
			
				
					|  |  |  |             return id; | 
			
		
	
		
			
				
					|  |  |  |           }) | 
			
		
	
		
			
				
					|  |  |  |         in: filtersByAccount.map(({ id }) => { | 
			
		
	
		
			
				
					|  |  |  |           return id; | 
			
		
	
		
			
				
					|  |  |  |         }) | 
			
		
	
		
			
				
					|  |  |  |       }; | 
			
		
	
		
			
				
					|  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  | 
 | 
			
		
	
								
							
						
					 
					
				 
			 
		
			
			
			
			
			
			
				
				
					
						
							
								
									
	
		
			
				
					|  |  | @ -188,12 +188,13 @@ export class OrderService { | 
			
		
	
		
			
				
					|  |  |  |   }): Promise<Activity[]> { | 
			
		
	
		
			
				
					|  |  |  |     const where: Prisma.OrderWhereInput = { userId }; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     const { ACCOUNT: filtersByAccount, TAG: filtersByTag } = groupBy( | 
			
		
	
		
			
				
					|  |  |  |       filters, | 
			
		
	
		
			
				
					|  |  |  |       (filter) => { | 
			
		
	
		
			
				
					|  |  |  |         return filter.type; | 
			
		
	
		
			
				
					|  |  |  |       } | 
			
		
	
		
			
				
					|  |  |  |     ); | 
			
		
	
		
			
				
					|  |  |  |     const { | 
			
		
	
		
			
				
					|  |  |  |       ACCOUNT: filtersByAccount, | 
			
		
	
		
			
				
					|  |  |  |       ASSET_CLASS: filtersByAssetClass, | 
			
		
	
		
			
				
					|  |  |  |       TAG: filtersByTag | 
			
		
	
		
			
				
					|  |  |  |     } = groupBy(filters, (filter) => { | 
			
		
	
		
			
				
					|  |  |  |       return filter.type; | 
			
		
	
		
			
				
					|  |  |  |     }); | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if (filtersByAccount?.length > 0) { | 
			
		
	
		
			
				
					|  |  |  |       where.accountId = { | 
			
		
	
	
		
			
				
					|  |  | @ -207,6 +208,34 @@ export class OrderService { | 
			
		
	
		
			
				
					|  |  |  |       where.isDraft = false; | 
			
		
	
		
			
				
					|  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if (filtersByAssetClass?.length > 0) { | 
			
		
	
		
			
				
					|  |  |  |       where.SymbolProfile = { | 
			
		
	
		
			
				
					|  |  |  |         OR: [ | 
			
		
	
		
			
				
					|  |  |  |           { | 
			
		
	
		
			
				
					|  |  |  |             AND: [ | 
			
		
	
		
			
				
					|  |  |  |               { | 
			
		
	
		
			
				
					|  |  |  |                 OR: filtersByAssetClass.map(({ id }) => { | 
			
		
	
		
			
				
					|  |  |  |                   return { assetClass: AssetClass[id] }; | 
			
		
	
		
			
				
					|  |  |  |                 }) | 
			
		
	
		
			
				
					|  |  |  |               }, | 
			
		
	
		
			
				
					|  |  |  |               { | 
			
		
	
		
			
				
					|  |  |  |                 SymbolProfileOverrides: { | 
			
		
	
		
			
				
					|  |  |  |                   is: null | 
			
		
	
		
			
				
					|  |  |  |                 } | 
			
		
	
		
			
				
					|  |  |  |               } | 
			
		
	
		
			
				
					|  |  |  |             ] | 
			
		
	
		
			
				
					|  |  |  |           }, | 
			
		
	
		
			
				
					|  |  |  |           { | 
			
		
	
		
			
				
					|  |  |  |             SymbolProfileOverrides: { | 
			
		
	
		
			
				
					|  |  |  |               OR: filtersByAssetClass.map(({ id }) => { | 
			
		
	
		
			
				
					|  |  |  |                 return { assetClass: AssetClass[id] }; | 
			
		
	
		
			
				
					|  |  |  |               }) | 
			
		
	
		
			
				
					|  |  |  |             } | 
			
		
	
		
			
				
					|  |  |  |           } | 
			
		
	
		
			
				
					|  |  |  |         ] | 
			
		
	
		
			
				
					|  |  |  |       }; | 
			
		
	
		
			
				
					|  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if (filtersByTag?.length > 0) { | 
			
		
	
		
			
				
					|  |  |  |       where.tags = { | 
			
		
	
		
			
				
					|  |  |  |         some: { | 
			
		
	
	
		
			
				
					|  |  | 
 | 
			
		
	
								
							
						
					 
					
				 
			 
		
			
			
			
			
			
			
				
				
					
						
							
								
									
	
		
			
				
					|  |  | @ -107,12 +107,14 @@ export class PortfolioController { | 
			
		
	
		
			
				
					|  |  |  |   public async getDetails( | 
			
		
	
		
			
				
					|  |  |  |     @Headers('impersonation-id') impersonationId: string, | 
			
		
	
		
			
				
					|  |  |  |     @Query('accounts') filterByAccounts?: string, | 
			
		
	
		
			
				
					|  |  |  |     @Query('assetClasses') filterByAssetClasses?: string, | 
			
		
	
		
			
				
					|  |  |  |     @Query('range') range?: DateRange, | 
			
		
	
		
			
				
					|  |  |  |     @Query('tags') filterByTags?: string | 
			
		
	
		
			
				
					|  |  |  |   ): Promise<PortfolioDetails & { hasError: boolean }> { | 
			
		
	
		
			
				
					|  |  |  |     let hasError = false; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     const accountIds = filterByAccounts?.split(',') ?? []; | 
			
		
	
		
			
				
					|  |  |  |     const assetClasses = filterByAssetClasses?.split(',') ?? []; | 
			
		
	
		
			
				
					|  |  |  |     const tagIds = filterByTags?.split(',') ?? []; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     const filters: Filter[] = [ | 
			
		
	
	
		
			
				
					|  |  | @ -122,6 +124,12 @@ export class PortfolioController { | 
			
		
	
		
			
				
					|  |  |  |           type: 'ACCOUNT' | 
			
		
	
		
			
				
					|  |  |  |         }; | 
			
		
	
		
			
				
					|  |  |  |       }), | 
			
		
	
		
			
				
					|  |  |  |       ...assetClasses.map((assetClass) => { | 
			
		
	
		
			
				
					|  |  |  |         return <Filter>{ | 
			
		
	
		
			
				
					|  |  |  |           id: assetClass, | 
			
		
	
		
			
				
					|  |  |  |           type: 'ASSET_CLASS' | 
			
		
	
		
			
				
					|  |  |  |         }; | 
			
		
	
		
			
				
					|  |  |  |       }), | 
			
		
	
		
			
				
					|  |  |  |       ...tagIds.map((tagId) => { | 
			
		
	
		
			
				
					|  |  |  |         return <Filter>{ | 
			
		
	
		
			
				
					|  |  |  |           id: tagId, | 
			
		
	
	
		
			
				
					|  |  | 
 | 
			
		
	
								
							
						
					 
					
				 
			 
		
			
			
			
			
			
			
				
				
					
						
							
								
									
	
		
			
				
					|  |  | @ -441,7 +441,12 @@ export class PortfolioService { | 
			
		
	
		
			
				
					|  |  |  |       }; | 
			
		
	
		
			
				
					|  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if (aFilters?.length === 0) { | 
			
		
	
		
			
				
					|  |  |  |     if ( | 
			
		
	
		
			
				
					|  |  |  |       aFilters?.length === 0 || | 
			
		
	
		
			
				
					|  |  |  |       (aFilters?.length === 1 && | 
			
		
	
		
			
				
					|  |  |  |         aFilters[0].type === 'ASSET_CLASS' && | 
			
		
	
		
			
				
					|  |  |  |         aFilters[0].id === 'CASH') | 
			
		
	
		
			
				
					|  |  |  |     ) { | 
			
		
	
		
			
				
					|  |  |  |       const cashPositions = await this.getCashPositions({ | 
			
		
	
		
			
				
					|  |  |  |         cashDetails, | 
			
		
	
		
			
				
					|  |  |  |         emergencyFund, | 
			
		
	
	
		
			
				
					|  |  | 
 | 
			
		
	
								
							
						
					 
					
				 
			 
		
			
			
			
			
			
			
				
				
					
						
							
								
									
	
		
			
				
					|  |  | @ -172,6 +172,15 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { | 
			
		
	
		
			
				
					|  |  |  |               }; | 
			
		
	
		
			
				
					|  |  |  |             }); | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |           const assetClassFilters: Filter[] = []; | 
			
		
	
		
			
				
					|  |  |  |           for (const assetClass of Object.keys(AssetClass)) { | 
			
		
	
		
			
				
					|  |  |  |             assetClassFilters.push({ | 
			
		
	
		
			
				
					|  |  |  |               id: assetClass, | 
			
		
	
		
			
				
					|  |  |  |               label: assetClass, | 
			
		
	
		
			
				
					|  |  |  |               type: 'ASSET_CLASS' | 
			
		
	
		
			
				
					|  |  |  |             }); | 
			
		
	
		
			
				
					|  |  |  |           } | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |           const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => { | 
			
		
	
		
			
				
					|  |  |  |             return { | 
			
		
	
		
			
				
					|  |  |  |               id, | 
			
		
	
	
		
			
				
					|  |  | @ -180,7 +189,11 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { | 
			
		
	
		
			
				
					|  |  |  |             }; | 
			
		
	
		
			
				
					|  |  |  |           }); | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |           this.allFilters = [...accountFilters, ...tagFilters]; | 
			
		
	
		
			
				
					|  |  |  |           this.allFilters = [ | 
			
		
	
		
			
				
					|  |  |  |             ...accountFilters, | 
			
		
	
		
			
				
					|  |  |  |             ...assetClassFilters, | 
			
		
	
		
			
				
					|  |  |  |             ...tagFilters | 
			
		
	
		
			
				
					|  |  |  |           ]; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |           this.changeDetectorRef.markForCheck(); | 
			
		
	
		
			
				
					|  |  |  |         } | 
			
		
	
	
		
			
				
					|  |  | 
 | 
			
		
	
								
							
						
					 
					
				 
			 
		
			
			
			
			
			
			
				
				
					
						
							
								
									
	
		
			
				
					|  |  | @ -187,12 +187,13 @@ export class DataService { | 
			
		
	
		
			
				
					|  |  |  |     let params = new HttpParams(); | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     if (filters?.length > 0) { | 
			
		
	
		
			
				
					|  |  |  |       const { ACCOUNT: filtersByAccount, TAG: filtersByTag } = groupBy( | 
			
		
	
		
			
				
					|  |  |  |         filters, | 
			
		
	
		
			
				
					|  |  |  |         (filter) => { | 
			
		
	
		
			
				
					|  |  |  |           return filter.type; | 
			
		
	
		
			
				
					|  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |       ); | 
			
		
	
		
			
				
					|  |  |  |       const { | 
			
		
	
		
			
				
					|  |  |  |         ACCOUNT: filtersByAccount, | 
			
		
	
		
			
				
					|  |  |  |         ASSET_CLASS: filtersByAssetClass, | 
			
		
	
		
			
				
					|  |  |  |         TAG: filtersByTag | 
			
		
	
		
			
				
					|  |  |  |       } = groupBy(filters, (filter) => { | 
			
		
	
		
			
				
					|  |  |  |         return filter.type; | 
			
		
	
		
			
				
					|  |  |  |       }); | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |       if (filtersByAccount) { | 
			
		
	
		
			
				
					|  |  |  |         params = params.append( | 
			
		
	
	
		
			
				
					|  |  | @ -205,6 +206,17 @@ export class DataService { | 
			
		
	
		
			
				
					|  |  |  |         ); | 
			
		
	
		
			
				
					|  |  |  |       } | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |       if (filtersByAssetClass) { | 
			
		
	
		
			
				
					|  |  |  |         params = params.append( | 
			
		
	
		
			
				
					|  |  |  |           'assetClasses', | 
			
		
	
		
			
				
					|  |  |  |           filtersByAssetClass | 
			
		
	
		
			
				
					|  |  |  |             .map(({ id }) => { | 
			
		
	
		
			
				
					|  |  |  |               return id; | 
			
		
	
		
			
				
					|  |  |  |             }) | 
			
		
	
		
			
				
					|  |  |  |             .join(',') | 
			
		
	
		
			
				
					|  |  |  |         ); | 
			
		
	
		
			
				
					|  |  |  |       } | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |       if (filtersByTag) { | 
			
		
	
		
			
				
					|  |  |  |         params = params.append( | 
			
		
	
		
			
				
					|  |  |  |           'tags', | 
			
		
	
	
		
			
				
					|  |  | 
 | 
			
		
	
								
							
						
					 
					
				 
			 
		
			
			
			
			
			
			
				
				
					
						
							
								
									
	
		
			
				
					|  |  | @ -1,5 +1,5 @@ | 
			
		
	
		
			
				
					|  |  |  | export interface Filter { | 
			
		
	
		
			
				
					|  |  |  |   id: string; | 
			
		
	
		
			
				
					|  |  |  |   label?: string; | 
			
		
	
		
			
				
					|  |  |  |   type: 'ACCOUNT' | 'SYMBOL' | 'TAG'; | 
			
		
	
		
			
				
					|  |  |  |   type: 'ACCOUNT' | 'ASSET_CLASS' | 'SYMBOL' | 'TAG'; | 
			
		
	
		
			
				
					|  |  |  | } | 
			
		
	
	
		
			
				
					|  |  | 
 |