Browse Source

Merge remote-tracking branch 'origin/main' into dockerpush

pull/5027/head
Dan 9 months ago
parent
commit
85484fb6a6
  1. 141
      .eslintrc.json
  2. 3
      .github/workflows/build-code.yml
  3. 6
      .husky/pre-commit
  4. 43
      CHANGELOG.md
  5. 1
      DEVELOPMENT.md
  6. 14
      README.md
  7. 7
      apps/api/src/app/account/account.controller.ts
  8. 9
      apps/api/src/app/account/account.service.ts
  9. 4
      apps/api/src/app/admin/admin.service.ts
  10. 2
      apps/api/src/app/admin/update-bulk-market-data.dto.ts
  11. 3
      apps/api/src/app/auth/google.strategy.ts
  12. 2
      apps/api/src/app/benchmark/benchmark.service.ts
  13. 7
      apps/api/src/app/endpoints/public/public.controller.ts
  14. 30
      apps/api/src/app/import/import.service.ts
  15. 2
      apps/api/src/app/info/info.module.ts
  16. 9
      apps/api/src/app/info/info.service.ts
  17. 16
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  18. 18
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  19. 17
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts
  20. 40
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts
  21. 88
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  22. 2
      apps/api/src/app/portfolio/current-rate.service.mock.ts
  23. 2
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  24. 77
      apps/api/src/app/portfolio/portfolio.controller.ts
  25. 270
      apps/api/src/app/portfolio/portfolio.service.ts
  26. 4
      apps/api/src/app/redis-cache/redis-cache.service.mock.ts
  27. 1
      apps/api/src/app/user/update-user-setting.dto.ts
  28. 7
      apps/api/src/app/user/user.service.ts
  29. 19
      apps/api/src/models/rule.ts
  30. 19
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  31. 17
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  32. 2
      apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts
  33. 2
      apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
  34. 2
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  35. 2
      apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts
  36. 1
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  37. 14
      apps/api/src/services/data-provider/data-provider.service.ts
  38. 6
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  39. 2
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  40. 2
      apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
  41. 10
      apps/api/src/services/data-provider/manual/manual.service.ts
  42. 2
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  43. 2
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  44. 7
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts
  45. 12
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  46. 2
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts
  47. 35
      apps/api/src/services/tag/tag.service.ts
  48. 5
      apps/api/src/validators/is-currency-code.ts
  49. 20
      apps/client-e2e/.eslintrc.json
  50. 2
      apps/client/src/app/adapter/custom-date-adapter.ts
  51. 2
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  52. 2
      apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
  53. 19
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  54. 2
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  55. 6
      apps/client/src/app/components/home-holdings/home-holdings.html
  56. 1
      apps/client/src/app/components/home-overview/home-overview.component.ts
  57. 2
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts
  58. 2
      apps/client/src/app/core/notification/notification.service.ts
  59. 2
      apps/client/src/app/pages/faq/faq-page.component.ts
  60. 2
      apps/client/src/app/pages/landing/landing-page.html
  61. 12
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  62. 1
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  63. 8
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  64. 99
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  65. 10
      apps/client/src/app/pages/portfolio/allocations/allocations-page.html
  66. 46
      apps/client/src/app/pages/public/public-page.component.ts
  67. 10
      apps/client/src/app/pages/public/public-page.html
  68. 2
      apps/client/src/app/services/data.service.ts
  69. 22
      apps/client/src/locales/messages.ca.xlf
  70. 22
      apps/client/src/locales/messages.de.xlf
  71. 22
      apps/client/src/locales/messages.es.xlf
  72. 22
      apps/client/src/locales/messages.fr.xlf
  73. 22
      apps/client/src/locales/messages.it.xlf
  74. 22
      apps/client/src/locales/messages.nl.xlf
  75. 22
      apps/client/src/locales/messages.pl.xlf
  76. 22
      apps/client/src/locales/messages.pt.xlf
  77. 22
      apps/client/src/locales/messages.tr.xlf
  78. 21
      apps/client/src/locales/messages.xlf
  79. 22
      apps/client/src/locales/messages.zh.xlf
  80. 2
      apps/client/tsconfig.json
  81. 3
      apps/ui-e2e/.eslintrc.json
  82. 26
      git-hooks/pre-commit
  83. 2
      libs/common/src/lib/helper.ts
  84. 3
      libs/common/src/lib/interfaces/info-item.interface.ts
  85. 15
      libs/common/src/lib/interfaces/portfolio-details.interface.ts
  86. 9
      libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts
  87. 2
      libs/common/src/lib/interfaces/user.interface.ts
  88. 7
      libs/ui/.eslintrc.json
  89. 28
      libs/ui/src/lib/activities-table/activities-table.component.ts
  90. 28
      libs/ui/src/lib/assistant/assistant.component.ts
  91. 4
      libs/ui/src/lib/carousel/carousel-item.directive.ts
  92. 2
      libs/ui/src/lib/fire-calculator/fire-calculator.service.ts
  93. 19
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  94. 2
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  95. 1
      libs/ui/src/lib/shared/abstract-mat-form-field.ts
  96. 6
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html
  97. 6
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts
  98. 7
      libs/ui/src/lib/top-holdings/top-holdings.component.ts
  99. 48
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
  100. 6
      libs/ui/tsconfig.json

