diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 3d81435ab..b15c3efc3 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -27,12 +27,13 @@ import { } from '@ghostfolio/common/interfaces'; import { MarketDataPreset } from '@ghostfolio/common/types'; -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { AssetClass, AssetSubClass, DataSource, Prisma, + PrismaClient, Property, SymbolProfile } from '@prisma/client'; @@ -212,98 +213,113 @@ export class AdminService { } } - let [assetProfiles, count] = await Promise.all([ - this.prismaService.symbolProfile.findMany({ - orderBy, - skip, - take, - where, - select: { - _count: { - select: { Order: true } - }, - assetClass: true, - assetSubClass: true, - comment: true, - countries: true, - currency: true, - dataSource: true, - id: true, - name: true, - Order: { - orderBy: [{ date: 'asc' }], - select: { date: true }, - take: 1 - }, - scraperConfiguration: true, - sectors: true, - symbol: true - } - }), - this.prismaService.symbolProfile.count({ where }) - ]); + const extendedPrismaClient = this.getExtendedPrismaClient(); - let marketData: AdminMarketDataItem[] = assetProfiles.map( - ({ - _count, - assetClass, - assetSubClass, - comment, - countries, - currency, - dataSource, - id, - name, - 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; + try { + let [assetProfiles, count] = await Promise.all([ + extendedPrismaClient.symbolProfile.findMany({ + orderBy, + skip, + take, + where, + select: { + _count: { + select: { Order: true } + }, + assetClass: true, + assetSubClass: true, + comment: true, + countries: true, + currency: true, + dataSource: true, + id: true, + isUsedByUsersWithSubscription: true, + name: true, + Order: { + orderBy: [{ date: 'asc' }], + select: { date: true }, + take: 1 + }, + scraperConfiguration: true, + sectors: true, + symbol: true + } + }), + this.prismaService.symbolProfile.count({ where }) + ]); - return { - assetClass, - assetSubClass, - comment, - currency, - countriesCount, - dataSource, - id, - name, - symbol, - marketDataItemCount, - sectorsCount, - activitiesCount: _count.Order, - date: Order?.[0]?.date - }; - } - ); + let marketData: AdminMarketDataItem[] = await Promise.all( + assetProfiles.map( + async ({ + _count, + assetClass, + assetSubClass, + comment, + countries, + currency, + dataSource, + id, + isUsedByUsersWithSubscription, + name, + 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, + currency, + countriesCount, + dataSource, + id, + name, + symbol, + marketDataItemCount, + sectorsCount, + activitiesCount: _count.Order, + date: Order?.[0]?.date, + isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription + }; + } + ) + ); - if (presetId) { - if (presetId === 'ETF_WITHOUT_COUNTRIES') { - marketData = marketData.filter(({ countriesCount }) => { - return countriesCount === 0; - }); - } else if (presetId === 'ETF_WITHOUT_SECTORS') { - marketData = marketData.filter(({ sectorsCount }) => { - return sectorsCount === 0; - }); + if (presetId) { + if (presetId === 'ETF_WITHOUT_COUNTRIES') { + marketData = marketData.filter(({ countriesCount }) => { + return countriesCount === 0; + }); + } else if (presetId === 'ETF_WITHOUT_SECTORS') { + marketData = marketData.filter(({ sectorsCount }) => { + return sectorsCount === 0; + }); + } + + count = marketData.length; } - count = marketData.length; - } + return { + count, + marketData + }; + } finally { + await extendedPrismaClient.$disconnect(); - return { - count, - marketData - }; + Logger.debug('Disconnect extended prisma client', 'AdminService'); + } } public async getMarketDataBySymbol({ @@ -431,6 +447,52 @@ export class AdminService { return response; } + private getExtendedPrismaClient() { + Logger.debug('Connect extended prisma client', 'AdminService'); + + const symbolProfileExtension = Prisma.defineExtension((client) => { + return client.$extends({ + result: { + symbolProfile: { + isUsedByUsersWithSubscription: { + compute: async ({ id }) => { + const { _count } = + await this.prismaService.symbolProfile.findUnique({ + select: { + _count: { + select: { + Order: { + where: { + User: { + Subscription: { + some: { + expiresAt: { + gt: new Date() + } + } + } + } + } + } + } + } + }, + where: { + id + } + }); + + return _count.Order > 0; + } + } + } + } + }); + }); + + return new PrismaClient().$extends(symbolProfileExtension); + } + private async getMarketDataForCurrencies(): Promise { const marketDataItems = await this.prismaService.marketData.groupBy({ _count: true, 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 5494e6842..e27283517 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 @@ -6,8 +6,14 @@ import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config'; import { getDateFormatString } from '@ghostfolio/common/helper'; -import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces'; +import { + Filter, + InfoItem, + UniqueAsset, + User +} from '@ghostfolio/common/interfaces'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { translate } from '@ghostfolio/ui/i18n'; import { SelectionModel } from '@angular/cdk/collections'; @@ -97,22 +103,11 @@ export class AdminMarketDataComponent new MatTableDataSource(); public defaultDateFormat: string; public deviceType: string; - public displayedColumns = [ - 'select', - 'nameWithSymbol', - 'dataSource', - 'assetClass', - 'assetSubClass', - 'date', - 'activitiesCount', - 'marketDataItemCount', - 'sectorsCount', - 'countriesCount', - 'comment', - 'actions' - ]; + public displayedColumns: string[] = []; public filters$ = new Subject(); public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix; + public hasPermissionForSubscription: boolean; + public info: InfoItem; public isLoading = false; public isUUID = isUUID; public placeholder = ''; @@ -134,6 +129,33 @@ export class AdminMarketDataComponent private router: Router, private userService: UserService ) { + this.info = this.dataService.fetchInfo(); + + this.hasPermissionForSubscription = hasPermission( + this.info?.globalPermissions, + permissions.enableSubscription + ); + + this.displayedColumns = [ + 'select', + 'nameWithSymbol', + 'dataSource', + 'assetClass', + 'assetSubClass', + 'date', + 'activitiesCount', + 'marketDataItemCount', + 'sectorsCount', + 'countriesCount' + ]; + + if (this.hasPermissionForSubscription) { + this.displayedColumns.push('isUsedByUsersWithSubscription'); + } + + this.displayedColumns.push('comment'); + this.displayedColumns.push('actions'); + this.route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html index 3dc3dd5a9..f3b2d8ddd 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.html +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html @@ -144,6 +144,15 @@ + + + + @if (element.isUsedByUsersWithSubscription) { + + } + + + diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts index 87562460a..224e3506b 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts @@ -1,5 +1,6 @@ import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter'; +import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; @@ -24,6 +25,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/ GfActivitiesFilterComponent, GfAssetProfileDialogModule, GfCreateAssetProfileDialogModule, + GfPremiumIndicatorComponent, GfSymbolModule, MatButtonModule, MatCheckboxModule, diff --git a/libs/common/src/lib/interfaces/admin-market-data.interface.ts b/libs/common/src/lib/interfaces/admin-market-data.interface.ts index d52ac03b9..420bde826 100644 --- a/libs/common/src/lib/interfaces/admin-market-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-market-data.interface.ts @@ -15,6 +15,7 @@ export interface AdminMarketDataItem { date: Date; id: string; isBenchmark?: boolean; + isUsedByUsersWithSubscription?: boolean; marketDataItemCount: number; name: string; sectorsCount: number;