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"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": { "rules": {
"@nx/enforce-module-boundaries": [ "@nx/enforce-module-boundaries": [
"error", "warn",
{ {
"enforceBuildableLibDependency": true, "enforceBuildableLibDependency": true,
"allow": [], "allow": [],
@ -18,30 +18,27 @@
} }
] ]
} }
] ],
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
} }
}, },
{ {
"files": ["*.ts", "*.tsx"], "files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"], "extends": ["plugin:@nx/typescript"]
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
}, },
{ {
"files": ["*.js", "*.jsx"], "files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"], "extends": ["plugin:@nx/javascript"]
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
}, },
{ {
"files": ["*.ts"], "files": ["*.ts"],
"plugins": ["eslint-plugin-import", "@typescript-eslint"], "plugins": ["eslint-plugin-import", "@typescript-eslint"],
"extends": [
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked"
],
"rules": { "rules": {
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/dot-notation": "off", "@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": [ "@typescript-eslint/explicit-member-accessibility": [
"off", "off",
@ -49,76 +46,112 @@
"accessibility": "explicit" "accessibility": "explicit"
} }
], ],
"@typescript-eslint/member-ordering": "error", "@typescript-eslint/member-ordering": "warn",
"@typescript-eslint/naming-convention": "off", "@typescript-eslint/naming-convention": [
"@typescript-eslint/no-empty-function": "off", "off",
"@typescript-eslint/no-empty-interface": "error", {
"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": [ "@typescript-eslint/no-inferrable-types": [
"error", "warn",
{ {
"ignoreParameters": true "ignoreParameters": true
} }
], ],
"@typescript-eslint/no-misused-new": "error", "@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-shadow": [ "@typescript-eslint/no-shadow": [
"error", "warn",
{ {
"hoist": "all" "hoist": "all"
} }
], ],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/unified-signatures": "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", "arrow-body-style": "off",
"constructor-super": "error", "constructor-super": "error",
"eqeqeq": ["error", "smart"], "eqeqeq": ["error", "smart"],
"guard-for-in": "error", "guard-for-in": "warn",
"id-blacklist": "off", "id-blacklist": "off",
"id-match": "off", "id-match": "off",
"import/no-deprecated": "warn", "import/no-deprecated": "warn",
"no-bitwise": "error", "no-bitwise": "error",
"no-caller": "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-debugger": "error",
"no-empty": "off", "no-empty": "off",
"no-eval": "error", "no-eval": "error",
"no-fallthrough": "error", "no-fallthrough": "error",
"no-new-wrappers": "error", "no-new-wrappers": "error",
"no-restricted-imports": ["error", "rxjs/Rx"], "no-restricted-imports": ["error", "rxjs/Rx"],
"no-throw-literal": "error",
"no-undef-init": "error", "no-undef-init": "error",
"no-underscore-dangle": "off", "no-underscore-dangle": "off",
"no-var": "error", "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 - name: Install dependencies
run: npm ci run: npm ci
- name: Check code style
run: npm run lint
- name: Check formatting - name: Check formatting
run: npm run format:check 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## 2.114.0 - 2024-10-10
### Added ### 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) - Added support to customize the rule thresholds in the _X-ray_ section (experimental)
### Changed ### Changed
- Optimized the portfolio calculations with smarter date interval selection
- Improved the language localization for German (`de`) - 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 ## 2.111.0 - 2024-09-28
### Added ### Added

1
DEVELOPMENT.md

@ -14,7 +14,6 @@
1. Run `npm install` 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 `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 `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. Start the [server](#start-server) and the [client](#start-client)
1. Open https://localhost:4200/en in your browser 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`) 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>`. 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 ### Import Activities
#### Prerequisites #### 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: {
id, id,
userId: this.request.user.id userId: this.request.user.id
} }
}, });
this.request.user.id
);
} }
@Get() @Get()

9
apps/api/src/app/account/account.service.ts