141
.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"]
}

3
.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

6
.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

43
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

1
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`)

14
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/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`.
### 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

7
apps/api/src/app/account/account.controller.ts

@ -74,15 +74,12 @@ export class AccountController {
);
}
return this.accountService.deleteAccount(
{
return this.accountService.deleteAccount({
id_userId: {
id,
userId: this.request.user.id
}
},
this.request.user.id
);
});
}
@Get()

9
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<Account> {
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;
});

4
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'],

2
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';

3
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({

2
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)

7
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: {

30
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,7 +488,8 @@ export class ImportService {
userCurrency: string;
userId: string;
}): Promise<Partial<Activity>[]> {
let { activities: existingActivities } = await this.orderService.getOrders({
const { activities: existingActivities } =
await this.orderService.getOrders({
userCurrency,
userId,
includeDrafts: true,

2
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
],

9
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()
};

16
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: {

18
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);

17
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: [],

40
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);
});

88
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)

2
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 };
}

2
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({})
};
})
};

77
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,8 +102,15 @@ export class PortfolioController {
filterByTags
});
const { accounts, hasErrors, holdings, platforms, summary } =
await this.portfolioService.getDetails({
const {
accounts,
hasErrors,
holdings,
markets,
marketsAdvanced,
platforms,
summary
} = await this.portfolioService.getDetails({
dateRange,
filters,
impersonationId,
@ -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
};
}

270
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,47 +1274,20 @@ export class PortfolioService {
@LogPerformance
public async getReport(impersonationId: string): Promise<PortfolioReport> {
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 = <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 = <UserSettings>this.request.user.Settings.settings;
return {
rules: {
accountClusterRisk: isEmpty(activities)
? undefined
: await this.rulesService.evaluate(
accountClusterRisk:
summary.ordersCount > 0
? await this.rulesService.evaluate(
[
new AccountClusterRiskCurrentInvestment(
this.exchangeRateDataService,
@ -1317,22 +1299,24 @@ export class PortfolioService {
)
],
userSettings
),
currencyClusterRisk: isEmpty(activities)
? undefined
: await this.rulesService.evaluate(
)
: undefined,
currencyClusterRisk:
summary.ordersCount > 0
? await this.rulesService.evaluate(
[
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
this.exchangeRateDataService,
positions
Object.values(holdings)
),
new CurrencyClusterRiskCurrentInvestment(
this.exchangeRateDataService,
positions
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
}
])[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
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
},
where: {
id
emergingMarkets: {
id: 'emergingMarkets',
valueInBaseCurrency: 0,
valueInPercentage: 0
},
otherMarkets: {
id: 'otherMarkets',
valueInBaseCurrency: 0,
valueInPercentage: 0
}
};
})
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,15 +1740,22 @@ export class PortfolioService {
.toNumber();
}
}
} else {
markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY])
.plus(valueInBaseCurrency)
}
markets[UNKNOWN_KEY] = new Big(1)
.minus(markets.developedMarkets)
.minus(markets.emergingMarkets)
.minus(markets.otherMarkets)
.toNumber();
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY])
.plus(valueInBaseCurrency)
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 ??

