diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts index 141fd4c82..ca8ef36b8 100644 --- a/apps/api/src/app/activities/activities.controller.ts +++ b/apps/api/src/app/activities/activities.controller.ts @@ -13,6 +13,7 @@ import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos'; +import { splitStringToArray } from '@ghostfolio/common/helper'; import { ActivitiesResponse, ActivityResponse @@ -37,7 +38,7 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { Order, Prisma } from '@prisma/client'; +import { Order, Prisma, Type as ActivityType } from '@prisma/client'; import { parseISO } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -120,8 +121,13 @@ export class ActivitiesController { @Query('sortDirection') sortDirection?: Prisma.SortOrder, @Query('symbol') filterBySymbol?: string, @Query('tags') filterByTags?: string, - @Query('take') take?: number + @Query('take') take?: number, + @Query('types') filterByTypes?: string ): Promise { + const types = filterByTypes + ? (splitStringToArray(filterByTypes) as ActivityType[]) + : undefined; + let endDate: Date; let startDate: Date; @@ -147,6 +153,7 @@ export class ActivitiesController { sortColumn, sortDirection, startDate, + types, userCurrency, includeDrafts: true, skip: isNaN(skip) ? undefined : skip, diff --git a/apps/api/src/app/export/export.controller.ts b/apps/api/src/app/export/export.controller.ts index 6fda8f17f..95f32ebb3 100644 --- a/apps/api/src/app/export/export.controller.ts +++ b/apps/api/src/app/export/export.controller.ts @@ -2,6 +2,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard' import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; +import { splitStringToArray } from '@ghostfolio/common/helper'; import { ExportResponse } from '@ghostfolio/common/interfaces'; import type { RequestWithUser } from '@ghostfolio/common/types'; @@ -15,6 +16,7 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; +import { Type as ActivityType } from '@prisma/client'; import { ExportService } from './export.service'; @@ -36,7 +38,8 @@ export class ExportController { @Query('assetClasses') filterByAssetClasses?: string, @Query('dataSource') filterByDataSource?: string, @Query('symbol') filterBySymbol?: string, - @Query('tags') filterByTags?: string + @Query('tags') filterByTags?: string, + @Query('types') filterByTypes?: string ): Promise { const activityIds = filterByActivityIds?.split(',') ?? []; const filters = this.apiService.buildFiltersFromQueryParams({ @@ -46,10 +49,14 @@ export class ExportController { filterBySymbol, filterByTags }); + const types = filterByTypes + ? (splitStringToArray(filterByTypes) as ActivityType[]) + : undefined; return this.exportService.export({ activityIds, filters, + types, userId: this.request.user.id, userSettings: this.request.user.settings.settings }); diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 4f2fb3309..82bb518dd 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -10,7 +10,7 @@ import { } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; -import { Platform, Prisma } from '@prisma/client'; +import { Platform, Prisma, Type as ActivityType } from '@prisma/client'; import { groupBy, uniqBy } from 'lodash'; @Injectable() @@ -25,11 +25,13 @@ export class ExportService { public async export({ activityIds, filters, + types, userId, userSettings }: { activityIds?: string[]; filters?: Filter[]; + types?: ActivityType[]; userId: string; userSettings: UserSettings; }): Promise { @@ -40,6 +42,7 @@ export class ExportService { let { activities } = await this.activitiesService.getActivities({ filters, + types, userId, includeDrafts: true, sortColumn: 'date', diff --git a/apps/api/src/services/api/api.service.ts b/apps/api/src/services/api/api.service.ts index 052119246..db0e71c04 100644 --- a/apps/api/src/services/api/api.service.ts +++ b/apps/api/src/services/api/api.service.ts @@ -1,3 +1,4 @@ +import { splitStringToArray } from '@ghostfolio/common/helper'; import { Filter } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; @@ -23,14 +24,14 @@ export class ApiService { filterBySymbol?: string; filterByTags?: string; }): Filter[] { - const accountIds = filterByAccounts?.split(',') ?? []; - const assetClasses = filterByAssetClasses?.split(',') ?? []; - const assetSubClasses = filterByAssetSubClasses?.split(',') ?? []; + const accountIds = splitStringToArray(filterByAccounts); + const assetClasses = splitStringToArray(filterByAssetClasses); + const assetSubClasses = splitStringToArray(filterByAssetSubClasses); const dataSource = filterByDataSource; const holdingType = filterByHoldingType; const searchQuery = filterBySearchQuery?.toLowerCase(); const symbol = filterBySymbol; - const tagIds = filterByTags?.split(',') ?? []; + const tagIds = splitStringToArray(filterByTags); const filters = [ ...accountIds.map((accountId) => { diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index b9dc9077c..d3d6b7ae1 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -54,11 +54,13 @@ import { ImportActivitiesDialogParams } from './import-activities-dialog/interfa templateUrl: './activities-page.html' }) export class GfActivitiesPageComponent implements OnInit { + public activityTypeFilter: string[] = []; public dataSource: MatTableDataSource; public deviceType: string; public hasImpersonationId: boolean; public hasPermissionToCreateActivity: boolean; public hasPermissionToDeleteActivity: boolean; + public hasPermissionToFilterByType = true; public pageIndex = 0; public pageSize = DEFAULT_PAGE_SIZE; public routeQueryParams: Subscription; @@ -145,7 +147,10 @@ export class GfActivitiesPageComponent implements OnInit { skip: this.pageIndex * this.pageSize, sortColumn: this.sortColumn, sortDirection: this.sortDirection, - take: this.pageSize + take: this.pageSize, + types: this.activityTypeFilter.length + ? this.activityTypeFilter + : undefined }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ activities, count }) => { @@ -217,7 +222,12 @@ export class GfActivitiesPageComponent implements OnInit { let fetchExportParams: any = { activityIds }; if (!activityIds) { - fetchExportParams = { filters: this.userService.getFilters() }; + fetchExportParams = { + filters: this.userService.getFilters(), + types: this.activityTypeFilter.length + ? this.activityTypeFilter + : undefined + }; } this.dataService @@ -310,6 +320,12 @@ export class GfActivitiesPageComponent implements OnInit { }); } + public onTypesFilterChanged(types: string[]) { + this.activityTypeFilter = types; + this.pageIndex = 0; + this.fetchActivities(); + } + public onSortChanged({ active, direction }: Sort) { this.pageIndex = 0; this.sortColumn = active; diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.html b/apps/client/src/app/pages/portfolio/activities/activities-page.html index 80ad71b79..5f67f0cee 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.html +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -10,6 +10,7 @@ [hasPermissionToCreateActivity]="hasPermissionToCreateActivity" [hasPermissionToDeleteActivity]="hasPermissionToDeleteActivity" [hasPermissionToExportActivities]="!hasImpersonationId" + [hasPermissionToFilterByType]="hasPermissionToFilterByType" [locale]="user?.settings?.locale" [pageIndex]="pageIndex" [pageSize]="pageSize" @@ -32,6 +33,7 @@ (importDividends)="onImportDividends()" (pageChanged)="onChangePage($event)" (sortChanged)="onSortChanged($event)" + (typesFilterChanged)="onTypesFilterChanged($event)" /> diff --git a/libs/common/src/lib/helper.spec.ts b/libs/common/src/lib/helper.spec.ts index a339c6dab..e794799a8 100644 --- a/libs/common/src/lib/helper.spec.ts +++ b/libs/common/src/lib/helper.spec.ts @@ -1,6 +1,7 @@ import { extractNumberFromString, - getNumberFormatGroup + getNumberFormatGroup, + splitStringToArray } from '@ghostfolio/common/helper'; describe('Helper', () => { @@ -116,4 +117,22 @@ describe('Helper', () => { expect(getNumberFormatGroup()).toEqual(','); }); }); + + describe('splitStringToArray', () => { + it('should split a comma-separated string', () => { + expect(splitStringToArray('a,b,c')).toEqual(['a', 'b', 'c']); + }); + + it('should return a single-element array for a string without commas', () => { + expect(splitStringToArray('a')).toEqual(['a']); + }); + + it('should return an empty array for undefined', () => { + expect(splitStringToArray(undefined)).toEqual([]); + }); + + it('should return an empty array for no argument', () => { + expect(splitStringToArray()).toEqual([]); + }); + }); }); diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 4db1fcf2d..79778df9a 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -463,6 +463,10 @@ export function resolveFearAndGreedIndex(aValue: number) { } } +export function splitStringToArray(aString?: string): string[] { + return aString?.split(',') ?? []; +} + export function resolveMarketCondition( aMarketCondition: Benchmark['marketCondition'] ) { diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index bdb1e6373..c557377a6 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -1,3 +1,21 @@ +@if (hasPermissionToFilterByType) { +
+ + Type + + @for ( + activityType of activityTypes | keyvalue; + track activityType.key + ) { + + {{ activityType.value }} + + } + + +
+} + @if (hasPermissionToCreateActivity) {