diff --git a/CHANGELOG.md b/CHANGELOG.md index d85f138b6..d8d63de05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Set the change detection strategy to `OnPush` in the _FIRE_ page +- Improved the language localization for German (`de`) + +## 3.21.0 - 2026-07-05 + +### 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 + +- Set the change detection strategy to `OnPush` in the alert dialog component +- Set the change detection strategy to `OnPush` in the confirmation dialog component +- 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` - Upgraded `stripe` from version `21.0.1` to `22.2.3` +### 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 + ## 3.20.0 - 2026-07-04 ### Changed diff --git a/README.md b/README.md index b72f03cd3..a9b3a3055 100644 --- a/README.md +++ b/README.md @@ -378,10 +378,6 @@ If you like to support this project, get [**Ghostfolio Premium**](https://ghostf -## Analytics - -![Alt](https://repobeats.axiom.co/api/embed/281a80b2d0c4af1162866c24c803f1f18e5ed60e.svg 'Repobeats analytics image') - ## License © 2021 - 2026 [Ghostfolio](https://ghostfol.io) 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/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index e50c0c77f..e6f5fd700 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -21,6 +21,7 @@ import { AdminUsersResponse, AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; +import { PropertyKey } from '@ghostfolio/common/types'; import { BadRequestException, @@ -347,9 +348,14 @@ export class AdminService { let response: Property; if (value) { - response = await this.propertyService.put({ key, value }); + response = await this.propertyService.put({ + value, + key: key as PropertyKey + }); } else { - response = await this.propertyService.delete({ key }); + response = await this.propertyService.delete({ + key: key as PropertyKey + }); } if (key === PROPERTY_IS_READ_ONLY_MODE && value === 'true') { diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 0a27faa64..04b1f3bf2 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -147,7 +147,9 @@ import { UserModule } from './user/user.module'; .split(',')[0] .split('-')[0]; - if (SUPPORTED_LANGUAGE_CODES.includes(code)) { + if ( + (SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(code) + ) { languageCode = code; } } catch {} 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/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts index 9a93d0419..f21418cb4 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts @@ -65,7 +65,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index c876d0db1..e2cba2e61 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -65,7 +65,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts index ae921d6d9..fa397de46 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -65,7 +65,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts index 6207f1417..db44fe0ae 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts @@ -65,7 +65,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts index 774c1d2f6..1afd9225d 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts @@ -87,7 +87,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts index 055356325..0e5750166 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts @@ -75,7 +75,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 11765fc49..1e556735d 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -77,7 +77,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts index 6a45f79c6..9d84540e3 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts @@ -75,7 +75,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts index 64882061f..0916e18a4 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts @@ -75,7 +75,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index 2d85330a3..589168989 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -125,7 +125,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); portfolioCalculatorFactory = new PortfolioCalculatorFactory( configurationService, @@ -148,15 +148,15 @@ describe('PortfolioCalculator', () => { balances: [ { accountId, - id: randomUUID(), date: parseDate('2023-12-31'), + id: randomUUID(), value: 1000, valueInBaseCurrency: 850 }, { accountId, - id: randomUUID(), date: parseDate('2024-12-31'), + id: randomUUID(), value: 2000, valueInBaseCurrency: 1800 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts index a3fbc0758..b55c94b2b 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts @@ -65,7 +65,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts index 122a9aaed..bb61d637e 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts @@ -77,7 +77,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts index d5b22e864..88afc971d 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts @@ -78,7 +78,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts index acbf6a66b..4f2058513 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts @@ -65,7 +65,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts index baa6ae1ed..36d1ea1ec 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts @@ -60,7 +60,7 @@ describe('PortfolioCalculator', () => { null, null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); portfolioCalculatorFactory = new PortfolioCalculatorFactory( configurationService, diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts index e7eff6682..c50ef2b34 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -65,7 +65,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts index 6c47af7ca..30ebaa8c4 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts @@ -60,7 +60,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index 3034e3a1f..cdfc78906 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -78,7 +78,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts index c79fdef58..2cd754c82 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -78,7 +78,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts index e518a5994..1d6cee0fa 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts @@ -65,7 +65,7 @@ describe('PortfolioCalculator', () => { null ); - portfolioSnapshotService = new PortfolioSnapshotService(null); + portfolioSnapshotService = new PortfolioSnapshotService(null, null); redisCacheService = new RedisCacheService(null, null); 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/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts index 074a9db0e..4018e4753 100644 --- a/apps/api/src/app/subscription/subscription.controller.ts +++ b/apps/api/src/app/subscription/subscription.controller.ts @@ -116,11 +116,11 @@ export class SubscriptionController { @Post('stripe/checkout-session') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public createStripeCheckoutSession( + public async createStripeCheckoutSession( @Body() { couponId, priceId }: { couponId?: string; priceId: string } ): Promise { try { - return this.subscriptionService.createStripeCheckoutSession({ + return await this.subscriptionService.createStripeCheckoutSession({ couponId, priceId, user: this.request.user diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index ab19d669a..07c2e7dbb 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -3,7 +3,8 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { DEFAULT_LANGUAGE_CODE, - PROPERTY_STRIPE_CONFIG + PROPERTY_STRIPE_CONFIG, + SUPPORTED_LANGUAGE_CODES } from '@ghostfolio/common/config'; import { SubscriptionType } from '@ghostfolio/common/enums'; import { parseDate } from '@ghostfolio/common/helper'; @@ -75,10 +76,7 @@ export class SubscriptionService { quantity: 1 } ], - locale: - (user.settings?.settings - ?.language as Stripe.Checkout.SessionCreateParams.Locale) ?? - DEFAULT_LANGUAGE_CODE, + locale: this.getStripeLocale(user.settings?.settings?.language), metadata: subscriptionOffer ? { subscriptionOffer: JSON.stringify(subscriptionOffer) } : {}, @@ -246,4 +244,28 @@ export class SubscriptionService { isRenewal: key.startsWith('renewal') }; } + + private getStripeLocale( + languageCode: string + ): Stripe.Checkout.SessionCreateParams.Locale { + const unsupportedLanguageCodes: Record< + Exclude< + (typeof SUPPORTED_LANGUAGE_CODES)[number], + Stripe.Checkout.SessionCreateParams.Locale + >, + true + > = { + ca: true, + uk: true + }; + + if ( + (SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(languageCode) && + !(languageCode in unsupportedLanguageCodes) + ) { + return languageCode as Stripe.Checkout.SessionCreateParams.Locale; + } + + return DEFAULT_LANGUAGE_CODE; + } } 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/middlewares/html-template.middleware.ts b/apps/api/src/middlewares/html-template.middleware.ts index c256ada56..325f64eaa 100644 --- a/apps/api/src/middlewares/html-template.middleware.ts +++ b/apps/api/src/middlewares/html-template.middleware.ts @@ -117,7 +117,9 @@ export class HtmlTemplateMiddleware implements NestMiddleware { const path = request.originalUrl.replace(/\/$/, ''); let languageCode = path.substr(1, 2); - if (!SUPPORTED_LANGUAGE_CODES.includes(languageCode)) { + if ( + !(SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(languageCode) + ) { languageCode = DEFAULT_LANGUAGE_CODE; } diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index 703a90c3a..c96ccd946 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -96,6 +96,9 @@ export class ConfigurationService { PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY: num({ default: DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY }), + PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_REMOVE_ON_FAIL: bool({ + default: true + }), PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT: num({ default: DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT }), diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 11bdeabf6..89463e201 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -47,6 +47,7 @@ export interface Environment extends CleanedEnvAccessors { PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY: number; PROCESSOR_GATHER_STATISTICS_CONCURRENCY: number; PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY: number; + PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_REMOVE_ON_FAIL: boolean; PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT: number; REDIS_DB: number; REDIS_HOST: string; diff --git a/apps/api/src/services/property/property.service.ts b/apps/api/src/services/property/property.service.ts index 212635f49..80643482f 100644 --- a/apps/api/src/services/property/property.service.ts +++ b/apps/api/src/services/property/property.service.ts @@ -3,6 +3,7 @@ import { PROPERTY_CURRENCIES, PROPERTY_IS_USER_SIGNUP_ENABLED } from '@ghostfolio/common/config'; +import { PropertyKey } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; @@ -12,7 +13,7 @@ import { PropertyValue } from './interfaces/interfaces'; export class PropertyService { public constructor(private readonly prismaService: PrismaService) {} - public async delete({ key }: { key: string }) { + public async delete({ key }: { key: PropertyKey }) { return this.prismaService.property.delete({ where: { key } }); @@ -40,7 +41,7 @@ export class PropertyService { return response; } - public async getByKey(aKey: string) { + public async getByKey(aKey: PropertyKey) { const properties = await this.get(); return properties[aKey] as TValue; } @@ -51,7 +52,7 @@ export class PropertyService { ); } - public async put({ key, value }: { key: string; value: string }) { + public async put({ key, value }: { key: PropertyKey; value: string }) { return this.prismaService.property.upsert({ create: { key, value }, update: { value }, diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts index d7449a9cc..99fa5aca2 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts @@ -1,3 +1,4 @@ +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE } from '@ghostfolio/common/config'; import { InjectQueue } from '@nestjs/bull'; @@ -9,6 +10,7 @@ import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue @Injectable() export class PortfolioSnapshotService { public constructor( + private readonly configurationService: ConfigurationService, @InjectQueue(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE) private readonly portfolioSnapshotQueue: Queue ) {} @@ -22,7 +24,12 @@ export class PortfolioSnapshotService { name: string; opts?: JobOptions; }) { - return this.portfolioSnapshotQueue.add(name, data, opts); + return this.portfolioSnapshotQueue.add(name, data, { + ...opts, + removeOnFail: this.configurationService.get( + 'PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_REMOVE_ON_FAIL' + ) + }); } public async getJob(jobId: string) { 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-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index 3b8fdc38c..b2c9b11a3 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -27,6 +27,7 @@ import { GfValueComponent } from '@ghostfolio/ui/value'; import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; import { + ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, @@ -64,6 +65,7 @@ import { import ms, { StringValue } from 'ms'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ClipboardModule, CommonModule, @@ -140,6 +142,8 @@ export class GfAdminOverviewComponent implements OnInit { permissions.toggleReadOnlyMode ); } + + this.changeDetectorRef.markForCheck(); }); addIcons({ 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 + + + + + +