@ -109,8 +109,7 @@ export class AccountService {
} }
public async deleteAccount( public async deleteAccount(
where: Prisma.AccountWhereUniqueInput, where: Prisma.AccountWhereUniqueInput
aUserId: string
): Promise<Account> { ): Promise<Account> {
const account = await this.prismaService.account.delete({ const account = await this.prismaService.account.delete({
where where
@ -172,11 +171,7 @@ export class AccountService {
where.isExcluded = false; where.isExcluded = false;
} }
const { const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => {
ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass,
TAG: filtersByTag
} = groupBy(filters, ({ type }) => {
return type; return type;
}); });

4
apps/api/src/app/admin/admin.service.ts

@ -237,7 +237,7 @@ export class AdminService {
const extendedPrismaClient = this.getExtendedPrismaClient(); const extendedPrismaClient = this.getExtendedPrismaClient();
try { try {
let [assetProfiles, count] = await Promise.all([ const symbolProfileResult = await Promise.all([
extendedPrismaClient.symbolProfile.findMany({ extendedPrismaClient.symbolProfile.findMany({
orderBy, orderBy,
skip, skip,
@ -269,6 +269,8 @@ export class AdminService {
}), }),
this.prismaService.symbolProfile.count({ where }) this.prismaService.symbolProfile.count({ where })
]); ]);
const assetProfiles = symbolProfileResult[0];
let count = symbolProfileResult[1];
const lastMarketPrices = await this.prismaService.marketData.findMany({ const lastMarketPrices = await this.prismaService.marketData.findMany({
distinct: ['dataSource', 'symbol'], 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 { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, isNotEmptyObject } from 'class-validator'; import { ArrayNotEmpty, IsArray } from 'class-validator';
import { UpdateMarketDataDto } from './update-market-data.dto'; 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, token: string,
refreshToken: string, refreshToken: string,
profile: Profile, profile: Profile,
done: Function, done: Function
done2: Function
) { ) {
try { try {
const jwt = await this.authService.validateOAuthLogin({ const jwt = await this.authService.validateOAuthLogin({

2
apps/api/src/app/benchmark/benchmark.service.ts

@ -234,7 +234,7 @@ export class BenchmarkService {
return { marketData }; return { marketData };
} }
for (let marketDataItem of marketDataItems) { for (const marketDataItem of marketDataItems) {
const exchangeRate = const exchangeRate =
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
format(marketDataItem.date, DATE_FORMAT) format(marketDataItem.date, DATE_FORMAT)

7
apps/api/src/app/endpoints/public/public.controller.ts

@ -57,7 +57,7 @@ export class PublicController {
} }
const [ const [
{ holdings }, { holdings, markets },
{ performance: performance1d }, { performance: performance1d },
{ performance: performanceMax }, { performance: performanceMax },
{ performance: performanceYtd } { performance: performanceYtd }
@ -76,8 +76,13 @@ export class PublicController {
}) })
]); ]);
Object.values(markets ?? {}).forEach((market) => {
delete market.valueInBaseCurrency;
});
const publicPortfolioResponse: PublicPortfolioResponse = { const publicPortfolioResponse: PublicPortfolioResponse = {
hasDetails, hasDetails,
markets,
alias: access.alias, alias: access.alias,
holdings: {}, holdings: {},
performance: { performance: {

30
apps/api/src/app/import/import.service.ts

@ -266,21 +266,18 @@ export class ImportService {
const activities: Activity[] = []; const activities: Activity[] = [];
for (let [ for (const [index, activity] of activitiesExtendedWithErrors.entries()) {
index, const accountId = activity.accountId;
{ const comment = activity.comment;
accountId, const currency = activity.currency;
comment, const date = activity.date;
currency, const error = activity.error;
date, let fee = activity.fee;
error, const quantity = activity.quantity;
fee, const SymbolProfile = activity.SymbolProfile;
quantity, const type = activity.type;
SymbolProfile, let unitPrice = activity.unitPrice;
type,
unitPrice
}
] of activitiesExtendedWithErrors.entries()) {
const assetProfile = assetProfiles[ const assetProfile = assetProfiles[
getAssetProfileIdentifier({ getAssetProfileIdentifier({
dataSource: SymbolProfile.dataSource, dataSource: SymbolProfile.dataSource,
@ -491,7 +488,8 @@ export class ImportService {
userCurrency: string; userCurrency: string;
userId: string; userId: string;
}): Promise<Partial<Activity>[]> { }): Promise<Partial<Activity>[]> {
let { activities: existingActivities } = await this.orderService.getOrders({ const { activities: existingActivities } =
await this.orderService.getOrders({
userCurrency, userCurrency,
userId, userId,
includeDrafts: true, 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 { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.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 { 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 { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
@ -33,7 +32,6 @@ import { InfoService } from './info.service';
PropertyModule, PropertyModule,
RedisCacheModule, RedisCacheModule,
SymbolProfileModule, SymbolProfileModule,
TagModule,
TransformDataSourceInResponseModule, TransformDataSourceInResponseModule,
UserModule 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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
PROPERTY_BETTER_UPTIME_MONITOR_ID, PROPERTY_BETTER_UPTIME_MONITOR_ID,
@ -47,7 +46,6 @@ export class InfoService {
private readonly platformService: PlatformService, private readonly platformService: PlatformService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
private readonly redisCacheService: RedisCacheService, private readonly redisCacheService: RedisCacheService,
private readonly tagService: TagService,
private readonly userService: UserService private readonly userService: UserService
) {} ) {}
@ -103,8 +101,7 @@ export class InfoService {
isUserSignupEnabled, isUserSignupEnabled,
platforms, platforms,
statistics, statistics,
subscriptions, subscriptions
tags
] = await Promise.all([ ] = await Promise.all([
this.benchmarkService.getBenchmarkAssetProfiles(), this.benchmarkService.getBenchmarkAssetProfiles(),
this.getDemoAuthToken(), this.getDemoAuthToken(),
@ -113,8 +110,7 @@ export class InfoService {
orderBy: { name: 'asc' } orderBy: { name: 'asc' }
}), }),
this.getStatistics(), this.getStatistics(),
this.getSubscriptions(), this.getSubscriptions()
this.tagService.getPublic()
]); ]);
if (isUserSignupEnabled) { if (isUserSignupEnabled) {
@ -130,7 +126,6 @@ export class InfoService {
platforms, platforms,
statistics, statistics,
subscriptions, subscriptions,
tags,
baseCurrency: DEFAULT_CURRENCY, baseCurrency: DEFAULT_CURRENCY,
currencies: this.exchangeRateDataService.getCurrencies() currencies: this.exchangeRateDataService.getCurrencies()
}; };

16
apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts

@ -3,24 +3,14 @@ import {
AssetProfileIdentifier, AssetProfileIdentifier,
SymbolMetrics SymbolMetrics
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; import { PortfolioSnapshot } from '@ghostfolio/common/models';
export class MWRPortfolioCalculator extends PortfolioCalculator { export class MWRPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance( protected calculateOverallPerformance(): PortfolioSnapshot {
positions: TimelinePosition[]
): PortfolioSnapshot {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
protected getSymbolMetrics({ protected getSymbolMetrics({}: {
dataSource,
end,
exchangeRates,
marketSymbolMap,
start,
step = 1,
symbol
}: {
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { 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({ await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: uniq(Object.values(currencies)), currencies: uniq(Object.values(currencies)),
endDate: endOfDay(this.endDate), endDate: endOfDay(this.endDate),
@ -262,7 +262,7 @@ export abstract class PortfolioCalculator {
const daysInMarket = differenceInDays(this.endDate, this.startDate); const daysInMarket = differenceInDays(this.endDate, this.startDate);
let chartDateMap = this.getChartDateMap({ const chartDateMap = this.getChartDateMap({
endDate: this.endDate, endDate: this.endDate,
startDate: this.startDate, startDate: this.startDate,
step: Math.round( step: Math.round(
@ -710,9 +710,9 @@ export abstract class PortfolioCalculator {
let netPerformanceAtStartDate: number; let netPerformanceAtStartDate: number;
let netPerformanceWithCurrencyEffectAtStartDate: 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)); const date = resetHours(parseDate(historicalDataItem.date));
if (!isBefore(date, start) && !isAfter(date, end)) { if (!isBefore(date, start) && !isAfter(date, end)) {
@ -843,13 +843,13 @@ export abstract class PortfolioCalculator {
}): { [date: string]: true } { }): { [date: string]: true } {
// Create a map of all relevant chart dates: // Create a map of all relevant chart dates:
// 1. Add transaction point dates // 1. Add transaction point dates
let chartDateMap = this.transactionPoints.reduce((result, { date }) => { const chartDateMap = this.transactionPoints.reduce((result, { date }) => {
result[date] = true; result[date] = true;
return result; return result;
}, {}); }, {});
// 2. Add dates between transactions respecting the specified step size // 2. Add dates between transactions respecting the specified step size
for (let date of eachDayOfInterval( for (const date of eachDayOfInterval(
{ end: endDate, start: startDate }, { end: endDate, start: startDate },
{ step } { step }
)) { )) {
@ -858,7 +858,7 @@ export abstract class PortfolioCalculator {
if (step > 1) { if (step > 1) {
// Reduce the step size of last 90 days // Reduce the step size of last 90 days
for (let date of eachDayOfInterval( for (const date of eachDayOfInterval(
{ end: endDate, start: subDays(endDate, 90) }, { end: endDate, start: subDays(endDate, 90) },
{ step: 3 } { step: 3 }
)) { )) {
@ -866,7 +866,7 @@ export abstract class PortfolioCalculator {
} }
// Reduce the step size of last 30 days // Reduce the step size of last 30 days
for (let date of eachDayOfInterval( for (const date of eachDayOfInterval(
{ end: endDate, start: subDays(endDate, 30) }, { end: endDate, start: subDays(endDate, 30) },
{ step: 1 } { step: 1 }
)) { )) {
@ -878,7 +878,7 @@ export abstract class PortfolioCalculator {
chartDateMap[format(endDate, DATE_FORMAT)] = true; chartDateMap[format(endDate, DATE_FORMAT)] = true;
// Make sure some key dates are present // 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 } = const { endDate: dateRangeEnd, startDate: dateRangeStart } =
getIntervalFromDateRange(dateRange); 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'), dividendInBaseCurrency: new Big('0.62'),
fee: new Big('19'), fee: new Big('19'),
firstBuyDate: '2021-09-16', 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'), investment: new Big('298.58'),
investmentWithCurrencyEffect: new Big('298.58'), investmentWithCurrencyEffect: new Big('298.58'),
marketPrice: 331.83, marketPrice: 331.83,
marketPriceInBaseCurrency: 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'), quantity: new Big('1'),
symbol: 'MSFT', symbol: 'MSFT',
tags: [], 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', () => { 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); 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 { Logger } from '@nestjs/common';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns';
addDays,
addMilliseconds,
differenceInDays,
eachDayOfInterval,
format,
isBefore
} from 'date-fns';
import { cloneDeep, first, last, sortBy } from 'lodash'; import { cloneDeep, first, last, sortBy } from 'lodash';
export class TWRPortfolioCalculator extends PortfolioCalculator { export class TWRPortfolioCalculator extends PortfolioCalculator {
private chartDates: string[];
protected calculateOverallPerformance( protected calculateOverallPerformance(
positions: TimelinePosition[] positions: TimelinePosition[]
): PortfolioSnapshot { ): PortfolioSnapshot {
@ -32,7 +27,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
let hasErrors = false; let hasErrors = false;
let netPerformance = new Big(0); let netPerformance = new Big(0);
let totalFeesWithCurrencyEffect = new Big(0); let totalFeesWithCurrencyEffect = new Big(0);
let totalInterestWithCurrencyEffect = new Big(0); const totalInterestWithCurrencyEffect = new Big(0);
let totalInvestment = new Big(0); let totalInvestment = new Big(0);
let totalInvestmentWithCurrencyEffect = new Big(0); let totalInvestmentWithCurrencyEffect = new Big(0);
let totalTimeWeightedInvestment = new Big(0); let totalTimeWeightedInvestment = new Big(0);
@ -163,7 +158,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
[date: string]: Big; [date: string]: Big;
} = {}; } = {};
let totalAccountBalanceInBaseCurrency = new Big(0); const totalAccountBalanceInBaseCurrency = new Big(0);
let totalDividend = new Big(0); let totalDividend = new Big(0);
let totalDividendInBaseCurrency = new Big(0); let totalDividendInBaseCurrency = new Big(0);
let totalInterest = 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 dateOfFirstTransaction = new Date(first(orders).date);
const unitPriceAtStartDate = const endDateString = format(end, DATE_FORMAT);
marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol]; const startDateString = format(start, DATE_FORMAT);
const unitPriceAtEndDate = const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol];
marketSymbolMap[format(end, DATE_FORMAT)]?.[symbol]; const unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol];
if ( if (
!unitPriceAtEndDate || !unitPriceAtEndDate ||
@ -283,7 +278,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
// Add a synthetic order at the start and the end date // Add a synthetic order at the start and the end date
orders.push({ orders.push({
date: format(start, DATE_FORMAT), date: startDateString,
fee: new Big(0), fee: new Big(0),
feeInBaseCurrency: new Big(0), feeInBaseCurrency: new Big(0),
itemType: 'start', itemType: 'start',
@ -297,7 +292,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
}); });
orders.push({ orders.push({
date: format(end, DATE_FORMAT), date: endDateString,
fee: new Big(0), fee: new Big(0),
feeInBaseCurrency: new Big(0), feeInBaseCurrency: new Big(0),
itemType: 'end', itemType: 'end',
@ -310,7 +305,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
unitPrice: unitPriceAtEndDate unitPrice: unitPriceAtEndDate
}); });
let day = start;
let lastUnitPrice: Big; let lastUnitPrice: Big;
const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {}; const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {};
@ -320,15 +314,23 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
ordersByDate[order.date].push(order); ordersByDate[order.date].push(order);
} }
while (isBefore(day, end)) { if (!this.chartDates) {
const dateString = format(day, DATE_FORMAT); 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) { if (ordersByDate[dateString]?.length > 0) {
for (let order of ordersByDate[dateString]) { for (const order of ordersByDate[dateString]) {
order.unitPriceFromMarketData = order.unitPriceFromMarketData =
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice; marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice;
} }
} else if (chartDateMap[dateString]) { } else {
orders.push({ orders.push({
date: dateString, date: dateString,
fee: new Big(0), fee: new Big(0),
@ -348,8 +350,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
const lastOrder = last(orders); const lastOrder = last(orders);
lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice; lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice;
day = addDays(day, 1);
} }
// Sort orders so that the start and end placeholder order are at the correct // 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'); // return format(date, 'yyyy');
// }) // })
]) { ]) {
// TODO: getIntervalFromDateRange(dateRange, start) const dateInterval = getIntervalFromDateRange(dateRange);
let { endDate, startDate } = getIntervalFromDateRange(dateRange); const endDate = dateInterval.endDate;
let startDate = dateInterval.startDate;
if (isBefore(startDate, start)) { if (isBefore(startDate, start)) {
startDate = start; startDate = start;
} }
const rangeEndDateString = format(endDate, DATE_FORMAT);
const rangeStartDateString = format(startDate, DATE_FORMAT);
const currentValuesAtDateRangeStartWithCurrencyEffect = const currentValuesAtDateRangeStartWithCurrencyEffect =
currentValuesWithCurrencyEffect[format(startDate, DATE_FORMAT)] ?? currentValuesWithCurrencyEffect[rangeStartDateString] ?? new Big(0);
new Big(0);
const investmentValuesAccumulatedAtStartDateWithCurrencyEffect = const investmentValuesAccumulatedAtStartDateWithCurrencyEffect =
investmentValuesAccumulatedWithCurrencyEffect[ investmentValuesAccumulatedWithCurrencyEffect[rangeStartDateString] ??
format(startDate, DATE_FORMAT) new Big(0);
] ?? new Big(0);
const grossPerformanceAtDateRangeStartWithCurrencyEffect = const grossPerformanceAtDateRangeStartWithCurrencyEffect =
currentValuesAtDateRangeStartWithCurrencyEffect.minus( currentValuesAtDateRangeStartWithCurrencyEffect.minus(
investmentValuesAccumulatedAtStartDateWithCurrencyEffect investmentValuesAccumulatedAtStartDateWithCurrencyEffect
); );
const dates = eachDayOfInterval({
end: endDate,
start: startDate
}).map((date) => {
return format(date, DATE_FORMAT);
});
let average = new Big(0); let average = new Big(0);
let dayCount = 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 ( if (
investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big &&
investmentValuesAccumulatedWithCurrencyEffect[date].gt(0) investmentValuesAccumulatedWithCurrencyEffect[date].gt(0)
@ -878,17 +881,14 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
} }
netPerformanceWithCurrencyEffectMap[dateRange] = netPerformanceWithCurrencyEffectMap[dateRange] =
netPerformanceValuesWithCurrencyEffect[ netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus(
format(endDate, DATE_FORMAT)
]?.minus(
// If the date range is 'max', take 0 as a start value. Otherwise, // 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 // the value of the end of the day of the start date is taken which
// differs from the buying price. // differs from the buying price.
dateRange === 'max' dateRange === 'max'
? new Big(0) ? new Big(0)
: (netPerformanceValuesWithCurrencyEffect[ : (netPerformanceValuesWithCurrencyEffect[rangeStartDateString] ??
format(startDate, DATE_FORMAT) new Big(0))
] ?? new Big(0))
) ?? new Big(0); ) ?? new Big(0);
netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(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 }; return { marketPrice: 89.12 };
} else if (isSameDay(parseDate('2021-11-16'), date)) { } else if (isSameDay(parseDate('2021-11-16'), date)) {
return { marketPrice: 339.51 }; return { marketPrice: 339.51 };
} else if (isSameDay(parseDate('2023-07-09'), date)) {
return { marketPrice: 337.22 };
} else if (isSameDay(parseDate('2023-07-10'), date)) { } else if (isSameDay(parseDate('2023-07-10'), date)) {
return { marketPrice: 331.83 }; 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 { return {
PropertyService: jest.fn().mockImplementation(() => { PropertyService: jest.fn().mockImplementation(() => {
return { 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 { OrderService } from '@ghostfolio/api/app/order/order.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; 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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; 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 { import {
PortfolioDetails, PortfolioDetails,
PortfolioDividends, PortfolioDividends,
@ -64,7 +66,6 @@ import { UpdateHoldingTagsDto } from './update-holding-tags.dto';
@Controller('portfolio') @Controller('portfolio')
export class PortfolioController { export class PortfolioController {
public constructor( public constructor(
private readonly accessService: AccessService,
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly impersonationService: ImpersonationService, private readonly impersonationService: ImpersonationService,
@ -101,8 +102,15 @@ export class PortfolioController {
filterByTags filterByTags
}); });
const { accounts, hasErrors, holdings, platforms, summary } = const {
await this.portfolioService.getDetails({ accounts,
hasErrors,
holdings,
markets,
marketsAdvanced,
platforms,
summary
} = await this.portfolioService.getDetails({
dateRange, dateRange,
filters, filters,
impersonationId, impersonationId,
@ -186,6 +194,13 @@ export class PortfolioController {
}) || }) ||
isRestrictedView(this.request.user) isRestrictedView(this.request.user)
) { ) {
Object.values(markets ?? {}).forEach((market) => {
delete market.valueInBaseCurrency;
});
Object.values(marketsAdvanced ?? {}).forEach((market) => {
delete market.valueInBaseCurrency;
});
portfolioSummary = nullifyValuesInObject(summary, [ portfolioSummary = nullifyValuesInObject(summary, [
'cash', 'cash',
'committedFunds', 'committedFunds',
@ -238,6 +253,58 @@ export class PortfolioController {
hasError, hasError,
holdings, holdings,
platforms, 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 summary: portfolioSummary
}; };
} }

270
apps/api/src/app/portfolio/portfolio.service.ts

@ -478,8 +478,7 @@ export class PortfolioService {
if (withMarkets) { if (withMarkets) {
({ markets, marketsAdvanced } = this.getMarkets({ ({ markets, marketsAdvanced } = this.getMarkets({
assetProfile, assetProfile
valueInBaseCurrency
})); }));
} }
@ -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; let summary: PortfolioSummary;
if (withSummary) { if (withSummary) {
@ -611,6 +617,8 @@ export class PortfolioService {
accounts, accounts,
hasErrors, hasErrors,
holdings, holdings,
markets,
marketsAdvanced,
platforms, platforms,
summary summary
}; };
@ -1048,7 +1056,9 @@ export class PortfolioService {
currency: this.request.user.Settings.settings.baseCurrency 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 }) => { positions = positions.filter(({ quantity }) => {
return !quantity.eq(0); return !quantity.eq(0);
@ -1161,7 +1171,6 @@ export class PortfolioService {
filters, filters,
impersonationId, impersonationId,
userId, userId,
withExcludedAccounts = false,
calculateTimeWeightedPerformance = false calculateTimeWeightedPerformance = false
}: { }: {
dateRange?: DateRange; dateRange?: DateRange;
@ -1265,47 +1274,20 @@ export class PortfolioService {
@LogPerformance @LogPerformance
public async getReport(impersonationId: string): Promise<PortfolioReport> { public async getReport(impersonationId: string): Promise<PortfolioReport> {
const userId = await this.getUserId(impersonationId, this.request.user.id); const userId = await this.getUserId(impersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId }); const userSettings = <UserSettings>this.request.user.Settings.settings;
const userCurrency = this.getUserCurrency(user);
const { activities } =
await this.orderService.getOrdersForPortfolioCalculator({
userCurrency,
userId
});
const portfolioCalculator = this.calculatorFactory.createCalculator({ const { accounts, holdings, summary } = await this.getDetails({
activities, impersonationId,
userId, userId,
calculationType: PerformanceCalculationType.TWR, withMarkets: true,
currency: this.request.user.Settings.settings.baseCurrency withSummary: true
});
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
}); });
const userSettings = <UserSettings>this.request.user.Settings.settings;
return { return {
rules: { rules: {
accountClusterRisk: isEmpty(activities) accountClusterRisk:
? undefined summary.ordersCount > 0
: await this.rulesService.evaluate( ? await this.rulesService.evaluate(
[ [
new AccountClusterRiskCurrentInvestment( new AccountClusterRiskCurrentInvestment(
this.exchangeRateDataService, this.exchangeRateDataService,
@ -1317,22 +1299,24 @@ export class PortfolioService {
) )
], ],
userSettings userSettings
), )
currencyClusterRisk: isEmpty(activities) : undefined,
? undefined currencyClusterRisk:
: await this.rulesService.evaluate( summary.ordersCount > 0
? await this.rulesService.evaluate(
[ [
new CurrencyClusterRiskBaseCurrencyCurrentInvestment( new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
this.exchangeRateDataService, this.exchangeRateDataService,
positions Object.values(holdings)
), ),
new CurrencyClusterRiskCurrentInvestment( new CurrencyClusterRiskCurrentInvestment(
this.exchangeRateDataService, this.exchangeRateDataService,
positions Object.values(holdings)
) )
], ],
userSettings userSettings
), )
: undefined,
emergencyFund: await this.rulesService.evaluate( emergencyFund: await this.rulesService.evaluate(
[ [
new EmergencyFundSetup( new EmergencyFundSetup(
@ -1346,8 +1330,8 @@ export class PortfolioService {
[ [
new FeeRatioInitialInvestment( new FeeRatioInitialInvestment(
this.exchangeRateDataService, this.exchangeRateDataService,
totalInvestment.toNumber(), summary.committedFunds,
totalFeesWithCurrencyEffect.toNumber() summary.fees
) )
], ],
userSettings userSettings
@ -1371,37 +1355,148 @@ export class PortfolioService {
userId: string; userId: string;
}) { }) {
userId = await this.getUserId(impersonationId, userId); userId = await this.getUserId(impersonationId, userId);
let symbolProfile = await this.symbolProfileService.getSymbolProfiles([ await this.orderService.assignTags({ dataSource, symbol, tags, userId });
{ }
dataSource,
symbol private getAggregatedMarkets(holdings: {
} [symbol: string]: PortfolioPosition;
])[0]; }): {
await this.symbolProfileService.updateSymbolProfile({ markets: PortfolioDetails['markets'];
assetClass: symbolProfile.assetClass, marketsAdvanced: PortfolioDetails['marketsAdvanced'];
assetSubClass: symbolProfile.assetSubClass, } {
countries: symbolProfile.countries, const markets: PortfolioDetails['markets'] = {
currency: symbolProfile.currency, [UNKNOWN_KEY]: {
dataSource, id: UNKNOWN_KEY,
holdings: symbolProfile.holdings, valueInBaseCurrency: 0,
name: symbolProfile.name, valueInPercentage: 0
sectors: symbolProfile.sectors, },
symbol, developedMarkets: {
tags: { id: 'developedMarkets',
connectOrCreate: tags.map(({ id, name }) => { valueInBaseCurrency: 0,
return { valueInPercentage: 0
create: {
id,
name
}, },
where: { emergingMarkets: {
id 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 @LogPerformance
@ -1581,11 +1676,9 @@ export class PortfolioService {
} }
private getMarkets({ private getMarkets({
assetProfile, assetProfile
valueInBaseCurrency
}: { }: {
assetProfile: EnhancedSymbolProfile; assetProfile: EnhancedSymbolProfile;
valueInBaseCurrency: Big;
}) { }) {
const markets = { const markets = {
[UNKNOWN_KEY]: 0, [UNKNOWN_KEY]: 0,
@ -1647,15 +1740,22 @@ export class PortfolioService {
.toNumber(); .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(); .toNumber();
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY]) marketsAdvanced[UNKNOWN_KEY] = new Big(1)
.plus(valueInBaseCurrency) .minus(marketsAdvanced.asiaPacific)
.minus(marketsAdvanced.emergingMarkets)
.minus(marketsAdvanced.europe)
.minus(marketsAdvanced.japan)
.minus(marketsAdvanced.northAmerica)
.minus(marketsAdvanced.otherMarkets)
.toNumber(); .toNumber();
}
return { markets, marketsAdvanced }; return { markets, marketsAdvanced };
} }
@ -2013,7 +2113,7 @@ export class PortfolioService {
SymbolProfile, SymbolProfile,
type type
} of ordersByAccount) { } of ordersByAccount) {
let currentValueOfSymbolInBaseCurrency = const currentValueOfSymbolInBaseCurrency =
getFactor(type) * getFactor(type) *
quantity * quantity *
(portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ?? (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 { Filter } from '@ghostfolio/common/interfaces';
import { Milliseconds } from 'cache-manager';
export const RedisCacheServiceMock = { export const RedisCacheServiceMock = {
cache: new Map<string, string>(), cache: new Map<string, string>(),
get: (key: string): Promise<string> => { get: (key: string): Promise<string> => {
@ -20,7 +18,7 @@ export const RedisCacheServiceMock = {
return `portfolio-snapshot-${userId}${filtersHash > 0 ? `-${filtersHash}` : ''}`; 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); RedisCacheServiceMock.cache.set(key, value);
return Promise.resolve(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 { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
import type { import type {
ColorScheme, ColorScheme,
DateRange, DateRange,

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

@ -56,7 +56,7 @@ export class UserService {
{ Account, id, permissions, Settings, subscription }: UserWithSettings, { Account, id, permissions, Settings, subscription }: UserWithSettings,
aLocale = locale aLocale = locale
): Promise<IUser> { ): Promise<IUser> {
let [access, firstActivity, tags] = await Promise.all([ const userData = await Promise.all([
this.prismaService.access.findMany({ this.prismaService.access.findMany({
include: { include: {
User: true User: true
@ -70,8 +70,11 @@ export class UserService {
}, },
where: { userId: id } 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; 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 { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { groupBy } from '@ghostfolio/common/helper'; import { groupBy } from '@ghostfolio/common/helper';
import { UserSettings } from '@ghostfolio/common/interfaces'; import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models';
import { Big } from 'big.js';
import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { EvaluationResult } from './interfaces/evaluation-result.interface';
import { RuleInterface } from './interfaces/rule.interface'; import { RuleInterface } from './interfaces/rule.interface';
@ -33,24 +34,26 @@ export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
return this.name; return this.name;
} }
public groupCurrentPositionsByAttribute( public groupCurrentHoldingsByAttribute(
positions: TimelinePosition[], holdings: PortfolioPosition[],
attribute: keyof TimelinePosition, attribute: keyof PortfolioPosition,
baseCurrency: string baseCurrency: string
) { ) {
return Array.from(groupBy(attribute, positions).entries()).map( return Array.from(groupBy(attribute, holdings).entries()).map(
([attributeValue, objs]) => ({ ([attributeValue, objs]) => ({
groupKey: attributeValue, groupKey: attributeValue,
investment: objs.reduce( investment: objs.reduce(
(previousValue, currentValue) => (previousValue, currentValue) =>
previousValue + currentValue.investment.toNumber(), previousValue + currentValue.investment,
0 0
), ),
value: objs.reduce( value: objs.reduce(
(previousValue, currentValue) => (previousValue, currentValue) =>
previousValue + previousValue +
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
currentValue.quantity.mul(currentValue.marketPrice).toNumber(), new Big(currentValue.quantity)
.mul(currentValue.marketPrice)
.toNumber(),
currentValue.currency, currentValue.currency,
baseCurrency 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 { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule'; import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces'; import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models';
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> { export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
private positions: TimelinePosition[]; private holdings: PortfolioPosition[];
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
positions: TimelinePosition[] holdings: PortfolioPosition[]
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: CurrencyClusterRiskBaseCurrencyCurrentInvestment.name, key: CurrencyClusterRiskBaseCurrencyCurrentInvestment.name,
name: 'Investment: Base Currency' name: 'Investment: Base Currency'
}); });
this.positions = positions; this.holdings = holdings;
} }
public evaluate(ruleSettings: Settings) { public evaluate(ruleSettings: Settings) {
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute( const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute(
this.positions, this.holdings,
'currency', 'currency',
ruleSettings.baseCurrency ruleSettings.baseCurrency
); );
let maxItem = positionsGroupedByCurrency[0]; let maxItem = holdingsGroupedByCurrency[0];
let totalValue = 0; let totalValue = 0;
positionsGroupedByCurrency.forEach((groupItem) => { holdingsGroupedByCurrency.forEach((groupItem) => {
// Calculate total value // Calculate total value
totalValue += groupItem.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; 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 { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule'; import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces'; import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models';
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> { export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
private positions: TimelinePosition[]; private holdings: PortfolioPosition[];
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
positions: TimelinePosition[] holdings: PortfolioPosition[]
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: CurrencyClusterRiskCurrentInvestment.name, key: CurrencyClusterRiskCurrentInvestment.name,
name: 'Investment' name: 'Investment'
}); });
this.positions = positions; this.holdings = holdings;
} }
public evaluate(ruleSettings: Settings) { public evaluate(ruleSettings: Settings) {
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute( const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute(
this.positions, this.holdings,
'currency', 'currency',
ruleSettings.baseCurrency ruleSettings.baseCurrency
); );
let maxItem = positionsGroupedByCurrency[0]; let maxItem = holdingsGroupedByCurrency[0];
let totalValue = 0; let totalValue = 0;
positionsGroupedByCurrency.forEach((groupItem) => { holdingsGroupedByCurrency.forEach((groupItem) => {
// Calculate total value // Calculate total value
totalValue += groupItem.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; this.emergencyFund = emergencyFund;
} }
public evaluate(ruleSettings: Settings) { public evaluate() {
if (!this.emergencyFund) { if (!this.emergencyFund) {
return { return {
evaluation: 'No emergency fund has been set up', 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'); 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; 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'); this.configurationService.get('API_KEY_OPEN_FIGI');
} }
let abortController = new AbortController(); const abortController = new AbortController();
setTimeout(() => { setTimeout(() => {
abortController.abort(); 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({ public async enhance({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
response, response,
symbol symbol
}: { }: {

14
apps/api/src/services/data-provider/data-provider.service.ts

@ -460,7 +460,9 @@ export class DataProviderService {
promises.push( promises.push(
promise.then(async (result) => { promise.then(async (result) => {
for (let [symbol, dataProviderResponse] of Object.entries(result)) { for (const [symbol, dataProviderResponse] of Object.entries(
result
)) {
if ( if (
[ [
...DERIVED_CURRENCIES.map(({ currency }) => { ...DERIVED_CURRENCIES.map(({ currency }) => {
@ -600,7 +602,7 @@ export class DataProviderService {
return { items: lookupItems }; return { items: lookupItems };
} }
let dataProviderServices = this.configurationService const dataProviderServices = this.configurationService
.get('DATA_SOURCES') .get('DATA_SOURCES')
.map((dataSource) => { .map((dataSource) => {
return this.getDataProvider(DataSource[dataSource]); return this.getDataProvider(DataSource[dataSource]);
@ -689,12 +691,14 @@ export class DataProviderService {
} = {}; } = {};
for (const date in rootData) { for (const date in rootData) {
if (isNumber(rootData[date].marketPrice)) {
data[date] = { data[date] = {
marketPrice: rootData[date].marketPrice marketPrice: new Big(factor)
? new Big(factor).mul(rootData[date].marketPrice).toNumber() .mul(rootData[date].marketPrice)
: null .toNumber()
}; };
} }
}
return data; 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'); this.apiKey = this.configurationService.get('API_KEY_EOD_HISTORICAL_DATA');
} }
public canHandle(symbol: string) { public canHandle() {
return true; return true;
} }
@ -163,7 +163,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
).json<any>(); ).json<any>();
return response.reduce( return response.reduce(
(result, { close, date }, index, array) => { (result, { close, date }) => {
if (isNumber(close)) { if (isNumber(close)) {
result[this.convertFromEodSymbol(symbol)][date] = { result[this.convertFromEodSymbol(symbol)][date] = {
marketPrice: close marketPrice: close
@ -203,7 +203,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols symbols
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { }: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> {
let response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: IDataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return response; 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; 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 private readonly symbolProfileService: SymbolProfileService
) {} ) {}
public canHandle(symbol: string) { public canHandle() {
return true; 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 private readonly symbolProfileService: SymbolProfileService
) {} ) {}
public canHandle(symbol: string) { public canHandle() {
return true; return true;
} }
@ -87,12 +87,8 @@ export class ManualService implements DataProviderInterface {
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
[{ symbol, dataSource: this.getName() }] [{ symbol, dataSource: this.getName() }]
); );
const { const { defaultMarketPrice, selector, url } =
defaultMarketPrice, symbolProfile?.scraperConfiguration ?? {};
headers = {},
selector,
url
} = symbolProfile?.scraperConfiguration ?? {};
if (defaultMarketPrice) { if (defaultMarketPrice) {
const historical: { 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 private readonly configurationService: ConfigurationService
) {} ) {}
public canHandle(symbol: string) { public canHandle() {
return !!this.configurationService.get('API_KEY_RAPID_API'); 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 private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService
) {} ) {}
public canHandle(symbol: string) { public canHandle() {
return true; return true;
} }

7
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts

@ -1,10 +1,5 @@
export const ExchangeRateDataServiceMock = { export const ExchangeRateDataServiceMock = {
getExchangeRatesByCurrency: ({ getExchangeRatesByCurrency: ({ targetCurrency }): Promise<any> => {
currencies,
endDate,
startDate,
targetCurrency
}): Promise<any> => {
if (targetCurrency === 'CHF') { if (targetCurrency === 'CHF') {
return Promise.resolve({ return Promise.resolve({
CHFCHF: { CHFCHF: {

12
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts

@ -63,11 +63,11 @@ export class ExchangeRateDataService {
return {}; return {};
} }
let exchangeRatesByCurrency: { const exchangeRatesByCurrency: {
[currency: string]: { [dateString: string]: number }; [currency: string]: { [dateString: string]: number };
} = {}; } = {};
for (let currency of currencies) { for (const currency of currencies) {
exchangeRatesByCurrency[`${currency}${targetCurrency}`] = exchangeRatesByCurrency[`${currency}${targetCurrency}`] =
await this.getExchangeRates({ await this.getExchangeRates({
startDate, startDate,
@ -94,7 +94,7 @@ export class ExchangeRateDataService {
!isBefore(date, startDate); !isBefore(date, startDate);
date = subDays(resetHours(date), 1) 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 // Check if the exchange rate for the current date is missing
if ( if (
@ -351,7 +351,7 @@ export class ExchangeRateDataService {
startDate: Date; startDate: Date;
}) { }) {
const dates = eachDayOfInterval({ end: endDate, start: startDate }); const dates = eachDayOfInterval({ end: endDate, start: startDate });
let factors: { [dateString: string]: number } = {}; const factors: { [dateString: string]: number } = {};
if (currencyFrom === currencyTo) { if (currencyFrom === currencyTo) {
for (const date of dates) { for (const date of dates) {
@ -379,10 +379,10 @@ export class ExchangeRateDataService {
} else { } else {
// Calculate indirectly via base currency // Calculate indirectly via base currency
let marketPriceBaseCurrencyFromCurrency: { const marketPriceBaseCurrencyFromCurrency: {
[dateString: string]: number; [dateString: string]: number;
} = {}; } = {};
let marketPriceBaseCurrencyToCurrency: { const marketPriceBaseCurrencyToCurrency: {
[dateString: string]: number; [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 = { export const PortfolioSnapshotServiceMock = {
addJobToQueue({ addJobToQueue({
data,
name,
opts opts
}: { }: {
data: IPortfolioSnapshotQueueJob; data: IPortfolioSnapshotQueueJob;

35
apps/api/src/services/tag/tag.service.ts

@ -6,38 +6,39 @@ import { Injectable } from '@nestjs/common';
export class TagService { export class TagService {
public constructor(private readonly prismaService: PrismaService) {} public constructor(private readonly prismaService: PrismaService) {}
public async getPublic() { public async getTagsForUser(userId: string) {
return this.prismaService.tag.findMany({ const tags = await this.prismaService.tag.findMany({
orderBy: { include: {
name: 'asc' _count: {
}, select: {
orders: {
where: { where: {
userId: null userId
} }
});
} }
}
public async getInUseByUser(userId: string) { }
return this.prismaService.tag.findMany({ },
orderBy: { orderBy: {
name: 'asc' name: 'asc'
}, },
where: { where: {
OR: [ OR: [
{ {
orders: {
some: {
userId userId
}
}
}, },
{ {
symbolProfile: { userId: null
some: {}
}
} }
] ]
} }
}); });
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, registerDecorator,
ValidationOptions, ValidationOptions,
ValidatorConstraint, ValidatorConstraint,
ValidatorConstraintInterface, ValidatorConstraintInterface
ValidationArguments
} from 'class-validator'; } from 'class-validator';
import { isISO4217CurrencyCode } from 'class-validator'; import { isISO4217CurrencyCode } from 'class-validator';
@ -25,7 +24,7 @@ export function IsCurrencyCode(validationOptions?: ValidationOptions) {
export class IsExtendedCurrencyConstraint export class IsExtendedCurrencyConstraint
implements ValidatorConstraintInterface implements ValidatorConstraintInterface
{ {
public defaultMessage(args: ValidationArguments) { public defaultMessage() {
return '$value must be a valid ISO4217 currency code'; 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 * Formats a date as a string
*/ */
public format(aDate: Date, aParseFormat: string): string { public format(aDate: Date): string {
return format(aDate, getDateFormatString(this.locale)); 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() { public onExport() {
let activityIds = this.dataSource.data.map(({ id }) => { const activityIds = this.dataSource.data.map(({ id }) => {
return 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'; } from '@ghostfolio/common/interfaces';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { EMPTY, catchError, finalize, forkJoin, takeUntil } from 'rxjs'; import { EMPTY, catchError, finalize, forkJoin } from 'rxjs';
@Injectable() @Injectable()
export class AdminMarketDataService { 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() { public ngOnInit() {
const { tags } = this.dataService.fetchInfo();
this.activityForm = this.formBuilder.group({ this.activityForm = this.formBuilder.group({
tags: <string[]>[] tags: <string[]>[]
}); });
@ -160,13 +158,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
{ id: this.data.symbol, type: 'SYMBOL' } { id: this.data.symbol, type: 'SYMBOL' }
]; ];
this.tagsAvailable = tags.map((tag) => {
return {
...tag,
name: translate(tag.name)
};
});
this.activityForm this.activityForm
.get('tags') .get('tags')
.valueChanges.pipe(takeUntil(this.unsubscribeSubject)) .valueChanges.pipe(takeUntil(this.unsubscribeSubject))
@ -452,6 +443,14 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.tagsAvailable =
this.user?.tags?.map((tag) => {
return {
...tag,
name: translate(tag.name)
};
}) ?? [];
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
}); });
@ -481,7 +480,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
} }
public onExport() { public onExport() {
let activityIds = this.dataSource.data.map(({ id }) => { const activityIds = this.dataSource.data.map(({ id }) => {
return 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(); this.initialize();
} }
public onSymbolClicked({ dataSource, symbol }: AssetProfileIdentifier) { public onHoldingClicked({ dataSource, symbol }: AssetProfileIdentifier) {
if (dataSource && symbol) { if (dataSource && symbol) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true } queryParams: { dataSource, symbol, holdingDetailDialog: true }

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

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

1
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -11,7 +11,6 @@ import {
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DeviceDetectorService } from 'ngx-device-detector'; 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, @Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams,
public dialogRef: MatDialogRef<GfRuleSettingsDialogComponent> public dialogRef: MatDialogRef<GfRuleSettingsDialogComponent>
) { ) {
console.log(this.data.rule);
this.settings = this.data.rule.settings; 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 title: aParams.title
}); });
return dialog.afterClosed().subscribe((result) => { return dialog.afterClosed().subscribe(() => {
if (isFunction(aParams.discardFn)) { if (isFunction(aParams.discardFn)) {
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 { 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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Component, OnDestroy, OnInit } from '@angular/core'; 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"> <div class="col-md-8 offset-md-2">
<gf-carousel [aria-label]="'Testimonials'"> <gf-carousel [aria-label]="'Testimonials'">
@for (testimonial of testimonials; track testimonial) { @for (testimonial of testimonials; track testimonial) {
<div #carouselItem gf-carousel-item> <div #carouselItem gfCarouselItem>
<div class="d-flex px-4"> <div class="d-flex px-4">
<gf-logo <gf-logo
class="mr-3 mt-2 pt-1" 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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { downloadAsFile } from '@ghostfolio/common/helper'; 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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
@ -138,6 +138,16 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
this.fetchActivities(); this.fetchActivities();
} }
public onClickActivity({ dataSource, symbol }: AssetProfileIdentifier) {
this.router.navigate([], {
queryParams: {
dataSource,
symbol,
holdingDetailDialog: true
}
});
}
public onCloneActivity(aActivity: Activity) { public onCloneActivity(aActivity: Activity) {
this.openCreateActivityDialog(aActivity); this.openCreateActivityDialog(aActivity);
} }

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

@ -21,6 +21,7 @@
[sortDirection]="sortDirection" [sortDirection]="sortDirection"
[totalItems]="totalItems" [totalItems]="totalItems"
(activitiesDeleted)="onDeleteActivities()" (activitiesDeleted)="onDeleteActivities()"
(activityClicked)="onClickActivity($event)"
(activityDeleted)="onDeleteActivity($event)" (activityDeleted)="onDeleteActivity($event)"
(activityToClone)="onCloneActivity($event)" (activityToClone)="onCloneActivity($event)"
(activityToUpdate)="onUpdateActivity($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.locale = this.data.user?.settings?.locale;
this.dateAdapter.setLocale(this.locale); this.dateAdapter.setLocale(this.locale);
const { currencies, platforms, tags } = this.dataService.fetchInfo(); const { currencies, platforms } = this.dataService.fetchInfo();
this.currencies = currencies; this.currencies = currencies;
this.defaultDateFormat = getDateFormatString(this.locale); this.defaultDateFormat = getDateFormatString(this.locale);
this.platforms = platforms; this.platforms = platforms;
this.tagsAvailable = tags.map((tag) => {
this.tagsAvailable =
this.data.user?.tags?.map((tag) => {
return { return {
...tag, ...tag,
name: translate(tag.name) name: translate(tag.name)
}; };
}); }) ?? [];
Object.keys(Type).forEach((type) => { Object.keys(Type).forEach((type) => {
this.typesTranslationMap[Type[type]] = translate(Type[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 hasImpersonationId: boolean;
public isLoading = false; public isLoading = false;
public markets: { public markets: {
[key in Market]: { name: string; value: number }; [key in Market]: { id: Market; valueInPercentage: number };
}; };
public marketsAdvanced: { public marketsAdvanced: {
[key in MarketAdvanced]: { [key in MarketAdvanced]: {
@ -222,24 +222,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
value: 0 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 = { this.marketsAdvanced = {
[UNKNOWN_KEY]: { [UNKNOWN_KEY]: {
id: 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( for (const [symbol, position] of Object.entries(
this.portfolioDetails.holdings 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 // Prepare analysis data by continents, countries, holdings and sectors except for liquidity
if (position.countries.length > 0) { 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) { for (const country of position.countries) {
const { code, continent, name, weight } = country; 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].valueInBaseCurrency
: this.portfolioDetails.holdings[symbol].valueInPercentage; : 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) { 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) this.topHoldings = Object.values(this.topHoldingsMap)
.map(({ name, value }) => { .map(({ name, value }) => {
if (this.hasImpersonationId || this.user.settings.isRestrictedView) { if (this.hasImpersonationId || this.user.settings.isRestrictedView) {

10
apps/client/src/app/pages/portfolio/allocations/allocations-page.html

@ -218,7 +218,7 @@
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.developedMarkets?.value" [value]="markets?.developedMarkets?.valueInPercentage"
>Developed Markets</gf-value >Developed Markets</gf-value
> >
</div> </div>
@ -227,7 +227,7 @@
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.emergingMarkets?.value" [value]="markets?.emergingMarkets?.valueInPercentage"
>Emerging Markets</gf-value >Emerging Markets</gf-value
> >
</div> </div>
@ -236,17 +236,17 @@
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.otherMarkets?.value" [value]="markets?.otherMarkets?.valueInPercentage"
>Other Markets</gf-value >Other Markets</gf-value
> >
</div> </div>
@if (markets?.[UNKNOWN_KEY]?.value > 0) { @if (markets?.[UNKNOWN_KEY]?.valueInPercentage > 0) {
<div class="col-xs-12 col-md my-2"> <div class="col-xs-12 col-md my-2">
<gf-value <gf-value
i18n i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.[UNKNOWN_KEY]?.value" [value]="markets?.[UNKNOWN_KEY]?.valueInPercentage"
>No data available</gf-value >No data available</gf-value
> >
</div> </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 deviceType: string;
public holdings: PublicPortfolioResponse['holdings'][string][]; public holdings: PublicPortfolioResponse['holdings'][string][];
public markets: { public markets: {
[key in Market]: { name: string; value: number }; [key in Market]: { id: Market; valueInPercentage: number };
}; };
public positions: { public positions: {
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name'> & { [symbol: string]: Pick<PortfolioPosition, 'currency' | 'name'> & {
@ -102,24 +102,7 @@ export class PublicPageComponent implements OnInit {
} }
}; };
this.holdings = []; this.holdings = [];
this.markets = { this.markets = this.publicPortfolioDetails.markets;
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
value: 0
},
developedMarkets: {
name: 'developedMarkets',
value: 0
},
emergingMarkets: {
name: 'emergingMarkets',
value: 0
},
otherMarkets: {
name: 'otherMarkets',
value: 0
}
};
this.positions = {}; this.positions = {};
this.sectors = { this.sectors = {
[UNKNOWN_KEY]: { [UNKNOWN_KEY]: {
@ -150,13 +133,6 @@ export class PublicPageComponent implements OnInit {
// Prepare analysis data by continents, countries, holdings and sectors except for liquidity // Prepare analysis data by continents, countries, holdings and sectors except for liquidity
if (position.countries.length > 0) { 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) { for (const country of position.countries) {
const { code, continent, name, weight } = country; const { code, continent, name, weight } = country;
@ -192,9 +168,6 @@ export class PublicPageComponent implements OnInit {
this.countries[UNKNOWN_KEY].value += this.countries[UNKNOWN_KEY].value +=
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency; this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
this.markets[UNKNOWN_KEY].value +=
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
} }
if (position.sectors.length > 0) { if (position.sectors.length > 0) {
@ -227,21 +200,6 @@ export class PublicPageComponent implements OnInit {
: position.valueInPercentage : 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() { public ngOnDestroy() {

10
apps/client/src/app/pages/public/public-page.html

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

2
apps/client/src/app/services/data.service.ts

@ -287,7 +287,7 @@ export class DataService {
} }
public deleteActivities({ filters }) { public deleteActivities({ filters }) {
let params = this.buildFiltersAsQueryParams({ filters }); const params = this.buildFiltersAsQueryParams({ filters });
return this.http.delete<any>(`/api/v1/order`, { params }); 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>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <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> <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> <target state="translated">Gestionar Activitats</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5486880308148746399" datatype="html"> <trans-unit id="5486880308148746399" datatype="html">
@ -5979,7 +5979,7 @@
<target state="new">Do you really want to delete these activities?</target> <target state="new">Do you really want to delete these activities?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="670983159637074283" datatype="html"> <trans-unit id="670983159637074283" datatype="html">
@ -5987,7 +5987,7 @@
<target state="new">Do you really want to delete this activity?</target> <target state="new">Do you really want to delete this activity?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4149127798893455354" datatype="html"> <trans-unit id="4149127798893455354" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target> <target state="new">Threshold Min</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html"> <trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target> <target state="new">Threshold Max</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html"> <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target> <target state="new">Close</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html"> <trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

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

@ -554,7 +554,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <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> <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> <target state="translated">Aktivitäten verwalten</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html"> <trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html">
@ -2550,7 +2550,7 @@
<target state="translated">Möchtest du diese Aktivität wirklich löschen?</target> <target state="translated">Möchtest du diese Aktivität wirklich löschen?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html"> <trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3914,7 +3914,7 @@
<target state="translated">Möchtest du diese Aktivitäten wirklich löschen?</target> <target state="translated">Möchtest du diese Aktivitäten wirklich löschen?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html"> <trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target> <target state="new">Threshold Min</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html"> <trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target> <target state="new">Threshold Max</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html"> <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="translated">Schliessen</target> <target state="translated">Schliessen</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html"> <trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

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

@ -555,7 +555,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <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> <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> <target state="translated">Gestión de las operaciones</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html"> <trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html">
@ -2551,7 +2551,7 @@
<target state="translated">¿Estás seguro de eliminar esta operación?</target> <target state="translated">¿Estás seguro de eliminar esta operación?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html"> <trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3915,7 +3915,7 @@
<target state="new">Do you really want to delete these activities?</target> <target state="new">Do you really want to delete these activities?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html"> <trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7056,7 +7056,7 @@
<target state="new">Threshold Min</target> <target state="new">Threshold Min</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html"> <trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7064,7 +7064,7 @@
<target state="new">Threshold Max</target> <target state="new">Threshold Max</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html"> <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7072,7 +7072,7 @@
<target state="new">Close</target> <target state="new">Close</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html"> <trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7259,6 +7259,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

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

@ -614,7 +614,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <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> <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> <target state="translated">Gérer les Activités</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5486880308148746399" datatype="html"> <trans-unit id="5486880308148746399" datatype="html">
@ -3070,7 +3070,7 @@
<target state="translated">Voulez-vous vraiment supprimer cette activité ?</target> <target state="translated">Voulez-vous vraiment supprimer cette activité ?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html"> <trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3914,7 +3914,7 @@
<target state="translated">Voulez-vous vraiment supprimer toutes vos activités ?</target> <target state="translated">Voulez-vous vraiment supprimer toutes vos activités ?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html"> <trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target> <target state="new">Threshold Min</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html"> <trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target> <target state="new">Threshold Max</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html"> <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target> <target state="new">Close</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html"> <trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

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

@ -555,7 +555,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <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> <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> <target state="translated">Gestione delle attività</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html"> <trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html">
@ -2551,7 +2551,7 @@
<target state="translated">Vuoi davvero eliminare questa attività?</target> <target state="translated">Vuoi davvero eliminare questa attività?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html"> <trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3915,7 +3915,7 @@
<target state="translated">Vuoi davvero eliminare tutte le tue attività?</target> <target state="translated">Vuoi davvero eliminare tutte le tue attività?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html"> <trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7056,7 +7056,7 @@
<target state="new">Threshold Min</target> <target state="new">Threshold Min</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html"> <trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7064,7 +7064,7 @@
<target state="new">Threshold Max</target> <target state="new">Threshold Max</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html"> <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7072,7 +7072,7 @@
<target state="new">Close</target> <target state="new">Close</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html"> <trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7259,6 +7259,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

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

@ -554,7 +554,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <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> <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> <target state="translated">Activiteiten beheren</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html"> <trans-unit id="ce718ababbce63d776cf8b1f91412beb4c0a6e04" datatype="html">
@ -2550,7 +2550,7 @@
<target state="translated">Wil je deze activiteit echt verwijderen?</target> <target state="translated">Wil je deze activiteit echt verwijderen?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html"> <trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3914,7 +3914,7 @@
<target state="new">Wil je echt al je activiteiten verwijderen?</target> <target state="new">Wil je echt al je activiteiten verwijderen?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html"> <trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target> <target state="new">Threshold Min</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html"> <trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target> <target state="new">Threshold Max</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html"> <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target> <target state="new">Close</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html"> <trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

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

@ -1363,7 +1363,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <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> <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> <target state="translated">Zarządzaj Aktywnościami</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5486880308148746399" datatype="html"> <trans-unit id="5486880308148746399" datatype="html">
@ -4151,7 +4151,7 @@
<target state="new">Do you really want to delete these activities?</target> <target state="new">Do you really want to delete these activities?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html"> <trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html">
@ -5503,7 +5503,7 @@
<target state="translated">Czy na pewno chcesz usunąć tę działalność?</target> <target state="translated">Czy na pewno chcesz usunąć tę działalność?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4149127798893455354" datatype="html"> <trans-unit id="4149127798893455354" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target> <target state="new">Threshold Min</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html"> <trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target> <target state="new">Threshold Max</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html"> <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target> <target state="new">Close</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html"> <trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

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

@ -614,7 +614,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <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> <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> <target state="translated">Gerir Atividades</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5486880308148746399" datatype="html"> <trans-unit id="5486880308148746399" datatype="html">
@ -2942,7 +2942,7 @@
<target state="translated">Deseja realmente eliminar esta atividade?</target> <target state="translated">Deseja realmente eliminar esta atividade?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html"> <trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -3914,7 +3914,7 @@
<target state="translated">Deseja mesmo eliminar estas atividades?</target> <target state="translated">Deseja mesmo eliminar estas atividades?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html"> <trans-unit id="166ccc92e1aa598f9056a260be209a0bab64d37a" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target> <target state="new">Threshold Min</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html"> <trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target> <target state="new">Threshold Max</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html"> <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target> <target state="new">Close</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html"> <trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

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

@ -1327,7 +1327,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <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> <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> <target state="translated">İşlemleri Yönet</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5486880308148746399" datatype="html"> <trans-unit id="5486880308148746399" datatype="html">
@ -3663,7 +3663,7 @@
<target state="new">Tüm işlemlerinizi silmeyi gerçekten istiyor musunuz?</target> <target state="new">Tüm işlemlerinizi silmeyi gerçekten istiyor musunuz?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html"> <trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html">
@ -5207,7 +5207,7 @@
<target state="translated">TBu işlemi silmeyi gerçekten istiyor musunuz?</target> <target state="translated">TBu işlemi silmeyi gerçekten istiyor musunuz?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html"> <trans-unit id="170f7de02b14690fb9c1999a16926c0044bfd5c1" datatype="html">
@ -7055,7 +7055,7 @@
<target state="new">Threshold Min</target> <target state="new">Threshold Min</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html"> <trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7063,7 +7063,7 @@
<target state="new">Threshold Max</target> <target state="new">Threshold Max</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html"> <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7071,7 +7071,7 @@
<target state="new">Close</target> <target state="new">Close</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html"> <trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

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

@ -1312,7 +1312,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <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> <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> <source>Manage Activities</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5486880308148746399" datatype="html"> <trans-unit id="5486880308148746399" datatype="html">
@ -3825,7 +3825,7 @@
<source>Do you really want to delete these activities?</source> <source>Do you really want to delete these activities?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html"> <trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html">
@ -5064,7 +5064,7 @@
<source>Do you really want to delete this activity?</source> <source>Do you really want to delete this activity?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4149127798893455354" datatype="html"> <trans-unit id="4149127798893455354" datatype="html">
@ -6359,7 +6359,7 @@
<source>Threshold Max</source> <source>Threshold Max</source>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html"> <trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -6380,7 +6380,7 @@
<source>Threshold Min</source> <source>Threshold Min</source>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="65ff514a2e167229e1a34b3712f2cf2908576d0f" datatype="html"> <trans-unit id="65ff514a2e167229e1a34b3712f2cf2908576d0f" datatype="html">
@ -6408,7 +6408,7 @@
<source>Close</source> <source>Close</source>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="06296af0cdaf7bed02043379359ed1975fc22077" datatype="html"> <trans-unit id="06296af0cdaf7bed02043379359ed1975fc22077" datatype="html">
@ -6565,6 +6565,13 @@
<context context-type="linenumber">49</context> <context context-type="linenumber">49</context>
</context-group> </context-group>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

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

@ -1372,7 +1372,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <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> <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> <target state="translated">管理活动</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-holdings/home-holdings.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5486880308148746399" datatype="html"> <trans-unit id="5486880308148746399" datatype="html">
@ -4168,7 +4168,7 @@
<target state="new">您真的要删除所有活动吗?</target> <target state="new">您真的要删除所有活动吗?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html"> <trans-unit id="72ba3bcdd8350cb8bf462e217a28ec7f7a48bb44" datatype="html">
@ -5552,7 +5552,7 @@
<target state="translated">您确实要删除此活动吗?</target> <target state="translated">您确实要删除此活动吗?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4149127798893455354" datatype="html"> <trans-unit id="4149127798893455354" datatype="html">
@ -7056,7 +7056,7 @@
<target state="new">Threshold Min</target> <target state="new">Threshold Min</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html"> <trans-unit id="012b48ee5281a77c66760b2007c3ccd7e34aa340" datatype="html">
@ -7064,7 +7064,7 @@
<target state="new">Threshold Max</target> <target state="new">Threshold Max</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html"> <trans-unit id="f4e529ae5ffd73001d1ff4bbdeeb0a72e342e5c8" datatype="html">
@ -7072,7 +7072,7 @@
<target state="new">Close</target> <target state="new">Close</target>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html"> <trans-unit id="072d4d4ec83a5a97345a1c13b90c213b47326d09" datatype="html">
@ -7259,6 +7259,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </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> </body>
</file> </file>
</xliff> </xliff>

2
apps/client/tsconfig.json

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

3
apps/ui-e2e/.eslintrc.json

@ -4,6 +4,9 @@
"overrides": [ "overrides": [
{ {
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["apps/ui-e2e/tsconfig.json"]
},
"rules": {} "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) // Remove non-numeric characters (excluding international formatting characters)
const numericValue = value.replace(/[^\d.,'’\s]/g, ''); const numericValue = value.replace(/[^\d.,'’\s]/g, '');
let parser = new NumberParser(locale); const parser = new NumberParser(locale);
return parser.parse(numericValue); return parser.parse(numericValue);
} catch { } catch {

3
libs/common/src/lib/interfaces/info-item.interface.ts

@ -1,6 +1,6 @@
import { SubscriptionOffer } from '@ghostfolio/common/types'; 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 { Statistics } from './statistics.interface';
import { Subscription } from './subscription.interface'; import { Subscription } from './subscription.interface';
@ -19,5 +19,4 @@ export interface InfoItem {
statistics: Statistics; statistics: Statistics;
stripePublicKey?: string; stripePublicKey?: string;
subscriptions: { [offer in SubscriptionOffer]: Subscription }; subscriptions: { [offer in SubscriptionOffer]: Subscription };
tags: Tag[];
} }

15
libs/common/src/lib/interfaces/portfolio-details.interface.ts

@ -2,6 +2,7 @@ import {
PortfolioPosition, PortfolioPosition,
PortfolioSummary PortfolioSummary
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Market, MarketAdvanced } from '@ghostfolio/common/types';
export interface PortfolioDetails { export interface PortfolioDetails {
accounts: { accounts: {
@ -14,6 +15,20 @@ export interface PortfolioDetails {
}; };
}; };
holdings: { [symbol: string]: PortfolioPosition }; 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: { platforms: {
[id: string]: { [id: string]: {
balance: number; 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 { export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
alias?: string; alias?: string;
@ -22,6 +23,12 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
| 'valueInPercentage' | 'valueInPercentage'
>; >;
}; };
markets: {
[key in Market]: Pick<
PortfolioDetails['markets'][key],
'id' | 'valueInPercentage'
>;
};
} }
interface PublicPortfolioResponseV1 { interface PublicPortfolioResponseV1 {

2
libs/common/src/lib/interfaces/user.interface.ts

@ -23,5 +23,5 @@ export interface User {
offer: SubscriptionOffer; offer: SubscriptionOffer;
type: SubscriptionType; type: SubscriptionType;
}; };
tags: Tag[]; tags: (Tag & { isUsed: boolean })[];
} }

7
libs/ui/.eslintrc.json

@ -1,9 +1,12 @@
{ {
"extends": ["../../.eslintrc.json"], "extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"], "ignorePatterns": ["!**/*", "**/*.stories.ts"],
"overrides": [ "overrides": [
{ {
"files": ["*.ts"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["libs/ui/tsconfig.*?.json"]
},
"extends": [ "extends": [
"plugin:@nx/angular", "plugin:@nx/angular",
"plugin:@angular-eslint/template/process-inline-templates" "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'; } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { Router, RouterModule } from '@angular/router';
import { isUUID } from 'class-validator'; import { isUUID } from 'class-validator';
import { endOfToday, isAfter } from 'date-fns'; import { endOfToday, isAfter } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -64,8 +63,7 @@ import { Subject, Subscription, takeUntil } from 'rxjs';
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,
MatTooltipModule, MatTooltipModule,
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule
RouterModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-activities-table', selector: 'gf-activities-table',
@ -95,6 +93,7 @@ export class GfActivitiesTableComponent
@Input() totalItems = Number.MAX_SAFE_INTEGER; @Input() totalItems = Number.MAX_SAFE_INTEGER;
@Output() activitiesDeleted = new EventEmitter<void>(); @Output() activitiesDeleted = new EventEmitter<void>();
@Output() activityClicked = new EventEmitter<AssetProfileIdentifier>();
@Output() activityDeleted = new EventEmitter<string>(); @Output() activityDeleted = new EventEmitter<string>();
@Output() activityToClone = new EventEmitter<OrderWithAccount>(); @Output() activityToClone = new EventEmitter<OrderWithAccount>();
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>(); @Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
@ -122,10 +121,7 @@ export class GfActivitiesTableComponent
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(private notificationService: NotificationService) {}
private notificationService: NotificationService,
private router: Router
) {}
public ngOnInit() { public ngOnInit() {
if (this.showCheckbox) { if (this.showCheckbox) {
@ -203,7 +199,7 @@ export class GfActivitiesTableComponent
activity.isDraft === false && activity.isDraft === false &&
['BUY', 'DIVIDEND', 'SELL'].includes(activity.type) ['BUY', 'DIVIDEND', 'SELL'].includes(activity.type)
) { ) {
this.onOpenPositionDialog({ this.activityClicked.emit({
dataSource: activity.SymbolProfile.dataSource, dataSource: activity.SymbolProfile.dataSource,
symbol: activity.SymbolProfile.symbol 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) { public onUpdateActivity(aActivity: OrderWithAccount) {
this.activityToUpdate.emit(aActivity); this.activityToUpdate.emit(aActivity);
} }
public toggleAllRows() { public toggleAllRows() {
this.areAllRowsSelected() if (this.areAllRowsSelected()) {
? this.selectedRows.clear() this.selectedRows.clear();
: this.dataSource.data.forEach((row) => this.selectedRows.select(row)); } else {
this.dataSource.data.forEach((row) => {
this.selectedRows.select(row);
});
}
this.selectedActivities.emit(this.selectedRows.selected); 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 { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { Account, AssetClass } from '@prisma/client'; import { Account, AssetClass } from '@prisma/client';
import { eachYearOfInterval, format } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs'; import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
import { import {
@ -157,7 +156,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
) {} ) {}
public ngOnInit() { public ngOnInit() {
this.accounts = this.user?.accounts;
this.assetClasses = Object.keys(AssetClass).map((assetClass) => { this.assetClasses = Object.keys(AssetClass).map((assetClass) => {
return { return {
id: assetClass, id: assetClass,
@ -165,13 +163,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
type: 'ASSET_CLASS' type: 'ASSET_CLASS'
}; };
}); });
this.tags = this.user?.tags.map(({ id, name }) => {
return {
id,
label: translate(name),
type: 'TAG'
};
});
this.searchFormControl.valueChanges this.searchFormControl.valueChanges
.pipe( .pipe(
@ -213,6 +204,8 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
} }
public ngOnChanges() { public ngOnChanges() {
this.accounts = this.user?.accounts ?? [];
this.dateRangeOptions = [ this.dateRangeOptions = [
{ label: $localize`Today`, value: '1d' }, { label: $localize`Today`, value: '1d' },
{ {
@ -280,6 +273,23 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
emitEvent: false 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[] }) { 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'; import { Directive, ElementRef } from '@angular/core';
@Directive({ @Directive({
selector: '[gf-carousel-item]' selector: '[gfCarouselItem]'
}) })
export class CarouselItem { export class CarouselItemDirective {
public constructor(readonly element: ElementRef<HTMLElement>) {} 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; r: number;
totalAmount: number; totalAmount: number;
}) { }) {
if (r == 0) { if (r === 0) {
// No compound interest // No compound interest
return (totalAmount - P) / PMT; return (totalAmount - P) / PMT;
} else if (totalAmount <= P) { } else if (totalAmount <= P) {

19
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -14,10 +14,11 @@ import {
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
EventEmitter,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@ -25,7 +26,6 @@ import { MatDialogModule } from '@angular/material/dialog';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { Router, RouterModule } from '@angular/router';
import { AssetSubClass } from '@prisma/client'; import { AssetSubClass } from '@prisma/client';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
@ -44,8 +44,7 @@ import { Subject, Subscription } from 'rxjs';
MatPaginatorModule, MatPaginatorModule,
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule
RouterModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-holdings-table', selector: 'gf-holdings-table',
@ -53,7 +52,7 @@ import { Subject, Subscription } from 'rxjs';
styleUrls: ['./holdings-table.component.scss'], styleUrls: ['./holdings-table.component.scss'],
templateUrl: './holdings-table.component.html' templateUrl: './holdings-table.component.html'
}) })
export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit { export class GfHoldingsTableComponent implements OnChanges, OnDestroy {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToCreateActivity: boolean; @Input() hasPermissionToCreateActivity: boolean;
@ -63,6 +62,8 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() locale = getLocale(); @Input() locale = getLocale();
@Input() pageSize = Number.MAX_SAFE_INTEGER; @Input() pageSize = Number.MAX_SAFE_INTEGER;
@Output() holdingClicked = new EventEmitter<AssetProfileIdentifier>();
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ -75,9 +76,7 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor(private router: Router) {} public constructor() {}
public ngOnInit() {}
public ngOnChanges() { public ngOnChanges() {
this.displayedColumns = ['icon', 'nameWithSymbol', 'dateOfFirstActivity']; this.displayedColumns = ['icon', 'nameWithSymbol', 'dateOfFirstActivity'];
@ -107,9 +106,7 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
public onOpenHoldingDialog({ dataSource, symbol }: AssetProfileIdentifier) { public onOpenHoldingDialog({ dataSource, symbol }: AssetProfileIdentifier) {
if (this.hasPermissionToOpenDetails) { if (this.hasPermissionToOpenDetails) {
this.router.navigate([], { this.holdingClicked.emit({ dataSource, symbol });
queryParams: { dataSource, symbol, holdingDetailDialog: true }
});
} }
} }

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; return name;
}); });

1
libs/ui/src/lib/shared/abstract-mat-form-field.ts

@ -16,6 +16,7 @@ import { Subject } from 'rxjs';
@Component({ @Component({
template: '' template: ''
}) })
// eslint-disable-next-line @angular-eslint/component-class-suffix
export abstract class AbstractMatFormField<T> export abstract class AbstractMatFormField<T>
implements ControlValueAccessor, DoCheck, MatFormFieldControl<T>, OnDestroy implements ControlValueAccessor, DoCheck, MatFormFieldControl<T>, OnDestroy
{ {

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

@ -31,6 +31,12 @@
} }
</small> </small>
</mat-option> </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> </mat-autocomplete>

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

@ -117,17 +117,17 @@ export class GfSymbolAutocompleteComponent
this.control.valueChanges this.control.valueChanges
.pipe( .pipe(
debounceTime(400),
distinctUntilChanged(),
filter((query) => { filter((query) => {
return isString(query) && query.length > 1; return isString(query) && query.length > 1;
}), }),
takeUntil(this.unsubscribeSubject),
tap(() => { tap(() => {
this.isLoading = true; this.isLoading = true;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}), }),
debounceTime(400),
distinctUntilChanged(),
takeUntil(this.unsubscribeSubject),
switchMap((query: string) => { switchMap((query: string) => {
return this.dataService.fetchSymbols({ return this.dataService.fetchSymbols({
query, query,

7
libs/ui/src/lib/top-holdings/top-holdings.component.ts

@ -10,7 +10,6 @@ import {
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@ -38,7 +37,7 @@ import { Subject } from 'rxjs';
styleUrls: ['./top-holdings.component.scss'], styleUrls: ['./top-holdings.component.scss'],
templateUrl: './top-holdings.component.html' templateUrl: './top-holdings.component.html'
}) })
export class GfTopHoldingsComponent implements OnChanges, OnDestroy, OnInit { export class GfTopHoldingsComponent implements OnChanges, OnDestroy {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() locale = getLocale(); @Input() locale = getLocale();
@Input() pageSize = Number.MAX_SAFE_INTEGER; @Input() pageSize = Number.MAX_SAFE_INTEGER;
@ -57,10 +56,6 @@ export class GfTopHoldingsComponent implements OnChanges, OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor() {}
public ngOnInit() {}
public ngOnChanges() { public ngOnChanges() {
this.isLoading = true; this.isLoading = true;

48
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -2,11 +2,13 @@ import {
getAnnualizedPerformancePercent, getAnnualizedPerformancePercent,
getIntervalFromDateRange getIntervalFromDateRange
} from '@ghostfolio/common/calculation-helper'; } from '@ghostfolio/common/calculation-helper';
import { getTooltipOptions } from '@ghostfolio/common/chart-helper';
import { getLocale } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
PortfolioPosition PortfolioPosition
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types'; import { ColorScheme, DateRange } from '@ghostfolio/common/types';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
@ -25,7 +27,7 @@ import { DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { ChartConfiguration } from 'chart.js'; import { ChartConfiguration } from 'chart.js';
import { LinearScale } 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 { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
import { differenceInDays, max } from 'date-fns'; import { differenceInDays, max } from 'date-fns';
import { orderBy } from 'lodash'; import { orderBy } from 'lodash';
@ -44,9 +46,12 @@ const { gray, green, red } = require('open-color');
export class GfTreemapChartComponent export class GfTreemapChartComponent
implements AfterViewInit, OnChanges, OnDestroy implements AfterViewInit, OnChanges, OnDestroy
{ {
@Input() baseCurrency: string;
@Input() colorScheme: ColorScheme;
@Input() cursor: string; @Input() cursor: string;
@Input() dateRange: DateRange; @Input() dateRange: DateRange;
@Input() holdings: PortfolioPosition[]; @Input() holdings: PortfolioPosition[];
@Input() locale = getLocale();
@Output() treemapChartClicked = new EventEmitter<AssetProfileIdentifier>(); @Output() treemapChartClicked = new EventEmitter<AssetProfileIdentifier>();
@ -58,7 +63,7 @@ export class GfTreemapChartComponent
public isLoading = true; public isLoading = true;
public constructor() { public constructor() {
Chart.register(LinearScale, TreemapController, TreemapElement); Chart.register(LinearScale, Tooltip, TreemapController, TreemapElement);
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -168,6 +173,9 @@ export class GfTreemapChartComponent
if (this.chartCanvas) { if (this.chartCanvas) {
if (this.chart) { if (this.chart) {
this.chart.data = data; this.chart.data = data;
this.chart.options.plugins.tooltip = <unknown>(
this.getTooltipPluginConfiguration()
);
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart(this.chartCanvas.nativeElement, {
@ -199,9 +207,7 @@ export class GfTreemapChartComponent
} }
}, },
plugins: { plugins: {
tooltip: { tooltip: this.getTooltipPluginConfiguration()
enabled: false
}
} }
}, },
type: 'treemap' type: 'treemap'
@ -211,4 +217,34 @@ export class GfTreemapChartComponent
this.isLoading = false; 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": { "compilerOptions": {
"forceConsistentCasingInFileNames": true, "target": "es2020",
// TODO: Remove once solved in tsconfig.base.json
"strict": false, "strict": false,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true
"target": "es2020"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"strictInjectionParameters": true, "strictInjectionParameters": true,

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

Loading…
Cancel
Save