diff --git a/.eslintrc.json b/.eslintrc.json index 1b7628ebf..d3f7edea9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,7 +7,7 @@ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "rules": { "@nx/enforce-module-boundaries": [ - "error", + "warn", { "enforceBuildableLibDependency": true, "allow": [], @@ -18,30 +18,27 @@ } ] } - ] + ], + "@typescript-eslint/no-extra-semi": "error", + "no-extra-semi": "off" } }, { "files": ["*.ts", "*.tsx"], - "extends": ["plugin:@nx/typescript"], - "rules": { - "@typescript-eslint/no-extra-semi": "error", - "no-extra-semi": "off" - } + "extends": ["plugin:@nx/typescript"] }, { "files": ["*.js", "*.jsx"], - "extends": ["plugin:@nx/javascript"], - "rules": { - "@typescript-eslint/no-extra-semi": "error", - "no-extra-semi": "off" - } + "extends": ["plugin:@nx/javascript"] }, { "files": ["*.ts"], "plugins": ["eslint-plugin-import", "@typescript-eslint"], + "extends": [ + "plugin:@typescript-eslint/recommended-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked" + ], "rules": { - "@typescript-eslint/consistent-type-definitions": "error", "@typescript-eslint/dot-notation": "off", "@typescript-eslint/explicit-member-accessibility": [ "off", @@ -49,76 +46,112 @@ "accessibility": "explicit" } ], - "@typescript-eslint/member-ordering": "error", - "@typescript-eslint/naming-convention": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-empty-interface": "error", + "@typescript-eslint/member-ordering": "warn", + "@typescript-eslint/naming-convention": [ + "off", + { + "selector": "default", + "format": ["camelCase"], + "leadingUnderscore": "allow", + "trailingUnderscore": "allow" + }, + { + "selector": ["variable", "classProperty", "typeProperty"], + "format": ["camelCase", "UPPER_CASE"], + "leadingUnderscore": "allow", + "trailingUnderscore": "allow" + }, + { + "selector": "objectLiteralProperty", + "format": null + }, + { + "selector": "enumMember", + "format": ["camelCase", "UPPER_CASE", "PascalCase"] + }, + { + "selector": "typeLike", + "format": ["PascalCase"] + } + ], + "@typescript-eslint/no-empty-interface": "warn", "@typescript-eslint/no-inferrable-types": [ - "error", + "warn", { "ignoreParameters": true } ], - "@typescript-eslint/no-misused-new": "error", - "@typescript-eslint/no-non-null-assertion": "error", + "@typescript-eslint/no-non-null-assertion": "warn", "@typescript-eslint/no-shadow": [ - "error", + "warn", { "hoist": "all" } ], - "@typescript-eslint/no-unused-expressions": "error", - "@typescript-eslint/prefer-function-type": "error", "@typescript-eslint/unified-signatures": "error", + "@typescript-eslint/no-loss-of-precision": "warn", + "@typescript-eslint/no-var-requires": "warn", + "@typescript-eslint/ban-types": "warn", "arrow-body-style": "off", "constructor-super": "error", "eqeqeq": ["error", "smart"], - "guard-for-in": "error", + "guard-for-in": "warn", "id-blacklist": "off", "id-match": "off", "import/no-deprecated": "warn", "no-bitwise": "error", "no-caller": "error", - "no-console": [ - "error", - { - "allow": [ - "log", - "warn", - "dir", - "timeLog", - "assert", - "clear", - "count", - "countReset", - "group", - "groupEnd", - "table", - "dirxml", - "error", - "groupCollapsed", - "Console", - "profile", - "profileEnd", - "timeStamp", - "context" - ] - } - ], "no-debugger": "error", "no-empty": "off", "no-eval": "error", "no-fallthrough": "error", "no-new-wrappers": "error", "no-restricted-imports": ["error", "rxjs/Rx"], - "no-throw-literal": "error", "no-undef-init": "error", "no-underscore-dangle": "off", "no-var": "error", - "prefer-const": "error", - "radix": "error" + "radix": "error", + "no-unsafe-optional-chaining": "warn", + "no-extra-boolean-cast": "warn", + "no-empty-pattern": "warn", + "no-useless-catch": "warn", + "no-unsafe-finally": "warn", + "no-prototype-builtins": "warn", + "no-async-promise-executor": "warn", + "no-constant-condition": "warn", + + // The following rules are part of @typescript-eslint/recommended-type-checked + // and can be remove once solved + "@typescript-eslint/await-thenable": "warn", + "@typescript-eslint/ban-ts-comment": "warn", + "@typescript-eslint/no-base-to-string": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-floating-promises": "warn", + "@typescript-eslint/no-misused-promises": "warn", + "@typescript-eslint/no-redundant-type-constituents": "warn", + "@typescript-eslint/no-unnecessary-type-assertion": "warn", + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-enum-comparison": "warn", + "@typescript-eslint/no-unsafe-member-access": "warn", + "@typescript-eslint/no-unsafe-return": "warn", + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/require-await": "warn", + "@typescript-eslint/restrict-template-expressions": "warn", + "@typescript-eslint/unbound-method": "warn", + + // The following rules are part of @typescript-eslint/stylistic-type-checked + // and can be remove once solved + "@typescript-eslint/consistent-type-definitions": "warn", + "@typescript-eslint/prefer-function-type": "warn", + "@typescript-eslint/no-empty-function": "warn", + "@typescript-eslint/prefer-nullish-coalescing": "warn", // TODO: Requires strictNullChecks: true + "@typescript-eslint/consistent-type-assertions": "warn", + "@typescript-eslint/prefer-optional-chain": "warn", + "@typescript-eslint/consistent-indexed-object-style": "warn", + "@typescript-eslint/consistent-generic-constructors": "warn" } } ], - "extends": [null, "plugin:storybook/recommended"] + "extends": ["plugin:storybook/recommended"] } diff --git a/.github/workflows/build-code.yml b/.github/workflows/build-code.yml index 5c072d760..18240ebc4 100644 --- a/.github/workflows/build-code.yml +++ b/.github/workflows/build-code.yml @@ -29,6 +29,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Check code style + run: npm run lint + - name: Check formatting run: npm run format:check diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..4bf77522a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,6 @@ +# Run linting and stop the commit process if any errors are found +# --quiet suppresses warnings (temporary until all warnings are fixed) +npm run lint --quiet || exit 1 + +# Check formatting on modified and uncommitted files, stop the commit if issues are found +npm run format:check --uncommitted || exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5150308cb..ab2f94cfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,16 +5,57 @@ 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.114.0 - 2024-10-10 ### Added +- Added a tooltip to the chart of the holdings tab on the home page (experimental) +- Extended the _Public API_ with the health check endpoint (experimental) + +### Changed + +- Moved the tags from the info to the user service +- Switched the `prefer-const` rule from `warn` to `error` in the `eslint` configuration + +### Fixed + +- Fixed an exception in the portfolio details endpoint caused by a calculation of the allocations by market + +## 2.113.0 - 2024-10-06 + +### Added + +- Set up a git-hook via `husky` to lint and format the changes before a commit +- Added the `typescript-eslint/recommended-type-checked` rule to the `eslint` configuration +- Added the `typescript-eslint/stylistic-type-checked` rule to the `eslint` configuration + +### Changed + +- Optimized the portfolio calculations by reusing date intervals +- Refactored the calculation of the allocations by market on the allocations page +- Refactored the calculation of the allocations by market on the public page + +### Fixed + +- Handled an exception in the historical market data gathering of derived currencies + +## 2.112.0 - 2024-10-03 + +### Added + +- Added a message to the search asset component if no results have been found in the create or update activity dialog - Added support to customize the rule thresholds in the _X-ray_ section (experimental) ### Changed +- Optimized the portfolio calculations with smarter date interval selection - Improved the language localization for German (`de`) +### Fixed + +- Fixed an issue in the calculation of allocations by market (_Unknown_) +- Fixed the `eslint` configuration + ## 2.111.0 - 2024-09-28 ### Added diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0c76a2924..b009679ac 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -14,7 +14,6 @@ 1. Run `npm install` 1. Run `docker compose --env-file ./.env -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io) 1. Run `npm run database:setup` to initialize the database schema -1. Run `git config core.hooksPath ./git-hooks/` to setup git hooks 1. Start the [server](#start-server) and the [client](#start-client) 1. Open https://localhost:4200/en in your browser 1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`) diff --git a/README.md b/README.md index 477554749..989cc37ea 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,20 @@ You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anony Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/` or `curl -s http://localhost:3333/api/v1/auth/anonymous/`. +### Health Check (experimental) + +#### Request + +`GET http://localhost:3333/api/v1/health` + +**Info:** No Bearer Token is required for health check + +#### Response + +##### Success + +`200 OK` + ### Import Activities #### Prerequisites diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 44e136793..c0f4dac6a 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -74,15 +74,12 @@ export class AccountController { ); } - return this.accountService.deleteAccount( - { - id_userId: { - id, - userId: this.request.user.id - } - }, - this.request.user.id - ); + return this.accountService.deleteAccount({ + id_userId: { + id, + userId: this.request.user.id + } + }); } @Get() diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index d48a54182..22ea82016 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -109,8 +109,7 @@ export class AccountService { } public async deleteAccount( - where: Prisma.AccountWhereUniqueInput, - aUserId: string + where: Prisma.AccountWhereUniqueInput ): Promise { const account = await this.prismaService.account.delete({ where @@ -172,11 +171,7 @@ export class AccountService { where.isExcluded = false; } - const { - ACCOUNT: filtersByAccount, - ASSET_CLASS: filtersByAssetClass, - TAG: filtersByTag - } = groupBy(filters, ({ type }) => { + const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => { return type; }); diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 5ac7526f8..324087a0c 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -237,7 +237,7 @@ export class AdminService { const extendedPrismaClient = this.getExtendedPrismaClient(); try { - let [assetProfiles, count] = await Promise.all([ + const symbolProfileResult = await Promise.all([ extendedPrismaClient.symbolProfile.findMany({ orderBy, skip, @@ -269,6 +269,8 @@ export class AdminService { }), this.prismaService.symbolProfile.count({ where }) ]); + const assetProfiles = symbolProfileResult[0]; + let count = symbolProfileResult[1]; const lastMarketPrices = await this.prismaService.marketData.findMany({ distinct: ['dataSource', 'symbol'], diff --git a/apps/api/src/app/admin/update-bulk-market-data.dto.ts b/apps/api/src/app/admin/update-bulk-market-data.dto.ts index 5177263a6..da0da1272 100644 --- a/apps/api/src/app/admin/update-bulk-market-data.dto.ts +++ b/apps/api/src/app/admin/update-bulk-market-data.dto.ts @@ -1,5 +1,5 @@ import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, isNotEmptyObject } from 'class-validator'; +import { ArrayNotEmpty, IsArray } from 'class-validator'; import { UpdateMarketDataDto } from './update-market-data.dto'; diff --git a/apps/api/src/app/auth/google.strategy.ts b/apps/api/src/app/auth/google.strategy.ts index 4d024603b..ea6772680 100644 --- a/apps/api/src/app/auth/google.strategy.ts +++ b/apps/api/src/app/auth/google.strategy.ts @@ -29,8 +29,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { token: string, refreshToken: string, profile: Profile, - done: Function, - done2: Function + done: Function ) { try { const jwt = await this.authService.validateOAuthLogin({ diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts index f57f5fa30..34f101a8e 100644 --- a/apps/api/src/app/benchmark/benchmark.service.ts +++ b/apps/api/src/app/benchmark/benchmark.service.ts @@ -234,7 +234,7 @@ export class BenchmarkService { return { marketData }; } - for (let marketDataItem of marketDataItems) { + for (const marketDataItem of marketDataItems) { const exchangeRate = exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ format(marketDataItem.date, DATE_FORMAT) diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index b0f7bb2c3..7488e4201 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -57,7 +57,7 @@ export class PublicController { } const [ - { holdings }, + { holdings, markets }, { performance: performance1d }, { performance: performanceMax }, { performance: performanceYtd } @@ -76,8 +76,13 @@ export class PublicController { }) ]); + Object.values(markets ?? {}).forEach((market) => { + delete market.valueInBaseCurrency; + }); + const publicPortfolioResponse: PublicPortfolioResponse = { hasDetails, + markets, alias: access.alias, holdings: {}, performance: { diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 11d7dc29c..5e676c0a6 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -266,21 +266,18 @@ export class ImportService { const activities: Activity[] = []; - for (let [ - index, - { - accountId, - comment, - currency, - date, - error, - fee, - quantity, - SymbolProfile, - type, - unitPrice - } - ] of activitiesExtendedWithErrors.entries()) { + for (const [index, activity] of activitiesExtendedWithErrors.entries()) { + const accountId = activity.accountId; + const comment = activity.comment; + const currency = activity.currency; + const date = activity.date; + const error = activity.error; + let fee = activity.fee; + const quantity = activity.quantity; + const SymbolProfile = activity.SymbolProfile; + const type = activity.type; + let unitPrice = activity.unitPrice; + const assetProfile = assetProfiles[ getAssetProfileIdentifier({ dataSource: SymbolProfile.dataSource, @@ -491,12 +488,13 @@ export class ImportService { userCurrency: string; userId: string; }): Promise[]> { - let { activities: existingActivities } = await this.orderService.getOrders({ - userCurrency, - userId, - includeDrafts: true, - withExcludedAccounts: true - }); + const { activities: existingActivities } = + await this.orderService.getOrders({ + userCurrency, + userId, + includeDrafts: true, + withExcludedAccounts: true + }); return activitiesDto.map( ({ diff --git a/apps/api/src/app/info/info.module.ts b/apps/api/src/app/info/info.module.ts index 9b7854160..7903ac397 100644 --- a/apps/api/src/app/info/info.module.ts +++ b/apps/api/src/app/info/info.module.ts @@ -9,7 +9,6 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; -import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; @@ -33,7 +32,6 @@ import { InfoService } from './info.service'; PropertyModule, RedisCacheModule, SymbolProfileModule, - TagModule, TransformDataSourceInResponseModule, UserModule ], diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index ee225e769..bd291c511 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -5,7 +5,6 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; -import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { DEFAULT_CURRENCY, PROPERTY_BETTER_UPTIME_MONITOR_ID, @@ -47,7 +46,6 @@ export class InfoService { private readonly platformService: PlatformService, private readonly propertyService: PropertyService, private readonly redisCacheService: RedisCacheService, - private readonly tagService: TagService, private readonly userService: UserService ) {} @@ -103,8 +101,7 @@ export class InfoService { isUserSignupEnabled, platforms, statistics, - subscriptions, - tags + subscriptions ] = await Promise.all([ this.benchmarkService.getBenchmarkAssetProfiles(), this.getDemoAuthToken(), @@ -113,8 +110,7 @@ export class InfoService { orderBy: { name: 'asc' } }), this.getStatistics(), - this.getSubscriptions(), - this.tagService.getPublic() + this.getSubscriptions() ]); if (isUserSignupEnabled) { @@ -130,7 +126,6 @@ export class InfoService { platforms, statistics, subscriptions, - tags, baseCurrency: DEFAULT_CURRENCY, currencies: this.exchangeRateDataService.getCurrencies() }; diff --git a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts index eb49b7cdb..e54f63422 100644 --- a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts @@ -3,24 +3,14 @@ import { AssetProfileIdentifier, SymbolMetrics } from '@ghostfolio/common/interfaces'; -import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; +import { PortfolioSnapshot } from '@ghostfolio/common/models'; export class MWRPortfolioCalculator extends PortfolioCalculator { - protected calculateOverallPerformance( - positions: TimelinePosition[] - ): PortfolioSnapshot { + protected calculateOverallPerformance(): PortfolioSnapshot { throw new Error('Method not implemented.'); } - protected getSymbolMetrics({ - dataSource, - end, - exchangeRates, - marketSymbolMap, - start, - step = 1, - symbol - }: { + protected getSymbolMetrics({}: { end: Date; exchangeRates: { [dateString: string]: number }; marketSymbolMap: { diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 29cbe3781..84bb3bcb5 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -218,7 +218,7 @@ export abstract class PortfolioCalculator { } } - let exchangeRatesByCurrency = + const exchangeRatesByCurrency = await this.exchangeRateDataService.getExchangeRatesByCurrency({ currencies: uniq(Object.values(currencies)), endDate: endOfDay(this.endDate), @@ -262,7 +262,7 @@ export abstract class PortfolioCalculator { const daysInMarket = differenceInDays(this.endDate, this.startDate); - let chartDateMap = this.getChartDateMap({ + const chartDateMap = this.getChartDateMap({ endDate: this.endDate, startDate: this.startDate, step: Math.round( @@ -710,9 +710,9 @@ export abstract class PortfolioCalculator { let netPerformanceAtStartDate: number; let netPerformanceWithCurrencyEffectAtStartDate: number; - let totalInvestmentValuesWithCurrencyEffect: number[] = []; + const totalInvestmentValuesWithCurrencyEffect: number[] = []; - for (let historicalDataItem of historicalData) { + for (const historicalDataItem of historicalData) { const date = resetHours(parseDate(historicalDataItem.date)); if (!isBefore(date, start) && !isAfter(date, end)) { @@ -843,13 +843,13 @@ export abstract class PortfolioCalculator { }): { [date: string]: true } { // Create a map of all relevant chart dates: // 1. Add transaction point dates - let chartDateMap = this.transactionPoints.reduce((result, { date }) => { + const chartDateMap = this.transactionPoints.reduce((result, { date }) => { result[date] = true; return result; }, {}); // 2. Add dates between transactions respecting the specified step size - for (let date of eachDayOfInterval( + for (const date of eachDayOfInterval( { end: endDate, start: startDate }, { step } )) { @@ -858,7 +858,7 @@ export abstract class PortfolioCalculator { if (step > 1) { // Reduce the step size of last 90 days - for (let date of eachDayOfInterval( + for (const date of eachDayOfInterval( { end: endDate, start: subDays(endDate, 90) }, { step: 3 } )) { @@ -866,7 +866,7 @@ export abstract class PortfolioCalculator { } // Reduce the step size of last 30 days - for (let date of eachDayOfInterval( + for (const date of eachDayOfInterval( { end: endDate, start: subDays(endDate, 30) }, { step: 1 } )) { @@ -878,7 +878,7 @@ export abstract class PortfolioCalculator { chartDateMap[format(endDate, DATE_FORMAT)] = true; // Make sure some key dates are present - for (let dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) { + for (const dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) { const { endDate: dateRangeEnd, startDate: dateRangeStart } = getIntervalFromDateRange(dateRange); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts index 89239850f..e4531c8ae 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -156,10 +156,27 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: new Big('0.62'), fee: new Big('19'), firstBuyDate: '2021-09-16', + grossPerformance: new Big('33.25'), + grossPerformancePercentage: new Big('0.11136043941322258691'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.11136043941322258691' + ), + grossPerformanceWithCurrencyEffect: new Big('33.25'), investment: new Big('298.58'), investmentWithCurrencyEffect: new Big('298.58'), marketPrice: 331.83, marketPriceInBaseCurrency: 331.83, + netPerformance: new Big('14.25'), + netPerformancePercentage: new Big('0.04772590260566682296'), + netPerformancePercentageWithCurrencyEffectMap: { + max: new Big('0.04772590260566682296') + }, + netPerformanceWithCurrencyEffectMap: { + '1d': new Big('-5.39'), + '5y': new Big('14.25'), + max: new Big('14.25'), + wtd: new Big('-5.39') + }, quantity: new Big('1'), symbol: 'MSFT', tags: [], diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts index dc4d642f2..99bee2c21 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts @@ -1,43 +1,3 @@ -import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; -import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; -import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; - describe('PortfolioCalculator', () => { - let configurationService: ConfigurationService; - let currentRateService: CurrentRateService; - let exchangeRateDataService: ExchangeRateDataService; - let portfolioCalculatorFactory: PortfolioCalculatorFactory; - let portfolioSnapshotService: PortfolioSnapshotService; - let redisCacheService: RedisCacheService; - - beforeEach(() => { - configurationService = new ConfigurationService(); - - currentRateService = new CurrentRateService(null, null, null, null); - - exchangeRateDataService = new ExchangeRateDataService( - null, - null, - null, - null - ); - - portfolioSnapshotService = new PortfolioSnapshotService(null); - - redisCacheService = new RedisCacheService(null, null); - - portfolioCalculatorFactory = new PortfolioCalculatorFactory( - configurationService, - currentRateService, - exchangeRateDataService, - portfolioSnapshotService, - redisCacheService, - null - ); - }); - test.skip('Skip empty test', () => 1); }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index 900bb3847..e2a523739 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -12,17 +12,12 @@ import { DateRange } from '@ghostfolio/common/types'; import { Logger } from '@nestjs/common'; import { Big } from 'big.js'; -import { - addDays, - addMilliseconds, - differenceInDays, - eachDayOfInterval, - format, - isBefore -} from 'date-fns'; +import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns'; import { cloneDeep, first, last, sortBy } from 'lodash'; export class TWRPortfolioCalculator extends PortfolioCalculator { + private chartDates: string[]; + protected calculateOverallPerformance( positions: TimelinePosition[] ): PortfolioSnapshot { @@ -32,7 +27,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { let hasErrors = false; let netPerformance = new Big(0); let totalFeesWithCurrencyEffect = new Big(0); - let totalInterestWithCurrencyEffect = new Big(0); + const totalInterestWithCurrencyEffect = new Big(0); let totalInvestment = new Big(0); let totalInvestmentWithCurrencyEffect = new Big(0); let totalTimeWeightedInvestment = new Big(0); @@ -163,7 +158,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { [date: string]: Big; } = {}; - let totalAccountBalanceInBaseCurrency = new Big(0); + const totalAccountBalanceInBaseCurrency = new Big(0); let totalDividend = new Big(0); let totalDividendInBaseCurrency = new Big(0); let totalInterest = new Big(0); @@ -231,11 +226,11 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { const dateOfFirstTransaction = new Date(first(orders).date); - const unitPriceAtStartDate = - marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol]; + const endDateString = format(end, DATE_FORMAT); + const startDateString = format(start, DATE_FORMAT); - const unitPriceAtEndDate = - marketSymbolMap[format(end, DATE_FORMAT)]?.[symbol]; + const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol]; + const unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol]; if ( !unitPriceAtEndDate || @@ -283,7 +278,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { // Add a synthetic order at the start and the end date orders.push({ - date: format(start, DATE_FORMAT), + date: startDateString, fee: new Big(0), feeInBaseCurrency: new Big(0), itemType: 'start', @@ -297,7 +292,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { }); orders.push({ - date: format(end, DATE_FORMAT), + date: endDateString, fee: new Big(0), feeInBaseCurrency: new Big(0), itemType: 'end', @@ -310,7 +305,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { unitPrice: unitPriceAtEndDate }); - let day = start; let lastUnitPrice: Big; const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {}; @@ -320,15 +314,23 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { ordersByDate[order.date].push(order); } - while (isBefore(day, end)) { - const dateString = format(day, DATE_FORMAT); + if (!this.chartDates) { + this.chartDates = Object.keys(chartDateMap).sort(); + } + + for (const dateString of this.chartDates) { + if (dateString < startDateString) { + continue; + } else if (dateString > endDateString) { + break; + } if (ordersByDate[dateString]?.length > 0) { - for (let order of ordersByDate[dateString]) { + for (const order of ordersByDate[dateString]) { order.unitPriceFromMarketData = marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice; } - } else if (chartDateMap[dateString]) { + } else { orders.push({ date: dateString, fee: new Big(0), @@ -348,8 +350,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { const lastOrder = last(orders); lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice; - - day = addDays(day, 1); } // Sort orders so that the start and end placeholder order are at the correct @@ -827,38 +827,41 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { // return format(date, 'yyyy'); // }) ]) { - // TODO: getIntervalFromDateRange(dateRange, start) - let { endDate, startDate } = getIntervalFromDateRange(dateRange); + const dateInterval = getIntervalFromDateRange(dateRange); + const endDate = dateInterval.endDate; + let startDate = dateInterval.startDate; if (isBefore(startDate, start)) { startDate = start; } + const rangeEndDateString = format(endDate, DATE_FORMAT); + const rangeStartDateString = format(startDate, DATE_FORMAT); + const currentValuesAtDateRangeStartWithCurrencyEffect = - currentValuesWithCurrencyEffect[format(startDate, DATE_FORMAT)] ?? - new Big(0); + currentValuesWithCurrencyEffect[rangeStartDateString] ?? new Big(0); const investmentValuesAccumulatedAtStartDateWithCurrencyEffect = - investmentValuesAccumulatedWithCurrencyEffect[ - format(startDate, DATE_FORMAT) - ] ?? new Big(0); + investmentValuesAccumulatedWithCurrencyEffect[rangeStartDateString] ?? + new Big(0); const grossPerformanceAtDateRangeStartWithCurrencyEffect = currentValuesAtDateRangeStartWithCurrencyEffect.minus( investmentValuesAccumulatedAtStartDateWithCurrencyEffect ); - const dates = eachDayOfInterval({ - end: endDate, - start: startDate - }).map((date) => { - return format(date, DATE_FORMAT); - }); - let average = new Big(0); let dayCount = 0; - for (const date of dates) { + for (let i = this.chartDates.length - 1; i >= 0; i -= 1) { + const date = this.chartDates[i]; + + if (date > rangeEndDateString) { + continue; + } else if (date < rangeStartDateString) { + break; + } + if ( investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && investmentValuesAccumulatedWithCurrencyEffect[date].gt(0) @@ -878,17 +881,14 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { } netPerformanceWithCurrencyEffectMap[dateRange] = - netPerformanceValuesWithCurrencyEffect[ - format(endDate, DATE_FORMAT) - ]?.minus( + netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus( // If the date range is 'max', take 0 as a start value. Otherwise, // the value of the end of the day of the start date is taken which // differs from the buying price. dateRange === 'max' ? new Big(0) - : (netPerformanceValuesWithCurrencyEffect[ - format(startDate, DATE_FORMAT) - ] ?? new Big(0)) + : (netPerformanceValuesWithCurrencyEffect[rangeStartDateString] ?? + new Big(0)) ) ?? new Big(0); netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0) diff --git a/apps/api/src/app/portfolio/current-rate.service.mock.ts b/apps/api/src/app/portfolio/current-rate.service.mock.ts index 313b09d67..fab25ae2d 100644 --- a/apps/api/src/app/portfolio/current-rate.service.mock.ts +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -65,6 +65,8 @@ function mockGetValue(symbol: string, date: Date) { return { marketPrice: 89.12 }; } else if (isSameDay(parseDate('2021-11-16'), date)) { return { marketPrice: 339.51 }; + } else if (isSameDay(parseDate('2023-07-09'), date)) { + return { marketPrice: 337.22 }; } else if (isSameDay(parseDate('2023-07-10'), date)) { return { marketPrice: 331.83 }; } diff --git a/apps/api/src/app/portfolio/current-rate.service.spec.ts b/apps/api/src/app/portfolio/current-rate.service.spec.ts index c86dde448..d0e61c8ce 100644 --- a/apps/api/src/app/portfolio/current-rate.service.spec.ts +++ b/apps/api/src/app/portfolio/current-rate.service.spec.ts @@ -79,7 +79,7 @@ jest.mock('@ghostfolio/api/services/property/property.service', () => { return { PropertyService: jest.fn().mockImplementation(() => { return { - getByKey: (key: string) => Promise.resolve({}) + getByKey: () => Promise.resolve({}) }; }) }; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index d812c1cc4..2a9e62112 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -1,4 +1,3 @@ -import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; @@ -17,7 +16,10 @@ import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; -import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; +import { + HEADER_KEY_IMPERSONATION, + UNKNOWN_KEY +} from '@ghostfolio/common/config'; import { PortfolioDetails, PortfolioDividends, @@ -64,7 +66,6 @@ import { UpdateHoldingTagsDto } from './update-holding-tags.dto'; @Controller('portfolio') export class PortfolioController { public constructor( - private readonly accessService: AccessService, private readonly apiService: ApiService, private readonly configurationService: ConfigurationService, private readonly impersonationService: ImpersonationService, @@ -101,15 +102,22 @@ export class PortfolioController { filterByTags }); - const { accounts, hasErrors, holdings, platforms, summary } = - await this.portfolioService.getDetails({ - dateRange, - filters, - impersonationId, - withMarkets, - userId: this.request.user.id, - withSummary: true - }); + const { + accounts, + hasErrors, + holdings, + markets, + marketsAdvanced, + platforms, + summary + } = await this.portfolioService.getDetails({ + dateRange, + filters, + impersonationId, + withMarkets, + userId: this.request.user.id, + withSummary: true + }); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { hasError = true; @@ -186,6 +194,13 @@ export class PortfolioController { }) || isRestrictedView(this.request.user) ) { + Object.values(markets ?? {}).forEach((market) => { + delete market.valueInBaseCurrency; + }); + Object.values(marketsAdvanced ?? {}).forEach((market) => { + delete market.valueInBaseCurrency; + }); + portfolioSummary = nullifyValuesInObject(summary, [ 'cash', 'committedFunds', @@ -238,6 +253,58 @@ export class PortfolioController { hasError, holdings, platforms, + markets: hasDetails + ? markets + : { + [UNKNOWN_KEY]: { + id: UNKNOWN_KEY, + valueInPercentage: 1 + }, + developedMarkets: { + id: 'developedMarkets', + valueInPercentage: 0 + }, + emergingMarkets: { + id: 'emergingMarkets', + valueInPercentage: 0 + }, + otherMarkets: { + id: 'otherMarkets', + valueInPercentage: 0 + } + }, + marketsAdvanced: hasDetails + ? marketsAdvanced + : { + [UNKNOWN_KEY]: { + id: UNKNOWN_KEY, + valueInPercentage: 0 + }, + asiaPacific: { + id: 'asiaPacific', + valueInPercentage: 0 + }, + emergingMarkets: { + id: 'emergingMarkets', + valueInPercentage: 0 + }, + europe: { + id: 'europe', + valueInPercentage: 0 + }, + japan: { + id: 'japan', + valueInPercentage: 0 + }, + northAmerica: { + id: 'northAmerica', + valueInPercentage: 0 + }, + otherMarkets: { + id: 'otherMarkets', + valueInPercentage: 0 + } + }, summary: portfolioSummary }; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 311aff4b6..3054cce47 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -478,8 +478,7 @@ export class PortfolioService { if (withMarkets) { ({ markets, marketsAdvanced } = this.getMarkets({ - assetProfile, - valueInBaseCurrency + assetProfile })); } @@ -590,6 +589,13 @@ export class PortfolioService { }; } + let markets: PortfolioDetails['markets']; + let marketsAdvanced: PortfolioDetails['marketsAdvanced']; + + if (withMarkets) { + ({ markets, marketsAdvanced } = this.getAggregatedMarkets(holdings)); + } + let summary: PortfolioSummary; if (withSummary) { @@ -611,6 +617,8 @@ export class PortfolioService { accounts, hasErrors, holdings, + markets, + marketsAdvanced, platforms, summary }; @@ -1048,7 +1056,9 @@ export class PortfolioService { currency: this.request.user.Settings.settings.baseCurrency }); - let { hasErrors, positions } = await portfolioCalculator.getSnapshot(); + const portfolioSnapshot = await portfolioCalculator.getSnapshot(); + const hasErrors = portfolioSnapshot.hasErrors; + let positions = portfolioSnapshot.positions; positions = positions.filter(({ quantity }) => { return !quantity.eq(0); @@ -1161,7 +1171,6 @@ export class PortfolioService { filters, impersonationId, userId, - withExcludedAccounts = false, calculateTimeWeightedPerformance = false }: { dateRange?: DateRange; @@ -1265,74 +1274,49 @@ export class PortfolioService { @LogPerformance public async getReport(impersonationId: string): Promise { const userId = await this.getUserId(impersonationId, this.request.user.id); - const user = await this.userService.user({ id: userId }); - const userCurrency = this.getUserCurrency(user); - - const { activities } = - await this.orderService.getOrdersForPortfolioCalculator({ - userCurrency, - userId - }); + const userSettings = this.request.user.Settings.settings; - const portfolioCalculator = this.calculatorFactory.createCalculator({ - activities, + const { accounts, holdings, summary } = await this.getDetails({ + impersonationId, userId, - calculationType: PerformanceCalculationType.TWR, - currency: this.request.user.Settings.settings.baseCurrency - }); - - let { totalFeesWithCurrencyEffect, positions, totalInvestment } = - await portfolioCalculator.getSnapshot(); - - positions = positions.filter((item) => !item.quantity.eq(0)); - - const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; - - for (const position of positions) { - portfolioItemsNow[position.symbol] = position; - } - - const { accounts } = await this.getValueOfAccountsAndPlatforms({ - activities, - portfolioItemsNow, - userCurrency, - userId + withMarkets: true, + withSummary: true }); - const userSettings = this.request.user.Settings.settings; - return { rules: { - accountClusterRisk: isEmpty(activities) - ? undefined - : await this.rulesService.evaluate( - [ - new AccountClusterRiskCurrentInvestment( - this.exchangeRateDataService, - accounts - ), - new AccountClusterRiskSingleAccount( - this.exchangeRateDataService, - accounts - ) - ], - userSettings - ), - currencyClusterRisk: isEmpty(activities) - ? undefined - : await this.rulesService.evaluate( - [ - new CurrencyClusterRiskBaseCurrencyCurrentInvestment( - this.exchangeRateDataService, - positions - ), - new CurrencyClusterRiskCurrentInvestment( - this.exchangeRateDataService, - positions - ) - ], - userSettings - ), + accountClusterRisk: + summary.ordersCount > 0 + ? await this.rulesService.evaluate( + [ + new AccountClusterRiskCurrentInvestment( + this.exchangeRateDataService, + accounts + ), + new AccountClusterRiskSingleAccount( + this.exchangeRateDataService, + accounts + ) + ], + userSettings + ) + : undefined, + currencyClusterRisk: + summary.ordersCount > 0 + ? await this.rulesService.evaluate( + [ + new CurrencyClusterRiskBaseCurrencyCurrentInvestment( + this.exchangeRateDataService, + Object.values(holdings) + ), + new CurrencyClusterRiskCurrentInvestment( + this.exchangeRateDataService, + Object.values(holdings) + ) + ], + userSettings + ) + : undefined, emergencyFund: await this.rulesService.evaluate( [ new EmergencyFundSetup( @@ -1346,8 +1330,8 @@ export class PortfolioService { [ new FeeRatioInitialInvestment( this.exchangeRateDataService, - totalInvestment.toNumber(), - totalFeesWithCurrencyEffect.toNumber() + summary.committedFunds, + summary.fees ) ], userSettings @@ -1371,37 +1355,148 @@ export class PortfolioService { userId: string; }) { userId = await this.getUserId(impersonationId, userId); - let symbolProfile = await this.symbolProfileService.getSymbolProfiles([ - { - dataSource, - symbol + await this.orderService.assignTags({ dataSource, symbol, tags, userId }); + } + + private getAggregatedMarkets(holdings: { + [symbol: string]: PortfolioPosition; + }): { + markets: PortfolioDetails['markets']; + marketsAdvanced: PortfolioDetails['marketsAdvanced']; + } { + const markets: PortfolioDetails['markets'] = { + [UNKNOWN_KEY]: { + id: UNKNOWN_KEY, + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + developedMarkets: { + id: 'developedMarkets', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + emergingMarkets: { + id: 'emergingMarkets', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + otherMarkets: { + id: 'otherMarkets', + valueInBaseCurrency: 0, + valueInPercentage: 0 } - ])[0]; - await this.symbolProfileService.updateSymbolProfile({ - assetClass: symbolProfile.assetClass, - assetSubClass: symbolProfile.assetSubClass, - countries: symbolProfile.countries, - currency: symbolProfile.currency, - dataSource, - holdings: symbolProfile.holdings, - name: symbolProfile.name, - sectors: symbolProfile.sectors, - symbol, - tags: { - connectOrCreate: tags.map(({ id, name }) => { - return { - create: { - id, - name - }, - where: { - id - } - }; - }) + }; + + const marketsAdvanced: PortfolioDetails['marketsAdvanced'] = { + [UNKNOWN_KEY]: { + id: UNKNOWN_KEY, + valueInBaseCurrency: 0, + valueInPercentage: 0 }, - url: symbolProfile.url - }); + asiaPacific: { + id: 'asiaPacific', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + emergingMarkets: { + id: 'emergingMarkets', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + europe: { + id: 'europe', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + japan: { + id: 'japan', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + northAmerica: { + id: 'northAmerica', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + otherMarkets: { + id: 'otherMarkets', + valueInBaseCurrency: 0, + valueInPercentage: 0 + } + }; + + for (const [, position] of Object.entries(holdings)) { + const value = position.valueInBaseCurrency; + + if (position.assetClass !== AssetClass.LIQUIDITY) { + if (position.countries.length > 0) { + markets.developedMarkets.valueInBaseCurrency += + position.markets.developedMarkets * value; + markets.emergingMarkets.valueInBaseCurrency += + position.markets.emergingMarkets * value; + markets.otherMarkets.valueInBaseCurrency += + position.markets.otherMarkets * value; + + marketsAdvanced.asiaPacific.valueInBaseCurrency += + position.marketsAdvanced.asiaPacific * value; + marketsAdvanced.emergingMarkets.valueInBaseCurrency += + position.marketsAdvanced.emergingMarkets * value; + marketsAdvanced.europe.valueInBaseCurrency += + position.marketsAdvanced.europe * value; + marketsAdvanced.japan.valueInBaseCurrency += + position.marketsAdvanced.japan * value; + marketsAdvanced.northAmerica.valueInBaseCurrency += + position.marketsAdvanced.northAmerica * value; + marketsAdvanced.otherMarkets.valueInBaseCurrency += + position.marketsAdvanced.otherMarkets * value; + } else { + markets[UNKNOWN_KEY].valueInBaseCurrency += value; + marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency += value; + } + } + } + + const marketsTotal = + markets.developedMarkets.valueInBaseCurrency + + markets.emergingMarkets.valueInBaseCurrency + + markets.otherMarkets.valueInBaseCurrency + + markets[UNKNOWN_KEY].valueInBaseCurrency; + + markets.developedMarkets.valueInPercentage = + markets.developedMarkets.valueInBaseCurrency / marketsTotal; + markets.emergingMarkets.valueInPercentage = + markets.emergingMarkets.valueInBaseCurrency / marketsTotal; + markets.otherMarkets.valueInPercentage = + markets.otherMarkets.valueInBaseCurrency / marketsTotal; + markets[UNKNOWN_KEY].valueInPercentage = + markets[UNKNOWN_KEY].valueInBaseCurrency / marketsTotal; + + const marketsAdvancedTotal = + marketsAdvanced.asiaPacific.valueInBaseCurrency + + marketsAdvanced.emergingMarkets.valueInBaseCurrency + + marketsAdvanced.europe.valueInBaseCurrency + + marketsAdvanced.japan.valueInBaseCurrency + + marketsAdvanced.northAmerica.valueInBaseCurrency + + marketsAdvanced.otherMarkets.valueInBaseCurrency + + marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency; + + marketsAdvanced.asiaPacific.valueInPercentage = + marketsAdvanced.asiaPacific.valueInBaseCurrency / marketsAdvancedTotal; + marketsAdvanced.emergingMarkets.valueInPercentage = + marketsAdvanced.emergingMarkets.valueInBaseCurrency / + marketsAdvancedTotal; + marketsAdvanced.europe.valueInPercentage = + marketsAdvanced.europe.valueInBaseCurrency / marketsAdvancedTotal; + marketsAdvanced.japan.valueInPercentage = + marketsAdvanced.japan.valueInBaseCurrency / marketsAdvancedTotal; + marketsAdvanced.northAmerica.valueInPercentage = + marketsAdvanced.northAmerica.valueInBaseCurrency / marketsAdvancedTotal; + marketsAdvanced.otherMarkets.valueInPercentage = + marketsAdvanced.otherMarkets.valueInBaseCurrency / marketsAdvancedTotal; + marketsAdvanced[UNKNOWN_KEY].valueInPercentage = + marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency / marketsAdvancedTotal; + + return { markets, marketsAdvanced }; } @LogPerformance @@ -1581,11 +1676,9 @@ export class PortfolioService { } private getMarkets({ - assetProfile, - valueInBaseCurrency + assetProfile }: { assetProfile: EnhancedSymbolProfile; - valueInBaseCurrency: Big; }) { const markets = { [UNKNOWN_KEY]: 0, @@ -1647,16 +1740,23 @@ export class PortfolioService { .toNumber(); } } - } else { - markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY]) - .plus(valueInBaseCurrency) - .toNumber(); - - marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY]) - .plus(valueInBaseCurrency) - .toNumber(); } + markets[UNKNOWN_KEY] = new Big(1) + .minus(markets.developedMarkets) + .minus(markets.emergingMarkets) + .minus(markets.otherMarkets) + .toNumber(); + + marketsAdvanced[UNKNOWN_KEY] = new Big(1) + .minus(marketsAdvanced.asiaPacific) + .minus(marketsAdvanced.emergingMarkets) + .minus(marketsAdvanced.europe) + .minus(marketsAdvanced.japan) + .minus(marketsAdvanced.northAmerica) + .minus(marketsAdvanced.otherMarkets) + .toNumber(); + return { markets, marketsAdvanced }; } @@ -2013,7 +2113,7 @@ export class PortfolioService { SymbolProfile, type } of ordersByAccount) { - let currentValueOfSymbolInBaseCurrency = + const currentValueOfSymbolInBaseCurrency = getFactor(type) * quantity * (portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ?? diff --git a/apps/api/src/app/redis-cache/redis-cache.service.mock.ts b/apps/api/src/app/redis-cache/redis-cache.service.mock.ts index 2779308bd..feb669ab0 100644 --- a/apps/api/src/app/redis-cache/redis-cache.service.mock.ts +++ b/apps/api/src/app/redis-cache/redis-cache.service.mock.ts @@ -1,7 +1,5 @@ import { Filter } from '@ghostfolio/common/interfaces'; -import { Milliseconds } from 'cache-manager'; - export const RedisCacheServiceMock = { cache: new Map(), get: (key: string): Promise => { @@ -20,7 +18,7 @@ export const RedisCacheServiceMock = { return `portfolio-snapshot-${userId}${filtersHash > 0 ? `-${filtersHash}` : ''}`; }, - set: (key: string, value: string, ttl?: Milliseconds): Promise => { + set: (key: string, value: string): Promise => { RedisCacheServiceMock.cache.set(key, value); return Promise.resolve(value); diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index 632eab98e..d73fa47eb 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -1,5 +1,4 @@ import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code'; -import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; import type { ColorScheme, DateRange, diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index a3dfc2fca..0f76b9540 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -56,7 +56,7 @@ export class UserService { { Account, id, permissions, Settings, subscription }: UserWithSettings, aLocale = locale ): Promise { - let [access, firstActivity, tags] = await Promise.all([ + const userData = await Promise.all([ this.prismaService.access.findMany({ include: { User: true @@ -70,8 +70,11 @@ export class UserService { }, where: { userId: id } }), - this.tagService.getInUseByUser(id) + this.tagService.getTagsForUser(id) ]); + const access = userData[0]; + const firstActivity = userData[1]; + let tags = userData[2]; let systemMessage: SystemMessage; diff --git a/apps/api/src/models/rule.ts b/apps/api/src/models/rule.ts index 8397f3e46..a1e0d9bee 100644 --- a/apps/api/src/models/rule.ts +++ b/apps/api/src/models/rule.ts @@ -1,8 +1,9 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { groupBy } from '@ghostfolio/common/helper'; -import { UserSettings } from '@ghostfolio/common/interfaces'; -import { TimelinePosition } from '@ghostfolio/common/models'; +import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces'; + +import { Big } from 'big.js'; import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { RuleInterface } from './interfaces/rule.interface'; @@ -33,24 +34,26 @@ export abstract class Rule implements RuleInterface { return this.name; } - public groupCurrentPositionsByAttribute( - positions: TimelinePosition[], - attribute: keyof TimelinePosition, + public groupCurrentHoldingsByAttribute( + holdings: PortfolioPosition[], + attribute: keyof PortfolioPosition, baseCurrency: string ) { - return Array.from(groupBy(attribute, positions).entries()).map( + return Array.from(groupBy(attribute, holdings).entries()).map( ([attributeValue, objs]) => ({ groupKey: attributeValue, investment: objs.reduce( (previousValue, currentValue) => - previousValue + currentValue.investment.toNumber(), + previousValue + currentValue.investment, 0 ), value: objs.reduce( (previousValue, currentValue) => previousValue + this.exchangeRateDataService.toCurrency( - currentValue.quantity.mul(currentValue.marketPrice).toNumber(), + new Big(currentValue.quantity) + .mul(currentValue.marketPrice) + .toNumber(), currentValue.currency, baseCurrency ), diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts index e3050efcc..39ee8b88d 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts @@ -1,35 +1,34 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { Rule } from '@ghostfolio/api/models/rule'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { UserSettings } from '@ghostfolio/common/interfaces'; -import { TimelinePosition } from '@ghostfolio/common/models'; +import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces'; export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { - private positions: TimelinePosition[]; + private holdings: PortfolioPosition[]; public constructor( protected exchangeRateDataService: ExchangeRateDataService, - positions: TimelinePosition[] + holdings: PortfolioPosition[] ) { super(exchangeRateDataService, { key: CurrencyClusterRiskBaseCurrencyCurrentInvestment.name, name: 'Investment: Base Currency' }); - this.positions = positions; + this.holdings = holdings; } public evaluate(ruleSettings: Settings) { - const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute( - this.positions, + const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute( + this.holdings, 'currency', ruleSettings.baseCurrency ); - let maxItem = positionsGroupedByCurrency[0]; + let maxItem = holdingsGroupedByCurrency[0]; let totalValue = 0; - positionsGroupedByCurrency.forEach((groupItem) => { + holdingsGroupedByCurrency.forEach((groupItem) => { // Calculate total value totalValue += groupItem.value; @@ -39,7 +38,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { + const baseCurrencyItem = holdingsGroupedByCurrency.find((item) => { return item.groupKey === ruleSettings.baseCurrency; }); diff --git a/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts index fadf47ba5..bdb36c78a 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts @@ -1,35 +1,34 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { Rule } from '@ghostfolio/api/models/rule'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { UserSettings } from '@ghostfolio/common/interfaces'; -import { TimelinePosition } from '@ghostfolio/common/models'; +import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces'; export class CurrencyClusterRiskCurrentInvestment extends Rule { - private positions: TimelinePosition[]; + private holdings: PortfolioPosition[]; public constructor( protected exchangeRateDataService: ExchangeRateDataService, - positions: TimelinePosition[] + holdings: PortfolioPosition[] ) { super(exchangeRateDataService, { key: CurrencyClusterRiskCurrentInvestment.name, name: 'Investment' }); - this.positions = positions; + this.holdings = holdings; } public evaluate(ruleSettings: Settings) { - const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute( - this.positions, + const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute( + this.holdings, 'currency', ruleSettings.baseCurrency ); - let maxItem = positionsGroupedByCurrency[0]; + let maxItem = holdingsGroupedByCurrency[0]; let totalValue = 0; - positionsGroupedByCurrency.forEach((groupItem) => { + holdingsGroupedByCurrency.forEach((groupItem) => { // Calculate total value totalValue += groupItem.value; diff --git a/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts index c59701438..819b8bd7b 100644 --- a/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts +++ b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts @@ -18,7 +18,7 @@ export class EmergencyFundSetup extends Rule { this.emergencyFund = emergencyFund; } - public evaluate(ruleSettings: Settings) { + public evaluate() { if (!this.emergencyFund) { return { evaluation: 'No emergency fund has been set up', diff --git a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts index 4e93bc757..016584949 100644 --- a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts +++ b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts @@ -33,7 +33,7 @@ export class AlphaVantageService implements DataProviderInterface { }); } - public canHandle(symbol: string) { + public canHandle() { return !!this.configurationService.get('API_KEY_ALPHA_VANTAGE'); } diff --git a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts index 067a6fbf9..d420c51fd 100644 --- a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts +++ b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts @@ -48,7 +48,7 @@ export class CoinGeckoService implements DataProviderInterface { } } - public canHandle(symbol: string) { + public canHandle() { return true; } diff --git a/apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts b/apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts index 4d26b31fb..8d2d180e3 100644 --- a/apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts @@ -43,7 +43,7 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface { this.configurationService.get('API_KEY_OPEN_FIGI'); } - let abortController = new AbortController(); + const abortController = new AbortController(); setTimeout(() => { abortController.abort(); diff --git a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts index 35fa9604a..1b1335b7e 100644 --- a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts @@ -83,7 +83,6 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { } public async enhance({ - requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), response, symbol }: { 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 f8347fc2d..9512b6530 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -460,7 +460,9 @@ export class DataProviderService { promises.push( promise.then(async (result) => { - for (let [symbol, dataProviderResponse] of Object.entries(result)) { + for (const [symbol, dataProviderResponse] of Object.entries( + result + )) { if ( [ ...DERIVED_CURRENCIES.map(({ currency }) => { @@ -600,7 +602,7 @@ export class DataProviderService { return { items: lookupItems }; } - let dataProviderServices = this.configurationService + const dataProviderServices = this.configurationService .get('DATA_SOURCES') .map((dataSource) => { return this.getDataProvider(DataSource[dataSource]); @@ -689,11 +691,13 @@ export class DataProviderService { } = {}; for (const date in rootData) { - data[date] = { - marketPrice: rootData[date].marketPrice - ? new Big(factor).mul(rootData[date].marketPrice).toNumber() - : null - }; + if (isNumber(rootData[date].marketPrice)) { + data[date] = { + marketPrice: new Big(factor) + .mul(rootData[date].marketPrice) + .toNumber() + }; + } } return data; diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index 8b2a1828b..3a840340e 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -43,7 +43,7 @@ export class EodHistoricalDataService implements DataProviderInterface { this.apiKey = this.configurationService.get('API_KEY_EOD_HISTORICAL_DATA'); } - public canHandle(symbol: string) { + public canHandle() { return true; } @@ -163,7 +163,7 @@ export class EodHistoricalDataService implements DataProviderInterface { ).json(); return response.reduce( - (result, { close, date }, index, array) => { + (result, { close, date }) => { if (isNumber(close)) { result[this.convertFromEodSymbol(symbol)][date] = { marketPrice: close @@ -203,7 +203,7 @@ export class EodHistoricalDataService implements DataProviderInterface { requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), symbols }: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { - let response: { [symbol: string]: IDataProviderResponse } = {}; + const response: { [symbol: string]: IDataProviderResponse } = {}; if (symbols.length <= 0) { return response; 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 cf9c5ef9b..7d5b38479 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 @@ -33,7 +33,7 @@ export class FinancialModelingPrepService implements DataProviderInterface { ); } - public canHandle(symbol: string) { + public canHandle() { return true; } diff --git a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts index c8ff87719..966069f22 100644 --- a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts +++ b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts @@ -29,7 +29,7 @@ export class GoogleSheetsService implements DataProviderInterface { private readonly symbolProfileService: SymbolProfileService ) {} - public canHandle(symbol: string) { + public canHandle() { return true; } diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts index e77122e05..b202f42d2 100644 --- a/apps/api/src/services/data-provider/manual/manual.service.ts +++ b/apps/api/src/services/data-provider/manual/manual.service.ts @@ -40,7 +40,7 @@ export class ManualService implements DataProviderInterface { private readonly symbolProfileService: SymbolProfileService ) {} - public canHandle(symbol: string) { + public canHandle() { return true; } @@ -87,12 +87,8 @@ export class ManualService implements DataProviderInterface { const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( [{ symbol, dataSource: this.getName() }] ); - const { - defaultMarketPrice, - headers = {}, - selector, - url - } = symbolProfile?.scraperConfiguration ?? {}; + const { defaultMarketPrice, selector, url } = + symbolProfile?.scraperConfiguration ?? {}; if (defaultMarketPrice) { const historical: { diff --git a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts index 7e866552e..e47e96d88 100644 --- a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts @@ -26,7 +26,7 @@ export class RapidApiService implements DataProviderInterface { private readonly configurationService: ConfigurationService ) {} - public canHandle(symbol: string) { + public canHandle() { return !!this.configurationService.get('API_KEY_RAPID_API'); } 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 a8f7d261e..2d67c646c 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 @@ -34,7 +34,7 @@ export class YahooFinanceService implements DataProviderInterface { private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService ) {} - public canHandle(symbol: string) { + public canHandle() { return true; } diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts index 59f5144d8..8f5d1c28a 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts @@ -1,10 +1,5 @@ export const ExchangeRateDataServiceMock = { - getExchangeRatesByCurrency: ({ - currencies, - endDate, - startDate, - targetCurrency - }): Promise => { + getExchangeRatesByCurrency: ({ targetCurrency }): Promise => { if (targetCurrency === 'CHF') { return Promise.resolve({ CHFCHF: { diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 31b2f885c..db95a3487 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -63,11 +63,11 @@ export class ExchangeRateDataService { return {}; } - let exchangeRatesByCurrency: { + const exchangeRatesByCurrency: { [currency: string]: { [dateString: string]: number }; } = {}; - for (let currency of currencies) { + for (const currency of currencies) { exchangeRatesByCurrency[`${currency}${targetCurrency}`] = await this.getExchangeRates({ startDate, @@ -94,7 +94,7 @@ export class ExchangeRateDataService { !isBefore(date, startDate); date = subDays(resetHours(date), 1) ) { - let dateString = format(date, DATE_FORMAT); + const dateString = format(date, DATE_FORMAT); // Check if the exchange rate for the current date is missing if ( @@ -351,7 +351,7 @@ export class ExchangeRateDataService { startDate: Date; }) { const dates = eachDayOfInterval({ end: endDate, start: startDate }); - let factors: { [dateString: string]: number } = {}; + const factors: { [dateString: string]: number } = {}; if (currencyFrom === currencyTo) { for (const date of dates) { @@ -379,10 +379,10 @@ export class ExchangeRateDataService { } else { // Calculate indirectly via base currency - let marketPriceBaseCurrencyFromCurrency: { + const marketPriceBaseCurrencyFromCurrency: { [dateString: string]: number; } = {}; - let marketPriceBaseCurrencyToCurrency: { + const marketPriceBaseCurrencyToCurrency: { [dateString: string]: number; } = {}; diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts index 8d7526906..59fdc5855 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts @@ -5,8 +5,6 @@ import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queu export const PortfolioSnapshotServiceMock = { addJobToQueue({ - data, - name, opts }: { data: IPortfolioSnapshotQueueJob; diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index 27c7310a7..b16f22fbb 100644 --- a/apps/api/src/services/tag/tag.service.ts +++ b/apps/api/src/services/tag/tag.service.ts @@ -6,38 +6,39 @@ import { Injectable } from '@nestjs/common'; export class TagService { public constructor(private readonly prismaService: PrismaService) {} - public async getPublic() { - return this.prismaService.tag.findMany({ - orderBy: { - name: 'asc' + public async getTagsForUser(userId: string) { + const tags = await this.prismaService.tag.findMany({ + include: { + _count: { + select: { + orders: { + where: { + userId + } + } + } + } }, - where: { - userId: null - } - }); - } - - public async getInUseByUser(userId: string) { - return this.prismaService.tag.findMany({ orderBy: { name: 'asc' }, where: { OR: [ { - orders: { - some: { - userId - } - } + userId }, { - symbolProfile: { - some: {} - } + userId: null } ] } }); + + return tags.map(({ _count, id, name, userId }) => ({ + id, + name, + userId, + isUsed: _count.orders > 0 + })); } } diff --git a/apps/api/src/validators/is-currency-code.ts b/apps/api/src/validators/is-currency-code.ts index 8e8530552..34a82c481 100644 --- a/apps/api/src/validators/is-currency-code.ts +++ b/apps/api/src/validators/is-currency-code.ts @@ -4,8 +4,7 @@ import { registerDecorator, ValidationOptions, ValidatorConstraint, - ValidatorConstraintInterface, - ValidationArguments + ValidatorConstraintInterface } from 'class-validator'; import { isISO4217CurrencyCode } from 'class-validator'; @@ -25,7 +24,7 @@ export function IsCurrencyCode(validationOptions?: ValidationOptions) { export class IsExtendedCurrencyConstraint implements ValidatorConstraintInterface { - public defaultMessage(args: ValidationArguments) { + public defaultMessage() { return '$value must be a valid ISO4217 currency code'; } diff --git a/apps/client-e2e/.eslintrc.json b/apps/client-e2e/.eslintrc.json new file mode 100644 index 000000000..dbedf6bd4 --- /dev/null +++ b/apps/client-e2e/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "parserOptions": { + "project": ["apps/client-e2e/tsconfig.*?.json"] + }, + "rules": {} + }, + { + "files": ["src/plugins/index.js"], + "rules": { + "@typescript-eslint/no-var-requires": "off", + "no-undef": "off" + } + } + ] +} diff --git a/apps/client/src/app/adapter/custom-date-adapter.ts b/apps/client/src/app/adapter/custom-date-adapter.ts index 724ea7211..a1326b823 100644 --- a/apps/client/src/app/adapter/custom-date-adapter.ts +++ b/apps/client/src/app/adapter/custom-date-adapter.ts @@ -15,7 +15,7 @@ export class CustomDateAdapter extends NativeDateAdapter { /** * Formats a date as a string */ - public format(aDate: Date, aParseFormat: string): string { + public format(aDate: Date): string { return format(aDate, getDateFormatString(this.locale)); } 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 d6791760b..d4bdad556 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 @@ -121,7 +121,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit { } public onExport() { - let activityIds = this.dataSource.data.map(({ id }) => { + const activityIds = this.dataSource.data.map(({ id }) => { return id; }); diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts index b1d7154e9..9695dc2a2 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts @@ -12,7 +12,7 @@ import { } from '@ghostfolio/common/interfaces'; import { Injectable } from '@angular/core'; -import { EMPTY, catchError, finalize, forkJoin, takeUntil } from 'rxjs'; +import { EMPTY, catchError, finalize, forkJoin } from 'rxjs'; @Injectable() export class AdminMarketDataService { 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 5339b9244..3b239ef18 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 @@ -149,8 +149,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { ) {} public ngOnInit() { - const { tags } = this.dataService.fetchInfo(); - this.activityForm = this.formBuilder.group({ tags: [] }); @@ -160,13 +158,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { { id: this.data.symbol, type: 'SYMBOL' } ]; - this.tagsAvailable = tags.map((tag) => { - return { - ...tag, - name: translate(tag.name) - }; - }); - this.activityForm .get('tags') .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) @@ -452,6 +443,14 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; + this.tagsAvailable = + this.user?.tags?.map((tag) => { + return { + ...tag, + name: translate(tag.name) + }; + }) ?? []; + this.changeDetectorRef.markForCheck(); } }); @@ -481,7 +480,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { } public onExport() { - let activityIds = this.dataSource.data.map(({ id }) => { + const activityIds = this.dataSource.data.map(({ id }) => { return id; }); 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 4f7de03b5..c76638d33 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 @@ -111,7 +111,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { this.initialize(); } - public onSymbolClicked({ dataSource, symbol }: AssetProfileIdentifier) { + public onHoldingClicked({ dataSource, symbol }: AssetProfileIdentifier) { if (dataSource && symbol) { this.router.navigate([], { queryParams: { dataSource, symbol, holdingDetailDialog: true } 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 bd9e57bb2..f1c4e7e88 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -38,9 +38,12 @@ }
@@ -50,6 +53,7 @@ [hasPermissionToCreateActivity]="hasPermissionToCreateOrder" [holdings]="holdings" [locale]="user?.settings?.locale" + (holdingClicked)="onHoldingClicked($event)" /> @if (hasPermissionToCreateOrder && holdings?.length > 0) {
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 a32041fce..cd6495a31 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 @@ -11,7 +11,6 @@ import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { DateRange } from '@ghostfolio/common/types'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DeviceDetectorService } from 'ngx-device-detector'; diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts index 4fb68e780..265d3c941 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts @@ -35,8 +35,6 @@ export class GfRuleSettingsDialogComponent { @Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams, public dialogRef: MatDialogRef ) { - console.log(this.data.rule); - this.settings = this.data.rule.settings; } } diff --git a/apps/client/src/app/core/notification/notification.service.ts b/apps/client/src/app/core/notification/notification.service.ts index 2e7d9de6c..189da67f5 100644 --- a/apps/client/src/app/core/notification/notification.service.ts +++ b/apps/client/src/app/core/notification/notification.service.ts @@ -33,7 +33,7 @@ export class NotificationService { title: aParams.title }); - return dialog.afterClosed().subscribe((result) => { + return dialog.afterClosed().subscribe(() => { if (isFunction(aParams.discardFn)) { aParams.discardFn(); } diff --git a/apps/client/src/app/pages/faq/faq-page.component.ts b/apps/client/src/app/pages/faq/faq-page.component.ts index 0bbe1c904..1e0d508ae 100644 --- a/apps/client/src/app/pages/faq/faq-page.component.ts +++ b/apps/client/src/app/pages/faq/faq-page.component.ts @@ -1,5 +1,5 @@ import { DataService } from '@ghostfolio/client/services/data.service'; -import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; +import { TabConfiguration } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Component, OnDestroy, OnInit } from '@angular/core'; diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html index f726a6020..7ce938227 100644 --- a/apps/client/src/app/pages/landing/landing-page.html +++ b/apps/client/src/app/pages/landing/landing-page.html @@ -331,7 +331,7 @@
@for (testimonial of testimonials; track testimonial) { -
+
{ - return { - ...tag, - name: translate(tag.name) - }; - }); + + this.tagsAvailable = + this.data.user?.tags?.map((tag) => { + return { + ...tag, + name: translate(tag.name) + }; + }) ?? []; Object.keys(Type).forEach((type) => { this.typesTranslationMap[Type[type]] = translate(Type[type]); diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index 724eb162d..2bacf3ea4 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -47,7 +47,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { public hasImpersonationId: boolean; public isLoading = false; public markets: { - [key in Market]: { name: string; value: number }; + [key in Market]: { id: Market; valueInPercentage: number }; }; public marketsAdvanced: { [key in MarketAdvanced]: { @@ -222,24 +222,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { value: 0 } }; - this.markets = { - [UNKNOWN_KEY]: { - name: UNKNOWN_KEY, - value: 0 - }, - developedMarkets: { - name: 'developedMarkets', - value: 0 - }, - emergingMarkets: { - name: 'emergingMarkets', - value: 0 - }, - otherMarkets: { - name: 'otherMarkets', - value: 0 - } - }; this.marketsAdvanced = { [UNKNOWN_KEY]: { id: UNKNOWN_KEY, @@ -321,6 +303,16 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { }; } + this.markets = this.portfolioDetails.markets; + + Object.values(this.portfolioDetails.marketsAdvanced).forEach( + ({ id, valueInBaseCurrency, valueInPercentage }) => { + this.marketsAdvanced[id].value = isNumber(valueInBaseCurrency) + ? valueInBaseCurrency + : valueInPercentage; + } + ); + for (const [symbol, position] of Object.entries( this.portfolioDetails.holdings )) { @@ -351,48 +343,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { // Prepare analysis data by continents, countries, holdings and sectors except for liquidity if (position.countries.length > 0) { - this.markets.developedMarkets.value += - position.markets.developedMarkets * - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage); - this.markets.emergingMarkets.value += - position.markets.emergingMarkets * - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage); - this.markets.otherMarkets.value += - position.markets.otherMarkets * - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage); - - this.marketsAdvanced.asiaPacific.value += - position.marketsAdvanced.asiaPacific * - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage); - this.marketsAdvanced.emergingMarkets.value += - position.marketsAdvanced.emergingMarkets * - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage); - this.marketsAdvanced.europe.value += - position.marketsAdvanced.europe * - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage); - this.marketsAdvanced.japan.value += - position.marketsAdvanced.japan * - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage); - this.marketsAdvanced.northAmerica.value += - position.marketsAdvanced.northAmerica * - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage); - for (const country of position.countries) { const { code, continent, name, weight } = country; @@ -442,18 +392,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { ) ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency : this.portfolioDetails.holdings[symbol].valueInPercentage; - - this.markets[UNKNOWN_KEY].value += isNumber( - position.valueInBaseCurrency - ) - ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency - : this.portfolioDetails.holdings[symbol].valueInPercentage; - - this.marketsAdvanced[UNKNOWN_KEY].value += isNumber( - position.valueInBaseCurrency - ) - ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency - : this.portfolioDetails.holdings[symbol].valueInPercentage; } if (position.holdings.length > 0) { @@ -541,21 +479,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { }; } - const marketsTotal = - this.markets.developedMarkets.value + - this.markets.emergingMarkets.value + - this.markets.otherMarkets.value + - this.markets[UNKNOWN_KEY].value; - - this.markets.developedMarkets.value = - this.markets.developedMarkets.value / marketsTotal; - this.markets.emergingMarkets.value = - this.markets.emergingMarkets.value / marketsTotal; - this.markets.otherMarkets.value = - this.markets.otherMarkets.value / marketsTotal; - this.markets[UNKNOWN_KEY].value = - this.markets[UNKNOWN_KEY].value / marketsTotal; - this.topHoldings = Object.values(this.topHoldingsMap) .map(({ name, value }) => { if (this.hasImpersonationId || this.user.settings.isRestrictedView) { diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html index 9b855592d..3431501f5 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -218,7 +218,7 @@ i18n size="large" [isPercent]="true" - [value]="markets?.developedMarkets?.value" + [value]="markets?.developedMarkets?.valueInPercentage" >Developed Markets
@@ -227,7 +227,7 @@ i18n size="large" [isPercent]="true" - [value]="markets?.emergingMarkets?.value" + [value]="markets?.emergingMarkets?.valueInPercentage" >Emerging Markets
@@ -236,17 +236,17 @@ i18n size="large" [isPercent]="true" - [value]="markets?.otherMarkets?.value" + [value]="markets?.otherMarkets?.valueInPercentage" >Other Markets
- @if (markets?.[UNKNOWN_KEY]?.value > 0) { + @if (markets?.[UNKNOWN_KEY]?.valueInPercentage > 0) {
No data available
diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index 56d6ebdb7..3dbce23ec 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -32,7 +32,7 @@ export class PublicPageComponent implements OnInit { public deviceType: string; public holdings: PublicPortfolioResponse['holdings'][string][]; public markets: { - [key in Market]: { name: string; value: number }; + [key in Market]: { id: Market; valueInPercentage: number }; }; public positions: { [symbol: string]: Pick & { @@ -102,24 +102,7 @@ export class PublicPageComponent implements OnInit { } }; this.holdings = []; - this.markets = { - [UNKNOWN_KEY]: { - name: UNKNOWN_KEY, - value: 0 - }, - developedMarkets: { - name: 'developedMarkets', - value: 0 - }, - emergingMarkets: { - name: 'emergingMarkets', - value: 0 - }, - otherMarkets: { - name: 'otherMarkets', - value: 0 - } - }; + this.markets = this.publicPortfolioDetails.markets; this.positions = {}; this.sectors = { [UNKNOWN_KEY]: { @@ -150,13 +133,6 @@ export class PublicPageComponent implements OnInit { // Prepare analysis data by continents, countries, holdings and sectors except for liquidity if (position.countries.length > 0) { - this.markets.developedMarkets.value += - position.markets.developedMarkets * position.valueInBaseCurrency; - this.markets.emergingMarkets.value += - position.markets.emergingMarkets * position.valueInBaseCurrency; - this.markets.otherMarkets.value += - position.markets.otherMarkets * position.valueInBaseCurrency; - for (const country of position.countries) { const { code, continent, name, weight } = country; @@ -192,9 +168,6 @@ export class PublicPageComponent implements OnInit { this.countries[UNKNOWN_KEY].value += this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency; - - this.markets[UNKNOWN_KEY].value += - this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency; } if (position.sectors.length > 0) { @@ -227,21 +200,6 @@ export class PublicPageComponent implements OnInit { : position.valueInPercentage }; } - - const marketsTotal = - this.markets.developedMarkets.value + - this.markets.emergingMarkets.value + - this.markets.otherMarkets.value + - this.markets[UNKNOWN_KEY].value; - - this.markets.developedMarkets.value = - this.markets.developedMarkets.value / marketsTotal; - this.markets.emergingMarkets.value = - this.markets.emergingMarkets.value / marketsTotal; - this.markets.otherMarkets.value = - this.markets.otherMarkets.value / marketsTotal; - this.markets[UNKNOWN_KEY].value = - this.markets[UNKNOWN_KEY].value / marketsTotal; } public ngOnDestroy() { diff --git a/apps/client/src/app/pages/public/public-page.html b/apps/client/src/app/pages/public/public-page.html index 369ea50f5..ac1628a3a 100644 --- a/apps/client/src/app/pages/public/public-page.html +++ b/apps/client/src/app/pages/public/public-page.html @@ -156,7 +156,7 @@ i18n size="large" [isPercent]="true" - [value]="markets?.developedMarkets?.value" + [value]="markets?.developedMarkets?.valueInPercentage" >Developed Markets
@@ -165,7 +165,7 @@ i18n size="large" [isPercent]="true" - [value]="markets?.emergingMarkets?.value" + [value]="markets?.emergingMarkets?.valueInPercentage" >Emerging Markets
@@ -174,17 +174,17 @@ i18n size="large" [isPercent]="true" - [value]="markets?.otherMarkets?.value" + [value]="markets?.otherMarkets?.valueInPercentage" >Other Markets
- @if (markets?.[UNKNOWN_KEY]?.value > 0) { + @if (markets?.[UNKNOWN_KEY]?.valueInPercentage > 0) {
No data available
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 0dbc5b555..f85410273 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -287,7 +287,7 @@ export class DataService { } public deleteActivities({ filters }) { - let params = this.buildFiltersAsQueryParams({ filters }); + const params = this.buildFiltersAsQueryParams({ filters }); return this.http.delete(`/api/v1/order`, { params }); } diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf index 597b88260..06f8a5647 100644 --- a/apps/client/src/locales/messages.ca.xlf +++ b/apps/client/src/locales/messages.ca.xlf @@ -1471,7 +1471,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 21 + 35 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -2635,7 +2635,7 @@ Gestionar Activitats apps/client/src/app/components/home-holdings/home-holdings.html - 61 + 62 @@ -5979,7 +5979,7 @@ Do you really want to delete these activities? libs/ui/src/lib/activities-table/activities-table.component.ts - 223 + 219 @@ -5987,7 +5987,7 @@ Do you really want to delete this activity? libs/ui/src/lib/activities-table/activities-table.component.ts - 233 + 229 @@ -7055,7 +7055,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 5 + 9 @@ -7063,7 +7063,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 9 + 22 @@ -7071,7 +7071,7 @@ Close apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 15 + 33 @@ -7258,6 +7258,14 @@ 280 + + Oops! Could not find any assets. + Oops! Could not find any assets. + + libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html + 37 + + diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index 6929db1ef..62302249d 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -554,7 +554,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 21 + 35 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -1058,7 +1058,7 @@ Aktivitäten verwalten apps/client/src/app/components/home-holdings/home-holdings.html - 61 + 62 @@ -2550,7 +2550,7 @@ Möchtest du diese Aktivität wirklich löschen? libs/ui/src/lib/activities-table/activities-table.component.ts - 233 + 229 @@ -3914,7 +3914,7 @@ Möchtest du diese Aktivitäten wirklich löschen? libs/ui/src/lib/activities-table/activities-table.component.ts - 223 + 219 @@ -7055,7 +7055,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 5 + 9 @@ -7063,7 +7063,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 9 + 22 @@ -7071,7 +7071,7 @@ Schliessen apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 15 + 33 @@ -7258,6 +7258,14 @@ 280 + + Oops! Could not find any assets. + Ups! Es konnten leider keine Assets gefunden werden. + + libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html + 37 + + diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index b5d7f41f9..1e897f596 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -555,7 +555,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 21 + 35 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -1059,7 +1059,7 @@ Gestión de las operaciones apps/client/src/app/components/home-holdings/home-holdings.html - 61 + 62 @@ -2551,7 +2551,7 @@ ¿Estás seguro de eliminar esta operación? libs/ui/src/lib/activities-table/activities-table.component.ts - 233 + 229 @@ -3915,7 +3915,7 @@ Do you really want to delete these activities? libs/ui/src/lib/activities-table/activities-table.component.ts - 223 + 219 @@ -7056,7 +7056,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 5 + 9 @@ -7064,7 +7064,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 9 + 22 @@ -7072,7 +7072,7 @@ Close apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 15 + 33 @@ -7259,6 +7259,14 @@ 280 + + Oops! Could not find any assets. + Oops! Could not find any assets. + + libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html + 37 + + diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index e880df353..aa366fcc4 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -614,7 +614,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 21 + 35 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -1378,7 +1378,7 @@ Gérer les Activités apps/client/src/app/components/home-holdings/home-holdings.html - 61 + 62 @@ -3070,7 +3070,7 @@ Voulez-vous vraiment supprimer cette activité ? libs/ui/src/lib/activities-table/activities-table.component.ts - 233 + 229 @@ -3914,7 +3914,7 @@ Voulez-vous vraiment supprimer toutes vos activités ? libs/ui/src/lib/activities-table/activities-table.component.ts - 223 + 219 @@ -7055,7 +7055,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 5 + 9 @@ -7063,7 +7063,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 9 + 22 @@ -7071,7 +7071,7 @@ Close apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 15 + 33 @@ -7258,6 +7258,14 @@ 280 + + Oops! Could not find any assets. + Oops! Could not find any assets. + + libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html + 37 + + diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index a2c1f0cb2..a9d6df210 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -555,7 +555,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 21 + 35 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -1059,7 +1059,7 @@ Gestione delle attività apps/client/src/app/components/home-holdings/home-holdings.html - 61 + 62 @@ -2551,7 +2551,7 @@ Vuoi davvero eliminare questa attività? libs/ui/src/lib/activities-table/activities-table.component.ts - 233 + 229 @@ -3915,7 +3915,7 @@ Vuoi davvero eliminare tutte le tue attività? libs/ui/src/lib/activities-table/activities-table.component.ts - 223 + 219 @@ -7056,7 +7056,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 5 + 9 @@ -7064,7 +7064,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 9 + 22 @@ -7072,7 +7072,7 @@ Close apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 15 + 33 @@ -7259,6 +7259,14 @@ 280 + + Oops! Could not find any assets. + Oops! Could not find any assets. + + libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html + 37 + + diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index e686b6559..441455ef8 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -554,7 +554,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 21 + 35 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -1058,7 +1058,7 @@ Activiteiten beheren apps/client/src/app/components/home-holdings/home-holdings.html - 61 + 62 @@ -2550,7 +2550,7 @@ Wil je deze activiteit echt verwijderen? libs/ui/src/lib/activities-table/activities-table.component.ts - 233 + 229 @@ -3914,7 +3914,7 @@ Wil je echt al je activiteiten verwijderen? libs/ui/src/lib/activities-table/activities-table.component.ts - 223 + 219 @@ -7055,7 +7055,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 5 + 9 @@ -7063,7 +7063,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 9 + 22 @@ -7071,7 +7071,7 @@ Close apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 15 + 33 @@ -7258,6 +7258,14 @@ 280 + + Oops! Could not find any assets. + Oops! Could not find any assets. + + libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html + 37 + + diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index a585e4b9d..e68b07f45 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -1363,7 +1363,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 21 + 35 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -2283,7 +2283,7 @@ Zarządzaj Aktywnościami apps/client/src/app/components/home-holdings/home-holdings.html - 61 + 62 @@ -4151,7 +4151,7 @@ Do you really want to delete these activities? libs/ui/src/lib/activities-table/activities-table.component.ts - 223 + 219 @@ -5503,7 +5503,7 @@ Czy na pewno chcesz usunąć tę działalność? libs/ui/src/lib/activities-table/activities-table.component.ts - 233 + 229 @@ -7055,7 +7055,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 5 + 9 @@ -7063,7 +7063,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 9 + 22 @@ -7071,7 +7071,7 @@ Close apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 15 + 33 @@ -7258,6 +7258,14 @@ 280 + + Oops! Could not find any assets. + Oops! Could not find any assets. + + libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html + 37 + + diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index e0ff5f7e7..8bb514d06 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -614,7 +614,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 21 + 35 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -1242,7 +1242,7 @@ Gerir Atividades apps/client/src/app/components/home-holdings/home-holdings.html - 61 + 62 @@ -2942,7 +2942,7 @@ Deseja realmente eliminar esta atividade? libs/ui/src/lib/activities-table/activities-table.component.ts - 233 + 229 @@ -3914,7 +3914,7 @@ Deseja mesmo eliminar estas atividades? libs/ui/src/lib/activities-table/activities-table.component.ts - 223 + 219 @@ -7055,7 +7055,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 5 + 9 @@ -7063,7 +7063,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 9 + 22 @@ -7071,7 +7071,7 @@ Close apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 15 + 33 @@ -7258,6 +7258,14 @@ 280 + + Oops! Could not find any assets. + Oops! Could not find any assets. + + libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html + 37 + + diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index 575dcf12b..e5292e342 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -1327,7 +1327,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 21 + 35 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -2135,7 +2135,7 @@ İşlemleri Yönet apps/client/src/app/components/home-holdings/home-holdings.html - 61 + 62 @@ -3663,7 +3663,7 @@ Tüm işlemlerinizi silmeyi gerçekten istiyor musunuz? libs/ui/src/lib/activities-table/activities-table.component.ts - 223 + 219 @@ -5207,7 +5207,7 @@ TBu işlemi silmeyi gerçekten istiyor musunuz? libs/ui/src/lib/activities-table/activities-table.component.ts - 233 + 229 @@ -7055,7 +7055,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 5 + 9 @@ -7063,7 +7063,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 9 + 22 @@ -7071,7 +7071,7 @@ Close apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 15 + 33 @@ -7258,6 +7258,14 @@ 280 + + Oops! Could not find any assets. + Oops! Could not find any assets. + + libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html + 37 + + diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index 735554838..849b1053e 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -1312,7 +1312,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 21 + 35 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -2151,7 +2151,7 @@ Manage Activities apps/client/src/app/components/home-holdings/home-holdings.html - 61 + 62 @@ -3825,7 +3825,7 @@ Do you really want to delete these activities? libs/ui/src/lib/activities-table/activities-table.component.ts - 223 + 219 @@ -5064,7 +5064,7 @@ Do you really want to delete this activity? libs/ui/src/lib/activities-table/activities-table.component.ts - 233 + 229 @@ -6359,7 +6359,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 9 + 22 @@ -6380,7 +6380,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 5 + 9 @@ -6408,7 +6408,7 @@ Close apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 15 + 33 @@ -6565,6 +6565,13 @@ 49 + + Oops! Could not find any assets. + + libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html + 37 + + diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index f4e75b333..22aa783d6 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -1372,7 +1372,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 21 + 35 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -2300,7 +2300,7 @@ 管理活动 apps/client/src/app/components/home-holdings/home-holdings.html - 61 + 62 @@ -4168,7 +4168,7 @@ 您真的要删除所有活动吗? libs/ui/src/lib/activities-table/activities-table.component.ts - 223 + 219 @@ -5552,7 +5552,7 @@ 您确实要删除此活动吗? libs/ui/src/lib/activities-table/activities-table.component.ts - 233 + 229 @@ -7056,7 +7056,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 5 + 9 @@ -7064,7 +7064,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 9 + 22 @@ -7072,7 +7072,7 @@ Close apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 15 + 33 @@ -7259,6 +7259,14 @@ 280 + + Oops! Could not find any assets. + Oops! Could not find any assets. + + libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html + 37 + + diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json index fa5cdf2a3..1dd30aad1 100644 --- a/apps/client/tsconfig.json +++ b/apps/client/tsconfig.json @@ -15,6 +15,8 @@ ], "angularCompilerOptions": { "strictInjectionParameters": true, + // TODO: Enable stricter rules for this project + "strictInputAccessModifiers": false, "strictTemplates": false }, "compilerOptions": { diff --git a/apps/ui-e2e/.eslintrc.json b/apps/ui-e2e/.eslintrc.json index 4c5989b23..e1ff28793 100644 --- a/apps/ui-e2e/.eslintrc.json +++ b/apps/ui-e2e/.eslintrc.json @@ -4,6 +4,9 @@ "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "parserOptions": { + "project": ["apps/ui-e2e/tsconfig.json"] + }, "rules": {} }, { diff --git a/git-hooks/pre-commit b/git-hooks/pre-commit deleted file mode 100755 index b2b86eac1..000000000 --- a/git-hooks/pre-commit +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Will check if "npm run format" is run before executing. -# Called by "git commit" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message if -# it wants to stop the commit. - -echo "Running npm run format" - -# Run the command and loop over its output -FILES_TO_STAGE="" -i=0 -while IFS= read -r line; do - # Process each line here - ((i++)) - if [ $i -le 2 ]; then - continue - fi - if [[ $line == Done* ]]; then - break - fi - FILES_TO_STAGE="$FILES_TO_STAGE $line" - -done < <(npm run format) -git add $FILES_TO_STAGE -echo "Files formatted. Committing..." diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 689150b68..005d3d77e 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -135,7 +135,7 @@ export function extractNumberFromString({ // Remove non-numeric characters (excluding international formatting characters) const numericValue = value.replace(/[^\d.,'’\s]/g, ''); - let parser = new NumberParser(locale); + const parser = new NumberParser(locale); return parser.parse(numericValue); } catch { diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index d279c74a4..1b3926331 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -1,6 +1,6 @@ import { SubscriptionOffer } from '@ghostfolio/common/types'; -import { Platform, SymbolProfile, Tag } from '@prisma/client'; +import { Platform, SymbolProfile } from '@prisma/client'; import { Statistics } from './statistics.interface'; import { Subscription } from './subscription.interface'; @@ -19,5 +19,4 @@ export interface InfoItem { statistics: Statistics; stripePublicKey?: string; subscriptions: { [offer in SubscriptionOffer]: Subscription }; - tags: Tag[]; } diff --git a/libs/common/src/lib/interfaces/portfolio-details.interface.ts b/libs/common/src/lib/interfaces/portfolio-details.interface.ts index 611ed8056..e455f73ca 100644 --- a/libs/common/src/lib/interfaces/portfolio-details.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-details.interface.ts @@ -2,6 +2,7 @@ import { PortfolioPosition, PortfolioSummary } from '@ghostfolio/common/interfaces'; +import { Market, MarketAdvanced } from '@ghostfolio/common/types'; export interface PortfolioDetails { accounts: { @@ -14,6 +15,20 @@ export interface PortfolioDetails { }; }; holdings: { [symbol: string]: PortfolioPosition }; + markets?: { + [key in Market]: { + id: Market; + valueInBaseCurrency?: number; + valueInPercentage: number; + }; + }; + marketsAdvanced?: { + [key in MarketAdvanced]: { + id: MarketAdvanced; + valueInBaseCurrency?: number; + valueInPercentage: number; + }; + }; platforms: { [id: string]: { balance: number; diff --git a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts index ce623a058..dc6e57587 100644 --- a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts @@ -1,4 +1,5 @@ -import { PortfolioPosition } from '../portfolio-position.interface'; +import { PortfolioDetails, PortfolioPosition } from '..'; +import { Market } from '../../types'; export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { alias?: string; @@ -22,6 +23,12 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { | 'valueInPercentage' >; }; + markets: { + [key in Market]: Pick< + PortfolioDetails['markets'][key], + 'id' | 'valueInPercentage' + >; + }; } interface PublicPortfolioResponseV1 { diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts index 2891314a0..27cd1a610 100644 --- a/libs/common/src/lib/interfaces/user.interface.ts +++ b/libs/common/src/lib/interfaces/user.interface.ts @@ -23,5 +23,5 @@ export interface User { offer: SubscriptionOffer; type: SubscriptionType; }; - tags: Tag[]; + tags: (Tag & { isUsed: boolean })[]; } diff --git a/libs/ui/.eslintrc.json b/libs/ui/.eslintrc.json index 2d3d3dcaf..6b88b020e 100644 --- a/libs/ui/.eslintrc.json +++ b/libs/ui/.eslintrc.json @@ -1,9 +1,12 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/*.stories.ts"], "overrides": [ { - "files": ["*.ts"], + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "parserOptions": { + "project": ["libs/ui/tsconfig.*?.json"] + }, "extends": [ "plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates" diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts index 482305bb7..67f81adce 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -42,7 +42,6 @@ import { } from '@angular/material/sort'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { Router, RouterModule } from '@angular/router'; import { isUUID } from 'class-validator'; import { endOfToday, isAfter } from 'date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -64,8 +63,7 @@ import { Subject, Subscription, takeUntil } from 'rxjs'; MatSortModule, MatTableModule, MatTooltipModule, - NgxSkeletonLoaderModule, - RouterModule + NgxSkeletonLoaderModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-activities-table', @@ -95,6 +93,7 @@ export class GfActivitiesTableComponent @Input() totalItems = Number.MAX_SAFE_INTEGER; @Output() activitiesDeleted = new EventEmitter(); + @Output() activityClicked = new EventEmitter(); @Output() activityDeleted = new EventEmitter(); @Output() activityToClone = new EventEmitter(); @Output() activityToUpdate = new EventEmitter(); @@ -122,10 +121,7 @@ export class GfActivitiesTableComponent private unsubscribeSubject = new Subject(); - public constructor( - private notificationService: NotificationService, - private router: Router - ) {} + public constructor(private notificationService: NotificationService) {} public ngOnInit() { if (this.showCheckbox) { @@ -203,7 +199,7 @@ export class GfActivitiesTableComponent activity.isDraft === false && ['BUY', 'DIVIDEND', 'SELL'].includes(activity.type) ) { - this.onOpenPositionDialog({ + this.activityClicked.emit({ dataSource: activity.SymbolProfile.dataSource, symbol: activity.SymbolProfile.symbol }); @@ -268,20 +264,18 @@ export class GfActivitiesTableComponent }); } - public onOpenPositionDialog({ dataSource, symbol }: AssetProfileIdentifier) { - this.router.navigate([], { - queryParams: { dataSource, symbol, holdingDetailDialog: true } - }); - } - public onUpdateActivity(aActivity: OrderWithAccount) { this.activityToUpdate.emit(aActivity); } public toggleAllRows() { - this.areAllRowsSelected() - ? this.selectedRows.clear() - : this.dataSource.data.forEach((row) => this.selectedRows.select(row)); + if (this.areAllRowsSelected()) { + this.selectedRows.clear(); + } else { + this.dataSource.data.forEach((row) => { + this.selectedRows.select(row); + }); + } this.selectedActivities.emit(this.selectedRows.selected); } diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts index c3e7eb292..a4e7aad7d 100644 --- a/libs/ui/src/lib/assistant/assistant.component.ts +++ b/libs/ui/src/lib/assistant/assistant.component.ts @@ -36,7 +36,6 @@ import { MatMenuTrigger } from '@angular/material/menu'; import { MatSelectModule } from '@angular/material/select'; import { RouterModule } from '@angular/router'; import { Account, AssetClass } from '@prisma/client'; -import { eachYearOfInterval, format } from 'date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs'; import { @@ -157,7 +156,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { ) {} public ngOnInit() { - this.accounts = this.user?.accounts; this.assetClasses = Object.keys(AssetClass).map((assetClass) => { return { id: assetClass, @@ -165,13 +163,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { type: 'ASSET_CLASS' }; }); - this.tags = this.user?.tags.map(({ id, name }) => { - return { - id, - label: translate(name), - type: 'TAG' - }; - }); this.searchFormControl.valueChanges .pipe( @@ -213,6 +204,8 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { } public ngOnChanges() { + this.accounts = this.user?.accounts ?? []; + this.dateRangeOptions = [ { label: $localize`Today`, value: '1d' }, { @@ -280,6 +273,23 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { emitEvent: false } ); + + this.tags = + this.user?.tags + ?.filter(({ isUsed }) => { + return isUsed; + }) + .map(({ id, name }) => { + return { + id, + label: translate(name), + type: 'TAG' + }; + }) ?? []; + + if (this.tags.length === 0) { + this.filterForm.get('tag').disable({ emitEvent: false }); + } } public hasFilter(aFormValue: { [key: string]: string[] }) { diff --git a/libs/ui/src/lib/carousel/carousel-item.directive.ts b/libs/ui/src/lib/carousel/carousel-item.directive.ts index 690c71884..38c3ab212 100644 --- a/libs/ui/src/lib/carousel/carousel-item.directive.ts +++ b/libs/ui/src/lib/carousel/carousel-item.directive.ts @@ -1,8 +1,8 @@ import { Directive, ElementRef } from '@angular/core'; @Directive({ - selector: '[gf-carousel-item]' + selector: '[gfCarouselItem]' }) -export class CarouselItem { +export class CarouselItemDirective { public constructor(readonly element: ElementRef) {} } diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.service.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.service.ts index 20247ef90..8a5e3afb9 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.service.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.service.ts @@ -52,7 +52,7 @@ export class FireCalculatorService { r: number; totalAmount: number; }) { - if (r == 0) { + if (r === 0) { // No compound interest return (totalAmount - P) / PMT; } else if (totalAmount <= P) { diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.ts b/libs/ui/src/lib/holdings-table/holdings-table.component.ts index 39a9baf5c..257fa2013 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.ts +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.ts @@ -14,10 +14,11 @@ import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, Component, + EventEmitter, Input, OnChanges, OnDestroy, - OnInit, + Output, ViewChild } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; @@ -25,7 +26,6 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; -import { Router, RouterModule } from '@angular/router'; import { AssetSubClass } from '@prisma/client'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { Subject, Subscription } from 'rxjs'; @@ -44,8 +44,7 @@ import { Subject, Subscription } from 'rxjs'; MatPaginatorModule, MatSortModule, MatTableModule, - NgxSkeletonLoaderModule, - RouterModule + NgxSkeletonLoaderModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-holdings-table', @@ -53,7 +52,7 @@ import { Subject, Subscription } from 'rxjs'; styleUrls: ['./holdings-table.component.scss'], templateUrl: './holdings-table.component.html' }) -export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit { +export class GfHoldingsTableComponent implements OnChanges, OnDestroy { @Input() baseCurrency: string; @Input() deviceType: string; @Input() hasPermissionToCreateActivity: boolean; @@ -63,6 +62,8 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit { @Input() locale = getLocale(); @Input() pageSize = Number.MAX_SAFE_INTEGER; + @Output() holdingClicked = new EventEmitter(); + @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -75,9 +76,7 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit { private unsubscribeSubject = new Subject(); - public constructor(private router: Router) {} - - public ngOnInit() {} + public constructor() {} public ngOnChanges() { this.displayedColumns = ['icon', 'nameWithSymbol', 'dateOfFirstActivity']; @@ -107,9 +106,7 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit { public onOpenHoldingDialog({ dataSource, symbol }: AssetProfileIdentifier) { if (this.hasPermissionToOpenDetails) { - this.router.navigate([], { - queryParams: { dataSource, symbol, holdingDetailDialog: true } - }); + this.holdingClicked.emit({ dataSource, symbol }); } } diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index ad11c54f3..d3a28dffe 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -270,7 +270,7 @@ export class GfPortfolioProportionChartComponent } ]; - let labels = chartDataSorted.map(([symbol, { name }]) => { + let labels = chartDataSorted.map(([, { name }]) => { return name; }); diff --git a/libs/ui/src/lib/shared/abstract-mat-form-field.ts b/libs/ui/src/lib/shared/abstract-mat-form-field.ts index 460b6969d..05491b8ab 100644 --- a/libs/ui/src/lib/shared/abstract-mat-form-field.ts +++ b/libs/ui/src/lib/shared/abstract-mat-form-field.ts @@ -16,6 +16,7 @@ import { Subject } from 'rxjs'; @Component({ template: '' }) +// eslint-disable-next-line @angular-eslint/component-class-suffix export abstract class AbstractMatFormField implements ControlValueAccessor, DoCheck, MatFormFieldControl, OnDestroy { diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html index 2a2f98d7d..d055a618a 100644 --- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html @@ -31,6 +31,12 @@ } + } @empty { + @if (control.value?.length > 1) { + Oops! Could not find any assets. + } } } diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts index 8e07ed674..da97aac05 100644 --- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts @@ -117,17 +117,17 @@ export class GfSymbolAutocompleteComponent this.control.valueChanges .pipe( - debounceTime(400), - distinctUntilChanged(), filter((query) => { return isString(query) && query.length > 1; }), - takeUntil(this.unsubscribeSubject), tap(() => { this.isLoading = true; this.changeDetectorRef.markForCheck(); }), + debounceTime(400), + distinctUntilChanged(), + takeUntil(this.unsubscribeSubject), switchMap((query: string) => { return this.dataService.fetchSymbols({ query, diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.ts b/libs/ui/src/lib/top-holdings/top-holdings.component.ts index 6f7695687..9c4a22cfb 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.ts +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.ts @@ -10,7 +10,6 @@ import { Input, OnChanges, OnDestroy, - OnInit, ViewChild } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; @@ -38,7 +37,7 @@ import { Subject } from 'rxjs'; styleUrls: ['./top-holdings.component.scss'], templateUrl: './top-holdings.component.html' }) -export class GfTopHoldingsComponent implements OnChanges, OnDestroy, OnInit { +export class GfTopHoldingsComponent implements OnChanges, OnDestroy { @Input() baseCurrency: string; @Input() locale = getLocale(); @Input() pageSize = Number.MAX_SAFE_INTEGER; @@ -57,10 +56,6 @@ export class GfTopHoldingsComponent implements OnChanges, OnDestroy, OnInit { private unsubscribeSubject = new Subject(); - public constructor() {} - - public ngOnInit() {} - public ngOnChanges() { this.isLoading = true; diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 8915707fa..b278180ea 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -2,11 +2,13 @@ import { getAnnualizedPerformancePercent, getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; +import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; +import { getLocale } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, PortfolioPosition } from '@ghostfolio/common/interfaces'; -import { DateRange } from '@ghostfolio/common/types'; +import { ColorScheme, DateRange } from '@ghostfolio/common/types'; import { CommonModule } from '@angular/common'; import { @@ -25,7 +27,7 @@ import { DataSource } from '@prisma/client'; import { Big } from 'big.js'; import { ChartConfiguration } from 'chart.js'; import { LinearScale } from 'chart.js'; -import { Chart } from 'chart.js'; +import { Chart, Tooltip } from 'chart.js'; import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; import { differenceInDays, max } from 'date-fns'; import { orderBy } from 'lodash'; @@ -44,9 +46,12 @@ const { gray, green, red } = require('open-color'); export class GfTreemapChartComponent implements AfterViewInit, OnChanges, OnDestroy { + @Input() baseCurrency: string; + @Input() colorScheme: ColorScheme; @Input() cursor: string; @Input() dateRange: DateRange; @Input() holdings: PortfolioPosition[]; + @Input() locale = getLocale(); @Output() treemapChartClicked = new EventEmitter(); @@ -58,7 +63,7 @@ export class GfTreemapChartComponent public isLoading = true; public constructor() { - Chart.register(LinearScale, TreemapController, TreemapElement); + Chart.register(LinearScale, Tooltip, TreemapController, TreemapElement); } public ngAfterViewInit() { @@ -168,6 +173,9 @@ export class GfTreemapChartComponent if (this.chartCanvas) { if (this.chart) { this.chart.data = data; + this.chart.options.plugins.tooltip = ( + this.getTooltipPluginConfiguration() + ); this.chart.update(); } else { this.chart = new Chart(this.chartCanvas.nativeElement, { @@ -199,9 +207,7 @@ export class GfTreemapChartComponent } }, plugins: { - tooltip: { - enabled: false - } + tooltip: this.getTooltipPluginConfiguration() } }, type: 'treemap' @@ -211,4 +217,34 @@ export class GfTreemapChartComponent this.isLoading = false; } + + private getTooltipPluginConfiguration() { + return { + ...getTooltipOptions({ + colorScheme: this.colorScheme, + currency: this.baseCurrency, + locale: this.locale + }), + callbacks: { + label: (context) => { + if (context.raw._data.valueInBaseCurrency !== null) { + const value = context.raw._data.valueInBaseCurrency; + return `${value.toLocaleString(this.locale, { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + })} ${this.baseCurrency}`; + } else { + const percentage = + context.raw._data.allocationInPercentage * 100; + return `${percentage.toFixed(2)}%`; + } + }, + title: () => { + return ''; + } + }, + xAlign: 'center', + yAlign: 'center' + }; + } } diff --git a/libs/ui/tsconfig.json b/libs/ui/tsconfig.json index 693f120c7..3ec4903b5 100644 --- a/libs/ui/tsconfig.json +++ b/libs/ui/tsconfig.json @@ -14,11 +14,11 @@ } ], "compilerOptions": { - "forceConsistentCasingInFileNames": true, + "target": "es2020", + // TODO: Remove once solved in tsconfig.base.json "strict": false, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "target": "es2020" + "noFallthroughCasesInSwitch": true }, "angularCompilerOptions": { "strictInjectionParameters": true, diff --git a/package-lock.json b/package-lock.json index 7f9e65f0c..1abc5b931 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "2.110.0", + "version": "2.112.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "2.110.0", + "version": "2.112.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -85,7 +85,6 @@ "passport": "0.7.0", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.1", - "prisma": "5.20.0", "reflect-metadata": "0.1.13", "rxjs": "7.5.6", "stripe": "15.11.0", @@ -144,12 +143,14 @@ "eslint-plugin-cypress": "2.15.1", "eslint-plugin-import": "2.29.1", "eslint-plugin-storybook": "0.6.15", + "husky": "9.1.6", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-preset-angular": "14.1.0", "nx": "19.5.6", "prettier": "3.3.3", "prettier-plugin-organize-attributes": "1.0.0", + "prisma": "5.20.0", "react": "18.2.0", "react-dom": "18.2.0", "replace-in-file": "7.0.1", @@ -9790,12 +9791,14 @@ "version": "5.20.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.20.0.tgz", "integrity": "sha512-oCx79MJ4HSujokA8S1g0xgZUGybD4SyIOydoHMngFYiwEwYDQ5tBQkK5XoEHuwOYDKUOKRn/J0MEymckc4IgsQ==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { "version": "5.20.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.20.0.tgz", "integrity": "sha512-DtqkP+hcZvPEbj8t8dK5df2b7d3B8GNauKqaddRRqQBBlgkbdhJkxhoJTrOowlS3vaRt2iMCkU0+CSNn0KhqAQ==", + "devOptional": true, "hasInstallScript": true, "dependencies": { "@prisma/debug": "5.20.0", @@ -9808,12 +9811,14 @@ "version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284.tgz", "integrity": "sha512-Lg8AS5lpi0auZe2Mn4gjuCg081UZf88k3cn0RCwHgR+6cyHHpttPZBElJTHf83ZGsRNAmVCZCfUGA57WB4u4JA==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { "version": "5.20.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.20.0.tgz", "integrity": "sha512-JVcaPXC940wOGpCOwuqQRTz6I9SaBK0c1BAyC1pcz9xBi+dzFgUu3G/p9GV1FhFs9OKpfSpIhQfUJE9y00zhqw==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.20.0", @@ -9825,6 +9830,7 @@ "version": "5.20.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.20.0.tgz", "integrity": "sha512-8/+CehTZZNzJlvuryRgc77hZCWrUDYd/PmlZ7p2yNXtmf2Una4BWnTbak3us6WVdqoz5wmptk6IhsXdG2v5fmA==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.20.0" @@ -21410,6 +21416,21 @@ "node": ">=8.12.0" } }, + "node_modules/husky": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", + "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==", + "dev": true, + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", @@ -29436,6 +29457,7 @@ "version": "5.20.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.20.0.tgz", "integrity": "sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig==", + "devOptional": true, "hasInstallScript": true, "dependencies": { "@prisma/engines": "5.20.0" diff --git a/package.json b/package.json index 08cc5768b..d54351c7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.111.0", + "version": "2.114.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", @@ -33,10 +33,11 @@ "format:check": "nx format:check", "format:write": "nx format:write", "help": "nx help", - "lint": "nx lint", + "lint": "nx run-many --target=lint --all", "ng": "nx", "nx": "nx", "postinstall": "prisma generate", + "prepare": "husky", "prisma": "prisma", "replace-placeholders-in-build": "node ./replace.build.js", "start": "node dist/apps/api/main", @@ -131,7 +132,6 @@ "passport": "0.7.0", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.1", - "prisma": "5.20.0", "reflect-metadata": "0.1.13", "rxjs": "7.5.6", "stripe": "15.11.0", @@ -190,12 +190,14 @@ "eslint-plugin-cypress": "2.15.1", "eslint-plugin-import": "2.29.1", "eslint-plugin-storybook": "0.6.15", + "husky": "9.1.6", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-preset-angular": "14.1.0", "nx": "19.5.6", "prettier": "3.3.3", "prettier-plugin-organize-attributes": "1.0.0", + "prisma": "5.20.0", "react": "18.2.0", "react-dom": "18.2.0", "replace-in-file": "7.0.1", diff --git a/tsconfig.base.json b/tsconfig.base.json index b67024e12..e977a9596 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,7 +20,20 @@ "@ghostfolio/client/*": ["apps/client/src/app/*"], "@ghostfolio/common/*": ["libs/common/src/lib/*"], "@ghostfolio/ui/*": ["libs/ui/src/lib/*"] - } + }, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "strict": false, + "strictNullChecks": false, + "strictPropertyInitialization": false, + "noImplicitReturns": false, + "noImplicitAny": false, + "noImplicitThis": false, + "noImplicitOverride": false, + "noPropertyAccessFromIndexSignature": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "allowUnreachableCode": true }, "exclude": ["node_modules", "tmp"] }