From c311d835fadc3b34713095b4ad51a3a0167e0182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20TISON?= <13355624+Airthee@users.noreply.github.com> Date: Fri, 3 Apr 2026 09:45:02 +0200 Subject: [PATCH] Feature/add type filter to activities table component (#6622) * Add type filter * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> --- CHANGELOG.md | 6 + .../app/activities/activities.controller.ts | 7 +- .../src/app/activities/activities.service.ts | 2 +- apps/api/src/app/export/export.controller.ts | 5 + apps/api/src/app/export/export.service.ts | 5 +- .../activities/activities-page.component.ts | 18 ++- .../portfolio/activities/activities-page.html | 2 + .../activities-table.component.html | 134 +++++++++++------- .../activities-table.component.ts | 27 +++- libs/ui/src/lib/services/data.service.ts | 12 ++ .../migration.sql | 2 + prisma/schema.prisma | 1 + 12 files changed, 161 insertions(+), 60 deletions(-) create mode 100644 prisma/migrations/20260321200654_added_index_for_type_to_order/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index d3984ca7f..53461fd8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added support for filtering by activity type on the activities page (experimental) + ## 2.252.0 - 2026-03-02 ### Added diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts index 141fd4c82..6b0440dc4 100644 --- a/apps/api/src/app/activities/activities.controller.ts +++ b/apps/api/src/app/activities/activities.controller.ts @@ -37,7 +37,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'; @@ -112,6 +112,7 @@ export class ActivitiesController { public async getAllActivities( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Query('accounts') filterByAccounts?: string, + @Query('activityTypes') filterByTypes?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('dataSource') filterByDataSource?: string, @Query('range') dateRange?: DateRange, @@ -139,6 +140,9 @@ export class ActivitiesController { const impersonationUserId = await this.impersonationService.validateImpersonationId(impersonationId); + + const types = (filterByTypes?.split(',') as ActivityType[]) ?? []; + const userCurrency = this.request.user.settings.settings.baseCurrency; const { activities, count } = await this.activitiesService.getActivities({ @@ -147,6 +151,7 @@ export class ActivitiesController { sortColumn, sortDirection, startDate, + types, userCurrency, includeDrafts: true, skip: isNaN(skip) ? undefined : skip, diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 89b9468f8..58b9c11a4 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -629,7 +629,7 @@ export class ActivitiesService { orderBy = [{ [sortColumn]: sortDirection }]; } - if (types) { + if (types?.length > 0) { where.type = { in: types }; } diff --git a/apps/api/src/app/export/export.controller.ts b/apps/api/src/app/export/export.controller.ts index 6fda8f17f..4f4f4e6dd 100644 --- a/apps/api/src/app/export/export.controller.ts +++ b/apps/api/src/app/export/export.controller.ts @@ -15,6 +15,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'; @@ -33,12 +34,15 @@ export class ExportController { public async export( @Query('accounts') filterByAccounts?: string, @Query('activityIds') filterByActivityIds?: string, + @Query('activityTypes') filterByTypes?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('dataSource') filterByDataSource?: string, @Query('symbol') filterBySymbol?: string, @Query('tags') filterByTags?: string ): Promise { const activityIds = filterByActivityIds?.split(',') ?? []; + const activityTypes = (filterByTypes?.split(',') as ActivityType[]) ?? []; + const filters = this.apiService.buildFiltersFromQueryParams({ filterByAccounts, filterByAssetClasses, @@ -49,6 +53,7 @@ export class ExportController { return this.exportService.export({ activityIds, + activityTypes, filters, 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..4da942cd7 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() @@ -24,11 +24,13 @@ export class ExportService { public async export({ activityIds, + activityTypes, filters, userId, userSettings }: { activityIds?: string[]; + activityTypes?: ActivityType[]; filters?: Filter[]; userId: string; userSettings: UserSettings; @@ -44,6 +46,7 @@ export class ExportService { includeDrafts: true, sortColumn: 'date', sortDirection: 'asc', + types: activityTypes, userCurrency: userSettings?.baseCurrency, withExcludedAccountsAndActivities: true }); 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..faadef54f 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,6 +54,7 @@ import { ImportActivitiesDialogParams } from './import-activities-dialog/interfa templateUrl: './activities-page.html' }) export class GfActivitiesPageComponent implements OnInit { + public activityTypesFilter: string[] = []; public dataSource: MatTableDataSource; public deviceType: string; public hasImpersonationId: boolean; @@ -141,6 +142,9 @@ export class GfActivitiesPageComponent implements OnInit { this.dataService .fetchActivities({ range, + activityTypes: this.activityTypesFilter.length + ? this.activityTypesFilter + : undefined, filters: this.userService.getFilters(), skip: this.pageIndex * this.pageSize, sortColumn: this.sortColumn, @@ -217,7 +221,12 @@ export class GfActivitiesPageComponent implements OnInit { let fetchExportParams: any = { activityIds }; if (!activityIds) { - fetchExportParams = { filters: this.userService.getFilters() }; + fetchExportParams = { + activityTypes: this.activityTypesFilter.length + ? this.activityTypesFilter + : undefined, + filters: this.userService.getFilters() + }; } this.dataService @@ -318,6 +327,13 @@ export class GfActivitiesPageComponent implements OnInit { this.fetchActivities(); } + public onTypesFilterChanged(aTypes: string[]) { + this.activityTypesFilter = aTypes; + this.pageIndex = 0; + + this.fetchActivities(); + } + public onUpdateActivity(aActivity: Activity) { this.router.navigate([], { queryParams: { activityId: aActivity.id, editDialog: true } 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..2a72dcfd2 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]="user?.settings?.isExperimentalFeatures" [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/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index bdb1e6373..b8fe962d7 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -1,77 +1,101 @@ -@if (hasPermissionToCreateActivity) { -
- - @if (hasPermissionToExportActivities) { - +
+
+ @if (hasPermissionToFilterByType) { + + Type + + @for ( + activityType of activityTypesTranslationMap | keyvalue; + track activityType.key + ) { + + {{ activityType.value }} + + } + + } - +
+ + @if (hasPermissionToCreateActivity) { +
@if (hasPermissionToExportActivities) { + } + + - } - @if (hasPermissionToExportActivities) { + @if (hasPermissionToExportActivities) { + + } + @if (hasPermissionToExportActivities) { + + } +
- } -
- -
-
-} + +
+ } +
(); @Output() selectedActivities = new EventEmitter(); @Output() sortChanged = new EventEmitter(); + @Output() typesFilterChanged = new EventEmitter(); @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; + public activityTypesTranslationMap = new Map(); public hasDrafts = false; public hasErrors = false; public isUUID = isUUID; public selectedRows = new SelectionModel(true, []); + public typesFilter = new FormControl([]); public readonly dataSource = input.required< MatTableDataSource | undefined @@ -189,6 +201,13 @@ export class GfActivitiesTableComponent private readonly unsubscribeSubject = new Subject(); public constructor() { + for (const type of Object.keys(ActivityType) as ActivityType[]) { + this.activityTypesTranslationMap.set( + ActivityType[type], + translate(ActivityType[type]) + ); + } + addIcons({ alertCircleOutline, calendarClearOutline, @@ -214,6 +233,12 @@ export class GfActivitiesTableComponent this.selectedActivities.emit(selectedRows.source.selected); }); } + + this.typesFilter.valueChanges + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((types) => { + this.typesFilterChanged.emit(types ?? []); + }); } public ngAfterViewInit() { diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index f3e3fb81f..7f2dac0b1 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -209,6 +209,7 @@ export class DataService { } public fetchActivities({ + activityTypes, filters, range, skip, @@ -216,6 +217,7 @@ export class DataService { sortDirection, take }: { + activityTypes?: string[]; filters?: Filter[]; range?: DateRange; skip?: number; @@ -225,6 +227,10 @@ export class DataService { }): Observable { let params = this.buildFiltersAsQueryParams({ filters }); + if (activityTypes?.length) { + params = params.append('activityTypes', activityTypes.join(',')); + } + if (range) { params = params.append('range', range); } @@ -411,9 +417,11 @@ export class DataService { public fetchExport({ activityIds, + activityTypes, filters }: { activityIds?: string[]; + activityTypes?: string[]; filters?: Filter[]; } = {}) { let params = this.buildFiltersAsQueryParams({ filters }); @@ -422,6 +430,10 @@ export class DataService { params = params.append('activityIds', activityIds.join(',')); } + if (activityTypes?.length) { + params = params.append('activityTypes', activityTypes.join(',')); + } + return this.http.get('/api/v1/export', { params }); diff --git a/prisma/migrations/20260321200654_added_index_for_type_to_order/migration.sql b/prisma/migrations/20260321200654_added_index_for_type_to_order/migration.sql new file mode 100644 index 000000000..ba4d1b1ff --- /dev/null +++ b/prisma/migrations/20260321200654_added_index_for_type_to_order/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "Order_type_idx" ON "Order"("type"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 232dde9ca..50aac91fb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -158,6 +158,7 @@ model Order { @@index([accountId]) @@index([date]) @@index([isDraft]) + @@index([type]) @@index([userId]) }