diff --git a/CHANGELOG.md b/CHANGELOG.md index 646b2805..074737a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added hints to the activity types in the create or edit activity dialog +- Added queries to the historical market data table of the admin control panel ### Changed diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 75b2b2bb..6077c19f 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -15,7 +15,10 @@ import { Filter } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import type { RequestWithUser } from '@ghostfolio/common/types'; +import type { + MarketDataQuery, + RequestWithUser +} from '@ghostfolio/common/types'; import { Body, Controller, @@ -249,6 +252,7 @@ export class AdminController { @UseGuards(AuthGuard('jwt')) public async getMarketData( @Query('assetSubClasses') filterByAssetSubClasses?: string, + @Query('queryId') queryId?: MarketDataQuery, @Query('skip') skip?: number, @Query('sortColumn') sortColumn?: string, @Query('sortDirection') sortDirection?: Prisma.SortOrder, @@ -279,6 +283,7 @@ export class AdminController { return this.adminService.getMarketData({ filters, + queryId, sortColumn, sortDirection, skip: isNaN(skip) ? undefined : skip, diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 4dce7798..003f930b 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -17,6 +17,7 @@ import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces'; +import { MarketDataQuery } from '@ghostfolio/common/types'; import { BadRequestException, Injectable } from '@nestjs/common'; import { AssetSubClass, Prisma, Property, SymbolProfile } from '@prisma/client'; import { differenceInDays } from 'date-fns'; @@ -103,12 +104,14 @@ export class AdminService { public async getMarketData({ filters, + queryId, sortColumn, sortDirection, skip, - take = DEFAULT_PAGE_SIZE + take = Number.MAX_SAFE_INTEGER }: { filters?: Filter[]; + queryId?: MarketDataQuery; skip?: number; sortColumn?: string; sortDirection?: Prisma.SortOrder; @@ -118,6 +121,13 @@ export class AdminService { [{ symbol: 'asc' }]; const where: Prisma.SymbolProfileWhereInput = {}; + if ( + queryId === 'ETF_WITHOUT_COUNTRIES' || + queryId === 'ETF_WITHOUT_SECTORS' + ) { + filters = [{ id: 'ETF', type: 'ASSET_SUB_CLASS' }]; + } + const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy( filters, (filter) => { @@ -146,7 +156,7 @@ export class AdminService { } } - const [assetProfiles, count] = await Promise.all([ + let [assetProfiles, count] = await Promise.all([ this.prismaService.symbolProfile.findMany({ orderBy, skip, @@ -174,44 +184,60 @@ export class AdminService { this.prismaService.symbolProfile.count({ where }) ]); - return { - count, - marketData: assetProfiles.map( - ({ - _count, + let marketData = assetProfiles.map( + ({ + _count, + assetClass, + assetSubClass, + comment, + countries, + dataSource, + Order, + sectors, + symbol + }) => { + const countriesCount = countries ? Object.keys(countries).length : 0; + const marketDataItemCount = + marketDataItems.find((marketDataItem) => { + return ( + marketDataItem.dataSource === dataSource && + marketDataItem.symbol === symbol + ); + })?._count ?? 0; + const sectorsCount = sectors ? Object.keys(sectors).length : 0; + + return { assetClass, assetSubClass, comment, - countries, + countriesCount, dataSource, - Order, - sectors, - symbol - }) => { - const countriesCount = countries ? Object.keys(countries).length : 0; - const marketDataItemCount = - marketDataItems.find((marketDataItem) => { - return ( - marketDataItem.dataSource === dataSource && - marketDataItem.symbol === symbol - ); - })?._count ?? 0; - const sectorsCount = sectors ? Object.keys(sectors).length : 0; + symbol, + marketDataItemCount, + sectorsCount, + activitiesCount: _count.Order, + date: Order?.[0]?.date + }; + } + ); - return { - assetClass, - assetSubClass, - comment, - countriesCount, - dataSource, - symbol, - marketDataItemCount, - sectorsCount, - activitiesCount: _count.Order, - date: Order?.[0]?.date - }; - } - ) + if (queryId) { + if (queryId === 'ETF_WITHOUT_COUNTRIES') { + marketData = marketData.filter(({ countriesCount }) => { + return countriesCount === 0; + }); + } else if (queryId === 'ETF_WITHOUT_SECTORS') { + marketData = marketData.filter(({ sectorsCount }) => { + return sectorsCount === 0; + }); + } + + count = marketData.length; + } + + return { + count, + marketData }; } diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index 37c7a666..716b5ff3 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -51,13 +51,26 @@ export class AdminMarketDataComponent AssetSubClass.PRECIOUS_METAL, AssetSubClass.PRIVATE_EQUITY, AssetSubClass.STOCK - ].map((assetSubClass) => { - return { - id: assetSubClass, - label: translate(assetSubClass), - type: 'ASSET_SUB_CLASS' - }; - }); + ] + .map((assetSubClass) => { + return { + id: assetSubClass.toString(), + label: translate(assetSubClass), + type: 'ASSET_SUB_CLASS' + }; + }) + .concat([ + { + id: 'ETF_WITHOUT_COUNTRIES', + label: $localize`ETFs without Countries`, + type: 'QUERY_ID' + }, + { + id: 'ETF_WITHOUT_SECTORS', + label: $localize`ETFs without Sectors`, + type: 'QUERY_ID' + } + ]); public currentDataSource: DataSource; public currentSymbol: string; public dataSource: MatTableDataSource = @@ -237,6 +250,12 @@ export class AdminMarketDataComponent ) { this.isLoading = true; + this.pageSize = + this.activeFilters.length === 1 && + this.activeFilters[0].type === 'QUERY_ID' + ? undefined + : DEFAULT_PAGE_SIZE; + if (pageIndex === 0 && this.paginator) { this.paginator.pageIndex = 0; } diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index d1967aaf..3f72ff01 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -95,7 +95,9 @@ export class AdminService { params = params.append('sortDirection', sortDirection); } - params = params.append('take', take); + if (take) { + params = params.append('take', take); + } return this.http.get('/api/v1/admin/market-data', { params diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index f2e5169d..99416ae5 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -57,6 +57,7 @@ export class DataService { ACCOUNT: filtersByAccount, ASSET_CLASS: filtersByAssetClass, ASSET_SUB_CLASS: filtersByAssetSubClass, + QUERY_ID: filtersByQueryId, TAG: filtersByTag } = groupBy(filters, (filter) => { return filter.type; @@ -95,6 +96,10 @@ export class DataService { ); } + if (filtersByQueryId) { + params = params.append('queryId', filtersByQueryId[0].id); + } + if (filtersByTag) { params = params.append( 'tags', diff --git a/libs/common/src/lib/interfaces/filter.interface.ts b/libs/common/src/lib/interfaces/filter.interface.ts index 6695de88..f6c83a23 100644 --- a/libs/common/src/lib/interfaces/filter.interface.ts +++ b/libs/common/src/lib/interfaces/filter.interface.ts @@ -1,5 +1,11 @@ export interface Filter { id: string; label?: string; - type: 'ACCOUNT' | 'ASSET_CLASS' | 'ASSET_SUB_CLASS' | 'SYMBOL' | 'TAG'; + type: + | 'ACCOUNT' + | 'ASSET_CLASS' + | 'ASSET_SUB_CLASS' + | 'QUERY_ID' + | 'SYMBOL' + | 'TAG'; } diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts index dc6f154d..79566bcf 100644 --- a/libs/common/src/lib/types/index.ts +++ b/libs/common/src/lib/types/index.ts @@ -5,6 +5,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 { MarketDataQuery } from './market-data-query.type'; import type { MarketState } from './market-state.type'; import type { Market } from './market.type'; import type { OrderWithAccount } from './order-with-account.type'; @@ -23,6 +24,7 @@ export type { Granularity, GroupBy, Market, + MarketDataQuery, MarketState, OrderWithAccount, RequestWithUser, diff --git a/libs/common/src/lib/types/market-data-query.type.ts b/libs/common/src/lib/types/market-data-query.type.ts new file mode 100644 index 00000000..fa14af61 --- /dev/null +++ b/libs/common/src/lib/types/market-data-query.type.ts @@ -0,0 +1 @@ +export type MarketDataQuery = 'ETF_WITHOUT_COUNTRIES' | 'ETF_WITHOUT_SECTORS'; diff --git a/libs/ui/src/lib/i18n.ts b/libs/ui/src/lib/i18n.ts index 38d92e5f..cd879497 100644 --- a/libs/ui/src/lib/i18n.ts +++ b/libs/ui/src/lib/i18n.ts @@ -16,6 +16,7 @@ const locales = { MONTH: $localize`Month`, MONTHS: $localize`Months`, OTHER: $localize`Other`, + QUERY_ID: $localize`Query`, RETIREMENT_PROVISION: $localize`Retirement Provision`, SATELLITE: $localize`Satellite`, SECURITIES: $localize`Securities`,