diff --git a/CHANGELOG.md b/CHANGELOG.md index 745c99df2..2036ce5ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,41 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 3.47.0 - 2026-08-10 + +### Changed + +- Extended the toggle component to support a disabled state +- Extended the toggle component to support icons +- Reused the toggle component on the portfolio holdings page +- Reused the currency selector component in the user account settings + +### Fixed + +- Fixed the handling of the disabled state in the currency selector and symbol autocomplete components +- Fixed the restoration of the current selection in the currency selector component when leaving the field without picking an option + +## 3.46.0 - 2026-08-09 + +### 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`) +- Upgraded `bull-board` from version `8.1.2` to `8.6.0` + +### 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_ + +## 3.45.0 - 2026-08-08 ### Added @@ -14,10 +48,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Aligned the x-axis of the dividend and investment timeline charts on the analysis page - Improved the check for duplicates in the preview step of the activities import (regardless of the account) - Improved the check for duplicates in the preview step of the import dividends dialog (regardless of the account) - Extended the activities import to reuse an existing account of the user by name and currency - Extended the activities import to resolve an ISIN to the symbol of the data provider +- Improved the style of the placeholder in the entity logo component +- Migrated the create, detail and edit account dialogs to dedicated routes +- Improved the language localization for German (`de`) ### Fixed 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 cb5e190ce..9f9aa5566 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 { adjustActivityBySplits } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.helper'; import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service'; @@ -23,11 +29,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 { @@ -51,7 +59,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'; @@ -115,7 +123,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; @@ -123,6 +131,7 @@ export class ActivitiesService { }); const activities = await this.prismaService.order.findMany({ + include: { tags: { select: { id: true } } }, where: { userId, SymbolProfile: { @@ -132,20 +141,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( @@ -264,17 +284,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 } @@ -640,8 +664,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) { @@ -983,6 +1011,7 @@ export class ActivitiesService { public async updateActivity({ data, + originalDate, userId, where }: { @@ -994,9 +1023,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({ @@ -1010,8 +1041,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' && @@ -1022,10 +1051,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: [ { @@ -1045,14 +1073,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/helper/country.helper.ts b/apps/api/src/helper/country.helper.ts index 1d9f8f99a..ecb28b8d3 100644 --- a/apps/api/src/helper/country.helper.ts +++ b/apps/api/src/helper/country.helper.ts @@ -1,3 +1,4 @@ +import { Logger } from '@nestjs/common'; import { countries } from 'countries-list'; export function getCountryCodeByName({ @@ -17,5 +18,11 @@ export function getCountryCodeByName({ } } + if (name) { + const logger = new Logger('getCountryCodeByName'); + + logger.warn(`Could not map the country "${name}" to a code`); + } + return undefined; } diff --git a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts index 29e4e5129..7f58448b7 100644 --- a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts @@ -16,6 +16,7 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { private static baseUrl = 'https://www.trackinsight.com'; private static countriesMapping = { + 'Czech Republic': 'CZ', 'Republic of Korea': 'KR', 'Russian Federation': 'RU', Turkey: 'TR', @@ -125,13 +126,14 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { for (const [name, value] of Object.entries( holdings?.countries ?? {} )) { - response.countries.push({ - code: getCountryCodeByName({ - name, - aliases: TrackinsightDataEnhancerService.countriesMapping - }), - weight: value.weight + const code = getCountryCodeByName({ + name, + aliases: TrackinsightDataEnhancerService.countriesMapping }); + + if (code) { + response.countries.push({ code, weight: value.weight }); + } } } diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index f7f2e7eb9..f4c36d256 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts @@ -169,9 +169,6 @@ export class FinancialModelingPrepService .then((res) => res.json()); response.countries = etfCountryWeightings - .filter(({ country: countryName }) => { - return countryName.toLowerCase() !== 'other'; - }) .map(({ country: countryName, weightPercentage }) => { return { code: getCountryCodeByName({ @@ -180,6 +177,9 @@ export class FinancialModelingPrepService }), weight: parseFloat(`${weightPercentage}`) / 100 }; + }) + .filter(({ code }) => { + return !!code; }); const etfHoldings = await this.fetchService 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/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index a0350ee6b..9aa0f6964 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -176,6 +176,8 @@ export class GfAccountDetailDialogComponent implements OnInit { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.initialize(); + + this.refreshUser(); }); } @@ -195,6 +197,8 @@ export class GfAccountDetailDialogComponent implements OnInit { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.initialize(); + + this.refreshUser(); }); } @@ -413,4 +417,11 @@ export class GfAccountDetailDialogComponent implements OnInit { this.fetchChart(); this.fetchPortfolioHoldings(); } + + private refreshUser() { + this.userService + .get(true) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(); + } } diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index 8fc012488..314fd21b5 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -848,13 +848,12 @@ export class GfAssetProfileDialogComponent implements OnInit { takeUntilDestroyed(this.destroyRef) ) .subscribe(({ price }) => { + const currency = this.assetProfileForm.controls.currency.value; + this.notificationService.alert({ - title: - $localize`The current market price is` + - ' ' + - price + - ' ' + - this.assetProfileForm.controls.currency.value + title: `${$localize`The current market price is`} ${price}${ + currency ? ` ${currency}` : '' + }` }); }); } diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html index 35f072d72..eaccbb332 100644 --- a/apps/client/src/app/components/header/header.component.html +++ b/apps/client/src/app/components/header/header.component.html @@ -217,7 +217,7 @@ Me - @for (accessItem of user()?.access; track accessItem) { + @for (accessItem of user()?.access; track accessItem.id) { - - +