4
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<string, string>(),
get: (key: string): Promise<string> => {
@ -20,7 +18,7 @@ export const RedisCacheServiceMock = {
return `portfolio-snapshot-${userId}${filtersHash > 0 ? `-${filtersHash}` : ''}`;
},
set: (key: string, value: string, ttl?: Milliseconds): Promise<string> => {
set: (key: string, value: string): Promise<string> => {
RedisCacheServiceMock.cache.set(key, value);
return Promise.resolve(value);

1
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,

7
apps/api/src/app/user/user.service.ts

@ -56,7 +56,7 @@ export class UserService {
{ Account, id, permissions, Settings, subscription }: UserWithSettings,
aLocale = locale
): Promise<IUser> {
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;

19
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<T extends RuleSettings> implements RuleInterface<T> {
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
),

19
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<Settings> {
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<Setti
}
});
const baseCurrencyItem = positionsGroupedByCurrency.find((item) => {
const baseCurrencyItem = holdingsGroupedByCurrency.find((item) => {
return item.groupKey === ruleSettings.baseCurrency;
});

17
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<Settings> {
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;

2
apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts

@ -18,7 +18,7 @@ export class EmergencyFundSetup extends Rule<Settings> {
this.emergencyFund = emergencyFund;
}
public evaluate(ruleSettings: Settings) {
public evaluate() {
if (!this.emergencyFund) {
return {
evaluation: 'No emergency fund has been set up',

2
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');
}

2
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;
}

2
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();

1
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
}: {

14
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,12 +691,14 @@ export class DataProviderService {
} = {};
for (const date in rootData) {
if (isNumber(rootData[date].marketPrice)) {
data[date] = {
marketPrice: rootData[date].marketPrice
? new Big(factor).mul(rootData[date].marketPrice).toNumber()
: null
marketPrice: new Big(factor)
.mul(rootData[date].marketPrice)
.toNumber()
};
}
}
return data;
}

6
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<any>();
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;

2
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;
}

2
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;
}

10
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: {

2
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');
}

2
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;
}

7
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<any> => {
getExchangeRatesByCurrency: ({ targetCurrency }): Promise<any> => {
if (targetCurrency === 'CHF') {
return Promise.resolve({
CHFCHF: {

12
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;
} = {};

2
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;

35
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: null
userId
}
});
}
public async getInUseByUser(userId: string) {
return this.prismaService.tag.findMany({
}
}
},
orderBy: {
name: 'asc'
},
where: {
OR: [
{
orders: {
some: {
userId
}
}
},
{
symbolProfile: {
some: {}
}
userId: null
}
]
}
});
return tags.map(({ _count, id, name, userId }) => ({
id,
name,
userId,
isUsed: _count.orders > 0
}));
}
}

5
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';
}

20
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"
}
}
]
}

2
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));
}

2
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;
});

2
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 {

19
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: <string[]>[]
});
@ -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;
});

2
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 }

6
apps/client/src/app/components/home-holdings/home-holdings.html

@ -38,9 +38,12 @@
<gf-treemap-chart
class="mt-3"
cursor="pointer"
[baseCurrency]="user?.settings?.baseCurrency"
[colorScheme]="user?.settings?.colorScheme"
[dateRange]="user?.settings?.dateRange"
[holdings]="holdings"
(treemapChartClicked)="onSymbolClicked($event)"
[locale]="user?.settings?.locale"
(treemapChartClicked)="onHoldingClicked($event)"
/>
}
<div [ngClass]="{ 'd-none': viewModeFormControl.value !== 'TABLE' }">
@ -50,6 +53,7 @@
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
[holdings]="holdings"
[locale]="user?.settings?.locale"
(holdingClicked)="onHoldingClicked($event)"
/>
@if (hasPermissionToCreateOrder && holdings?.length > 0) {
<div class="text-center">

1
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';

2
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<GfRuleSettingsDialogComponent>
) {
console.log(this.data.rule);
this.settings = this.data.rule.settings;
}
}

