diff --git a/apps/api/src/app/activities/activities-filter.dto.ts b/apps/api/src/app/activities/activities-filter.dto.ts index e5a22c020..0c56aacc8 100644 --- a/apps/api/src/app/activities/activities-filter.dto.ts +++ b/apps/api/src/app/activities/activities-filter.dto.ts @@ -1,19 +1,13 @@ -import { DATE_RANGES } from '@ghostfolio/common/config'; +import { DATE_RANGE_PATTERN } from '@ghostfolio/api/dtos/date-range-filter.dto'; +import { FilterDto } from '@ghostfolio/api/dtos/filter.dto'; import { DateRange } from '@ghostfolio/common/types'; import { Type as ActivityType } from '@prisma/client'; import { Transform, TransformFnParams } from 'class-transformer'; -import { IsEnum, IsOptional, IsString, Matches } from 'class-validator'; +import { IsEnum, IsOptional, Matches } from 'class-validator'; import { isString } from 'lodash'; -// A named date range or a calendar year like '2024', '2023', '2022', etc. -const DATE_RANGE_PATTERN = new RegExp(`^(${DATE_RANGES.join('|')}|\\d{4})$`); - -export class ActivitiesFilterDto { - @IsOptional() - @IsString() - accounts?: string; - +export class ActivitiesFilterDto extends FilterDto { @IsEnum(ActivityType, { each: true }) @IsOptional() @Transform(({ value }: TransformFnParams) => { @@ -21,23 +15,7 @@ export class ActivitiesFilterDto { }) activityTypes?: ActivityType[]; - @IsOptional() - @IsString() - assetClasses?: string; - - @IsOptional() - @IsString() - dataSource?: string; - @IsOptional() @Matches(DATE_RANGE_PATTERN) range?: DateRange; - - @IsOptional() - @IsString() - symbol?: string; - - @IsOptional() - @IsString() - tags?: string; } diff --git a/apps/api/src/app/endpoints/ai/ai.controller.ts b/apps/api/src/app/endpoints/ai/ai.controller.ts index b1607b53b..6c8102db1 100644 --- a/apps/api/src/app/endpoints/ai/ai.controller.ts +++ b/apps/api/src/app/endpoints/ai/ai.controller.ts @@ -1,4 +1,5 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { FilterDto } from '@ghostfolio/api/dtos/filter.dto'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { AiPromptResponse } from '@ghostfolio/common/interfaces'; @@ -31,18 +32,15 @@ export class AiController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getPrompt( @Param('mode') mode: AiPromptMode, - @Query('accounts') filterByAccounts?: string, - @Query('assetClasses') filterByAssetClasses?: string, - @Query('dataSource') filterByDataSource?: string, - @Query('symbol') filterBySymbol?: string, - @Query('tags') filterByTags?: string + @Query() + { accounts, assetClasses, dataSource, symbol, tags }: FilterDto ): Promise { const filters = this.apiService.buildFiltersFromQueryParams({ - filterByAccounts, - filterByAssetClasses, - filterByDataSource, - filterBySymbol, - filterByTags + filterByAccounts: accounts, + filterByAssetClasses: assetClasses, + filterByDataSource: dataSource, + filterBySymbol: symbol, + filterByTags: tags }); const prompt = await this.aiService.getPrompt({ diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts index 74bb6b672..53df9bd92 100644 --- a/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts +++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts @@ -5,17 +5,14 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; -import { - DEFAULT_DATE_RANGE, - HEADER_KEY_IMPERSONATION -} from '@ghostfolio/common/config'; +import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import type { AssetProfileIdentifier, BenchmarkMarketDataDetailsResponse, BenchmarkResponse } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; -import type { DateRange, RequestWithUser } from '@ghostfolio/common/types'; +import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, @@ -37,6 +34,7 @@ import { DataSource } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { BenchmarksService } from './benchmarks.service'; +import { GetBenchmarkMarketDataDto } from './get-benchmark-market-data.dto'; @Controller('benchmarks') export class BenchmarksController { @@ -121,38 +119,39 @@ export class BenchmarksController { @Param('dataSource') dataSource: DataSource, @Param('startDateString') startDateString: string, @Param('symbol') symbol: string, - @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, - @Query('accounts') filterByAccounts?: string, - @Query('assetClasses') filterByAssetClasses?: string, - @Query('dataSource') filterByDataSource?: string, - @Query('symbol') filterBySymbol?: string, - @Query('tags') filterByTags?: string, - @Query('withExcludedAccounts') withExcludedAccountsParam = 'false' + @Query() + { + accounts, + assetClasses, + dataSource: filterByDataSource, + range, + symbol: filterBySymbol, + tags, + withExcludedAccounts + }: GetBenchmarkMarketDataDto ): Promise { const { endDate, startDate } = getIntervalFromDateRange({ - dateRange, + dateRange: range, startDate: new Date(startDateString) }); const filters = this.apiService.buildFiltersFromQueryParams({ - filterByAccounts, - filterByAssetClasses, filterByDataSource, filterBySymbol, - filterByTags + filterByAccounts: accounts, + filterByAssetClasses: assetClasses, + filterByTags: tags }); - const withExcludedAccounts = withExcludedAccountsParam === 'true'; - return this.benchmarksService.getMarketDataForUser({ dataSource, - dateRange, endDate, filters, impersonationId, startDate, symbol, withExcludedAccounts, + dateRange: range, user: this.request.user }); } diff --git a/apps/api/src/app/endpoints/benchmarks/get-benchmark-market-data.dto.ts b/apps/api/src/app/endpoints/benchmarks/get-benchmark-market-data.dto.ts new file mode 100644 index 000000000..9599ade59 --- /dev/null +++ b/apps/api/src/app/endpoints/benchmarks/get-benchmark-market-data.dto.ts @@ -0,0 +1,12 @@ +import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto'; + +import { Transform, TransformFnParams } from 'class-transformer'; +import { IsBoolean } from 'class-validator'; + +export class GetBenchmarkMarketDataDto extends DateRangeFilterDto { + @IsBoolean() + @Transform(({ value }: TransformFnParams) => { + return value === 'true'; + }) + withExcludedAccounts? = false; +} diff --git a/apps/api/src/app/portfolio/get-details.dto.ts b/apps/api/src/app/portfolio/get-details.dto.ts new file mode 100644 index 000000000..e2a13e3b6 --- /dev/null +++ b/apps/api/src/app/portfolio/get-details.dto.ts @@ -0,0 +1,12 @@ +import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto'; + +import { Transform, TransformFnParams } from 'class-transformer'; +import { IsBoolean } from 'class-validator'; + +export class GetDetailsDto extends DateRangeFilterDto { + @IsBoolean() + @Transform(({ value }: TransformFnParams) => { + return value === 'true'; + }) + withMarkets? = false; +} diff --git a/apps/api/src/app/portfolio/get-dividends.dto.ts b/apps/api/src/app/portfolio/get-dividends.dto.ts new file mode 100644 index 000000000..f1dff4a97 --- /dev/null +++ b/apps/api/src/app/portfolio/get-dividends.dto.ts @@ -0,0 +1,10 @@ +import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto'; +import { GroupBy } from '@ghostfolio/common/types'; + +import { IsIn, IsOptional } from 'class-validator'; + +export class GetDividendsDto extends DateRangeFilterDto { + @IsIn(['month', 'year'] as GroupBy[]) + @IsOptional() + groupBy?: GroupBy; +} diff --git a/apps/api/src/app/portfolio/get-holdings.dto.ts b/apps/api/src/app/portfolio/get-holdings.dto.ts new file mode 100644 index 000000000..b776bfe0d --- /dev/null +++ b/apps/api/src/app/portfolio/get-holdings.dto.ts @@ -0,0 +1,14 @@ +import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto'; +import { HoldingType } from '@ghostfolio/common/types'; + +import { IsIn, IsOptional, IsString } from 'class-validator'; + +export class GetHoldingsDto extends DateRangeFilterDto { + @IsIn(['ACTIVE', 'CLOSED'] as HoldingType[]) + @IsOptional() + holdingType?: HoldingType; + + @IsOptional() + @IsString() + query?: string; +} diff --git a/apps/api/src/app/portfolio/get-investments.dto.ts b/apps/api/src/app/portfolio/get-investments.dto.ts new file mode 100644 index 000000000..bf312a543 --- /dev/null +++ b/apps/api/src/app/portfolio/get-investments.dto.ts @@ -0,0 +1,10 @@ +import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto'; +import { GroupBy } from '@ghostfolio/common/types'; + +import { IsIn, IsOptional } from 'class-validator'; + +export class GetInvestmentsDto extends DateRangeFilterDto { + @IsIn(['month', 'year'] as GroupBy[]) + @IsOptional() + groupBy?: GroupBy; +} diff --git a/apps/api/src/app/portfolio/get-performance.dto.ts b/apps/api/src/app/portfolio/get-performance.dto.ts new file mode 100644 index 000000000..5992c2a09 --- /dev/null +++ b/apps/api/src/app/portfolio/get-performance.dto.ts @@ -0,0 +1,12 @@ +import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto'; + +import { Transform, TransformFnParams } from 'class-transformer'; +import { IsBoolean } from 'class-validator'; + +export class GetPerformanceDto extends DateRangeFilterDto { + @IsBoolean() + @Transform(({ value }: TransformFnParams) => { + return value === 'true'; + }) + withExcludedAccounts? = false; +} diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 5239de24f..f6e8648d5 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -15,7 +15,6 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { - DEFAULT_DATE_RANGE, HEADER_KEY_IMPERSONATION, UNKNOWN_KEY } from '@ghostfolio/common/config'; @@ -34,11 +33,7 @@ import { isRestrictedView, permissions } from '@ghostfolio/common/permissions'; -import type { - DateRange, - GroupBy, - RequestWithUser -} from '@ghostfolio/common/types'; +import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, @@ -60,6 +55,11 @@ import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import { Big } from 'big.js'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import { GetDetailsDto } from './get-details.dto'; +import { GetDividendsDto } from './get-dividends.dto'; +import { GetHoldingsDto } from './get-holdings.dto'; +import { GetInvestmentsDto } from './get-investments.dto'; +import { GetPerformanceDto } from './get-performance.dto'; import { PortfolioService } from './portfolio.service'; import { UpdateHoldingTagsDto } from './update-holding-tags.dto'; @@ -82,16 +82,17 @@ export class PortfolioController { @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getDetails( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, - @Query('accounts') filterByAccounts?: string, - @Query('assetClasses') filterByAssetClasses?: string, - @Query('dataSource') filterByDataSource?: string, - @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, - @Query('symbol') filterBySymbol?: string, - @Query('tags') filterByTags?: string, - @Query('withMarkets') withMarketsParam = 'false' + @Query() + { + accounts: filterByAccounts, + assetClasses: filterByAssetClasses, + dataSource: filterByDataSource, + range, + symbol: filterBySymbol, + tags: filterByTags, + withMarkets + }: GetDetailsDto ): Promise { - const withMarkets = withMarketsParam === 'true'; - let hasDetails = true; let hasError = false; @@ -118,10 +119,10 @@ export class PortfolioController { platforms, summary } = await this.portfolioService.getDetails({ - dateRange, filters, impersonationId, withMarkets, + dateRange: range, userId: this.request.user.id, withSummary: true }); @@ -326,20 +327,23 @@ export class PortfolioController { @UseInterceptors(TransformDataSourceInRequestInterceptor) public async getDividends( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, - @Query('accounts') filterByAccounts?: string, - @Query('assetClasses') filterByAssetClasses?: string, - @Query('dataSource') filterByDataSource?: string, - @Query('groupBy') groupBy?: GroupBy, - @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, - @Query('symbol') filterBySymbol?: string, - @Query('tags') filterByTags?: string + @Query() + { + accounts, + assetClasses, + dataSource, + groupBy, + range, + symbol, + tags + }: GetDividendsDto ): Promise { const filters = this.apiService.buildFiltersFromQueryParams({ - filterByAccounts, - filterByAssetClasses, - filterByDataSource, - filterBySymbol, - filterByTags + filterByAccounts: accounts, + filterByAssetClasses: assetClasses, + filterByDataSource: dataSource, + filterBySymbol: symbol, + filterByTags: tags }); const impersonationUserId = @@ -349,7 +353,9 @@ export class PortfolioController { const { settings } = await this.userService.user({ id: userId }); const userCurrency = settings.settings.baseCurrency; - const { endDate, startDate } = getIntervalFromDateRange({ dateRange }); + const { endDate, startDate } = getIntervalFromDateRange({ + dateRange: range + }); const { activities } = await this.activitiesService.getActivities({ endDate, @@ -429,29 +435,32 @@ export class PortfolioController { @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getHoldings( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, - @Query('accounts') filterByAccounts?: string, - @Query('assetClasses') filterByAssetClasses?: string, - @Query('dataSource') filterByDataSource?: string, - @Query('holdingType') filterByHoldingType?: string, - @Query('query') filterBySearchQuery?: string, - @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, - @Query('symbol') filterBySymbol?: string, - @Query('tags') filterByTags?: string + @Query() + { + accounts, + assetClasses, + dataSource, + holdingType, + query, + range, + symbol, + tags + }: GetHoldingsDto ): Promise { const filters = this.apiService.buildFiltersFromQueryParams({ - filterByAccounts, - filterByAssetClasses, - filterByDataSource, - filterByHoldingType, - filterBySearchQuery, - filterBySymbol, - filterByTags + filterByAccounts: accounts, + filterByAssetClasses: assetClasses, + filterByDataSource: dataSource, + filterByHoldingType: holdingType, + filterBySearchQuery: query, + filterBySymbol: symbol, + filterByTags: tags }); const holdings = await this.portfolioService.getHoldings({ - dateRange, filters, impersonationId, + dateRange: range, userId: this.request.user.id }); @@ -463,27 +472,30 @@ export class PortfolioController { @UseInterceptors(TransformDataSourceInRequestInterceptor) public async getInvestments( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, - @Query('accounts') filterByAccounts?: string, - @Query('assetClasses') filterByAssetClasses?: string, - @Query('dataSource') filterByDataSource?: string, - @Query('groupBy') groupBy?: GroupBy, - @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, - @Query('symbol') filterBySymbol?: string, - @Query('tags') filterByTags?: string + @Query() + { + accounts, + assetClasses, + dataSource, + groupBy, + range, + symbol, + tags + }: GetInvestmentsDto ): Promise { const filters = this.apiService.buildFiltersFromQueryParams({ - filterByAccounts, - filterByAssetClasses, - filterByDataSource, - filterBySymbol, - filterByTags + filterByAccounts: accounts, + filterByAssetClasses: assetClasses, + filterByDataSource: dataSource, + filterBySymbol: symbol, + filterByTags: tags }); let { investments, streaks } = await this.portfolioService.getInvestments({ - dateRange, filters, groupBy, impersonationId, + dateRange: range, savingsRate: this.request.user?.settings?.settings.savingsRate, userId: this.request.user.id }); @@ -536,29 +548,30 @@ export class PortfolioController { @Version('2') public async getPerformanceV2( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, - @Query('accounts') filterByAccounts?: string, - @Query('assetClasses') filterByAssetClasses?: string, - @Query('dataSource') filterByDataSource?: string, - @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, - @Query('symbol') filterBySymbol?: string, - @Query('tags') filterByTags?: string, - @Query('withExcludedAccounts') withExcludedAccountsParam = 'false' + @Query() + { + accounts, + assetClasses, + dataSource, + range, + symbol, + tags, + withExcludedAccounts + }: GetPerformanceDto ): Promise { - const withExcludedAccounts = withExcludedAccountsParam === 'true'; - const filters = this.apiService.buildFiltersFromQueryParams({ - filterByAccounts, - filterByAssetClasses, - filterByDataSource, - filterBySymbol, - filterByTags + filterByAccounts: accounts, + filterByAssetClasses: assetClasses, + filterByDataSource: dataSource, + filterBySymbol: symbol, + filterByTags: tags }); const performanceInformation = await this.portfolioService.getPerformance({ - dateRange, filters, impersonationId, withExcludedAccounts, + dateRange: range, userId: this.request.user.id }); diff --git a/apps/api/src/dtos/date-range-filter.dto.ts b/apps/api/src/dtos/date-range-filter.dto.ts new file mode 100644 index 000000000..bf440ac7c --- /dev/null +++ b/apps/api/src/dtos/date-range-filter.dto.ts @@ -0,0 +1,16 @@ +import { DATE_RANGES, DEFAULT_DATE_RANGE } from '@ghostfolio/common/config'; +import { DateRange } from '@ghostfolio/common/types'; + +import { Matches } from 'class-validator'; + +import { FilterDto } from './filter.dto'; + +// A named date range or a calendar year like '2024', '2023', '2022', etc. +export const DATE_RANGE_PATTERN = new RegExp( + `^(${DATE_RANGES.join('|')}|\\d{4})$` +); + +export class DateRangeFilterDto extends FilterDto { + @Matches(DATE_RANGE_PATTERN) + range?: DateRange = DEFAULT_DATE_RANGE; +} diff --git a/apps/api/src/dtos/filter.dto.ts b/apps/api/src/dtos/filter.dto.ts new file mode 100644 index 000000000..cb26d582b --- /dev/null +++ b/apps/api/src/dtos/filter.dto.ts @@ -0,0 +1,23 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class FilterDto { + @IsOptional() + @IsString() + accounts?: string; + + @IsOptional() + @IsString() + assetClasses?: string; + + @IsOptional() + @IsString() + dataSource?: string; + + @IsOptional() + @IsString() + symbol?: string; + + @IsOptional() + @IsString() + tags?: string; +} diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index fdb0ba97a..d1845222a 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -341,8 +341,7 @@ export class GfAccountDetailDialogComponent implements OnInit { } ], range: DEFAULT_DATE_RANGE, - withExcludedAccounts: true, - withItems: true + withExcludedAccounts: true }) .pipe(takeUntilDestroyed(this.destroyRef)) }).subscribe({ diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index 7ccd9d1d9..7081f9b2c 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -731,13 +731,11 @@ export class DataService { public fetchPortfolioPerformance({ filters, range, - withExcludedAccounts = false, - withItems = false + withExcludedAccounts = false }: { filters?: Filter[]; range: DateRange; withExcludedAccounts?: boolean; - withItems?: boolean; }): Observable { let params = this.buildFiltersAsQueryParams({ filters }); params = params.append('range', range); @@ -746,10 +744,6 @@ export class DataService { params = params.append('withExcludedAccounts', withExcludedAccounts); } - if (withItems) { - params = params.append('withItems', withItems); - } - return this.http .get(`/api/v2/portfolio/performance`, { params