diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bc6b88a5..75e40bcb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added support to exclude an activity from analysis based on tags - Added a _Storybook_ story for the membership card component ### Changed diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 54fd0763d..09168b505 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -41,7 +41,7 @@ export class ExportService { includeDrafts: true, sortColumn: 'date', sortDirection: 'asc', - withExcludedAccounts: true + withExcludedAccountsAndActivities: true }); if (activityIds?.length > 0) { diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index d23427616..ef9d25f53 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -533,7 +533,7 @@ export class ImportService { userCurrency, userId, includeDrafts: true, - withExcludedAccounts: true + withExcludedAccountsAndActivities: true }); return activitiesDto.map( diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index cd8012cec..9bd45050e 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -144,7 +144,7 @@ export class OrderController { skip: isNaN(skip) ? undefined : skip, take: isNaN(take) ? undefined : take, userId: impersonationUserId || this.request.user.id, - withExcludedAccounts: true + withExcludedAccountsAndActivities: true }); return { activities, count }; @@ -165,7 +165,7 @@ export class OrderController { const { activities } = await this.orderService.getOrders({ userCurrency, userId: impersonationUserId || this.request.user.id, - withExcludedAccounts: true + withExcludedAccountsAndActivities: true }); const activity = activities.find((activity) => { diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 21fa0d076..e9d72233e 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -9,7 +9,8 @@ import { DATA_GATHERING_QUEUE_PRIORITY_HIGH, GATHER_ASSET_PROFILE_PROCESS_JOB_NAME, GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS, - ghostfolioPrefix + ghostfolioPrefix, + TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { @@ -275,7 +276,7 @@ export class OrderService { userId, includeDrafts: true, userCurrency: undefined, - withExcludedAccounts: true + withExcludedAccountsAndActivities: true }); const { count } = await this.prismaService.order.deleteMany({ @@ -332,7 +333,7 @@ export class OrderService { types, userCurrency, userId, - withExcludedAccounts = false + withExcludedAccountsAndActivities = false }: { endDate?: Date; filters?: Filter[]; @@ -345,7 +346,7 @@ export class OrderService { types?: ActivityType[]; userCurrency: string; userId: string; - withExcludedAccounts?: boolean; + withExcludedAccountsAndActivities?: boolean; }): Promise { let orderBy: Prisma.Enumerable = [ { date: 'asc' }, @@ -491,11 +492,18 @@ export class OrderService { where.type = { in: types }; } - if (withExcludedAccounts === false) { + if (withExcludedAccountsAndActivities === false) { where.OR = [ { account: null }, { account: { NOT: { isExcluded: true } } } ]; + + where.tags = { + ...where.tags, + none: { + id: TAG_ID_EXCLUDE_FROM_ANALYSIS + } + }; } const [orders, count] = await Promise.all([ @@ -609,7 +617,7 @@ export class OrderService { filters, userCurrency, userId, - withExcludedAccounts: false // TODO + withExcludedAccountsAndActivities: false // TODO }); } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 02804a847..d1b9af892 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -33,6 +33,7 @@ import { import { DEFAULT_CURRENCY, TAG_ID_EMERGENCY_FUND, + TAG_ID_EXCLUDE_FROM_ANALYSIS, UNKNOWN_KEY } from '@ghostfolio/common/config'; import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper'; @@ -1799,14 +1800,19 @@ export class PortfolioService { const { activities } = await this.orderService.getOrders({ userCurrency, userId, - withExcludedAccounts: true + withExcludedAccountsAndActivities: true }); const excludedActivities: Activity[] = []; const nonExcludedActivities: Activity[] = []; for (const activity of activities) { - if (activity.account?.isExcluded) { + if ( + activity.account?.isExcluded || + activity.tags?.some(({ id }) => { + return id === TAG_ID_EXCLUDE_FROM_ANALYSIS; + }) + ) { excludedActivities.push(activity); } else { nonExcludedActivities.push(activity); diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index eb2d7bfef..c38abe592 100644 --- a/apps/api/src/services/tag/tag.service.ts +++ b/apps/api/src/services/tag/tag.service.ts @@ -1,4 +1,5 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { Injectable } from '@nestjs/common'; import { Prisma, Tag } from '@prisma/client'; @@ -79,7 +80,8 @@ export class TagService { id, name, userId, - isUsed: _count.activities > 0 + isUsed: + _count.activities > 0 && ![TAG_ID_EXCLUDE_FROM_ANALYSIS].includes(id) })); } diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 467716575..02a12cfd1 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -200,6 +200,8 @@ export const SUPPORTED_LANGUAGE_CODES = [ ]; export const TAG_ID_EMERGENCY_FUND = '4452656d-9fa4-4bd0-ba38-70492e31d180'; +export const TAG_ID_EXCLUDE_FROM_ANALYSIS = + 'f2e868af-8333-459f-b161-cbc6544c24bd'; export const TAG_ID_DEMO = 'efa08cb3-9b9d-4974-ac68-db13a19c4874'; export const UNKNOWN_KEY = 'UNKNOWN'; 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 59ec6315f..4ee7c689a 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -468,7 +468,7 @@ [ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && - row.account?.isExcluded !== true && + isExcludedFromAnalysis(row) === false && row.isDraft === false && ['BUY', 'DIVIDEND', 'SELL'].includes(row.type) }" diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts index df1211787..c13251771 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -2,7 +2,10 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; -import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; +import { + DEFAULT_PAGE_SIZE, + TAG_ID_EXCLUDE_FROM_ANALYSIS +} from '@ghostfolio/common/config'; import { getDateFormatString, getLocale } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { OrderWithAccount } from '@ghostfolio/common/types'; @@ -171,12 +174,6 @@ export class GfActivitiesTableComponent }); } - public areAllRowsSelected() { - const numSelectedRows = this.selectedRows.selected.length; - const numTotalRows = this.dataSource.data.length; - return numSelectedRows === numTotalRows; - } - public ngOnChanges() { this.defaultDateFormat = getDateFormatString(this.locale); @@ -215,6 +212,21 @@ export class GfActivitiesTableComponent } } + public areAllRowsSelected() { + const numSelectedRows = this.selectedRows.selected.length; + const numTotalRows = this.dataSource.data.length; + return numSelectedRows === numTotalRows; + } + + public isExcludedFromAnalysis(activity: Activity) { + return ( + activity.account?.isExcluded || + activity.tags?.some(({ id }) => { + return id === TAG_ID_EXCLUDE_FROM_ANALYSIS; + }) + ); + } + public onChangePage(page: PageEvent) { this.pageChanged.emit(page); } @@ -226,7 +238,7 @@ export class GfActivitiesTableComponent } } else if ( this.hasPermissionToOpenDetails && - activity.account?.isExcluded !== true && + this.isExcludedFromAnalysis(activity) === false && activity.isDraft === false && ['BUY', 'DIVIDEND', 'SELL'].includes(activity.type) ) { diff --git a/libs/ui/src/lib/i18n.ts b/libs/ui/src/lib/i18n.ts index f02da40e1..2b68063e6 100644 --- a/libs/ui/src/lib/i18n.ts +++ b/libs/ui/src/lib/i18n.ts @@ -13,6 +13,7 @@ const locales = { DATA_IMPORT_AND_EXPORT_TOOLTIP_OSS: $localize`Switch to Ghostfolio Premium easily`, DATA_IMPORT_AND_EXPORT_TOOLTIP_PREMIUM: $localize`Switch to Ghostfolio Open Source or Ghostfolio Basic easily`, EMERGENCY_FUND: $localize`Emergency Fund`, + EXCLUDE_FROM_ANALYSIS: $localize`Exclude from Analysis`, Global: $localize`Global`, GRANT: $localize`Grant`, HIGHER_RISK: $localize`Higher Risk`, diff --git a/prisma/seed.ts b/prisma/seed.ts index f68f8375b..18389aab1 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -8,6 +8,10 @@ async function main() { { id: '4452656d-9fa4-4bd0-ba38-70492e31d180', name: 'EMERGENCY_FUND' + }, + { + id: 'f2e868af-8333-459f-b161-cbc6544c24bd', + name: 'EXCLUDE_FROM_ANALYSIS' } ], skipDuplicates: true