diff --git a/.github/workflows/build-code.yml b/.github/workflows/build-code.yml index 18240ebc4..7a8f72ac4 100644 --- a/.github/workflows/build-code.yml +++ b/.github/workflows/build-code.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: node_version: - - 20 + - 22 steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 47943977f..6b4601733 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -19,7 +19,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghostfolio/ghostfolio + images: ${{ vars.DOCKER_REPOSITORY || 'ghostfolio/ghostfolio' }} tags: | type=semver,pattern={{major}} type=semver,pattern={{version}} diff --git a/.nvmrc b/.nvmrc index 9a2a0e219..53d1c14db 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20 +v22 diff --git a/CHANGELOG.md b/CHANGELOG.md index e140bef44..73bf118d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,30 +5,63 @@ 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 +## 2.164.0 - 2025-05-28 + +### Changed + +- Improved the language localization for Dutch (`nl`) +- Improved the language localization for French (`fr`) +- Improved the language localization for Polish (`pl`) +- Improved the language localization for Spanish (`es`) +- Upgraded `Node.js` from version `20` to `22` (`Dockerfile`) +- Upgraded `yahoo-finance2` from version `3.3.4` to `3.3.5` + +## 2.163.0 - 2025-05-26 + +### Changed + +- Improved the language localization for Italian (`it`) +- Improved the language localization for Turkish (`tr`) +- Upgraded `yahoo-finance2` from version `3.3.3` to `3.3.4` + +## 2.162.1 - 2025-05-24 ### Added +- Added a hint about delayed market data to the markets overview - Added the asset profile count per data provider to the endpoint `GET api/v1/admin` ### Changed +- Increased the robustness of the search in the _Yahoo Finance_ service by catching schema validation errors +- Improved the symbol lookup results by removing the currency from the name of cryptocurrencies (experimental) - Harmonized the data providers management style of the admin control panel - Extended the data providers management of the admin control panel by the asset profile count - Restricted the permissions of the demo user - Renamed `Order` to `activities` in the `User` database schema +- Removed the deprecated endpoint `GET api/v1/admin/market-data/:dataSource/:symbol` +- Removed the deprecated endpoint `POST api/v1/admin/market-data/:dataSource/:symbol` +- Removed the deprecated endpoint `PUT api/v1/admin/market-data/:dataSource/:symbol/:dateString` - Improved the language localization for Catalan (`ca`) - Improved the language localization for Chinese (`zh`) - Improved the language localization for Dutch (`nl`) +- Improved the language localization for French (`fr`) - Improved the language localization for German (`de`) - Improved the language localization for Italian (`it`) +- Improved the language localization for Polish (`pl`) +- Improved the language localization for Portuguese (`pt`) +- Improved the language localization for Spanish (`es`) - Upgraded `countup.js` from version `2.8.0` to `2.8.2` - Upgraded `nestjs` from version `10.4.15` to `11.0.12` -- Upgraded `yahoo-finance2` from version `2.11.3` to `3.3.1` +- Upgraded `prisma` from version `6.7.0` to `6.8.2` +- Upgraded `twitter-api-v2` from version `1.14.2` to `1.23.0` +- Upgraded `yahoo-finance2` from version `2.11.3` to `3.3.3` ### Fixed +- Displayed the button to fetch the current market price only if the activity is not in a custom currency - Fixed an issue in the watchlist endpoint (`POST`) related to the `HasPermissionGuard` +- Improved the text alignment of the allocations by ETF holding on the allocations page (experimental) ## 2.161.0 - 2025-05-06 @@ -74,7 +107,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Improved the language localization for Français (`fr`) +- Improved the language localization for French (`fr`) - Upgraded `bootstrap` from version `4.6.0` to `4.6.2` ### Fixed @@ -115,7 +148,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the error message of the currency code validation - Tightened the currency code validation by requiring uppercase letters - Respected the watcher count for the delete asset profiles checkbox in the historical market data table of the admin control panel -- Improved the language localization for Français (`fr`) +- Improved the language localization for French (`fr`) - Upgraded `ngx-skeleton-loader` from version `10.0.0` to `11.0.0` - Upgraded `Nx` from version `20.8.0` to `20.8.1` @@ -215,7 +248,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the check for duplicates in the preview step of the activities import (allow different comments) -- Improved the language localization for Français (`fr`) +- Improved the language localization for French (`fr`) - Improved the language localization for German (`de`) - Improved the language localization for Polish (`pl`) - Upgraded `ng-extract-i18n-merge` from version `2.14.1` to `2.14.3` @@ -3670,7 +3703,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added the language localization for Français (`fr`) +- Added the language localization for French (`fr`) - Extended the landing page by a global heat map of subscribers - Added support for the thousand separator in the global heat map component @@ -3699,7 +3732,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for the dividend timeline grouped by year - Added support for the investment timeline grouped by year -- Set up the language localization for Français (`fr`) +- Set up the language localization for French (`fr`) ### Changed @@ -3808,7 +3841,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the value redaction interceptor (including `comment`) -- Improved the language localization for Español (`es`) +- Improved the language localization for Spanish (`es`) - Upgraded `cheerio` from version `1.0.0-rc.6` to `1.0.0-rc.12` - Upgraded `prisma` from version `4.6.1` to `4.7.1` @@ -4037,7 +4070,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the usage of the value component in the admin control panel -- Improved the language localization for Español (`es`) +- Improved the language localization for Spanish (`es`) ### Fixed @@ -4059,7 +4092,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Set up the language localization for Español (`es`) +- Set up the language localization for Spanish (`es`) - Added support for sectors in mutual funds ## 1.198.0 - 25.09.2022 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 1c45aeca1..51de2ad25 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -5,7 +5,7 @@ ### Prerequisites - [Docker](https://www.docker.com/products/docker-desktop) -- [Node.js](https://nodejs.org/en/download) (version 20+) +- [Node.js](https://nodejs.org/en/download) (version 22+) - Create a local copy of this Git repository (clone) - Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`) diff --git a/Dockerfile b/Dockerfile index 103dc3b9e..922b880e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM node:20-slim AS builder +FROM --platform=$BUILDPLATFORM node:22-slim AS builder # Build application and add additional files WORKDIR /ghostfolio @@ -50,7 +50,7 @@ COPY package.json /ghostfolio/dist/apps/api RUN npm run database:generate-typings # Image to run, copy everything needed from builder -FROM node:20-slim +FROM node:22-slim LABEL org.opencontainers.image.source="https://github.com/ghostfolio/ghostfolio" ENV NODE_ENV=production diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index d8507bbb0..736f6da33 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -3,7 +3,6 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard' import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; -import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { @@ -16,7 +15,6 @@ import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { AdminData, AdminMarketData, - AdminMarketDataDetails, AdminUsers, EnhancedSymbolProfile } from '@ghostfolio/common/interfaces'; @@ -50,8 +48,6 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { AdminService } from './admin.service'; import { UpdateAssetProfileDto } from './update-asset-profile.dto'; -import { UpdateBulkMarketDataDto } from './update-bulk-market-data.dto'; -import { UpdateMarketDataDto } from './update-market-data.dto'; @Controller('admin') export class AdminController { @@ -60,7 +56,6 @@ export class AdminController { private readonly apiService: ApiService, private readonly dataGatheringService: DataGatheringService, private readonly manualService: ManualService, - private readonly marketDataService: MarketDataService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} @@ -214,19 +209,6 @@ export class AdminController { }); } - /** - * @deprecated - */ - @Get('market-data/:dataSource/:symbol') - @HasPermission(permissions.accessAdminControl) - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async getMarketDataBySymbol( - @Param('dataSource') dataSource: DataSource, - @Param('symbol') symbol: string - ): Promise { - return this.adminService.getMarketDataBySymbol({ dataSource, symbol }); - } - @HasPermission(permissions.accessAdminControl) @Post('market-data/:dataSource/:symbol/test') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @@ -253,58 +235,6 @@ export class AdminController { } } - /** - * @deprecated - */ - @HasPermission(permissions.accessAdminControl) - @Post('market-data/:dataSource/:symbol') - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async updateMarketData( - @Body() data: UpdateBulkMarketDataDto, - @Param('dataSource') dataSource: DataSource, - @Param('symbol') symbol: string - ) { - const dataBulkUpdate: Prisma.MarketDataUpdateInput[] = data.marketData.map( - ({ date, marketPrice }) => ({ - dataSource, - marketPrice, - symbol, - date: parseISO(date), - state: 'CLOSE' - }) - ); - - return this.marketDataService.updateMany({ - data: dataBulkUpdate - }); - } - - /** - * @deprecated - */ - @HasPermission(permissions.accessAdminControl) - @Put('market-data/:dataSource/:symbol/:dateString') - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async update( - @Param('dataSource') dataSource: DataSource, - @Param('dateString') dateString: string, - @Param('symbol') symbol: string, - @Body() data: UpdateMarketDataDto - ) { - const date = parseISO(dateString); - - return this.marketDataService.updateMarketData({ - data: { marketPrice: data.marketPrice, state: 'CLOSE' }, - where: { - dataSource_date_symbol: { - dataSource, - date, - symbol - } - } - }); - } - @HasPermission(permissions.accessAdminControl) @Post('profile-data/:dataSource/:symbol') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 35b4ea73d..258d8556f 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -135,7 +135,10 @@ export class AdminService { } public async get({ user }: { user: UserWithSettings }): Promise { - const dataSources = await this.dataProviderService.getDataSources({ user }); + const dataSources = await this.dataProviderService.getDataSources({ + user, + includeGhostfolio: true + }); const [settings, transactionCount, userCount] = await Promise.all([ this.propertyService.get(), diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 0aca4e62c..87a4db5fc 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -101,7 +101,7 @@ import { UserModule } from './user/user.module'; RedisCacheModule, ScheduleModule.forRoot(), ServeStaticModule.forRoot({ - exclude: ['/api*', '/sitemap.xml'], + exclude: ['/api/*wildcard', '/sitemap.xml'], rootPath: join(__dirname, '..', 'client'), serveStaticOptions: { setHeaders: (res) => { diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts index 83e1b5ced..7cb2520bb 100644 --- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts @@ -74,48 +74,6 @@ export class GhostfolioController { } } - /** - * @deprecated - */ - @Get('dividends/:symbol') - @HasPermission(permissions.enableDataProviderGhostfolio) - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async getDividendsV1( - @Param('symbol') symbol: string, - @Query() query: GetDividendsDto - ): Promise { - const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); - - if ( - this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), - StatusCodes.TOO_MANY_REQUESTS - ); - } - - try { - const dividends = await this.ghostfolioService.getDividends({ - symbol, - from: parseDate(query.from), - granularity: query.granularity, - to: parseDate(query.to) - }); - - await this.ghostfolioService.incrementDailyRequests({ - userId: this.request.user.id - }); - - return dividends; - } catch { - throw new HttpException( - getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), - StatusCodes.INTERNAL_SERVER_ERROR - ); - } - } - @Get('dividends/:symbol') @HasPermission(permissions.enableDataProviderGhostfolio) @UseGuards(AuthGuard('api-key'), HasPermissionGuard) @@ -156,48 +114,6 @@ export class GhostfolioController { } } - /** - * @deprecated - */ - @Get('historical/:symbol') - @HasPermission(permissions.enableDataProviderGhostfolio) - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async getHistoricalV1( - @Param('symbol') symbol: string, - @Query() query: GetHistoricalDto - ): Promise { - const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); - - if ( - this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), - StatusCodes.TOO_MANY_REQUESTS - ); - } - - try { - const historicalData = await this.ghostfolioService.getHistorical({ - symbol, - from: parseDate(query.from), - granularity: query.granularity, - to: parseDate(query.to) - }); - - await this.ghostfolioService.incrementDailyRequests({ - userId: this.request.user.id - }); - - return historicalData; - } catch { - throw new HttpException( - getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), - StatusCodes.INTERNAL_SERVER_ERROR - ); - } - } - @Get('historical/:symbol') @HasPermission(permissions.enableDataProviderGhostfolio) @UseGuards(AuthGuard('api-key'), HasPermissionGuard) @@ -238,47 +154,6 @@ export class GhostfolioController { } } - /** - * @deprecated - */ - @Get('lookup') - @HasPermission(permissions.enableDataProviderGhostfolio) - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async lookupSymbolV1( - @Query('includeIndices') includeIndicesParam = 'false', - @Query('query') query = '' - ): Promise { - const includeIndices = includeIndicesParam === 'true'; - const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); - - if ( - this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), - StatusCodes.TOO_MANY_REQUESTS - ); - } - - try { - const result = await this.ghostfolioService.lookup({ - includeIndices, - query: query.toLowerCase() - }); - - await this.ghostfolioService.incrementDailyRequests({ - userId: this.request.user.id - }); - - return result; - } catch { - throw new HttpException( - getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), - StatusCodes.INTERNAL_SERVER_ERROR - ); - } - } - @Get('lookup') @HasPermission(permissions.enableDataProviderGhostfolio) @UseGuards(AuthGuard('api-key'), HasPermissionGuard) @@ -320,44 +195,6 @@ export class GhostfolioController { } } - /** - * @deprecated - */ - @Get('quotes') - @HasPermission(permissions.enableDataProviderGhostfolio) - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async getQuotesV1( - @Query() query: GetQuotesDto - ): Promise { - const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); - - if ( - this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), - StatusCodes.TOO_MANY_REQUESTS - ); - } - - try { - const quotes = await this.ghostfolioService.getQuotes({ - symbols: query.symbols - }); - - await this.ghostfolioService.incrementDailyRequests({ - userId: this.request.user.id - }); - - return quotes; - } catch { - throw new HttpException( - getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), - StatusCodes.INTERNAL_SERVER_ERROR - ); - } - } - @Get('quotes') @HasPermission(permissions.enableDataProviderGhostfolio) @UseGuards(AuthGuard('api-key'), HasPermissionGuard) @@ -394,16 +231,6 @@ export class GhostfolioController { } } - /** - * @deprecated - */ - @Get('status') - @HasPermission(permissions.enableDataProviderGhostfolio) - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async getStatusV1(): Promise { - return this.ghostfolioService.getStatus({ user: this.request.user }); - } - @Get('status') @HasPermission(permissions.enableDataProviderGhostfolio) @UseGuards(AuthGuard('api-key'), HasPermissionGuard) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index c580ce149..7e373c4cc 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1249,7 +1249,7 @@ export class PortfolioService { const rules: PortfolioReportResponse['rules'] = { accountClusterRisk: - summary.ordersCount > 0 + summary.activityCount > 0 ? await this.rulesService.evaluate( [ new AccountClusterRiskCurrentInvestment( @@ -1265,7 +1265,7 @@ export class PortfolioService { ) : undefined, assetClassClusterRisk: - summary.ordersCount > 0 + summary.activityCount > 0 ? await this.rulesService.evaluate( [ new AssetClassClusterRiskEquity( @@ -1281,7 +1281,7 @@ export class PortfolioService { ) : undefined, currencyClusterRisk: - summary.ordersCount > 0 + summary.activityCount > 0 ? await this.rulesService.evaluate( [ new CurrencyClusterRiskBaseCurrencyCurrentInvestment( @@ -1297,7 +1297,7 @@ export class PortfolioService { ) : undefined, economicMarketClusterRisk: - summary.ordersCount > 0 + summary.activityCount > 0 ? await this.rulesService.evaluate( [ new EconomicMarketClusterRiskDevelopedMarkets( @@ -1338,7 +1338,7 @@ export class PortfolioService { userSettings ), regionalMarketClusterRisk: - summary.ordersCount > 0 + summary.activityCount > 0 ? await this.rulesService.evaluate( [ new RegionalMarketClusterRiskAsiaPacific( @@ -1981,6 +1981,9 @@ export class PortfolioService { netPerformanceWithCurrencyEffect, totalBuy, totalSell, + activityCount: activities.filter(({ type }) => { + return ['BUY', 'SELL'].includes(type); + }).length, committedFunds: committedFunds.toNumber(), currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(), dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), @@ -2008,9 +2011,6 @@ export class PortfolioService { interest: interest.toNumber(), items: valuables.toNumber(), liabilities: liabilities.toNumber(), - ordersCount: activities.filter(({ type }) => { - return ['BUY', 'SELL'].includes(type); - }).length, totalInvestment: totalInvestment.toNumber(), totalValueInBaseCurrency: netWorth }; diff --git a/apps/api/src/app/redis-cache/redis-cache.service.ts b/apps/api/src/app/redis-cache/redis-cache.service.ts index 97d71ae61..e6e98d622 100644 --- a/apps/api/src/app/redis-cache/redis-cache.service.ts +++ b/apps/api/src/app/redis-cache/redis-cache.service.ts @@ -80,7 +80,7 @@ export class RedisCacheService { public async isHealthy() { try { - const isHealthy = await Promise.race([ + await Promise.race([ this.getKeys(), new Promise((_, reject) => setTimeout( @@ -90,7 +90,7 @@ export class RedisCacheService { ) ]); - return isHealthy === 'PONG'; + return true; } catch (error) { return false; } diff --git a/apps/api/src/assets/sitemap.xml b/apps/api/src/assets/sitemap.xml index 2343f6c01..a2876c696 100644 --- a/apps/api/src/assets/sitemap.xml +++ b/apps/api/src/assets/sitemap.xml @@ -317,7 +317,7 @@ ${currentDate}T00:00:00+00:00 - https://ghostfol.io/fr/a-propos/changelog + https://ghostfol.io/fr/a-propos/journal-des-modifications ${currentDate}T00:00:00+00:00 @@ -383,7 +383,7 @@ ${currentDate}T00:00:00+00:00 - https://ghostfol.io/it/informazioni-su/changelog + https://ghostfol.io/it/informazioni-su/registro-delle-modifiche ${currentDate}T00:00:00+00:00 diff --git a/apps/api/src/helper/object.helper.spec.ts b/apps/api/src/helper/object.helper.spec.ts index b0370fa3f..d7caf9bc9 100644 --- a/apps/api/src/helper/object.helper.spec.ts +++ b/apps/api/src/helper/object.helper.spec.ts @@ -1515,6 +1515,7 @@ describe('redactAttributes', () => { } }, summary: { + activityCount: 29, annualizedPerformancePercent: 0.16690880197786, annualizedPerformancePercentWithCurrencyEffect: 0.1694019484552876, cash: null, @@ -1538,7 +1539,6 @@ describe('redactAttributes', () => { interest: null, items: null, liabilities: null, - ordersCount: 29, totalInvestment: null, totalValueInBaseCurrency: null, currentNetWorth: null @@ -3018,6 +3018,7 @@ describe('redactAttributes', () => { } }, summary: { + activityCount: 29, annualizedPerformancePercent: 0.16690880197786, annualizedPerformancePercentWithCurrencyEffect: 0.1694019484552876, cash: null, @@ -3041,7 +3042,6 @@ describe('redactAttributes', () => { interest: null, items: null, liabilities: null, - ordersCount: 29, totalInvestment: null, totalValueInBaseCurrency: null, currentNetWorth: null diff --git a/apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts index b24fb8404..1600bd137 100644 --- a/apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts @@ -43,7 +43,16 @@ export class TransformDataSourceInRequestInterceptor const dataSourceValue = request[type]?.dataSource; if (dataSourceValue && !DataSource[dataSourceValue]) { - request[type].dataSource = decodeDataSource(dataSourceValue); + // In Express 5, request.query is read-only, so request[type].dataSource cannot be directly modified + Object.defineProperty(request, type, { + configurable: true, + enumerable: true, + value: { + ...request[type], + dataSource: decodeDataSource(dataSourceValue) + }, + writable: true + }); } } } diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index 3d8f2e553..348935101 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -163,8 +163,10 @@ export class DataProviderService { } public async getDataSources({ + includeGhostfolio = false, user }: { + includeGhostfolio?: boolean; user: UserWithSettings; }): Promise { let dataSourcesKey: 'DATA_SOURCES' | 'DATA_SOURCES_LEGACY' = 'DATA_SOURCES'; @@ -187,7 +189,7 @@ export class DataProviderService { PROPERTY_API_KEY_GHOSTFOLIO )) as string; - if (ghostfolioApiKey || hasRole(user, 'ADMIN')) { + if (includeGhostfolio || ghostfolioApiKey) { dataSources.push('GHOSTFOLIO'); } @@ -663,9 +665,6 @@ export class DataProviderService { // Only allow symbols with supported currency return currency ? true : false; }) - .sort(({ name: name1 }, { name: name2 }) => { - return name1?.toLowerCase().localeCompare(name2?.toLowerCase()); - }) .map((lookupItem) => { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (user.subscription.type === 'Premium') { @@ -679,7 +678,21 @@ export class DataProviderService { lookupItem.dataProviderInfo.isPremium = false; } + if ( + lookupItem.assetSubClass === 'CRYPTOCURRENCY' && + user?.Settings?.settings.isExperimentalFeatures + ) { + // Remove DEFAULT_CURRENCY at the end of cryptocurrency names + lookupItem.name = lookupItem.name.replace( + new RegExp(` ${DEFAULT_CURRENCY}$`), + '' + ); + } + return lookupItem; + }) + .sort(({ name: name1 }, { name: name2 }) => { + return name1?.toLowerCase().localeCompare(name2?.toLowerCase()); }); return { diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index d5a132b41..06e4674fb 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts @@ -30,8 +30,11 @@ import { HistoricalDividendsResult, HistoricalHistoryResult } from 'yahoo-finance2/esm/src/modules/historical'; -import { Quote } from 'yahoo-finance2/esm/src/modules/quote'; -import { SearchQuoteNonYahoo } from 'yahoo-finance2/script/src/modules/search'; +import { + Quote, + QuoteResponseArray +} from 'yahoo-finance2/esm/src/modules/quote'; +import { SearchQuoteNonYahoo } from 'yahoo-finance2/esm/src/modules/search'; @Injectable() export class YahooFinanceService implements DataProviderInterface { @@ -281,11 +284,19 @@ export class YahooFinanceService implements DataProviderInterface { return true; }); - const marketData = await this.yahooFinance.quote( - quotes.map(({ symbol }) => { - return symbol; - }) - ); + let marketData: QuoteResponseArray = []; + + try { + marketData = await this.yahooFinance.quote( + quotes.map(({ symbol }) => { + return symbol; + }) + ); + } catch (error) { + if (error?.result?.length > 0) { + marketData = error.result; + } + } for (const marketDataItem of marketData) { const quote = quotes.find((currentQuote) => { diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index f4b61ea33..b3be70bb2 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -1,6 +1,6 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; -import { paths } from '@ghostfolio/client/core/paths'; import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy'; +import { paths } from '@ghostfolio/common/paths'; import { NgModule } from '@angular/core'; import { RouterModule, Routes, TitleStrategy } from '@angular/router'; @@ -14,21 +14,21 @@ const routes: Routes = [ import('./pages/about/about-page.module').then((m) => m.AboutPageModule) }, { - path: 'account', + path: paths.account, loadChildren: () => import('./pages/user-account/user-account-page.module').then( (m) => m.UserAccountPageModule ) }, { - path: 'accounts', + path: paths.accounts, loadChildren: () => import('./pages/accounts/accounts-page.module').then( (m) => m.AccountsPageModule ) }, { - path: 'admin', + path: paths.adminControl, loadChildren: () => import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule) }, @@ -38,16 +38,16 @@ const routes: Routes = [ import('./pages/api/api-page.component').then( (c) => c.GfApiPageComponent ), - path: 'api', + path: paths.api, title: 'Ghostfolio API' }, { - path: 'auth', + path: paths.auth, loadChildren: () => import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule) }, { - path: 'blog', + path: paths.blog, loadChildren: () => import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule) }, @@ -57,7 +57,7 @@ const routes: Routes = [ import('./pages/demo/demo-page.component').then( (c) => c.GfDemoPageComponent ), - path: 'demo' + path: paths.demo }, { path: paths.faq, @@ -74,7 +74,7 @@ const routes: Routes = [ title: $localize`Features` }, { - path: 'home', + path: paths.home, loadChildren: () => import('./pages/home/home-page.module').then((m) => m.HomePageModule) }, @@ -84,7 +84,7 @@ const routes: Routes = [ import('./pages/i18n/i18n-page.component').then( (c) => c.GfI18nPageComponent ), - path: 'i18n', + path: paths.i18n, title: $localize`Internationalization` }, { @@ -95,19 +95,12 @@ const routes: Routes = [ ) }, { - path: 'open', + path: paths.openStartup, loadChildren: () => import('./pages/open/open-page.module').then((m) => m.OpenPageModule) }, { - path: 'p', - loadChildren: () => - import('./pages/public/public-page.module').then( - (m) => m.PublicPageModule - ) - }, - { - path: 'portfolio', + path: paths.portfolio, loadChildren: () => import('./pages/portfolio/portfolio-page.module').then( (m) => m.PortfolioPageModule @@ -120,6 +113,13 @@ const routes: Routes = [ (m) => m.PricingPageModule ) }, + { + path: paths.public, + loadChildren: () => + import('./pages/public/public-page.module').then( + (m) => m.PublicPageModule + ) + }, { path: paths.register, loadChildren: () => @@ -135,7 +135,7 @@ const routes: Routes = [ ) }, { - path: 'start', + path: paths.start, loadChildren: () => import('./pages/landing/landing-page.module').then( (m) => m.LandingPageModule @@ -146,11 +146,11 @@ const routes: Routes = [ import('./pages/webauthn/webauthn-page.component').then( (c) => c.GfWebauthnPageComponent ), - path: 'webauthn', + path: paths.webauthn, title: $localize`Sign in` }, { - path: 'zen', + path: paths.zen, loadChildren: () => import('./pages/zen/zen-page.module').then((m) => m.ZenPageModule) }, diff --git a/apps/client/src/app/app.component.html b/apps/client/src/app/app.component.html index d5e56b517..a04e457ba 100644 --- a/apps/client/src/app/app.component.html +++ b/apps/client/src/app/app.component.html @@ -70,7 +70,7 @@
  • About
  • @if (hasPermissionForSubscription) {
  • - Blog + Blog
  • }
  • @@ -91,7 +91,7 @@ } @if (hasPermissionForStatistics) {
  • - Open Startup + Open Startup
  • } @if (hasPermissionForSubscription) { diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index 38e48f139..5d198580e 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -2,6 +2,7 @@ import { GfHoldingDetailDialogComponent } from '@ghostfolio/client/components/ho import { HoldingDetailDialogParams } from '@ghostfolio/client/components/holding-detail-dialog/interfaces/interfaces'; import { getCssVariable } from '@ghostfolio/common/helper'; import { InfoItem, User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { ColorScheme } from '@ghostfolio/common/types'; @@ -62,29 +63,25 @@ export class AppComponent implements OnDestroy, OnInit { public hasTabs = false; public info: InfoItem; public pageTitle: string; - public routerLinkAbout = ['/' + $localize`:snake-case:about`]; - public routerLinkAboutChangelog = [ - '/' + $localize`:snake-case:about`, - 'changelog' - ]; - public routerLinkAboutLicense = [ - '/' + $localize`:snake-case:about`, - $localize`:snake-case:license` - ]; + public routerLinkAbout = ['/' + paths.about]; + public routerLinkAboutChangelog = ['/' + paths.about, paths.changelog]; + public routerLinkAboutLicense = ['/' + paths.about, paths.license]; public routerLinkAboutPrivacyPolicy = [ - '/' + $localize`:snake-case:about`, - $localize`:snake-case:privacy-policy` + '/' + paths.about, + paths.privacyPolicy ]; public routerLinkAboutTermsOfService = [ - '/' + $localize`:snake-case:about`, - $localize`:snake-case:terms-of-service` + '/' + paths.about, + paths.termsOfService ]; - public routerLinkFaq = ['/' + $localize`:snake-case:faq`]; - public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; - public routerLinkMarkets = ['/' + $localize`:snake-case:markets`]; - public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; - public routerLinkRegister = ['/' + $localize`:snake-case:register`]; - public routerLinkResources = ['/' + $localize`:snake-case:resources`]; + public routerLinkBlog = ['/' + paths.blog]; + public routerLinkFaq = ['/' + paths.faq]; + public routerLinkFeatures = ['/' + paths.features]; + public routerLinkMarkets = ['/' + paths.markets]; + public routerLinkOpenStartup = ['/' + paths.openStartup]; + public routerLinkPricing = ['/' + paths.pricing]; + public routerLinkRegister = ['/' + paths.register]; + public routerLinkResources = ['/' + paths.resources]; public showFooter = false; public user: User; diff --git a/apps/client/src/app/components/access-table/access-table.component.ts b/apps/client/src/app/components/access-table/access-table.component.ts index 34c5fbda2..e70b6684a 100644 --- a/apps/client/src/app/components/access-table/access-table.component.ts +++ b/apps/client/src/app/components/access-table/access-table.component.ts @@ -2,6 +2,7 @@ import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/con import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { Access, User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { Clipboard } from '@angular/cdk/clipboard'; import { @@ -55,7 +56,7 @@ export class AccessTableComponent implements OnChanges { public getPublicUrl(aId: string): string { const languageCode = this.user?.settings?.language ?? DEFAULT_LANGUAGE_CODE; - return `${this.baseUrl}/${languageCode}/p/${aId}`; + return `${this.baseUrl}/${languageCode}/${paths.public}/${aId}`; } public onCopyUrlToClipboard(aId: string): void { 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 c77d8cb4a..ba3b9e871 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 @@ -9,6 +9,7 @@ import { PortfolioPosition, User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { OrderWithAccount } from '@ghostfolio/common/types'; @@ -92,7 +93,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit { } public onCloneActivity(aActivity: Activity) { - this.router.navigate(['/portfolio', 'activities'], { + this.router.navigate(['/' + paths.portfolio, paths.activities], { queryParams: { activityId: aActivity.id, createDialog: true } }); @@ -151,7 +152,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit { } public onUpdateActivity(aActivity: Activity) { - this.router.navigate(['/portfolio', 'activities'], { + this.router.navigate(['/' + paths.portfolio, paths.activities], { queryParams: { activityId: aActivity.id, editDialog: true } }); diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.ts b/apps/client/src/app/components/admin-settings/admin-settings.component.ts index 5c071c60c..3ff8370cd 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.component.ts +++ b/apps/client/src/app/components/admin-settings/admin-settings.component.ts @@ -13,6 +13,7 @@ import { DataProviderInfo, User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { ChangeDetectionStrategy, @@ -75,9 +76,7 @@ export class AdminSettingsComponent implements OnDestroy, OnInit { const languageCode = this.user?.settings?.language ?? DEFAULT_LANGUAGE_CODE; - this.pricingUrl = - `https://ghostfol.io/${languageCode}/` + - $localize`:snake-case:pricing`; + this.pricingUrl = `https://ghostfol.io/${languageCode}/${paths.pricing}`; this.changeDetectorRef.markForCheck(); } @@ -146,29 +145,35 @@ export class AdminSettingsComponent implements OnDestroy, OnInit { this.dataSource = new MatTableDataSource(filteredProviders); - this.adminService - .fetchGhostfolioDataProviderStatus( - settings[PROPERTY_API_KEY_GHOSTFOLIO] as string - ) - .pipe( - catchError(() => { - this.isGhostfolioApiKeyValid = false; + const ghostfolioApiKey = settings[ + PROPERTY_API_KEY_GHOSTFOLIO + ] as string; + + if (ghostfolioApiKey) { + this.adminService + .fetchGhostfolioDataProviderStatus(ghostfolioApiKey) + .pipe( + catchError(() => { + this.isGhostfolioApiKeyValid = false; + + this.changeDetectorRef.markForCheck(); + + return of(null); + }), + filter((status) => { + return status !== null; + }), + takeUntil(this.unsubscribeSubject) + ) + .subscribe((status) => { + this.ghostfolioApiStatus = status; + this.isGhostfolioApiKeyValid = true; this.changeDetectorRef.markForCheck(); - - return of(null); - }), - filter((status) => { - return status !== null; - }), - takeUntil(this.unsubscribeSubject) - ) - .subscribe((status) => { - this.ghostfolioApiStatus = status; - this.isGhostfolioApiKeyValid = true; - - this.changeDetectorRef.markForCheck(); - }); + }); + } else { + this.isGhostfolioApiKeyValid = false; + } this.isLoading = false; diff --git a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html index eab89f53d..21d68984a 100644 --- a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html +++ b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html @@ -29,7 +29,7 @@ }} } @if (hasPermissionToAccessAdminControl) { - +
    Manage Benchmarks diff --git a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts index afcb561f6..c98f01fb7 100644 --- a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts +++ b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts @@ -12,6 +12,7 @@ import { parseDate } from '@ghostfolio/common/helper'; import { LineChartItem, User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { ColorScheme } from '@ghostfolio/common/types'; @@ -63,6 +64,10 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy { public chart: Chart<'line'>; public hasPermissionToAccessAdminControl: boolean; + public routerLinkAdminControlMarketData = [ + '/' + paths.adminControl, + paths.marketData + ]; public constructor() { Chart.register( diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html index b14d142f4..74737ca05 100644 --- a/apps/client/src/app/components/header/header.component.html +++ b/apps/client/src/app/components/header/header.component.html @@ -20,9 +20,9 @@ mat-flat-button [ngClass]="{ 'font-weight-bold': - currentRoute === 'home' || currentRoute === 'zen', + currentRoute === paths.home || currentRoute === paths.zen, 'text-decoration-underline': - currentRoute === 'home' || currentRoute === 'zen' + currentRoute === paths.home || currentRoute === paths.zen }" [routerLink]="['/']" >OverviewPortfolio @@ -47,10 +47,10 @@ i18n mat-flat-button [ngClass]="{ - 'font-weight-bold': currentRoute === 'accounts', - 'text-decoration-underline': currentRoute === 'accounts' + 'font-weight-bold': currentRoute === paths.accounts, + 'text-decoration-underline': currentRoute === paths.accounts }" - [routerLink]="['/accounts']" + [routerLink]="routerLinkAccounts" >Accounts @@ -61,10 +61,10 @@ i18n mat-flat-button [ngClass]="{ - 'font-weight-bold': currentRoute === 'admin', - 'text-decoration-underline': currentRoute === 'admin' + 'font-weight-bold': currentRoute === paths.adminControl, + 'text-decoration-underline': currentRoute === paths.adminControl }" - [routerLink]="['/admin']" + [routerLink]="routerLinkAdminControl" >Admin Control @@ -235,7 +235,7 @@ mat-menu-item [ngClass]="{ 'font-weight-bold': - currentRoute === 'home' || currentRoute === 'zen' + currentRoute === paths.home || currentRoute === paths.zen }" [routerLink]="['/']" >OverviewPortfolio Accounts My Ghostfolio @if (hasPermissionToAccessAdminControl) { @@ -270,8 +270,10 @@ class="d-flex d-sm-none" i18n mat-menu-item - [ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }" - [routerLink]="['/admin']" + [ngClass]="{ + 'font-weight-bold': currentRoute === paths.adminControl + }" + [routerLink]="routerLinkAdminControl" >Admin Control } diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index 728320aef..52418bcb7 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -11,6 +11,7 @@ import { import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { Filter, InfoItem, User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DateRange } from '@ghostfolio/common/types'; import { GfAssistantComponent } from '@ghostfolio/ui/assistant/assistant.component'; @@ -79,17 +80,22 @@ export class HeaderComponent implements OnChanges { public hasPermissionToCreateUser: boolean; public impersonationId: string; public isMenuOpen: boolean; - public routeAbout = $localize`:snake-case:about`; - public routeFeatures = $localize`:snake-case:features`; - public routeMarkets = $localize`:snake-case:markets`; - public routePricing = $localize`:snake-case:pricing`; - public routeResources = $localize`:snake-case:resources`; - public routerLinkAbout = ['/' + $localize`:snake-case:about`]; - public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; - public routerLinkMarkets = ['/' + $localize`:snake-case:markets`]; - public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; - public routerLinkRegister = ['/' + $localize`:snake-case:register`]; - public routerLinkResources = ['/' + $localize`:snake-case:resources`]; + public paths = paths; + public routeAbout = paths.about; + public routeFeatures = paths.features; + public routeMarkets = paths.markets; + public routePricing = paths.pricing; + public routeResources = paths.resources; + public routerLinkAbout = ['/' + paths.about]; + public routerLinkAccount = ['/' + paths.account]; + public routerLinkAccounts = ['/' + paths.accounts]; + public routerLinkAdminControl = ['/' + paths.adminControl]; + public routerLinkFeatures = ['/' + paths.features]; + public routerLinkMarkets = ['/' + paths.markets]; + public routerLinkPortfolio = ['/' + paths.portfolio]; + public routerLinkPricing = ['/' + paths.pricing]; + public routerLinkRegister = ['/' + paths.register]; + public routerLinkResources = ['/' + paths.resources]; private unsubscribeSubject = new Subject(); diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 925a64429..028866009 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -13,6 +13,7 @@ import { LineChartItem, User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits'; @@ -468,7 +469,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { } public onCloneActivity(aActivity: Activity) { - this.router.navigate(['/portfolio', 'activities'], { + this.router.navigate(['/' + paths.portfolio, paths.activities], { queryParams: { activityId: aActivity.id, createDialog: true } }); @@ -510,7 +511,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { } public onUpdateActivity(aActivity: Activity) { - this.router.navigate(['/portfolio', 'activities'], { + this.router.navigate(['/' + paths.portfolio, paths.activities], { queryParams: { activityId: aActivity.id, editDialog: true } }); diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index dd411f6cc..9f772a3e4 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -7,6 +7,7 @@ import { ToggleOption, User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { HoldingType, HoldingsViewMode } from '@ghostfolio/common/types'; @@ -36,6 +37,10 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { { label: $localize`Active`, value: 'ACTIVE' }, { label: $localize`Closed`, value: 'CLOSED' } ]; + public routerLinkPortfolioActivities = [ + '/' + paths.portfolio, + paths.activities + ]; public user: User; public viewModeFormControl = new FormControl( HomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index f981e50a1..6040ffe2a 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -58,7 +58,7 @@ class="mt-3" i18n mat-stroked-button - [routerLink]="['/portfolio', 'activities']" + [routerLink]="routerLinkPortfolioActivities" >Manage Activities
    diff --git a/apps/client/src/app/components/home-market/home-market.html b/apps/client/src/app/components/home-market/home-market.html index 2fcdb5716..189c87c8f 100644 --- a/apps/client/src/app/components/home-market/home-market.html +++ b/apps/client/src/app/components/home-market/home-market.html @@ -36,6 +36,14 @@ [locale]="user?.settings?.locale || undefined" [user]="user" /> + @if (benchmarks?.length > 0) { +
    + + Calculations are based on delayed market data and may not be + displayed in real-time. +
    + } diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts index b0e7be320..783b7d1b9 100644 --- a/apps/client/src/app/components/home-overview/home-overview.component.ts +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -10,6 +10,7 @@ import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; @@ -36,6 +37,12 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { public isLoadingPerformance = true; public performance: PortfolioPerformance; public precision = 2; + public routerLinkAccounts = ['/' + paths.accounts]; + public routerLinkPortfolio = ['/' + paths.portfolio]; + public routerLinkPortfolioActivities = [ + '/' + paths.portfolio, + paths.activities + ]; public showDetails = false; public unit: string; public user: User; diff --git a/apps/client/src/app/components/home-overview/home-overview.html b/apps/client/src/app/components/home-overview/home-overview.html index c13c8f043..04b47277f 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -11,7 +11,7 @@ class="mb-2" [ngClass]="{ 'text-muted': user?.accounts?.length > 1 }" > - Setup your accounts
    Get a comprehensive financial overview by adding your bank and @@ -20,7 +20,7 @@ >
  • - + Capture your activities
    Record your investment activities to keep your portfolio up to @@ -29,7 +29,7 @@ >
  • - + Monitor and analyze your portfolio
    Track your progress in real-time with comprehensive analysis @@ -40,14 +40,18 @@
    @if (user?.accounts?.length === 1) { - + Setup accounts } @else if (user?.accounts?.length > 1) { Add activity diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 1a52bd646..265904b88 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -7,11 +7,11 @@
    - {{ summary?.ordersCount }} - {summary?.ordersCount, plural, + {{ summary?.activityCount }} + {summary?.activityCount, plural, =1 {activity} other {activities} } diff --git a/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts index 201a63927..233493d13 100644 --- a/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts +++ b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts @@ -1,3 +1,5 @@ +import { paths } from '@ghostfolio/common/paths'; + import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -26,7 +28,7 @@ export class SubscriptionInterstitialDialog implements OnInit { public remainingSkipButtonDelay = SubscriptionInterstitialDialog.SKIP_BUTTON_DELAY_IN_SECONDS; - public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; + public routerLinkPricing = ['/' + paths.pricing]; public variantIndex: number; private unsubscribeSubject = new Subject(); diff --git a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts index 8d54f737c..82810392c 100644 --- a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts +++ b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts @@ -4,6 +4,7 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { getDateFormatString } from '@ghostfolio/common/helper'; import { User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { @@ -36,7 +37,7 @@ export class UserAccountMembershipComponent implements OnDestroy { public hasPermissionToUpdateUserSettings: boolean; public price: number; public priceId: string; - public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; + public routerLinkPricing = ['/' + paths.pricing]; public trySubscriptionMail = 'mailto:hi@ghostfol.io?Subject=Ghostfolio Premium Trial&body=Hello%0D%0DI am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?%0D%0DKind regards'; public user: User; diff --git a/apps/client/src/app/core/auth.guard.ts b/apps/client/src/app/core/auth.guard.ts index 7a75728ca..87ef23d60 100644 --- a/apps/client/src/app/core/auth.guard.ts +++ b/apps/client/src/app/core/auth.guard.ts @@ -1,6 +1,7 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { paths } from '@ghostfolio/common/paths'; import { Injectable } from '@angular/core'; import { @@ -11,20 +12,18 @@ import { import { EMPTY } from 'rxjs'; import { catchError } from 'rxjs/operators'; -import { paths } from './paths'; - @Injectable({ providedIn: 'root' }) export class AuthGuard { private static PUBLIC_PAGE_ROUTES = [ `/${paths.about}`, - '/blog', - '/demo', + `/${paths.blog}`, + `/${paths.demo}`, `/${paths.faq}`, `/${paths.features}`, `/${paths.markets}`, - '/open', - '/p', + `/${paths.openStartup}`, `/${paths.pricing}`, + `/${paths.public}`, `/${paths.register}`, `/${paths.resources}` ]; @@ -49,21 +48,21 @@ export class AuthGuard { .pipe( catchError(() => { if (utmSource === 'ios') { - this.router.navigate(['/demo']); + this.router.navigate(['/' + paths.demo]); resolve(false); } else if (utmSource === 'trusted-web-activity') { - this.router.navigate(['/' + $localize`register`]); + this.router.navigate(['/' + paths.register]); resolve(false); } else if ( - AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) => { - const [, url] = state.url.split('/'); + AuthGuard.PUBLIC_PAGE_ROUTES.some((publicPageRoute) => { + const [, url] = decodeURIComponent(state.url).split('/'); return `/${url}` === publicPageRoute; - })?.length > 0 + }) ) { resolve(true); return EMPTY; } else if (state.url !== '/start') { - this.router.navigate(['/start']); + this.router.navigate(['/' + paths.start]); resolve(false); return EMPTY; } @@ -89,26 +88,26 @@ export class AuthGuard { resolve(true); return; } else if ( - state.url.startsWith('/home') && + state.url.startsWith(`/${paths.home}`) && user.settings.viewMode === 'ZEN' ) { - this.router.navigate(['/zen']); + this.router.navigate(['/' + paths.zen]); resolve(false); return; - } else if (state.url.startsWith('/start')) { + } else if (state.url.startsWith(`/${paths.start}`)) { if (user.settings.viewMode === 'ZEN') { - this.router.navigate(['/zen']); + this.router.navigate(['/' + paths.zen]); } else { - this.router.navigate(['/home']); + this.router.navigate(['/' + paths.home]); } resolve(false); return; } else if ( - state.url.startsWith('/zen') && + state.url.startsWith(`/${paths.zen}`) && user.settings.viewMode === 'DEFAULT' ) { - this.router.navigate(['/home']); + this.router.navigate(['/' + paths.home]); resolve(false); return; } diff --git a/apps/client/src/app/core/http-response.interceptor.ts b/apps/client/src/app/core/http-response.interceptor.ts index 62c3540f7..32c5350bb 100644 --- a/apps/client/src/app/core/http-response.interceptor.ts +++ b/apps/client/src/app/core/http-response.interceptor.ts @@ -2,6 +2,7 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { InfoItem } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { HTTP_INTERCEPTORS, @@ -74,7 +75,7 @@ export class HttpResponseInterceptor implements HttpInterceptor { }); this.snackBarRef.onAction().subscribe(() => { - this.router.navigate(['/' + $localize`pricing`]); + this.router.navigate(['/' + paths.pricing]); }); } } else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) { @@ -110,7 +111,7 @@ export class HttpResponseInterceptor implements HttpInterceptor { } else if (error.status === StatusCodes.UNAUTHORIZED) { if (!error.url.includes('/data-providers/ghostfolio/status')) { if (this.webAuthnService.isEnabled()) { - this.router.navigate(['/webauthn']); + this.router.navigate(['/' + paths.webauthn]); } else { this.tokenStorageService.signOut(); } diff --git a/apps/client/src/app/core/paths.ts b/apps/client/src/app/core/paths.ts deleted file mode 100644 index 17ce75c7c..000000000 --- a/apps/client/src/app/core/paths.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const paths = { - about: $localize`about`, - faq: $localize`faq`, - features: $localize`features`, - license: $localize`license`, - markets: $localize`markets`, - pricing: $localize`pricing`, - privacyPolicy: $localize`privacy-policy`, - register: $localize`register`, - resources: $localize`resources`, - termsOfService: $localize`terms-of-service` -}; diff --git a/apps/client/src/app/pages/about/about-page-routing.module.ts b/apps/client/src/app/pages/about/about-page-routing.module.ts index a7312001f..d8d70a2da 100644 --- a/apps/client/src/app/pages/about/about-page-routing.module.ts +++ b/apps/client/src/app/pages/about/about-page-routing.module.ts @@ -1,5 +1,5 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; -import { paths } from '@ghostfolio/client/core/paths'; +import { paths } from '@ghostfolio/common/paths'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; @@ -18,7 +18,7 @@ const routes: Routes = [ ) }, { - path: 'changelog', + path: paths.changelog, loadChildren: () => import('./changelog/changelog-page.module').then( (m) => m.ChangelogPageModule @@ -32,7 +32,7 @@ const routes: Routes = [ ) }, { - path: 'oss-friends', + path: paths.ossFriends, loadChildren: () => import('./oss-friends/oss-friends-page.module').then( (m) => m.OpenSourceSoftwareFriendsPageModule diff --git a/apps/client/src/app/pages/about/about-page.component.ts b/apps/client/src/app/pages/about/about-page.component.ts index 46a080383..deeffa166 100644 --- a/apps/client/src/app/pages/about/about-page.component.ts +++ b/apps/client/src/app/pages/about/about-page.component.ts @@ -1,6 +1,7 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; @@ -43,17 +44,17 @@ export class AboutPageComponent implements OnDestroy, OnInit { { iconName: 'information-circle-outline', label: $localize`About`, - path: ['/' + $localize`about`] + path: ['/' + paths.about] }, { iconName: 'sparkles-outline', label: $localize`Changelog`, - path: ['/' + $localize`about`, 'changelog'] + path: ['/' + paths.about, paths.changelog] }, { iconName: 'ribbon-outline', label: $localize`License`, - path: ['/' + $localize`about`, $localize`license`], + path: ['/' + paths.about, paths.license], showCondition: !this.hasPermissionForSubscription } ]; @@ -62,14 +63,14 @@ export class AboutPageComponent implements OnDestroy, OnInit { this.tabs.push({ iconName: 'shield-checkmark-outline', label: $localize`Privacy Policy`, - path: ['/' + $localize`about`, $localize`privacy-policy`], + path: ['/' + paths.about, paths.privacyPolicy], showCondition: this.hasPermissionForSubscription }); this.tabs.push({ iconName: 'document-text-outline', label: $localize`Terms of Service`, - path: ['/' + $localize`about`, $localize`terms-of-service`], + path: ['/' + paths.about, paths.termsOfService], showCondition: this.hasPermissionForSubscription }); @@ -81,7 +82,7 @@ export class AboutPageComponent implements OnDestroy, OnInit { this.tabs.push({ iconName: 'happy-outline', label: 'OSS Friends', - path: ['/' + $localize`about`, 'oss-friends'] + path: ['/' + paths.about, paths.ossFriends] }); }); } diff --git a/apps/client/src/app/pages/about/overview/about-overview-page.component.ts b/apps/client/src/app/pages/about/overview/about-overview-page.component.ts index 9dae545fa..ec135e4d3 100644 --- a/apps/client/src/app/pages/about/overview/about-overview-page.component.ts +++ b/apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -1,6 +1,7 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { User } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; @@ -17,8 +18,10 @@ export class AboutOverviewPageComponent implements OnDestroy, OnInit { public hasPermissionForStatistics: boolean; public hasPermissionForSubscription: boolean; public isLoggedIn: boolean; - public routerLinkFaq = ['/' + $localize`:snake-case:faq`]; - public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; + public routerLinkBlog = ['/' + paths.blog]; + public routerLinkFaq = ['/' + paths.faq]; + public routerLinkFeatures = ['/' + paths.features]; + public routerLinkOpenStartup = ['/' + paths.openStartup]; public user: User; private unsubscribeSubject = new Subject(); diff --git a/apps/client/src/app/pages/about/overview/about-overview-page.html b/apps/client/src/app/pages/about/overview/about-overview-page.html index 3891f929d..9a7dd142e 100644 --- a/apps/client/src/app/pages/about/overview/about-overview-page.html +++ b/apps/client/src/app/pages/about/overview/about-overview-page.html @@ -23,7 +23,9 @@ > @if (hasPermissionForStatistics) { and we share aggregated - key metrics + key metrics of the platform’s performance } . The project has been initiated by @@ -160,7 +162,7 @@ class="py-4 w-100" color="primary" mat-flat-button - [routerLink]="['/blog']" + [routerLink]="routerLinkBlog" >Blog
    diff --git a/apps/client/src/app/pages/admin/admin-page-routing.module.ts b/apps/client/src/app/pages/admin/admin-page-routing.module.ts index a6a69641c..d22c754bb 100644 --- a/apps/client/src/app/pages/admin/admin-page-routing.module.ts +++ b/apps/client/src/app/pages/admin/admin-page-routing.module.ts @@ -4,6 +4,7 @@ import { AdminOverviewComponent } from '@ghostfolio/client/components/admin-over import { AdminSettingsComponent } from '@ghostfolio/client/components/admin-settings/admin-settings.component'; import { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; +import { paths } from '@ghostfolio/common/paths'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; @@ -20,22 +21,22 @@ const routes: Routes = [ title: $localize`Admin Control` }, { - path: 'jobs', + path: paths.jobs, component: AdminJobsComponent, title: $localize`Job Queue` }, { - path: 'market-data', + path: paths.marketData, component: AdminMarketDataComponent, title: $localize`Market Data` }, { - path: 'settings', + path: paths.settings, component: AdminSettingsComponent, title: $localize`Settings` }, { - path: 'users', + path: paths.users, component: AdminUsersComponent, title: $localize`Users` } diff --git a/apps/client/src/app/pages/admin/admin-page.component.ts b/apps/client/src/app/pages/admin/admin-page.component.ts index 5896a18d7..e787cb0fc 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/apps/client/src/app/pages/admin/admin-page.component.ts @@ -1,4 +1,5 @@ import { TabConfiguration } from '@ghostfolio/common/interfaces'; +import { paths } from '@ghostfolio/common/paths'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { DeviceDetectorService } from 'ngx-device-detector'; @@ -26,27 +27,27 @@ export class AdminPageComponent implements OnDestroy, OnInit { { iconName: 'reader-outline', label: $localize`Overview`, - path: ['/admin'] + path: ['/' + paths.adminControl] }, { iconName: 'settings-outline', label: $localize`Settings`, - path: ['/admin', 'settings'] + path: ['/' + paths.adminControl, paths.settings] }, { iconName: 'server-outline', label: $localize`Market Data`, - path: ['/admin', 'market-data'] + path: ['/' + paths.adminControl, paths.marketData] }, { iconName: 'flash-outline', label: $localize`Job Queue`, - path: ['/admin', 'jobs'] + path: ['/' + paths.adminControl, paths.jobs] }, { iconName: 'people-outline', label: $localize`Users`, - path: ['/admin', 'users'] + path: ['/' + paths.adminControl, paths.users] } ]; } diff --git a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts index 2e346bf40..222a79648 100644 --- a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts +++ b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts @@ -1,3 +1,5 @@ +import { paths } from '@ghostfolio/common/paths'; + import { Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router'; templateUrl: './hallo-ghostfolio-page.html' }) export class HalloGhostfolioPageComponent { - public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; - public routerLinkResources = ['/' + $localize`:snake-case:resources`]; + public routerLinkBlog = ['/' + paths.blog]; + public routerLinkPricing = ['/' + paths.pricing]; + public routerLinkResources = ['/' + paths.resources]; } diff --git a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html index e0364f747..28e30d16b 100644 --- a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html +++ b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html @@ -201,7 +201,7 @@
    @@ -60,7 +60,7 @@ @if (hasPermissionForDemo) {
    or
    -
    Live Demo + Live Demo } diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index dce045a4a..5f651195a 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -39,6 +39,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { return { id: assetSubClass, label: translate(assetSubClass) }; }); public currencies: string[] = []; + public currencyOfAssetProfile: string; public currentMarketPrice = null; public defaultDateFormat: string; public isLoading = false; @@ -63,8 +64,10 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { ) {} public ngOnInit() { - this.mode = this.data.activity.id ? 'update' : 'create'; + this.currencyOfAssetProfile = this.data.activity?.SymbolProfile?.currency; this.locale = this.data.user?.settings?.locale; + this.mode = this.data.activity?.id ? 'update' : 'create'; + this.dateAdapter.setLocale(this.locale); const { currencies, platforms } = this.dataService.fetchInfo(); @@ -210,7 +213,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.activityForm.get('type').value ) ) { - this.updateSymbol(); + this.updateAssetProfile(); } this.changeDetectorRef.markForCheck(); @@ -397,7 +400,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.dialogRef.close(activity); } else { - (activity as UpdateOrderDto).id = this.data.activity.id; + (activity as UpdateOrderDto).id = this.data.activity?.id; await validateObjectForForm({ classDto: UpdateOrderDto, @@ -422,7 +425,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.unsubscribeSubject.complete(); } - private updateSymbol() { + private updateAssetProfile() { this.isLoading = true; this.changeDetectorRef.markForCheck(); @@ -450,6 +453,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.activityForm.get('dataSource').setValue(dataSource); } + this.currencyOfAssetProfile = currency; this.currentMarketPrice = marketPrice; this.isLoading = false; diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index b0521530f..08e1b5162 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -230,8 +230,10 @@ @if ( + currencyOfAssetProfile === + activityForm.get('currencyOfUnitPrice').value && currentMarketPrice && - (data.activity.type === 'BUY' || data.activity.type === 'SELL') && + ['BUY', 'SELL'].includes(data.activity.type) && isToday(activityForm.get('date')?.value) ) {