From cb6da5c24f1a0960ccfa0aa827cf1a2a49f2c740 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:49:19 +0200 Subject: [PATCH] Task/migrate isDraft of activity to draft tag (#7551) * Migrate isDraft of activity to draft tag * Update changelog --- CHANGELOG.md | 8 ++ apps/api/src/app/account/account.service.ts | 22 ++-- .../app/activities/activities.controller.ts | 6 +- .../src/app/activities/activities.service.ts | 108 +++++++++++++----- apps/api/src/app/import/import.service.ts | 21 +++- .../src/app/portfolio/portfolio.service.ts | 32 ++++-- apps/api/src/app/user/user.service.ts | 3 +- apps/api/src/helper/activity.helper.ts | 74 ++++++++++++ apps/api/src/services/tag/tag.service.ts | 20 ++++ .../holding-detail-dialog.component.ts | 21 ++-- ...eate-or-update-account-dialog.component.ts | 19 ++- libs/common/src/lib/config.ts | 2 + libs/common/src/lib/helper.ts | 9 ++ .../activities-table.component.html | 4 +- .../activities-table.component.ts | 11 +- libs/ui/src/lib/i18n.ts | 1 + .../migration.sql | 13 +++ prisma/schema.prisma | 1 + prisma/seed.mts | 4 + 19 files changed, 308 insertions(+), 71 deletions(-) create mode 100644 apps/api/src/helper/activity.helper.ts create mode 100644 prisma/migrations/20260808120000_added_draft_tag_to_order/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index e402c4c70..cebaf629c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the _Draft_ tag, assigned automatically to activities dated in the future + ### Changed +- Deprecated the `isDraft` attribute of the activity in favor of the _Draft_ tag +- Changed the activities count of an account to include draft activities +- Extended the _Draft_ tag to activities with a custom asset profile of type `BUY` - Improved the language localization for German (`de`) ### Fixed +- Fixed the dividend and interest of an account by excluding draft activities - Resolved an issue with unknown country names in the country weightings of the _Financial Modeling Prep_ service - Resolved an issue with unknown country names in the data enhancer for asset profile data via _Trackinsight_ diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 3d0bb91bd..ccd7d24bf 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -89,7 +89,10 @@ export class AccountService { orderBy?: Prisma.AccountOrderByWithRelationInput; }): Promise< (AccountWithBalance & { - activities?: (Order & { SymbolProfile?: SymbolProfile })[]; + activities?: (Order & { + SymbolProfile?: SymbolProfile; + tags?: Pick[]; + })[]; balances?: AccountBalance[]; platform?: Platform; tags?: Tag[]; @@ -172,7 +175,7 @@ export class AccountService { tagIds?: string[]; userId: string; }): Promise { - await this.tagService.validateTagIds({ tagIds, userId }); + await this.tagService.validateTagIdsWithoutDraftTag({ tagIds, userId }); const account = await this.prismaService.account.create({ data: { @@ -237,15 +240,10 @@ export class AccountService { }); return accounts.map((account) => { - let activitiesCount = 0; - - for (const { isDraft } of account.activities) { - if (!isDraft) { - activitiesCount += 1; - } - } - - const result = { ...account, activitiesCount }; + const result = { + ...account, + activitiesCount: account.activities.length + }; delete result.activities; @@ -317,7 +315,7 @@ export class AccountService { userId: string; where: Prisma.AccountWhereUniqueInput; }): Promise { - await this.tagService.validateTagIds({ tagIds, userId }); + await this.tagService.validateTagIdsWithoutDraftTag({ tagIds, userId }); const account = await this.prismaService.account.update({ data: { diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts index a1b559c84..72056737c 100644 --- a/apps/api/src/app/activities/activities.controller.ts +++ b/apps/api/src/app/activities/activities.controller.ts @@ -1,5 +1,6 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { isActivityInFuture } from '@ghostfolio/api/helper/activity.helper'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; 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'; @@ -287,9 +288,9 @@ export class ActivitiesController { userId: this.request.user.id }); - if (dataSource && !activity.isDraft) { + if (dataSource && !isActivityInFuture({ date: activity.date })) { // Gather symbol data in the background, if data source is set - // (not MANUAL) and not draft + // (not MANUAL) and the date is not in the future this.dataGatheringService.gatherSymbols({ dataGatheringItems: [ { @@ -369,6 +370,7 @@ export class ActivitiesController { }), user: { connect: { id: this.request.user.id } } }, + originalDate: originalActivity.date, userId: this.request.user.id, where: { id diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 140726aeb..5cdb83dfd 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -7,6 +7,12 @@ import { isAccountBalanceInFuture, WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper'; +import { + getTagsWithDraftTag, + isActivityInFuture, + isDraftTagToBeAssigned, + WHERE_ACTIVITY_NOT_DRAFT +} from '@ghostfolio/api/helper/activity.helper'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; @@ -21,11 +27,13 @@ import { GATHER_ASSET_PROFILE_PROCESS_JOB_NAME, GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS, NON_INVESTMENT_ACTIVITY_TYPES, + TAG_ID_DRAFT, TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { canDeleteAssetProfile, getAssetProfileIdentifier, + isDraftActivity, isValidCustomAssetProfileSymbol } from '@ghostfolio/common/helper'; import { @@ -49,7 +57,7 @@ import { Type as ActivityType } from '@prisma/client'; import { Big } from 'big.js'; -import { endOfToday, isAfter } from 'date-fns'; +import { endOfToday } from 'date-fns'; import { groupBy, uniqBy } from 'lodash'; import { randomUUID } from 'node:crypto'; @@ -112,7 +120,7 @@ export class ActivitiesService { tags, userId }: { tags: Tag[]; userId: string } & AssetProfileIdentifier) { - await this.tagService.validateTagIds({ + await this.tagService.validateTagIdsWithoutDraftTag({ userId, tagIds: tags.map(({ id }) => { return id; @@ -120,6 +128,7 @@ export class ActivitiesService { }); const activities = await this.prismaService.order.findMany({ + include: { tags: { select: { id: true } } }, where: { userId, SymbolProfile: { @@ -129,20 +138,31 @@ export class ActivitiesService { } }); + const tagsToAssign = tags.map(({ id }) => { + return { id }; + }); + await Promise.all( - activities.map(({ id }) => - this.prismaService.order.update({ + activities.map((activity) => { + // The set operation replaces all existing connections with the provided + // ones, hence the "Draft" tag of an individual activity is carried over + const isDraft = isDraftActivity(activity); + + const tagsToSet = isDraft + ? [...tagsToAssign, { id: TAG_ID_DRAFT }] + : tagsToAssign; + + return this.prismaService.order.update({ data: { + // @deprecated Mirrors the "Draft" tag until the attribute is removed + isDraft, tags: { - // The set operation replaces all existing connections with the provided ones - set: tags.map((tag) => { - return { id: tag.id }; - }) + set: tagsToSet } }, - where: { id } - }) - ) + where: { id: activity.id } + }); + }) ); this.eventEmitter.emit( @@ -261,17 +281,21 @@ export class ActivitiesService { const orderData: Prisma.OrderCreateInput = data; - const isDraft = NON_INVESTMENT_ACTIVITY_TYPES.includes(data.type) - ? false - : isAfter(data.date as Date, endOfToday()); + const tagsToConnect = getTagsWithDraftTag({ + tags, + date: data.date as Date, + draftTag: { id: TAG_ID_DRAFT }, + type: data.type + }); const activity = await this.prismaService.order.create({ data: { ...orderData, account, - isDraft, + // @deprecated Mirrors the "Draft" tag until the attribute is removed + isDraft: isDraftActivity({ tags: tagsToConnect }), tags: { - connect: tags + connect: tagsToConnect } }, include: { SymbolProfile: true } @@ -637,8 +661,12 @@ export class ActivitiesService { }; } - if (includeDrafts === false) { - where.isDraft = false; + const isFilteredByDraftTag = filtersByTag.some(({ id }) => { + return id === TAG_ID_DRAFT; + }); + + if (includeDrafts === false && !isFilteredByDraftTag) { + andConditions.push(WHERE_ACTIVITY_NOT_DRAFT); } if (filtersByAssetClass.length > 0) { @@ -952,6 +980,7 @@ export class ActivitiesService { public async updateActivity({ data, + originalDate, userId, where }: { @@ -963,9 +992,11 @@ export class ActivitiesService { tags?: { id: string }[]; type?: ActivityType; }; + originalDate: Date; userId: string; where: Prisma.OrderWhereUniqueInput; }): Promise { + const areTagsProvided = data.tags !== undefined; const tags = data.tags ?? []; await this.tagService.validateTagIds({ @@ -979,8 +1010,6 @@ export class ActivitiesService { data.comment = null; } - let isDraft = false; - if ( NON_INVESTMENT_ACTIVITY_TYPES.includes(data.type) || (data.SymbolProfile.connect.dataSource_symbol.dataSource === 'MANUAL' && @@ -991,10 +1020,9 @@ export class ActivitiesService { } else { delete data.SymbolProfile.update; - isDraft = isAfter(data.date as Date, endOfToday()); - - if (!isDraft) { - // Gather symbol data of order in the background, if not draft + if (!isActivityInFuture({ date: data.date as Date })) { + // Gather symbol data of order in the background, if the date is not in + // the future this.dataGatheringService.gatherSymbols({ dataGatheringItems: [ { @@ -1014,14 +1042,40 @@ export class ActivitiesService { delete data.symbol; delete data.tags; + // Leave the tags untouched if the request does not provide them, so that a + // partial update cannot drop the "Draft" tag + let isDraft: boolean; + let tagsToUpdate: Prisma.OrderUpdateInput['tags']; + + if (areTagsProvided) { + const tagsToSet = getTagsWithDraftTag({ + originalDate, + tags, + date: data.date as Date, + draftTag: { id: TAG_ID_DRAFT }, + type: data.type + }); + + isDraft = isDraftActivity({ tags: tagsToSet }); + tagsToUpdate = { set: tagsToSet }; + } else if ( + isDraftTagToBeAssigned({ + originalDate, + date: data.date as Date, + type: data.type + }) + ) { + isDraft = true; + tagsToUpdate = { connect: { id: TAG_ID_DRAFT } }; + } + const activity = await this.prismaService.order.update({ where, data: { ...data, + // @deprecated Mirrors the "Draft" tag until the attribute is removed isDraft, - tags: { - set: tags - } + tags: tagsToUpdate } }); diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 1bc164686..5875afba0 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -2,6 +2,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import { PlatformService } from '@ghostfolio/api/app/platform/platform.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; +import { getTagsWithDraftTag } from '@ghostfolio/api/helper/activity.helper'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -13,6 +14,7 @@ import { DATA_GATHERING_QUEUE_PRIORITY_HIGH, ghostfolioPrefix, NON_INVESTMENT_ACTIVITY_TYPES, + TAG_ID_DRAFT, TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { @@ -22,6 +24,7 @@ import { } from '@ghostfolio/common/dtos'; import { getAssetProfileIdentifier, + isDraftActivity, isValidCustomAssetProfileSymbol, parseDate } from '@ghostfolio/common/helper'; @@ -41,7 +44,7 @@ import { Injectable } from '@nestjs/common'; import { Account, DataSource, Prisma } from '@prisma/client'; import { Big } from 'big.js'; import { isISIN } from 'class-validator'; -import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; +import { isSameSecond, parseISO } from 'date-fns'; import { omit, uniqBy } from 'lodash'; import { randomUUID } from 'node:crypto'; @@ -713,6 +716,11 @@ export class ImportService { }); } + // Preview the "Draft" tag which createActivity() assigns in a real run + const draftTag = tags.find(({ id }) => { + return id === TAG_ID_DRAFT; + }) ?? { id: TAG_ID_DRAFT, name: 'DRAFT' }; + const activities: Activity[] = []; for (const activity of activitiesExtendedWithErrors) { @@ -775,6 +783,13 @@ export class ImportService { }); if (isDryRun) { + const previewTags = getTagsWithDraftTag({ + date, + draftTag, + type, + tags: validatedTags + }); + order = { comment, currency, @@ -788,7 +803,7 @@ export class ImportService { accountUserId: undefined, createdAt: new Date(), id: randomUUID(), - isDraft: isAfter(date, endOfToday()), + isDraft: isDraftActivity({ tags: previewTags }), SymbolProfile: { assetClass, assetSubClass, @@ -817,7 +832,7 @@ export class ImportService { userId: dataSource === 'MANUAL' ? user.id : undefined }, symbolProfileId: undefined, - tags: validatedTags, + tags: previewTags, updatedAt: new Date(), userId: user.id }; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b665b635a..704de93f2 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -34,6 +34,7 @@ import { import { DEFAULT_CURRENCY, DEFAULT_DATE_RANGE, + TAG_ID_DRAFT, TAG_ID_EMERGENCY_FUND, TAG_ID_EXCLUDE_FROM_ANALYSIS, UNKNOWN_KEY @@ -43,6 +44,7 @@ import { getAssetProfileIdentifier, getSum, isAccountExcluded, + isDraftActivity, parseDate } from '@ghostfolio/common/helper'; import { @@ -174,7 +176,19 @@ export class PortfolioService { this.accountService.accounts({ where, include: { - activities: { include: { SymbolProfile: true } }, + activities: { + include: { + SymbolProfile: true, + tags: { + select: { + id: true + }, + where: { + id: TAG_ID_DRAFT + } + } + } + }, platform: true, tags: true }, @@ -200,12 +214,18 @@ export class PortfolioService { for (const { currency, date, - isDraft, quantity, SymbolProfile, + tags, type, unitPrice } of account.activities) { + activitiesCount += 1; + + if (isDraftActivity({ tags })) { + continue; + } + switch (type) { case ActivityType.DIVIDEND: dividendInBaseCurrency += @@ -226,10 +246,6 @@ export class PortfolioService { )) ?? 0; break; } - - if (!isDraft) { - activitiesCount += 1; - } } const valueInBaseCurrency = @@ -2101,8 +2117,8 @@ export class PortfolioService { }) { return getSum( activities - .filter(({ isDraft, type }) => { - return isDraft === false && type === activityType; + .filter((activity) => { + return !isDraftActivity(activity) && activity.type === activityType; }) .map(({ assetProfile, currency, quantity, unitPrice }) => { return new Big( diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 516b68e8e..a055f029e 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -35,6 +35,7 @@ import { PROPERTY_MAX_DAILY_REQUESTS, PROPERTY_REFERRAL_PARTNERS, PROPERTY_SYSTEM_MESSAGE, + TAG_ID_DRAFT, TAG_ID_EXCLUDE_FROM_ANALYSIS, THROTTLE_DAILY_KEY, THROTTLE_DAILY_TTL @@ -195,7 +196,7 @@ export class UserService { subscription.type === SubscriptionType.Basic ) { tags = tags.filter(({ id }) => { - return id === TAG_ID_EXCLUDE_FROM_ANALYSIS; + return [TAG_ID_DRAFT, TAG_ID_EXCLUDE_FROM_ANALYSIS].includes(id); }); } diff --git a/apps/api/src/helper/activity.helper.ts b/apps/api/src/helper/activity.helper.ts new file mode 100644 index 000000000..b281106c7 --- /dev/null +++ b/apps/api/src/helper/activity.helper.ts @@ -0,0 +1,74 @@ +import { + NON_INVESTMENT_ACTIVITY_TYPES, + TAG_ID_DRAFT +} from '@ghostfolio/common/config'; + +import { Prisma, Type as ActivityType } from '@prisma/client'; +import { endOfToday, isAfter } from 'date-fns'; +import { uniqBy } from 'lodash'; + +export const WHERE_ACTIVITY_NOT_DRAFT: Prisma.OrderWhereInput = { + tags: { + none: { + id: TAG_ID_DRAFT + } + } +}; + +export function getTagsWithDraftTag({ + date, + draftTag, + endOfTodayDate = endOfToday(), + originalDate, + tags, + type +}: { + date: Date; + draftTag: T; + endOfTodayDate?: Date; + originalDate?: Date; + tags: T[]; + type: ActivityType; +}) { + if (!isDraftTagToBeAssigned({ date, endOfTodayDate, originalDate, type })) { + return tags; + } + + return uniqBy([...tags, draftTag], 'id'); +} + +export function isActivityInFuture({ + date, + endOfTodayDate = endOfToday() +}: { + date: Date; + endOfTodayDate?: Date; +}) { + return isAfter(date, endOfTodayDate); +} + +export function isDraftTagToBeAssigned({ + date, + endOfTodayDate = endOfToday(), + originalDate, + type +}: { + date: Date; + endOfTodayDate?: Date; + originalDate?: Date; + type: ActivityType; +}) { + if (NON_INVESTMENT_ACTIVITY_TYPES.includes(type)) { + return false; + } + + if (!isActivityInFuture({ date, endOfTodayDate })) { + return false; + } + + // Assign only when the date newly moves into the future, so that a tag the + // user has removed is not restored by an unrelated change + return originalDate + ? !isActivityInFuture({ endOfTodayDate, date: originalDate }) + : true; +} diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index de052f9a1..abcf349d9 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_DRAFT } from '@ghostfolio/common/config'; import { HttpException, Injectable } from '@nestjs/common'; import { Prisma, Tag } from '@prisma/client'; @@ -160,4 +161,23 @@ export class TagService { ); } } + + public async validateTagIdsWithoutDraftTag({ + tagIds, + userId + }: { + tagIds: string[]; + userId: string; + }) { + // The "DRAFT" tag qualifies an individual activity and can therefore + // neither be assigned to an account nor to all activities of a holding + if (tagIds?.includes(TAG_ID_DRAFT)) { + throw new HttpException( + getReasonPhrase(StatusCodes.BAD_REQUEST), + StatusCodes.BAD_REQUEST + ); + } + + return this.validateTagIds({ tagIds, userId }); + } } diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 57b8196d6..28e713a41 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -3,7 +3,8 @@ import { DEFAULT_PAGE_SIZE, E_MAIL_LINE_BREAK, NUMERICAL_PRECISION_THRESHOLD_3_FIGURES, - NUMERICAL_PRECISION_THRESHOLD_4_FIGURES + NUMERICAL_PRECISION_THRESHOLD_4_FIGURES, + TAG_ID_DRAFT } from '@ghostfolio/common/config'; import { CreateOrderDto } from '@ghostfolio/common/dtos'; import { @@ -591,12 +592,18 @@ export class GfHoldingDetailDialogComponent implements OnInit { ); this.tagsAvailable = - this.user?.tags?.map((tag) => { - return { - ...tag, - name: translate(tag.name) - }; - }) ?? []; + this.user?.tags + ?.filter(({ id }) => { + // The "DRAFT" tag qualifies an individual activity and cannot + // be assigned to all activities of a holding + return id !== TAG_ID_DRAFT; + }) + .map((tag) => { + return { + ...tag, + name: translate(tag.name) + }; + }) ?? []; this.changeDetectorRef.markForCheck(); } diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts index c1d171b6f..b46ef3aff 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts @@ -1,4 +1,5 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { TAG_ID_DRAFT } from '@ghostfolio/common/config'; import { CreateAccountDto, UpdateAccountDto } from '@ghostfolio/common/dtos'; import { getStringOrNull } from '@ghostfolio/common/helper'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -92,12 +93,18 @@ export class GfCreateOrUpdateAccountDialogComponent { ); this.tagsAvailable = - this.data.user?.tags?.map((tag) => { - return { - ...tag, - name: translate(tag.name) - }; - }) ?? []; + this.data.user?.tags + ?.filter(({ id }) => { + // The "DRAFT" tag qualifies an individual activity and cannot be + // assigned to an account + return id !== TAG_ID_DRAFT; + }) + .map((tag) => { + return { + ...tag, + name: translate(tag.name) + }; + }) ?? []; this.accountForm = this.formBuilder.group({ accountId: [{ disabled: true, value: this.data.account.id }], diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 23c8aab69..761f7c3ed 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -343,12 +343,14 @@ export const SUPPORTED_LANGUAGE_CODES = [ ] as const; export const TAG_ID_DEMO = 'efa08cb3-9b9d-4974-ac68-db13a19c4874'; +export const TAG_ID_DRAFT = '0c077abd-eca2-4cbb-818c-6cefbf2d169a'; 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_IDS_SYSTEM = [ TAG_ID_DEMO, + TAG_ID_DRAFT, TAG_ID_EMERGENCY_FUND, TAG_ID_EXCLUDE_FROM_ANALYSIS ]; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 44fd89aa2..d67abe03c 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -43,6 +43,7 @@ import { ghostfolioFearAndGreedIndexSymbolStocks, ghostfolioPrefix, SEARCH_QUERY_MINIMUM_LENGTH, + TAG_ID_DRAFT, TAG_ID_EXCLUDE_FROM_ANALYSIS, TAG_IDS_SYSTEM } from './config'; @@ -537,6 +538,14 @@ export function isDerivedCurrency(aCurrency: string) { }); } +export function isDraftActivity(activity?: { tags?: { id: string }[] }) { + return ( + activity?.tags?.some(({ id }) => { + return id === TAG_ID_DRAFT; + }) === true + ); +} + export function isRootCurrency(aCurrency: string) { if (aCurrency === 'USD') { return true; 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 7f53da4fa..9c10b5cb7 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -177,7 +177,7 @@
{{ element.assetProfile?.name }} - @if (element.isDraft) { + @if (isDraftActivity(element)) { Draft }
@@ -513,7 +513,7 @@