From 79e382a8f7218435c86d0b9cbb408f4a1c1e19a9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 5 Jul 2026 17:10:32 +0200 Subject: [PATCH 1/4] Feature/add tags support in accounts (#7242) * Add tags support in accounts * Update changelog --- CHANGELOG.md | 3 + .../account-balance.service.ts | 3 +- .../api/src/app/account/account.controller.ts | 40 +++--- apps/api/src/app/account/account.service.ts | 109 ++++++++++++--- .../src/app/activities/activities.service.ts | 72 ++++++---- .../src/app/endpoints/tags/tags.controller.ts | 2 +- apps/api/src/app/export/export.service.ts | 24 ++-- apps/api/src/app/import/import.service.ts | 126 ++++++++++-------- .../src/app/portfolio/portfolio.service.ts | 13 +- apps/api/src/helper/account.helper.ts | 12 ++ apps/api/src/services/tag/tag.service.ts | 16 ++- .../admin-tag/admin-tag.component.html | 20 ++- .../admin-tag/admin-tag.component.ts | 1 + .../holding-detail-dialog.component.ts | 15 ++- .../holding-detail-dialog.html | 1 + .../pages/accounts/accounts-page.component.ts | 19 ++- ...eate-or-update-account-dialog.component.ts | 90 ++++++++++++- .../create-or-update-account-dialog.html | 9 ++ .../interfaces/interfaces.ts | 6 +- .../common/src/lib/dtos/create-account.dto.ts | 11 ++ .../common/src/lib/dtos/update-account.dto.ts | 11 ++ libs/common/src/lib/helper.ts | 15 ++- .../responses/export-response.interface.ts | 1 + .../lib/types/account-with-platform.type.ts | 7 +- .../src/lib/types/account-with-value.type.ts | 3 +- .../accounts-table.component.html | 2 +- .../accounts-table.component.ts | 10 +- .../activities-table.component.ts | 6 +- .../tags-selector.component.html | 22 ++- .../tags-selector/tags-selector.component.ts | 31 +++-- .../migration.sql | 20 +++ prisma/schema.prisma | 20 ++- 32 files changed, 572 insertions(+), 168 deletions(-) create mode 100644 apps/api/src/helper/account.helper.ts create mode 100644 prisma/migrations/20260705095904_added_tags_on_accounts/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f165ada..f6dc69bf6 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 for tags in the account (experimental) - Exposed the `PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_REMOVE_ON_FAIL` environment variable to control the removal of failed jobs in the portfolio snapshot computation queue ### Changed @@ -18,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Set the change detection strategy to `OnPush` in the prompt dialog component - Set the change detection strategy to `OnPush` in the overview of the admin control panel - Set the change detection strategy to `OnPush` in the portfolio page +- Deprecated the `isExcluded` attribute of the account in favor of the _Exclude from Analysis_ tag - Improved the language localization in the users table of the admin control panel - Improved the language localization for German (`de`) - Upgraded `envalid` from version `8.1.1` to `8.2.0` @@ -25,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed an issue with the custom tags of the user in the import functionality - Fixed the creation of the _Stripe_ checkout session for languages not supported by _Stripe_ (`ca` and `uk`) - Fixed the error handling in the endpoint to create a _Stripe_ checkout session diff --git a/apps/api/src/app/account-balance/account-balance.service.ts b/apps/api/src/app/account-balance/account-balance.service.ts index 4851a1293..656fc2f63 100644 --- a/apps/api/src/app/account-balance/account-balance.service.ts +++ b/apps/api/src/app/account-balance/account-balance.service.ts @@ -1,4 +1,5 @@ import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; +import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; @@ -154,7 +155,7 @@ export class AccountBalanceService { } if (withExcludedAccounts === false) { - where.account = { isExcluded: false }; + where.account = WHERE_ACCOUNT_NOT_EXCLUDED; } const balances = await this.prismaService.accountBalance.findMany({ diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index d44b716c0..6466a13b2 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -156,27 +156,31 @@ export class AccountController { public async createAccount( @Body() data: CreateAccountDto ): Promise { - if (data.platformId) { - const platformId = data.platformId; - delete data.platformId; + const { tags: tagIds, ...accountData } = data; + + if (accountData.platformId) { + const platformId = accountData.platformId; + delete accountData.platformId; return this.accountService.createAccount( { - ...data, + ...accountData, platform: { connect: { id: platformId } }, user: { connect: { id: this.request.user.id } } }, - this.request.user.id + this.request.user.id, + tagIds ); } else { - delete data.platformId; + delete accountData.platformId; return this.accountService.createAccount( { - ...data, + ...accountData, user: { connect: { id: this.request.user.id } } }, - this.request.user.id + this.request.user.id, + tagIds ); } } @@ -253,14 +257,16 @@ export class AccountController { ); } - if (data.platformId) { - const platformId = data.platformId; - delete data.platformId; + const { tags: tagIds, ...accountData } = data; + + if (accountData.platformId) { + const platformId = accountData.platformId; + delete accountData.platformId; return this.accountService.updateAccount( { data: { - ...data, + ...accountData, platform: { connect: { id: platformId } }, user: { connect: { id: this.request.user.id } } }, @@ -271,16 +277,17 @@ export class AccountController { } } }, - this.request.user.id + this.request.user.id, + tagIds ); } else { // platformId is null, remove it - delete data.platformId; + delete accountData.platformId; return this.accountService.updateAccount( { data: { - ...data, + ...accountData, platform: originalAccount.platformId ? { disconnect: true } : undefined, @@ -293,7 +300,8 @@ export class AccountController { } } }, - this.request.user.id + this.request.user.id, + tagIds ); } } diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index f030b73da..d1f4009a9 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -1,11 +1,12 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; +import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Filter } from '@ghostfolio/common/interfaces'; -import { Injectable } from '@nestjs/common'; +import { HttpException, Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { Account, @@ -13,10 +14,12 @@ import { Order, Platform, Prisma, - SymbolProfile + SymbolProfile, + Tag } from '@prisma/client'; import { Big } from 'big.js'; import { format } from 'date-fns'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { groupBy } from 'lodash'; import { CashDetails } from './interfaces/cash-details.interface'; @@ -66,17 +69,27 @@ export class AccountService { activities?: (Order & { SymbolProfile?: SymbolProfile })[]; balances?: AccountBalance[]; platform?: Platform; + tags?: Tag[]; })[] > { const { include = {}, skip, take, cursor, where, orderBy } = params; const isBalancesIncluded = !!include.balances; + const isTagsIncluded = !!include.tags; include.balances = { orderBy: { date: 'desc' }, ...(isBalancesIncluded ? {} : { take: 1 }) }; + if (isTagsIncluded) { + include.tags = { + include: { + tag: true + } + }; + } + const accounts = await this.prismaService.account.findMany({ cursor, include, @@ -87,22 +100,48 @@ export class AccountService { }); return accounts.map((account) => { - account = { ...account, balance: account.balances[0]?.value ?? 0 }; + const result = { + ...account, + balance: account.balances[0]?.value ?? 0, + tags: isTagsIncluded + ? (account.tags as unknown as { tag: Tag }[]).map(({ tag }) => { + return tag; + }) + : undefined + }; if (!isBalancesIncluded) { - delete account.balances; + delete result.balances; + } + + if (!isTagsIncluded) { + delete result.tags; } - return account; + return result; }); } public async createAccount( data: Prisma.AccountCreateInput, - aUserId: string + aUserId: string, + tagIds?: string[] ): Promise { + await this.validateTagIds(tagIds, aUserId); + const account = await this.prismaService.account.create({ - data + data: { + ...data, + tags: tagIds + ? { + create: tagIds.map((tagId) => { + return { + tag: { connect: { id: tagId } } + }; + }) + } + : undefined + } }); await this.accountBalanceService.createOrUpdateAccountBalance({ @@ -143,7 +182,8 @@ export class AccountService { const accounts = await this.accounts({ include: { activities: true, - platform: true + platform: true, + tags: true }, orderBy: { name: 'asc' }, where: { userId: aUserId } @@ -184,7 +224,7 @@ export class AccountService { }; if (withExcludedAccounts === false) { - where.isExcluded = false; + where.AND = [WHERE_ACCOUNT_NOT_EXCLUDED]; } const { ACCOUNT: filtersByAccount = [] } = groupBy(filters, ({ type }) => { @@ -222,22 +262,37 @@ export class AccountService { where: Prisma.AccountWhereUniqueInput; data: Prisma.AccountUpdateInput; }, - aUserId: string + aUserId: string, + tagIds?: string[] ): Promise { const { data, where } = params; + await this.validateTagIds(tagIds, aUserId); + + const account = await this.prismaService.account.update({ + data: { + ...data, + tags: tagIds + ? { + create: tagIds.map((tagId) => { + return { + tag: { connect: { id: tagId } } + }; + }), + deleteMany: {} + } + : undefined + }, + where + }); + await this.accountBalanceService.createOrUpdateAccountBalance({ - accountId: data.id as string, + accountId: account.id, balance: data.balance as number, date: format(new Date(), DATE_FORMAT), userId: aUserId }); - const account = await this.prismaService.account.update({ - data, - where - }); - this.eventEmitter.emit( PortfolioChangedEvent.getName(), new PortfolioChangedEvent({ @@ -285,4 +340,26 @@ export class AccountService { }); } } + + private async validateTagIds(tagIds: string[], userId: string) { + if (!tagIds?.length) { + return; + } + + const uniqueTagIds = Array.from(new Set(tagIds)); + + const tagsCount = await this.prismaService.tag.count({ + where: { + id: { in: uniqueTagIds }, + OR: [{ userId }, { userId: null }] + } + }); + + if (tagsCount !== uniqueTagIds.length) { + throw new HttpException( + getReasonPhrase(StatusCodes.BAD_REQUEST), + StatusCodes.BAD_REQUEST + ); + } + } } diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 816d2328d..b404b6a37 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -3,6 +3,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface'; import { AssetProfileChangedEvent } from '@ghostfolio/api/events/asset-profile-changed.event'; import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; +import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.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'; @@ -568,18 +569,15 @@ export class ActivitiesService { { date: 'asc' } ]; - const where: Prisma.OrderWhereInput = { userId }; + const andConditions: Prisma.OrderWhereInput[] = []; + const where: Prisma.OrderWhereInput = { userId, AND: andConditions }; - if (endDate || startDate) { - where.AND = []; - - if (endDate) { - where.AND.push({ date: { lte: endDate } }); - } + if (endDate) { + andConditions.push({ date: { lte: endDate } }); + } - if (startDate) { - where.AND.push({ date: { gt: startDate } }); - } + if (startDate) { + andConditions.push({ date: { gt: startDate } }); } const { @@ -682,13 +680,30 @@ export class ActivitiesService { } if (filtersByTag.length > 0) { - where.tags = { - some: { - OR: filtersByTag.map(({ id }) => { - return { id }; - }) - } - }; + andConditions.push({ + OR: [ + { + tags: { + some: { + OR: filtersByTag.map(({ id }) => { + return { id }; + }) + } + } + }, + { + account: { + tags: { + some: { + OR: filtersByTag.map(({ id }) => { + return { tagId: id }; + }) + } + } + } + } + ] + }); } if (sortColumn) { @@ -700,13 +715,9 @@ export class ActivitiesService { } if (withExcludedAccountsAndActivities === false) { - where.OR = [ - { account: null }, - { account: { NOT: { isExcluded: true } } } - ]; + where.OR = [{ account: null }, { account: WHERE_ACCOUNT_NOT_EXCLUDED }]; where.tags = { - ...where.tags, none: { id: TAG_ID_EXCLUDE_FROM_ANALYSIS } @@ -721,7 +732,12 @@ export class ActivitiesService { include: { account: { include: { - platform: true + platform: true, + tags: { + include: { + tag: true + } + } } }, // eslint-disable-next-line @typescript-eslint/naming-convention @@ -733,6 +749,16 @@ export class ActivitiesService { this.prismaService.order.count({ where }) ]); + for (const order of orders) { + if (order.account) { + order.account.tags = ( + order.account.tags as unknown as { tag: Tag }[] + ).map(({ tag }) => { + return tag; + }); + } + } + const assetProfileIdentifiers = uniqBy( orders.map(({ SymbolProfile }) => { return { diff --git a/apps/api/src/app/endpoints/tags/tags.controller.ts b/apps/api/src/app/endpoints/tags/tags.controller.ts index 925e1e0ed..21e29c6ab 100644 --- a/apps/api/src/app/endpoints/tags/tags.controller.ts +++ b/apps/api/src/app/endpoints/tags/tags.controller.ts @@ -83,7 +83,7 @@ export class TagsController { @HasPermission(permissions.readTags) @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getTags() { - return this.tagService.getTagsWithActivityCount(); + return this.tagService.getTagsWithAccountAndActivityCount(); } @HasPermission(permissions.updateTag) diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index b94b2aa74..8ebfde13d 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -72,7 +72,8 @@ export class ExportService { where, include: { balances: true, - platform: true + platform: true, + tags: true }, orderBy: { name: 'asc' @@ -96,7 +97,8 @@ export class ExportService { isExcluded, name, platform, - platformId + platformId, + tags }) => { if (platformId) { platformsMap[platformId] = platform; @@ -112,7 +114,10 @@ export class ExportService { id, isExcluded, name, - platformId + platformId, + tags: tags.map(({ id: tagId }) => { + return tagId; + }) }; } ); @@ -151,11 +156,14 @@ export class ExportService { .filter(({ id, isUsed }) => { return ( isUsed && - activities.some((activity) => { - return activity.tags.some(({ id: tagId }) => { - return tagId === id; - }); - }) + (accounts.some(({ tags: tagIds }) => { + return tagIds.includes(id); + }) || + activities.some((activity) => { + return activity.tags.some(({ id: tagId }) => { + return tagId === id; + }); + })) ); }) .map(({ id, name }) => { diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index c7dab9823..9b1aa417c 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -10,11 +10,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathe import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config'; -import { - CreateAssetProfileDto, - CreateAccountDto, - CreateOrderDto -} from '@ghostfolio/common/dtos'; +import { CreateAssetProfileDto, CreateOrderDto } from '@ghostfolio/common/dtos'; import { getAssetProfileIdentifier, parseDate @@ -194,6 +190,60 @@ export class ImportService { const tagIdMapping: { [oldTagId: string]: string } = {}; const userCurrency = user.settings.settings.baseCurrency; + const existingTagsOfUser = + tagsDto?.length || (!isDryRun && accountsWithBalancesDto?.length) + ? await this.tagService.getTagsForUser(user.id) + : []; + + if (tagsDto?.length) { + const canCreateOwnTag = hasPermission( + user.permissions, + permissions.createOwnTag + ); + + for (const tag of tagsDto) { + const existingTagOfUser = existingTagsOfUser.find(({ id }) => { + return id === tag.id; + }); + + if (!existingTagOfUser) { + if (!canCreateOwnTag) { + throw new Error( + `Insufficient permissions to create custom tag ("${tag.name}")` + ); + } + + if (!isDryRun) { + const existingTag = await this.tagService.getTag({ id: tag.id }); + let oldTagId: string; + + if (existingTag) { + oldTagId = tag.id; + delete tag.id; + } + + const tagObject: Prisma.TagCreateInput = { + ...tag, + user: { connect: { id: user.id } } + }; + + const newTag = await this.tagService.createTag(tagObject); + + if (existingTag && oldTagId) { + tagIdMapping[oldTagId] = newTag.id; + } + + existingTagsOfUser.push({ + id: newTag.id, + isUsed: false, + name: newTag.name, + userId: newTag.userId + }); + } + } + } + } + if (!isDryRun && accountsWithBalancesDto?.length) { const [existingAccounts, existingPlatforms] = await Promise.all([ this.accountService.accounts({ @@ -208,6 +258,12 @@ export class ImportService { this.platformService.getPlatforms() ]); + const existingTagIds = new Set( + existingTagsOfUser.map(({ id }) => { + return id; + }) + ); + for (const accountWithBalances of accountsWithBalancesDto) { // Check if there is any existing account with the same ID const accountWithSameId = existingAccounts.find((existingAccount) => { @@ -216,10 +272,7 @@ export class ImportService { // If there is no account or if the account belongs to a different user then create a new account if (!accountWithSameId || accountWithSameId.userId !== user.id) { - const account: CreateAccountDto = omit( - accountWithBalances, - 'balances' - ); + const account = omit(accountWithBalances, ['balances', 'tags']); let oldAccountId: string; const platformId = account.platformId; @@ -231,6 +284,14 @@ export class ImportService { delete account.id; } + const tagIds = (accountWithBalances.tags ?? []) + .map((tagId) => { + return tagIdMapping[tagId] ?? tagId; + }) + .filter((tagId) => { + return existingTagIds.has(tagId); + }); + let accountObject: Prisma.AccountCreateInput = { ...account, balances: { @@ -252,7 +313,8 @@ export class ImportService { const newAccount = await this.accountService.createAccount( accountObject, - user.id + user.id, + tagIds ); // Store the new to old account ID mappings for updating activities @@ -320,50 +382,6 @@ export class ImportService { } } - if (tagsDto?.length) { - const existingTagsOfUser = await this.tagService.getTagsForUser(user.id); - - const canCreateOwnTag = hasPermission( - user.permissions, - permissions.createOwnTag - ); - - for (const tag of tagsDto) { - const existingTagOfUser = existingTagsOfUser.find(({ id }) => { - return id === tag.id; - }); - - if (!existingTagOfUser || existingTagOfUser.userId !== null) { - if (!canCreateOwnTag) { - throw new Error( - `Insufficient permissions to create custom tag ("${tag.name}")` - ); - } - - if (!isDryRun) { - const existingTag = await this.tagService.getTag({ id: tag.id }); - let oldTagId: string; - - if (existingTag) { - oldTagId = tag.id; - delete tag.id; - } - - const tagObject: Prisma.TagCreateInput = { - ...tag, - user: { connect: { id: user.id } } - }; - - const newTag = await this.tagService.createTag(tagObject); - - if (existingTag && oldTagId) { - tagIdMapping[oldTagId] = newTag.id; - } - } - } - } - } - for (const activity of activitiesDto) { if (!activity.dataSource) { if (['FEE', 'INTEREST', 'LIABILITY'].includes(activity.type)) { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b5ea9ba00..e75e54890 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -41,6 +41,7 @@ import { DATE_FORMAT, getAssetProfileIdentifier, getSum, + isAccountExcluded, parseDate } from '@ghostfolio/common/helper'; import { @@ -169,7 +170,8 @@ export class PortfolioService { where, include: { activities: { include: { SymbolProfile: true } }, - platform: true + platform: true, + tags: true }, orderBy: { name: 'asc' } }), @@ -1879,7 +1881,7 @@ export class PortfolioService { for (const activity of activities) { if ( - activity.account?.isExcluded || + (activity.account && isAccountExcluded(activity.account)) || activity.tags?.some(({ id }) => { return id === TAG_ID_EXCLUDE_FROM_ANALYSIS; }) @@ -2123,13 +2125,14 @@ export class PortfolioService { let currentAccounts: (Account & { Order?: Order[]; platform?: Platform; + tags?: Tag[]; })[] = []; if (filters.length === 0) { currentAccounts = await this.accountService.getAccounts(userId); } else if (filters.length === 1 && filters[0].type === 'ACCOUNT') { currentAccounts = await this.accountService.accounts({ - include: { platform: true }, + include: { platform: true, tags: true }, where: { id: filters[0].id } }); } else { @@ -2146,13 +2149,13 @@ export class PortfolioService { ); currentAccounts = await this.accountService.accounts({ - include: { platform: true }, + include: { platform: true, tags: true }, where: { id: { in: accountIds } } }); } currentAccounts = currentAccounts.filter((account) => { - return withExcludedAccounts || account.isExcluded === false; + return withExcludedAccounts || !isAccountExcluded(account); }); // Iterate over the accounts plus a null entry to group activities without diff --git a/apps/api/src/helper/account.helper.ts b/apps/api/src/helper/account.helper.ts new file mode 100644 index 000000000..0800c022e --- /dev/null +++ b/apps/api/src/helper/account.helper.ts @@ -0,0 +1,12 @@ +import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; + +import { Prisma } from '@prisma/client'; + +export const WHERE_ACCOUNT_NOT_EXCLUDED: Prisma.AccountWhereInput = { + isExcluded: false, + tags: { + none: { + tagId: TAG_ID_EXCLUDE_FROM_ANALYSIS + } + } +}; diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index f4cbd4cb1..d967ce854 100644 --- a/apps/api/src/services/tag/tag.service.ts +++ b/apps/api/src/services/tag/tag.service.ts @@ -52,6 +52,11 @@ export class TagService { include: { _count: { select: { + accounts: { + where: { + userId + } + }, activities: { where: { userId @@ -80,27 +85,28 @@ export class TagService { id, name, userId, - isUsed: _count.activities > 0 + isUsed: _count.accounts > 0 || _count.activities > 0 })) .sort((a, b) => { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }); } - public async getTagsWithActivityCount() { - const tagsWithOrderCount = await this.prismaService.tag.findMany({ + public async getTagsWithAccountAndActivityCount() { + const tagsWithAccountAndOrderCount = await this.prismaService.tag.findMany({ include: { _count: { - select: { activities: true } + select: { accounts: true, activities: true } } } }); - return tagsWithOrderCount.map(({ _count, id, name, userId }) => { + return tagsWithAccountAndOrderCount.map(({ _count, id, name, userId }) => { return { id, name, userId, + accountCount: _count.accounts, activityCount: _count.activities }; }); diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.html b/apps/client/src/app/components/admin-tag/admin-tag.component.html index a84cbb283..14b011ddc 100644 --- a/apps/client/src/app/components/admin-tag/admin-tag.component.html +++ b/apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -35,6 +35,24 @@ + + + Accounts + + + + + +