2
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();
}

2
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';

2
apps/client/src/app/pages/landing/landing-page.html

@ -331,7 +331,7 @@
<div class="col-md-8 offset-md-2">
<gf-carousel [aria-label]="'Testimonials'">
@for (testimonial of testimonials; track testimonial) {
<div #carouselItem gf-carousel-item>
<div #carouselItem gfCarouselItem>
<div class="d-flex px-4">
<gf-logo
class="mr-3 mt-2 pt-1"

12
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -7,7 +7,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { downloadAsFile } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces';
import { AssetProfileIdentifier, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
@ -138,6 +138,16 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
this.fetchActivities();
}
public onClickActivity({ dataSource, symbol }: AssetProfileIdentifier) {
this.router.navigate([], {
queryParams: {
dataSource,
symbol,
holdingDetailDialog: true
}
});
}
public onCloneActivity(aActivity: Activity) {
this.openCreateActivityDialog(aActivity);
}

1
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -21,6 +21,7 @@
[sortDirection]="sortDirection"
[totalItems]="totalItems"
(activitiesDeleted)="onDeleteActivities()"
(activityClicked)="onClickActivity($event)"
(activityDeleted)="onDeleteActivity($event)"
(activityToClone)="onCloneActivity($event)"
(activityToUpdate)="onUpdateActivity($event)"

8
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -76,17 +76,19 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.locale = this.data.user?.settings?.locale;
this.dateAdapter.setLocale(this.locale);
const { currencies, platforms, tags } = this.dataService.fetchInfo();
const { currencies, platforms } = this.dataService.fetchInfo();
this.currencies = currencies;
this.defaultDateFormat = getDateFormatString(this.locale);
this.platforms = platforms;
this.tagsAvailable = tags.map((tag) => {
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]);

99
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) {

10
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</gf-value
>
</div>
@ -227,7 +227,7 @@
i18n
size="large"
[isPercent]="true"
[value]="markets?.emergingMarkets?.value"
[value]="markets?.emergingMarkets?.valueInPercentage"
>Emerging Markets</gf-value
>
</div>
@ -236,17 +236,17 @@
i18n
size="large"
[isPercent]="true"
[value]="markets?.otherMarkets?.value"
[value]="markets?.otherMarkets?.valueInPercentage"
>Other Markets</gf-value
>
</div>
@if (markets?.[UNKNOWN_KEY]?.value > 0) {
@if (markets?.[UNKNOWN_KEY]?.valueInPercentage > 0) {
<div class="col-xs-12 col-md my-2">
<gf-value
i18n
size="large"
[isPercent]="true"
[value]="markets?.[UNKNOWN_KEY]?.value"
[value]="markets?.[UNKNOWN_KEY]?.valueInPercentage"
>No data available</gf-value
>
</div>

46
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<PortfolioPosition, 'currency' | 'name'> & {
@ -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() {

10
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</gf-value
>
</div>
@ -165,7 +165,7 @@
i18n
size="large"
[isPercent]="true"
[value]="markets?.emergingMarkets?.value"
[value]="markets?.emergingMarkets?.valueInPercentage"
>Emerging Markets</gf-value
>
</div>
@ -174,17 +174,17 @@
i18n
size="large"
[isPercent]="true"
[value]="markets?.otherMarkets?.value"
[value]="markets?.otherMarkets?.valueInPercentage"
>Other Markets</gf-value
>
</div>
@if (markets?.[UNKNOWN_KEY]?.value > 0) {
@if (markets?.[UNKNOWN_KEY]?.valueInPercentage > 0) {
<div class="col-xs-12 col-md my-2">
<gf-value
i18n
size="large"
[isPercent]="true"
[value]="markets?.[UNKNOWN_KEY]?.value"
[value]="markets?.[UNKNOWN_KEY]?.valueInPercentage"
>No data available</gf-value
>
</div>

2
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<any>(`/api/v1/order`, { params });
}

22
apps/client/src/locales/messages.ca.xlf

@ -1471,7 +1471,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -2635,7 +2635,7 @@
<target state="translated">Gestionar Activitats</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -5979,7 +5979,7 @@
<target state="new">Do you really want to delete these activities?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="670983159637074283" datatype="html">
@ -5987,7 +5987,7 @@
<target state="new">Do you really want to delete this activity?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="4149127798893455354" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context>
</context-group>
</trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

22
apps/client/src/locales/messages.de.xlf

@ -554,7 +554,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -1058,7 +1058,7 @@
<target state="translated">Aktivitäten verwalten</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html">
@ -2550,7 +2550,7 @@
<target state="translated">Möchtest du diese Aktivität wirklich löschen?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3914,7 +3914,7 @@
<target state="translated">Möchtest du diese Aktivitäten wirklich löschen?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="translated">Schliessen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context>
</context-group>
</trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="translated">Ups! Es konnten leider keine Assets gefunden werden.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

22
apps/client/src/locales/messages.es.xlf

@ -555,7 +555,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -1059,7 +1059,7 @@
<target state="translated">Gestión de las operaciones</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html">
@ -2551,7 +2551,7 @@
<target state="translated">¿Estás seguro de eliminar esta operación?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3915,7 +3915,7 @@
<target state="new">Do you really want to delete these activities?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7056,7 +7056,7 @@
<target state="new">Threshold Min</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7064,7 +7064,7 @@
<target state="new">Threshold Max</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7072,7 +7072,7 @@
<target state="new">Close</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7259,6 +7259,14 @@
<context context-type="linenumber">280</context>
</context-group>
</trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

22
apps/client/src/locales/messages.fr.xlf

@ -614,7 +614,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -1378,7 +1378,7 @@
<target state="translated">Gérer les Activités</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -3070,7 +3070,7 @@
<target state="translated">Voulez-vous vraiment supprimer cette activité ?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3914,7 +3914,7 @@
<target state="translated">Voulez-vous vraiment supprimer toutes vos activités ?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context>
</context-group>
</trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

22
apps/client/src/locales/messages.it.xlf

@ -555,7 +555,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -1059,7 +1059,7 @@
<target state="translated">Gestione delle attività</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html">
@ -2551,7 +2551,7 @@
<target state="translated">Vuoi davvero eliminare questa attività?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3915,7 +3915,7 @@
<target state="translated">Vuoi davvero eliminare tutte le tue attività?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7056,7 +7056,7 @@
<target state="new">Threshold Min</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7064,7 +7064,7 @@
<target state="new">Threshold Max</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7072,7 +7072,7 @@
<target state="new">Close</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7259,6 +7259,14 @@
<context context-type="linenumber">280</context>
</context-group>
</trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

22
apps/client/src/locales/messages.nl.xlf

@ -554,7 +554,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -1058,7 +1058,7 @@
<target state="translated">Activiteiten beheren</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html">
@ -2550,7 +2550,7 @@
<target state="translated">Wil je deze activiteit echt verwijderen?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3914,7 +3914,7 @@
<target state="new">Wil je echt al je activiteiten verwijderen?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context>
</context-group>
</trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

22
apps/client/src/locales/messages.pl.xlf

@ -1363,7 +1363,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -2283,7 +2283,7 @@
<target state="translated">Zarządzaj Aktywnościami</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -4151,7 +4151,7 @@
<target state="new">Do you really want to delete these activities?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html">
@ -5503,7 +5503,7 @@
<target state="translated">Czy na pewno chcesz usunąć tę działalność?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="4149127798893455354" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context>
</context-group>
</trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

22
apps/client/src/locales/messages.pt.xlf

@ -614,7 +614,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -1242,7 +1242,7 @@
<target state="translated">Gerir Atividades</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -2942,7 +2942,7 @@
<target state="translated">Deseja realmente eliminar esta atividade?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3914,7 +3914,7 @@
<target state="translated">Deseja mesmo eliminar estas atividades?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context>
</context-group>
</trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

22
apps/client/src/locales/messages.tr.xlf

@ -1327,7 +1327,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -2135,7 +2135,7 @@
<target state="translated">İşlemleri Yönet</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -3663,7 +3663,7 @@
<target state="new">Tüm işlemlerinizi silmeyi gerçekten istiyor musunuz?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html">
@ -5207,7 +5207,7 @@
<target state="translated">TBu işlemi silmeyi gerçekten istiyor musunuz?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context>
</context-group>
</trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

21
apps/client/src/locales/messages.xlf

@ -1312,7 +1312,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -2151,7 +2151,7 @@
<source>Manage Activities</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -3825,7 +3825,7 @@
<source>Do you really want to delete these activities?</source>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html">
@ -5064,7 +5064,7 @@
<source>Do you really want to delete this activity?</source>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="4149127798893455354" datatype="html">
@ -6359,7 +6359,7 @@
<source>Threshold Max</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -6380,7 +6380,7 @@
<source>Threshold Min</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="65ff514a2e167229e1a34b3712f2cf2908576d0f" datatype="html">
@ -6408,7 +6408,7 @@
<source>Close</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="06296af0cdaf7bed02043379359ed1975fc22077" datatype="html">
@ -6565,6 +6565,13 @@
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

22
apps/client/src/locales/messages.zh.xlf

@ -1372,7 +1372,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -2300,7 +2300,7 @@
<target state="translated">管理活动</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="5486880308148746399" datatype="html">
@ -4168,7 +4168,7 @@
<target state="new">您真的要删除所有活动吗?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">223</context>
<context context-type="linenumber">219</context>
</context-group>
</trans-unit>
<trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html">
@ -5552,7 +5552,7 @@
<target state="translated">您确实要删除此活动吗?</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="4149127798893455354" datatype="html">
@ -7056,7 +7056,7 @@
<target state="new">Threshold Min</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7064,7 +7064,7 @@
<target state="new">Threshold Max</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7072,7 +7072,7 @@
<target state="new">Close</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html</context>
<context context-type="linenumber">15</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7259,6 +7259,14 @@
<context context-type="linenumber">280</context>
</context-group>
</trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

2
apps/client/tsconfig.json

@ -15,6 +15,8 @@
],
"angularCompilerOptions": {
"strictInjectionParameters": true,
// TODO: Enable stricter rules for this project
"strictInputAccessModifiers": false,
"strictTemplates": false
},
"compilerOptions": {

3
apps/ui-e2e/.eslintrc.json

@ -4,6 +4,9 @@
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["apps/ui-e2e/tsconfig.json"]
},
"rules": {}
},
{

26
git-hooks/pre-commit

@ -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..."

2
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 {

3
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[];
}

15
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;

9
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 {

2
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 })[];
}

7
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"

28
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<void>();
@Output() activityClicked = new EventEmitter<AssetProfileIdentifier>();
@Output() activityDeleted = new EventEmitter<string>();
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
@ -122,10 +121,7 @@ export class GfActivitiesTableComponent
private unsubscribeSubject = new Subject<void>();
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);
}

28
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[] }) {

4
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<HTMLElement>) {}
}

2
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) {

19
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<AssetProfileIdentifier>();
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
@ -75,9 +76,7 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
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 });
}
}

2
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;
});

1
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<T>
implements ControlValueAccessor, DoCheck, MatFormFieldControl<T>, OnDestroy
{

6
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html

@ -31,6 +31,12 @@
}
</small>
</mat-option>
} @empty {
@if (control.value?.length > 1) {
<mat-option class="line-height-1" disabled="true" i18n
>Oops! Could not find any assets.</mat-option
>
}
}
}
</mat-autocomplete>

6
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,

7
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<void>();
public constructor() {}
public ngOnInit() {}
public ngOnChanges() {
this.isLoading = true;

48
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<AssetProfileIdentifier>();
@ -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 = <unknown>(
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 = <number>context.raw._data.valueInBaseCurrency;
return `${value.toLocaleString(this.locale, {
maximumFractionDigits: 2,
minimumFractionDigits: 2
})} ${this.baseCurrency}`;
} else {
const percentage =
<number>context.raw._data.allocationInPercentage * 100;
return `${percentage.toFixed(2)}%`;
}
},
title: () => {
return '';
}
},
xAlign: 'center',
yAlign: 'center'
};
}
}

6
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,

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save