Browse Source

Merge branch 'main' into main

pull/1318/head
alfredonodo 3 years ago
committed by GitHub
parent
commit
90680faba2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 62
      CHANGELOG.md
  2. 29
      angular.json
  3. 7
      apps/api/src/app/account/account.controller.ts
  4. 12
      apps/api/src/app/account/account.service.ts
  5. 12
      apps/api/src/app/account/create-account.dto.ts
  6. 12
      apps/api/src/app/account/update-account.dto.ts
  7. 25
      apps/api/src/app/benchmark/benchmark.service.ts
  8. 47
      apps/api/src/app/frontend.middleware.ts
  9. 24
      apps/api/src/app/info/info.service.ts
  10. 3
      apps/api/src/app/order/order.controller.ts
  11. 42
      apps/api/src/app/order/order.service.ts
  12. 29
      apps/api/src/app/portfolio/portfolio-calculator.ts
  13. 113
      apps/api/src/app/portfolio/portfolio.controller.ts
  14. 336
      apps/api/src/app/portfolio/portfolio.service.ts
  15. 56
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  16. 7
      apps/client/src/app/app-routing.module.ts
  17. 2
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  18. 6
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  19. 7
      apps/client/src/app/components/admin-jobs/admin-jobs.html
  20. 29
      apps/client/src/app/components/admin-overview/admin-overview.html
  21. 6
      apps/client/src/app/components/admin-users/admin-users.html
  22. 8
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html
  23. 51
      apps/client/src/app/components/home-overview/home-overview.component.ts
  24. 50
      apps/client/src/app/components/home-summary/home-summary.component.ts
  25. 11
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  26. 2
      apps/client/src/app/pages/account/account-page.component.ts
  27. 23
      apps/client/src/app/pages/account/account-page.html
  28. 7
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  29. 8
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
  30. 2
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts
  31. 20
      apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page-routing.module.ts
  32. 9
      apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.component.ts
  33. 178
      apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.html
  34. 13
      apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.module.ts
  35. 3
      apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.scss
  36. 24
      apps/client/src/app/pages/blog/blog-page.html
  37. 11
      apps/client/src/app/pages/features/features-page.html
  38. 16
      apps/client/src/app/pages/landing/landing-page.component.ts
  39. 165
      apps/client/src/app/pages/landing/landing-page.html
  40. 4
      apps/client/src/app/pages/landing/landing-page.module.ts
  41. 75
      apps/client/src/app/pages/landing/landing-page.scss
  42. 12
      apps/client/src/app/pages/portfolio/allocations/allocations-page.html
  43. 1
      apps/client/src/app/pages/portfolio/allocations/allocations-page.scss
  44. 5
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  45. 8
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  46. 50
      apps/client/src/app/services/data.service.ts
  47. BIN
      apps/client/src/assets/images/blog/hacktoberfest-2022.png
  48. 0
      apps/client/src/assets/images/logo-AGPLv3.svg
  49. 1
      apps/client/src/assets/images/logo-alternative-to.svg
  50. BIN
      apps/client/src/assets/images/logo-awesome.png
  51. BIN
      apps/client/src/assets/images/logo-openstartup.png
  52. 35
      apps/client/src/assets/images/logo-privacy-tools.svg
  53. BIN
      apps/client/src/assets/images/logo-product-hunt.png
  54. 9
      apps/client/src/assets/images/logo-unraid.svg
  55. 38
      apps/client/src/assets/sitemap.xml
  56. 122
      apps/client/src/locales/messages.de.xlf
  57. 4
      apps/client/src/locales/messages.es.xlf
  58. 14
      apps/client/src/locales/messages.it.xlf
  59. 2678
      apps/client/src/locales/messages.nl.xlf
  60. 120
      apps/client/src/locales/messages.xlf
  61. 51
      apps/client/src/styles.scss
  62. 8
      libs/common/src/lib/helper.ts
  63. 2
      libs/common/src/lib/interfaces/historical-data-item.interface.ts
  64. 6
      libs/common/src/lib/interfaces/portfolio-details.interface.ts
  65. 3
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  66. 2
      libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts
  67. 1
      libs/common/src/lib/interfaces/statistics.interface.ts
  68. 2
      libs/ui/src/lib/line-chart/line-chart.component.ts
  69. 128
      libs/ui/src/lib/value/value.component.html
  70. 2
      libs/ui/src/lib/value/value.component.scss
  71. 1
      libs/ui/src/lib/value/value.component.ts
  72. 4
      libs/ui/src/lib/value/value.module.ts
  73. 6
      package.json
  74. 2
      prisma/migrations/20210605161257_added_symbol_profile/migration.sql
  75. 2
      prisma/migrations/20210612110542_added_auth_device/migration.sql
  76. 2
      prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql
  77. 4
      prisma/migrations/20210703194509_added_balance_to_account/migration.sql
  78. 2
      prisma/migrations/20210724160404_added_currency_to_symbol_profile/migration.sql
  79. 2
      prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql
  80. 2
      prisma/migrations/20210808075949_added_asset_class_to_symbol_profile/migration.sql
  81. 2
      prisma/migrations/20210815180121_added_settings_to_settings/migration.sql
  82. 2
      prisma/migrations/20210822200534_added_asset_sub_class_to_symbol_profile/migration.sql
  83. 2
      prisma/migrations/20210916182355_added_data_source_to_market_data/migration.sql
  84. 2
      prisma/migrations/20211107082008_added_symbol_mapping_to_symbol_profile/migration.sql
  85. 2
      prisma/migrations/20211107171624_added_scraper_configuration_to_symbol_profile/migration.sql
  86. 2
      prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql
  87. 2
      prisma/migrations/20220924175215_added_is_excluded_to_account/migration.sql
  88. 1
      prisma/schema.prisma
  89. 36
      yarn.lock

62
CHANGELOG.md

@ -5,6 +5,66 @@ 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).
## 1.201.0 - 01.10.2022
### Added
- Added a blog post: _Hacktoberfest 2022_
### Changed
- Improved the usage of the value component in the admin control panel
- Improved the language localization for Español (`es`)
### Fixed
- Fixed the usage of the value component on the allocations page
## 1.200.0 - 01.10.2022
### Added
- Added a mini statistics section to the landing page including pulls on _Docker Hub_
- Added an _As seen in_ section to the landing page
- Added support for an icon in the value component
### Changed
- Upgraded `prisma` from version `4.1.1` to `4.4.0`
## 1.199.1 - 27.09.2022
### Added
- Set up the language localization for Español (`es`)
- Added support for sectors in mutual funds
## 1.198.0 - 25.09.2022
### Added
- Added support to exclude an account from analysis
- Set up the language localization for Nederlands (`nl`)
## 1.197.0 - 24.09.2022
### Added
- Added the value of the active filter in percentage on the allocations page
- Extended the feature overview page by multi-language support (English, German, Italian)
### Changed
- Combined the performance and chart calculation
- Improved the style of various selectors (density)
## 1.196.0 - 22.09.2022
### Added
- Set up the language localization for Italiano (`it`)
- Extended the landing page
## 1.195.0 - 20.09.2022 ## 1.195.0 - 20.09.2022
### Changed ### Changed
@ -195,7 +255,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow - Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow
- Set up language localization for German (`de`) - Set up the language localization for German (`de`)
- Resolved the feature graphic of the blog post - Resolved the feature graphic of the blog post
### Changed ### Changed

29
angular.json

@ -136,10 +136,18 @@
"baseHref": "/en/", "baseHref": "/en/",
"localize": ["en"] "localize": ["en"]
}, },
"development-es": {
"baseHref": "/es/",
"localize": ["es"]
},
"development-it": { "development-it": {
"baseHref": "/it/", "baseHref": "/it/",
"localize": ["it"] "localize": ["it"]
}, },
"development-nl": {
"baseHref": "/nl/",
"localize": ["nl"]
},
"production": { "production": {
"fileReplacements": [ "fileReplacements": [
{ {
@ -184,9 +192,15 @@
"development-en": { "development-en": {
"browserTarget": "client:build:development-en" "browserTarget": "client:build:development-en"
}, },
"development-es": {
"browserTarget": "client:build:development-es"
},
"development-it": { "development-it": {
"browserTarget": "client:build:development-it" "browserTarget": "client:build:development-it"
}, },
"development-nl": {
"browserTarget": "client:build:development-nl"
},
"production": { "production": {
"browserTarget": "client:build:production" "browserTarget": "client:build:production"
} }
@ -198,7 +212,12 @@
"browserTarget": "client:build", "browserTarget": "client:build",
"includeContext": true, "includeContext": true,
"outputPath": "src/locales", "outputPath": "src/locales",
"targetFiles": ["messages.de.xlf", "messages.it.xlf"] "targetFiles": [
"messages.de.xlf",
"messages.es.xlf",
"messages.it.xlf",
"messages.nl.xlf"
]
} }
}, },
"lint": { "lint": {
@ -222,9 +241,17 @@
"baseHref": "/de/", "baseHref": "/de/",
"translation": "apps/client/src/locales/messages.de.xlf" "translation": "apps/client/src/locales/messages.de.xlf"
}, },
"es": {
"baseHref": "/es/",
"translation": "apps/client/src/locales/messages.es.xlf"
},
"it": { "it": {
"baseHref": "/it/", "baseHref": "/it/",
"translation": "apps/client/src/locales/messages.it.xlf" "translation": "apps/client/src/locales/messages.it.xlf"
},
"nl": {
"baseHref": "/nl/",
"translation": "apps/client/src/locales/messages.nl.xlf"
} }
}, },
"sourceLocale": "en" "sourceLocale": "en"

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

@ -96,7 +96,9 @@ export class AccountController {
let accountsWithAggregations = let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations( await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id impersonationUserId || this.request.user.id,
undefined,
true
); );
if ( if (
@ -139,7 +141,8 @@ export class AccountController {
let accountsWithAggregations = let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations( await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id, impersonationUserId || this.request.user.id,
[{ id, type: 'ACCOUNT' }] [{ id, type: 'ACCOUNT' }],
true
); );
if ( if (

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

@ -107,15 +107,23 @@ export class AccountService {
public async getCashDetails({ public async getCashDetails({
currency, currency,
filters = [], filters = [],
userId userId,
withExcludedAccounts = false
}: { }: {
currency: string; currency: string;
filters?: Filter[]; filters?: Filter[];
userId: string; userId: string;
withExcludedAccounts?: boolean;
}): Promise<CashDetails> { }): Promise<CashDetails> {
let totalCashBalanceInBaseCurrency = new Big(0); let totalCashBalanceInBaseCurrency = new Big(0);
const where: Prisma.AccountWhereInput = { userId }; const where: Prisma.AccountWhereInput = {
userId
};
if (withExcludedAccounts === false) {
where.isExcluded = false;
}
const { const {
ACCOUNT: filtersByAccount, ACCOUNT: filtersByAccount,

12
apps/api/src/app/account/create-account.dto.ts

@ -1,5 +1,11 @@
import { AccountType } from '@prisma/client'; import { AccountType } from '@prisma/client';
import { IsNumber, IsString, ValidateIf } from 'class-validator'; import {
IsBoolean,
IsNumber,
IsOptional,
IsString,
ValidateIf
} from 'class-validator';
export class CreateAccountDto { export class CreateAccountDto {
@IsString() @IsString()
@ -11,6 +17,10 @@ export class CreateAccountDto {
@IsString() @IsString()
currency: string; currency: string;
@IsBoolean()
@IsOptional()
isExcluded?: boolean;
@IsString() @IsString()
name: string; name: string;

12
apps/api/src/app/account/update-account.dto.ts

@ -1,5 +1,11 @@
import { AccountType } from '@prisma/client'; import { AccountType } from '@prisma/client';
import { IsNumber, IsString, ValidateIf } from 'class-validator'; import {
IsBoolean,
IsNumber,
IsOptional,
IsString,
ValidateIf
} from 'class-validator';
export class UpdateAccountDto { export class UpdateAccountDto {
@IsString() @IsString()
@ -14,6 +20,10 @@ export class UpdateAccountDto {
@IsString() @IsString()
id: string; id: string;
@IsBoolean()
@IsOptional()
isExcluded?: boolean;
@IsString() @IsString()
name: string; name: string;

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

@ -164,7 +164,7 @@ export class BenchmarkService {
); );
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0; const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
return { const response = {
marketData: [ marketData: [
...marketDataItems ...marketDataItems
.filter((marketDataItem, index) => { .filter((marketDataItem, index) => {
@ -181,17 +181,22 @@ export class BenchmarkService {
marketDataItem.marketPrice marketDataItem.marketPrice
) * 100 ) * 100
}; };
}), })
{
date: format(new Date(), DATE_FORMAT),
value:
this.calculateChangeInPercentage(
marketPriceAtStartDate,
currentSymbolItem.marketPrice
) * 100
}
] ]
}; };
if (currentSymbolItem?.marketPrice) {
response.marketData.push({
date: format(new Date(), DATE_FORMAT),
value:
this.calculateChangeInPercentage(
marketPriceAtStartDate,
currentSymbolItem.marketPrice
) * 100
});
}
return response;
} }
private getMarketCondition(aPerformanceInPercent: number) { private getMarketCondition(aPerformanceInPercent: number) {

47
apps/api/src/app/frontend.middleware.ts

@ -11,6 +11,9 @@ import { NextFunction, Request, Response } from 'express';
export class FrontendMiddleware implements NestMiddleware { export class FrontendMiddleware implements NestMiddleware {
public indexHtmlDe = ''; public indexHtmlDe = '';
public indexHtmlEn = ''; public indexHtmlEn = '';
public indexHtmlEs = '';
public indexHtmlIt = '';
public indexHtmlNl = '';
public isProduction: boolean; public isProduction: boolean;
public constructor( public constructor(
@ -32,6 +35,18 @@ export class FrontendMiddleware implements NestMiddleware {
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE), this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
'utf8' 'utf8'
); );
this.indexHtmlEs = fs.readFileSync(
this.getPathOfIndexHtmlFile('es'),
'utf8'
);
this.indexHtmlIt = fs.readFileSync(
this.getPathOfIndexHtmlFile('it'),
'utf8'
);
this.indexHtmlNl = fs.readFileSync(
this.getPathOfIndexHtmlFile('nl'),
'utf8'
);
} catch {} } catch {}
} }
@ -43,6 +58,11 @@ export class FrontendMiddleware implements NestMiddleware {
req.path === '/en/blog/2022/08/500-stars-on-github/' req.path === '/en/blog/2022/08/500-stars-on-github/'
) { ) {
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg'; featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
} else if (
req.path === '/en/blog/2022/10/hacktoberfest-2022' ||
req.path === '/en/blog/2022/10/hacktoberfest-2022/'
) {
featureGraphicPath = 'assets/images/blog/hacktoberfest-2022.png';
} }
if ( if (
@ -61,6 +81,33 @@ export class FrontendMiddleware implements NestMiddleware {
rootUrl: this.configurationService.get('ROOT_URL') rootUrl: this.configurationService.get('ROOT_URL')
}) })
); );
} else if (req.path === '/es' || req.path.startsWith('/es/')) {
res.send(
this.interpolate(this.indexHtmlEs, {
featureGraphicPath,
languageCode: 'es',
path: req.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (req.path === '/it' || req.path.startsWith('/it/')) {
res.send(
this.interpolate(this.indexHtmlIt, {
featureGraphicPath,
languageCode: 'it',
path: req.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (req.path === '/nl' || req.path.startsWith('/nl/')) {
res.send(
this.interpolate(this.indexHtmlNl, {
featureGraphicPath,
languageCode: 'nl',
path: req.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else { } else {
res.send( res.send(
this.interpolate(this.indexHtmlEn, { this.interpolate(this.indexHtmlEn, {

24
apps/api/src/app/info/info.service.ts

@ -145,6 +145,27 @@ export class InfoService {
}); });
} }
private async countDockerHubPulls(): Promise<number> {
try {
const get = bent(
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
'GET',
'json',
200,
{
'User-Agent': 'request'
}
);
const { pull_count } = await get();
return pull_count;
} catch (error) {
Logger.error(error, 'InfoService');
return undefined;
}
}
private async countGitHubContributors(): Promise<number> { private async countGitHubContributors(): Promise<number> {
try { try {
const get = bent( const get = bent(
@ -245,6 +266,8 @@ export class InfoService {
const activeUsers1d = await this.countActiveUsers(1); const activeUsers1d = await this.countActiveUsers(1);
const activeUsers30d = await this.countActiveUsers(30); const activeUsers30d = await this.countActiveUsers(30);
const newUsers30d = await this.countNewUsers(30); const newUsers30d = await this.countNewUsers(30);
const dockerHubPulls = await this.countDockerHubPulls();
const gitHubContributors = await this.countGitHubContributors(); const gitHubContributors = await this.countGitHubContributors();
const gitHubStargazers = await this.countGitHubStargazers(); const gitHubStargazers = await this.countGitHubStargazers();
const slackCommunityUsers = await this.countSlackCommunityUsers(); const slackCommunityUsers = await this.countSlackCommunityUsers();
@ -252,6 +275,7 @@ export class InfoService {
statistics = { statistics = {
activeUsers1d, activeUsers1d,
activeUsers30d, activeUsers30d,
dockerHubPulls,
gitHubContributors, gitHubContributors,
gitHubStargazers, gitHubStargazers,
newUsers30d, newUsers30d,

3
apps/api/src/app/order/order.controller.ts

@ -109,7 +109,8 @@ export class OrderController {
filters, filters,
userCurrency, userCurrency,
includeDrafts: true, includeDrafts: true,
userId: impersonationUserId || this.request.user.id userId: impersonationUserId || this.request.user.id,
withExcludedAccounts: true
}); });
if ( if (

42
apps/api/src/app/order/order.service.ts

@ -189,13 +189,15 @@ export class OrderService {
includeDrafts = false, includeDrafts = false,
types, types,
userCurrency, userCurrency,
userId userId,
withExcludedAccounts = false
}: { }: {
filters?: Filter[]; filters?: Filter[];
includeDrafts?: boolean; includeDrafts?: boolean;
types?: TypeOfOrder[]; types?: TypeOfOrder[];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
withExcludedAccounts?: boolean;
}): Promise<Activity[]> { }): Promise<Activity[]> {
const where: Prisma.OrderWhereInput = { userId }; const where: Prisma.OrderWhereInput = { userId };
@ -284,24 +286,28 @@ export class OrderService {
}, },
orderBy: { date: 'asc' } orderBy: { date: 'asc' }
}) })
).map((order) => { )
const value = new Big(order.quantity).mul(order.unitPrice).toNumber(); .filter((order) => {
return withExcludedAccounts || order.Account?.isExcluded === false;
return { })
...order, .map((order) => {
value, const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee, return {
order.SymbolProfile.currency, ...order,
userCurrency
),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value, value,
order.SymbolProfile.currency, feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
userCurrency order.fee,
) order.SymbolProfile.currency,
}; userCurrency
}); ),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
order.SymbolProfile.currency,
userCurrency
)
};
});
} }
public async updateOrder({ public async updateOrder({

29
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -272,23 +272,20 @@ export class PortfolioCalculator {
} }
} }
const isInPercentage = true;
return Object.keys(totalNetPerformanceValues).map((date) => { return Object.keys(totalNetPerformanceValues).map((date) => {
return isInPercentage const netPerformanceInPercentage = totalInvestmentValues[date].eq(0)
? { ? 0
date, : totalNetPerformanceValues[date]
value: totalInvestmentValues[date].eq(0) .div(totalInvestmentValues[date])
? 0 .mul(100)
: totalNetPerformanceValues[date] .toNumber();
.div(totalInvestmentValues[date])
.mul(100) return {
.toNumber() date,
} netPerformanceInPercentage,
: { netPerformance: totalNetPerformanceValues[date].toNumber(),
date, value: netPerformanceInPercentage
value: totalNetPerformanceValues[date].toNumber() };
};
}); });
} }

113
apps/api/src/app/portfolio/portfolio.controller.ts

@ -110,26 +110,6 @@ export class PortfolioController {
}; };
} }
@Get('chart')
@UseGuards(AuthGuard('jwt'))
@Version('2')
public async getChartV2(
@Headers('impersonation-id') impersonationId: string,
@Query('range') range
): Promise<PortfolioChart> {
const historicalDataContainer = await this.portfolioService.getChartV2(
impersonationId,
range
);
return {
chart: historicalDataContainer.items,
hasError: false,
isAllTimeHigh: false,
isAllTimeLow: false
};
}
@Get('details') @Get('details')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@ -168,12 +148,15 @@ export class PortfolioController {
}) })
]; ];
let portfolioSummary: PortfolioSummary;
const { const {
accounts, accounts,
filteredValueInBaseCurrency, filteredValueInBaseCurrency,
filteredValueInPercentage, filteredValueInPercentage,
hasErrors, hasErrors,
holdings, holdings,
summary,
totalValueInBaseCurrency totalValueInBaseCurrency
} = await this.portfolioService.getDetails( } = await this.portfolioService.getDetails(
impersonationId, impersonationId,
@ -186,6 +169,8 @@ export class PortfolioController {
hasError = true; hasError = true;
} }
portfolioSummary = summary;
if ( if (
impersonationId || impersonationId ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
@ -219,6 +204,22 @@ export class PortfolioController {
accounts[name].current = current / totalValue; accounts[name].current = current / totalValue;
accounts[name].original = original / totalInvestment; accounts[name].original = original / totalInvestment;
} }
portfolioSummary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
'items',
'netWorth',
'totalBuy',
'totalSell'
]);
} }
let hasDetails = true; let hasDetails = true;
@ -244,7 +245,8 @@ export class PortfolioController {
filteredValueInPercentage, filteredValueInPercentage,
hasError, hasError,
holdings, holdings,
totalValueInBaseCurrency totalValueInBaseCurrency,
summary: hasDetails ? portfolioSummary : undefined
}; };
} }
@ -319,6 +321,35 @@ export class PortfolioController {
return performanceInformation; return performanceInformation;
} }
@Get('performance')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@Version('2')
public async getPerformanceV2(
@Headers('impersonation-id') impersonationId: string,
@Query('range') dateRange
): Promise<PortfolioPerformanceResponse> {
const performanceInformation = await this.portfolioService.getPerformanceV2(
{
dateRange,
impersonationId
}
);
if (
impersonationId ||
this.request.user.Settings.settings.viewMode === 'ZEN' ||
this.userService.isRestrictedView(this.request.user)
) {
performanceInformation.performance = nullifyValuesInObject(
performanceInformation.performance,
['currentGrossPerformance', 'currentNetPerformance', 'currentValue']
);
}
return performanceInformation;
}
@Get('positions') @Get('positions')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
@ -411,46 +442,6 @@ export class PortfolioController {
return portfolioPublicDetails; return portfolioPublicDetails;
} }
@Get('summary')
@UseGuards(AuthGuard('jwt'))
public async getSummary(
@Headers('impersonation-id') impersonationId
): Promise<PortfolioSummary> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
let summary = await this.portfolioService.getSummary(impersonationId);
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
summary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'fees',
'items',
'netWorth',
'totalBuy',
'totalSell'
]);
}
return summary;
}
@Get('position/:dataSource/:symbol') @Get('position/:dataSource/:symbol')
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)

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

@ -50,8 +50,11 @@ import type {
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { import {
Account,
AssetClass, AssetClass,
DataSource, DataSource,
Order,
Platform,
Prisma, Prisma,
Tag, Tag,
Type as TypeOfOrder Type as TypeOfOrder
@ -106,7 +109,8 @@ export class PortfolioService {
public async getAccounts( public async getAccounts(
aUserId: string, aUserId: string,
aFilters?: Filter[] aFilters?: Filter[],
withExcludedAccounts = false
): Promise<AccountWithValue[]> { ): Promise<AccountWithValue[]> {
const where: Prisma.AccountWhereInput = { userId: aUserId }; const where: Prisma.AccountWhereInput = { userId: aUserId };
@ -120,7 +124,13 @@ export class PortfolioService {
include: { Order: true, Platform: true }, include: { Order: true, Platform: true },
orderBy: { name: 'asc' } orderBy: { name: 'asc' }
}), }),
this.getDetails(aUserId, aUserId, undefined, aFilters) this.getDetails(
aUserId,
aUserId,
undefined,
aFilters,
withExcludedAccounts
)
]); ]);
const userCurrency = this.request.user.Settings.settings.baseCurrency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
@ -160,9 +170,14 @@ export class PortfolioService {
public async getAccountsWithAggregations( public async getAccountsWithAggregations(
aUserId: string, aUserId: string,
aFilters?: Filter[] aFilters?: Filter[],
withExcludedAccounts = false
): Promise<Accounts> { ): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId, aFilters); const accounts = await this.getAccounts(
aUserId,
aFilters,
withExcludedAccounts
);
let totalBalanceInBaseCurrency = new Big(0); let totalBalanceInBaseCurrency = new Big(0);
let totalValueInBaseCurrency = new Big(0); let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0; let transactionCount = 0;
@ -355,11 +370,14 @@ export class PortfolioService {
}; };
} }
public async getChartV2( public async getChartV2({
aImpersonationId: string, dateRange = 'max',
aDateRange: DateRange = 'max' impersonationId
): Promise<HistoricalDataContainer> { }: {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); dateRange?: DateRange;
impersonationId: string;
}): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } = const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({ await this.getTransactionPoints({
@ -383,7 +401,7 @@ export class PortfolioService {
const endDate = new Date(); const endDate = new Date();
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(aDateRange, portfolioStart); const startDate = this.getStartDate(dateRange, portfolioStart);
const daysInMarket = differenceInDays(new Date(), startDate); const daysInMarket = differenceInDays(new Date(), startDate);
const step = Math.round( const step = Math.round(
@ -407,7 +425,8 @@ export class PortfolioService {
aImpersonationId: string, aImpersonationId: string,
aUserId: string, aUserId: string,
aDateRange: DateRange = 'max', aDateRange: DateRange = 'max',
aFilters?: Filter[] aFilters?: Filter[],
withExcludedAccounts = false
): Promise<PortfolioDetails & { hasErrors: boolean }> { ): Promise<PortfolioDetails & { hasErrors: boolean }> {
const userId = await this.getUserId(aImpersonationId, aUserId); const userId = await this.getUserId(aImpersonationId, aUserId);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
@ -423,6 +442,7 @@ export class PortfolioService {
const { orders, portfolioOrders, transactionPoints } = const { orders, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({ await this.getTransactionPoints({
userId, userId,
withExcludedAccounts,
filters: aFilters filters: aFilters
}); });
@ -577,6 +597,7 @@ export class PortfolioService {
portfolioItemsNow, portfolioItemsNow,
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts,
filters: aFilters filters: aFilters
}); });
@ -585,6 +606,7 @@ export class PortfolioService {
return { return {
accounts, accounts,
holdings, holdings,
summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth filteredValueInPercentage: summary.netWorth
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() ? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
@ -603,7 +625,11 @@ export class PortfolioService {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const orders = ( const orders = (
await this.orderService.getOrders({ userCurrency, userId }) await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ SymbolProfile }) => { ).filter(({ SymbolProfile }) => {
return ( return (
SymbolProfile.dataSource === aDataSource && SymbolProfile.dataSource === aDataSource &&
@ -987,6 +1013,105 @@ export class PortfolioService {
}; };
} }
public async getPerformanceV2({
dateRange = 'max',
impersonationId
}: {
dateRange?: DateRange;
impersonationId: string;
}): Promise<PortfolioPerformanceResponse> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
userId
});
const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
if (transactionPoints?.length <= 0) {
return {
chart: [],
hasErrors: false,
performance: {
currentGrossPerformance: 0,
currentGrossPerformancePercent: 0,
currentNetPerformance: 0,
currentNetPerformancePercent: 0,
currentValue: 0
}
};
}
portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(dateRange, portfolioStart);
const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate
);
const hasErrors = currentPositions.hasErrors;
const currentValue = currentPositions.currentValue.toNumber();
const currentGrossPerformance = currentPositions.grossPerformance;
const currentGrossPerformancePercent =
currentPositions.grossPerformancePercentage;
let currentNetPerformance = currentPositions.netPerformance;
let currentNetPerformancePercent =
currentPositions.netPerformancePercentage;
// if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) {
// // If algebraic sign is different, harmonize it
// currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1);
// }
// if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) {
// // If algebraic sign is different, harmonize it
// currentNetPerformancePercent = currentNetPerformancePercent.mul(-1);
// }
const historicalDataContainer = await this.getChartV2({
dateRange,
impersonationId
});
const itemOfToday = historicalDataContainer.items.find((item) => {
return item.date === format(new Date(), DATE_FORMAT);
});
if (itemOfToday) {
currentNetPerformance = new Big(itemOfToday.netPerformance);
currentNetPerformancePercent = new Big(
itemOfToday.netPerformanceInPercentage
).div(100);
}
return {
chart: historicalDataContainer.items.map(
({ date, netPerformanceInPercentage }) => {
return {
date,
value: netPerformanceInPercentage
};
}
),
errors: currentPositions.errors,
hasErrors: currentPositions.hasErrors || hasErrors,
performance: {
currentValue,
currentGrossPerformance: currentGrossPerformance.toNumber(),
currentGrossPerformancePercent:
currentGrossPerformancePercent.toNumber(),
currentNetPerformance: currentNetPerformance.toNumber(),
currentNetPerformancePercent: currentNetPerformancePercent.toNumber()
}
};
}
public async getReport(impersonationId: string): Promise<PortfolioReport> { public async getReport(impersonationId: string): Promise<PortfolioReport> {
const currency = this.request.user.Settings.settings.baseCurrency; const currency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(impersonationId, this.request.user.id); const userId = await this.getUserId(impersonationId, this.request.user.id);
@ -1079,74 +1204,6 @@ export class PortfolioService {
}; };
} }
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const performanceInformation = await this.getPerformance(aImpersonationId);
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
userId,
currency: userCurrency
});
const orders = await this.orderService.getOrders({
userCurrency,
userId
});
const dividend = this.getDividend(orders).toNumber();
const emergencyFund = new Big(
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
);
const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date;
const items = this.getItems(orders).toNumber();
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
const committedFunds = new Big(totalBuy).minus(totalSell);
const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue)
.plus(items)
.toNumber();
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
const annualizedPerformancePercent = new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
orders: []
})
.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercent
)
})
?.toNumber();
return {
...performanceInformation.performance,
annualizedPerformancePercent,
cash,
dividend,
fees,
firstOrderDate,
items,
netWorth,
totalBuy,
totalSell,
committedFunds: committedFunds.toNumber(),
emergencyFund: emergencyFund.toNumber(),
ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL';
}).length
};
}
private async getCashPositions({ private async getCashPositions({
cashDetails, cashDetails,
emergencyFund, emergencyFund,
@ -1322,14 +1379,117 @@ export class PortfolioService {
return portfolioStart; return portfolioStart;
} }
private async getSummary(
aImpersonationId: string
): Promise<PortfolioSummary> {
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const performanceInformation = await this.getPerformance(aImpersonationId);
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
userId,
currency: userCurrency
});
const orders = await this.orderService.getOrders({
userCurrency,
userId
});
const excludedActivities = (
await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ Account: account }) => {
return account?.isExcluded ?? false;
});
const dividend = this.getDividend(orders).toNumber();
const emergencyFund = new Big(
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
);
const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date;
const items = this.getItems(orders).toNumber();
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
const committedFunds = new Big(totalBuy).minus(totalSell);
const totalOfExcludedActivities = new Big(
this.getTotalByType(excludedActivities, userCurrency, 'BUY')
).minus(this.getTotalByType(excludedActivities, userCurrency, 'SELL'));
const cashDetailsWithExcludedAccounts =
await this.accountService.getCashDetails({
userId,
currency: userCurrency,
withExcludedAccounts: true
});
const excludedBalanceInBaseCurrency = new Big(
cashDetailsWithExcludedAccounts.balanceInBaseCurrency
).minus(balanceInBaseCurrency);
const excludedAccountsAndActivities = excludedBalanceInBaseCurrency
.plus(totalOfExcludedActivities)
.toNumber();
const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue)
.plus(items)
.plus(excludedAccountsAndActivities)
.toNumber();
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
const annualizedPerformancePercent = new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
orders: []
})
.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercent
)
})
?.toNumber();
return {
...performanceInformation.performance,
annualizedPerformancePercent,
cash,
dividend,
excludedAccountsAndActivities,
fees,
firstOrderDate,
items,
netWorth,
totalBuy,
totalSell,
committedFunds: committedFunds.toNumber(),
emergencyFund: emergencyFund.toNumber(),
ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL';
}).length
};
}
private async getTransactionPoints({ private async getTransactionPoints({
filters, filters,
includeDrafts = false, includeDrafts = false,
userId userId,
withExcludedAccounts
}: { }: {
filters?: Filter[]; filters?: Filter[];
includeDrafts?: boolean; includeDrafts?: boolean;
userId: string; userId: string;
withExcludedAccounts?: boolean;
}): Promise<{ }): Promise<{
transactionPoints: TransactionPoint[]; transactionPoints: TransactionPoint[];
orders: OrderWithAccount[]; orders: OrderWithAccount[];
@ -1343,6 +1503,7 @@ export class PortfolioService {
includeDrafts, includeDrafts,
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts,
types: ['BUY', 'SELL'] types: ['BUY', 'SELL']
}); });
@ -1394,17 +1555,22 @@ export class PortfolioService {
orders, orders,
portfolioItemsNow, portfolioItemsNow,
userCurrency, userCurrency,
userId userId,
withExcludedAccounts
}: { }: {
filters?: Filter[]; filters?: Filter[];
orders: OrderWithAccount[]; orders: OrderWithAccount[];
portfolioItemsNow: { [p: string]: TimelinePosition }; portfolioItemsNow: { [p: string]: TimelinePosition };
userCurrency: string; userCurrency: string;
userId: string; userId: string;
withExcludedAccounts?: boolean;
}) { }) {
const accounts: PortfolioDetails['accounts'] = {}; const accounts: PortfolioDetails['accounts'] = {};
let currentAccounts = []; let currentAccounts: (Account & {
Order?: Order[];
Platform?: Platform;
})[] = [];
if (filters.length === 0) { if (filters.length === 0) {
currentAccounts = await this.accountService.getAccounts(userId); currentAccounts = await this.accountService.getAccounts(userId);
@ -1424,6 +1590,10 @@ export class PortfolioService {
}); });
} }
currentAccounts = currentAccounts.filter((account) => {
return withExcludedAccounts || account.isExcluded === false;
});
for (const account of currentAccounts) { for (const account of currentAccounts) {
const ordersByAccount = orders.filter(({ accountId }) => { const ordersByAccount = orders.filter(({ accountId }) => {
return accountId === account.id; return accountId === account.id;

56
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -6,6 +6,7 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
@ -90,7 +91,7 @@ export class YahooFinanceService implements DataProviderInterface {
try { try {
const symbol = this.convertToYahooFinanceSymbol(aSymbol); const symbol = this.convertToYahooFinanceSymbol(aSymbol);
const assetProfile = await yahooFinance.quoteSummary(symbol, { const assetProfile = await yahooFinance.quoteSummary(symbol, {
modules: ['price', 'summaryProfile'] modules: ['price', 'summaryProfile', 'topHoldings']
}); });
const { assetClass, assetSubClass } = this.parseAssetClass( const { assetClass, assetSubClass } = this.parseAssetClass(
@ -109,7 +110,16 @@ export class YahooFinanceService implements DataProviderInterface {
}); });
response.symbol = aSymbol; response.symbol = aSymbol;
if ( if (assetSubClass === AssetSubClass.MUTUALFUND) {
response.sectors = [];
for (const sectorWeighting of assetProfile.topHoldings
?.sectorWeightings ?? []) {
for (const [sector, weight] of Object.entries(sectorWeighting)) {
response.sectors.push({ weight, name: this.parseSector(sector) });
}
}
} else if (
assetSubClass === AssetSubClass.STOCK && assetSubClass === AssetSubClass.STOCK &&
assetProfile.summaryProfile?.country assetProfile.summaryProfile?.country
) { ) {
@ -437,4 +447,46 @@ export class YahooFinanceService implements DataProviderInterface {
return { assetClass, assetSubClass }; return { assetClass, assetSubClass };
} }
private parseSector(aString: string): string {
let sector = UNKNOWN_KEY;
switch (aString) {
case 'basic_materials':
sector = 'Basic Materials';
break;
case 'communication_services':
sector = 'Communication Services';
break;
case 'consumer_cyclical':
sector = 'Consumer Cyclical';
break;
case 'consumer_defensive':
sector = 'Consumer Staples';
break;
case 'energy':
sector = 'Energy';
break;
case 'financial_services':
sector = 'Financial Services';
break;
case 'healthcare':
sector = 'Healthcare';
break;
case 'industrials':
sector = 'Industrials';
break;
case 'realestate':
sector = 'Real Estate';
break;
case 'technology':
sector = 'Technology';
break;
case 'utilities':
sector = 'Utilities';
break;
}
return sector;
}
} }

7
apps/client/src/app/app-routing.module.ts

@ -95,6 +95,13 @@ const routes: Routes = [
'./pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.module' './pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.module'
).then((m) => m.FiveHundredStarsOnGitHubPageModule) ).then((m) => m.FiveHundredStarsOnGitHubPageModule)
}, },
{
path: 'blog/2022/10/hacktoberfest-2022',
loadChildren: () =>
import(
'./pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.module'
).then((m) => m.Hacktoberfest2022PageModule)
},
{ {
path: 'demo', path: 'demo',
loadChildren: () => loadChildren: () =>

2
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -61,7 +61,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => { .subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
this.accountType = accountType; this.accountType = accountType;
this.name = name; this.name = name;
this.platformName = Platform?.name; this.platformName = Platform?.name ?? '-';
this.valueInBaseCurrency = valueInBaseCurrency; this.valueInBaseCurrency = valueInBaseCurrency;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();

6
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -21,10 +21,12 @@
<div class="row"> <div class="row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value size="medium" [value]="accountType">Account Type</gf-value> <gf-value i18n size="medium" [value]="accountType"
>Account Type</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value size="medium" [value]="platformName">Platform</gf-value> <gf-value i18n size="medium" [value]="platformName">Platform</gf-value>
</div> </div>
</div> </div>

7
apps/client/src/app/components/admin-jobs/admin-jobs.html

@ -2,7 +2,10 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<form class="align-items-center d-flex" [formGroup]="filterForm"> <form class="align-items-center d-flex" [formGroup]="filterForm">
<mat-form-field appearance="outline" class="flex-grow-1"> <mat-form-field
appearance="outline"
class="compact-with-outline flex-grow-1 mr-2 without-hint"
>
<mat-select formControlName="status"> <mat-select formControlName="status">
<mat-option></mat-option> <mat-option></mat-option>
<mat-option <mat-option
@ -13,7 +16,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<button <button
class="ml-1" class="mt-1"
color="warn" color="warn"
mat-flat-button mat-flat-button
(click)="onDeleteJobs()" (click)="onDeleteJobs()"

29
apps/client/src/app/components/admin-overview/admin-overview.html

@ -5,15 +5,26 @@
<mat-card-content> <mat-card-content>
<div class="d-flex my-3"> <div class="d-flex my-3">
<div class="w-50" i18n>User Count</div> <div class="w-50" i18n>User Count</div>
<div class="w-50">{{ userCount }}</div> <div class="w-50">
<gf-value
precision="0"
[locale]="user?.settings?.locale"
[value]="userCount"
></gf-value>
</div>
</div> </div>
<div class="d-flex my-3"> <div class="d-flex my-3">
<div class="w-50" i18n>Activity Count</div> <div class="w-50" i18n>Activity Count</div>
<div class="w-50"> <div class="w-50">
<ng-container *ngIf="transactionCount"> <gf-value
{{ transactionCount }} ({{ transactionCount / userCount | number precision="0"
: '1.2-2' }} <span i18n>per User</span>) [locale]="user?.settings?.locale"
</ng-container> [value]="transactionCount"
></gf-value>
<div *ngIf="transactionCount && userCount">
{{ transactionCount / userCount | number : '1.2-2' }}
<span i18n>per User</span>
</div>
</div> </div>
</div> </div>
<div class="d-flex my-3"> <div class="d-flex my-3">
@ -162,8 +173,11 @@
</button> </button>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<form #couponForm="ngForm"> <form #couponForm="ngForm" class="align-items-center d-flex">
<mat-form-field appearance="outline" class="mr-2"> <mat-form-field
appearance="outline"
class="compact-with-outline mr-2 without-hint"
>
<mat-select <mat-select
name="duration" name="duration"
[value]="couponDuration" [value]="couponDuration"
@ -176,6 +190,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<button <button
class="mt-1"
color="primary" color="primary"
mat-flat-button mat-flat-button
(click)="onAddCoupon()" (click)="onAddCoupon()"

6
apps/client/src/app/components/admin-users/admin-users.html

@ -45,21 +45,21 @@
</td> </td>
<td class="mat-cell px-1 py-2"> <td class="mat-cell px-1 py-2">
<gf-value <gf-value
class="align-items-end" class="justify-content-end"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="userItem.accountCount" [value]="userItem.accountCount"
></gf-value> ></gf-value>
</td> </td>
<td class="mat-cell px-1 py-2"> <td class="mat-cell px-1 py-2">
<gf-value <gf-value
class="align-items-end" class="justify-content-end"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="userItem.transactionCount" [value]="userItem.transactionCount"
></gf-value> ></gf-value>
</td> </td>
<td class="mat-cell px-1 py-2"> <td class="mat-cell px-1 py-2">
<gf-value <gf-value
class="align-items-end" class="justify-content-end"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[precision]="0" [precision]="0"
[value]="userItem.engagement" [value]="userItem.engagement"

8
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html

@ -10,7 +10,11 @@
</div> </div>
</div> </div>
<div class="col-md-6 col-xs-12 d-flex justify-content-end"> <div class="col-md-6 col-xs-12 d-flex justify-content-end">
<mat-form-field appearance="outline" class="w-100" color="accent"> <mat-form-field
appearance="outline"
class="w-100 without-hint"
color="accent"
>
<mat-label i18n>Compare with...</mat-label> <mat-label i18n>Compare with...</mat-label>
<mat-select <mat-select
name="benchmark" name="benchmark"
@ -26,7 +30,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center"> <div *ngIf="user.settings.viewMode !== 'ZEN'" class="my-2 text-center">
<gf-toggle <gf-toggle
[defaultValue]="user?.settings?.dateRange" [defaultValue]="user?.settings?.dateRange"
[isLoading]="isLoading" [isLoading]="isLoading"

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

@ -76,8 +76,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
!this.hasImpersonationId && !this.hasImpersonationId &&
!this.user.settings.isRestrictedView && !this.user.settings.isRestrictedView &&
this.user.settings.viewMode !== 'ZEN'; this.user.settings.viewMode !== 'ZEN';
this.update();
} }
public onChangeDateRange(dateRange: DateRange) { public onChangeDateRange(dateRange: DateRange) {
@ -104,36 +102,51 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
} }
private update() { private update() {
this.historicalDataItems = null;
this.isLoadingPerformance = true; this.isLoadingPerformance = true;
this.dataService this.dataService
.fetchChart({ .fetchPortfolioPerformance({
range: this.user?.settings?.dateRange, range: this.user?.settings?.dateRange,
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1 version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
});
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioPerformance({ range: this.user?.settings?.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe((response) => {
this.errors = response.errors; this.errors = response.errors;
this.hasError = response.hasErrors; this.hasError = response.hasErrors;
this.performance = response.performance; this.performance = response.performance;
this.isLoadingPerformance = false; this.isLoadingPerformance = false;
if (this.user?.settings?.isExperimentalFeatures) {
this.historicalDataItems = response.chart.map(({ date, value }) => {
return {
date,
value
};
});
} else {
this.dataService
.fetchChart({
range: this.user?.settings?.dateRange,
version: 1
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map(
({ date, value }) => {
return {
date,
value
};
}
);
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });

50
apps/client/src/app/components/home-summary/home-summary.component.ts

@ -1,8 +1,18 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import {
MatSnackBar,
MatSnackBarRef,
TextOnlySnackBar
} from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { PortfolioSummary, User } from '@ghostfolio/common/interfaces'; import {
InfoItem,
PortfolioSummary,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -14,8 +24,11 @@ import { takeUntil } from 'rxjs/operators';
}) })
export class HomeSummaryComponent implements OnDestroy, OnInit { export class HomeSummaryComponent implements OnDestroy, OnInit {
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToUpdateUserSettings: boolean; public hasPermissionToUpdateUserSettings: boolean;
public info: InfoItem;
public isLoading = true; public isLoading = true;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
public summary: PortfolioSummary; public summary: PortfolioSummary;
public user: User; public user: User;
@ -25,8 +38,17 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private router: Router,
private snackBar: MatSnackBar,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions,
permissions.enableSubscription
);
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
@ -50,8 +72,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
.subscribe((aId) => { .subscribe((aId) => {
this.hasImpersonationId = !!aId; this.hasImpersonationId = !!aId;
}); });
this.update();
} }
public onChangeEmergencyFund(emergencyFund: number) { public onChangeEmergencyFund(emergencyFund: number) {
@ -81,12 +101,30 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
this.isLoading = true; this.isLoading = true;
this.dataService this.dataService
.fetchPortfolioSummary() .fetchPortfolioDetails({})
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe(({ summary }) => {
this.summary = response; this.summary = summary;
this.isLoading = false; this.isLoading = false;
if (!this.summary) {
this.snackBarRef = this.snackBar.open(
$localize`This feature requires a subscription.`,
this.hasPermissionForSubscription
? $localize`Upgrade Plan`
: undefined,
{ duration: 6000 }
);
this.snackBarRef.afterDismissed().subscribe(() => {
this.snackBarRef = undefined;
});
this.snackBarRef.onAction().subscribe(() => {
this.router.navigate(['/pricing']);
});
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });

11
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -172,6 +172,17 @@
></gf-value> ></gf-value>
</div> </div>
</div> </div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Excluded from Analysis</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.excludedAccountsAndActivities"
></gf-value>
</div>
</div>
<div class="row"> <div class="row">
<div class="col"><hr /></div> <div class="col"><hr /></div>
</div> </div>

2
apps/client/src/app/pages/account/account-page.component.ts

@ -54,7 +54,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
public hasPermissionToUpdateViewMode: boolean; public hasPermissionToUpdateViewMode: boolean;
public hasPermissionToUpdateUserSettings: boolean; public hasPermissionToUpdateUserSettings: boolean;
public language = document.documentElement.lang; public language = document.documentElement.lang;
public locales = ['de', 'de-CH', 'en-GB', 'en-US']; public locales = ['de', 'de-CH', 'en-GB', 'en-US', 'es', 'it', 'nl'];
public price: number; public price: number;
public priceId: string; public priceId: string;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>; public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;

23
apps/client/src/app/pages/account/account-page.html

@ -94,7 +94,10 @@
<ng-container i18n>Base Currency</ng-container> <ng-container i18n>Base Currency</ng-container>
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select <mat-select
name="baseCurrency" name="baseCurrency"
[disabled]="!hasPermissionToUpdateUserSettings" [disabled]="!hasPermissionToUpdateUserSettings"
@ -116,7 +119,10 @@
<div class="hint-text text-muted" i18n>Beta</div> <div class="hint-text text-muted" i18n>Beta</div>
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select <mat-select
name="language" name="language"
[disabled]="!hasPermissionToUpdateUserSettings" [disabled]="!hasPermissionToUpdateUserSettings"
@ -126,6 +132,9 @@
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>
<mat-option value="de">Deutsch</mat-option> <mat-option value="de">Deutsch</mat-option>
<mat-option value="en">English</mat-option> <mat-option value="en">English</mat-option>
<mat-option value="es">Español</mat-option>
<mat-option value="it">Italiano</mat-option>
<mat-option value="nl">Nederlands</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -138,7 +147,10 @@
</div> </div>
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select <mat-select
name="locale" name="locale"
[disabled]="!hasPermissionToUpdateUserSettings" [disabled]="!hasPermissionToUpdateUserSettings"
@ -161,7 +173,10 @@
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<div class="align-items-center d-flex overflow-hidden"> <div class="align-items-center d-flex overflow-hidden">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select <mat-select
name="viewMode" name="viewMode"
[disabled]="!hasPermissionToUpdateViewMode" [disabled]="!hasPermissionToUpdateViewMode"

7
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -59,8 +59,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.openCreateAccountDialog(); this.openCreateAccountDialog();
} else if (params['editDialog']) { } else if (params['editDialog']) {
if (this.accounts) { if (this.accounts) {
const account = this.accounts.find((account) => { const account = this.accounts.find(({ id }) => {
return account.id === params['accountId']; return id === params['accountId'];
}); });
this.openUpdateAccountDialog(account); this.openUpdateAccountDialog(account);
@ -155,6 +155,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
balance, balance,
currency, currency,
id, id,
isExcluded,
name, name,
platformId platformId
}: AccountModel): void { }: AccountModel): void {
@ -165,6 +166,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
balance, balance,
currency, currency,
id, id,
isExcluded,
name, name,
platformId platformId
} }
@ -231,6 +233,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
accountType: AccountType.SECURITIES, accountType: AccountType.SECURITIES,
balance: 0, balance: 0,
currency: this.user?.settings?.baseCurrency, currency: this.user?.settings?.baseCurrency,
isExcluded: false,
name: null, name: null,
platformId: null platformId: null
} }

8
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html

@ -50,6 +50,14 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3 px-2">
<mat-checkbox
color="primary"
name="isExcluded"
[(ngModel)]="data.account.isExcluded"
>Exclude from Analysis</mat-checkbox
>
</div>
<div *ngIf="data.account.id"> <div *ngIf="data.account.id">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account ID</mat-label> <mat-label i18n>Account ID</mat-label>

2
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
@ -15,6 +16,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
CommonModule, CommonModule,
FormsModule, FormsModule,
MatButtonModule, MatButtonModule,
MatCheckboxModule,
MatDialogModule, MatDialogModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,

20
apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page-routing.module.ts

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { Hacktoberfest2022PageComponent } from './hacktoberfest-2022-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: Hacktoberfest2022PageComponent,
path: '',
title: 'Hacktoberfest 2022'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class Hacktoberfest2022RoutingModule {}

9
apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.component.ts

@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
host: { class: 'page' },
selector: 'gf-hacktoberfest-2022-page',
styleUrls: ['./hacktoberfest-2022-page.scss'],
templateUrl: './hacktoberfest-2022-page.html'
})
export class Hacktoberfest2022PageComponent {}

178
apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.html

@ -0,0 +1,178 @@
<div class="blog container">
<div class="row">
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1">Hacktoberfest 2022</h1>
<div class="mb-3 text-muted"><small>2022-10-01</small></div>
<img
alt="Hacktoberfest 2022 with Ghostfolio Teaser"
class="rounded w-100"
src="../assets/images/blog/hacktoberfest-2022.png"
title="Hacktoberfest 2022 with Ghostfolio"
/>
</div>
<section class="mb-4">
<p>
We are very excited to join
<a href="https://hacktoberfest.com">Hacktoberfest</a> for the first
time with <a href="https://ghostfol.io">Ghostfolio</a> and meet new
and ambitious open-source contributors. Hacktoberfest is a
month-long celebration of open-source projects, their maintainers,
and the entire community of contributors. Each October, open source
maintainers from all over the world give extra attention to new
contributors while guiding them through their first pull requests on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
</p>
</section>
<section class="mb-4">
<h2 class="h4">About Ghostfolio</h2>
<p>
Ghostfolio is a modern web application to manage your personal
finance. The software presents the current assets in real time and
supports the decision making of future investments. Whether
rebalancing the asset classes (stocks, ETFs, cryptocurrencies, etc.)
of your portfolio or financing an apartment, Ghostfolio offers
solid, data-driven decision support.
</p>
<p>
Ghostfolio is written in
<a href="https://www.typescriptlang.org">TypeScript</a> and
organized as an <a href="https://nx.dev">Nx</a> workspace. The
backend is based on <a href="https://nestjs.com">NestJS</a> using
<a href="https://www.postgresql.org">PostgreSQL</a> as a database
together with <a href="https://www.prisma.io">Prisma</a> and
<a href="https://redis.io">Redis</a> for caching. The frontend is
built with <a href="https://angular.io">Angular</a>.
</p>
</section>
<section class="mb-4">
<h2 class="h4">How to contribute?</h2>
<p>
Every contribution matters. This can be implementing new features,
fixing bugs, refactoring the code, improving the documentation,
adding more unit tests, or translating into another language.
</p>
<p>
Are you not yet familiar with our code base? That is not a problem.
We have applied the label <code>hacktoberfest</code> to a few
<a
href="https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3Ahacktoberfest"
>issues</a
>
and
<a
href="https://github.com/ghostfolio/ghostfolio/discussions?discussions_q=label%3Ahacktoberfest"
>ideas</a
>
that are well suited for newcomers.
</p>
<p>
The official Hacktoberfest website provides some valuable
<a
href="https://hacktoberfest.com/participation/#beginner-resources"
>resources for beginners</a
>
to start contributing in open source.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Get support</h2>
<p>
If you have further questions or ideas, please join our growing
<a href="https://ghostfolio.slack.com">Slack community</a> or get in
touch on Twitter
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a> or by
email via <a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a>.
</p>
<p>
We look forward to hearing from you.<br />
Thomas from Ghostfolio
</p>
</section>
<section class="mb-4">
<ul class="list-inline">
<li class="list-inline-item">
<span class="badge badge-light">Angular</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Community</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Cryptocurrency</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">ETF</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Fintech</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">GitHub</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Hacktoberfest</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Investment</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">NestJS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Nx</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">October</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Open Source</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">OSS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Personal Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio Tracker</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Prisma</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Software</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Stock</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">TypeScript</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth Management</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Web3</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Web 3.0</span>
</li>
</ul>
</section>
</article>
</div>
</div>
</div>

13
apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.module.ts

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Hacktoberfest2022RoutingModule } from './hacktoberfest-2022-page-routing.module';
import { Hacktoberfest2022PageComponent } from './hacktoberfest-2022-page.component';
@NgModule({
declarations: [Hacktoberfest2022PageComponent],
imports: [CommonModule, Hacktoberfest2022RoutingModule, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class Hacktoberfest2022PageModule {}

3
apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.scss

@ -0,0 +1,3 @@
:host {
display: block;
}

24
apps/client/src/app/pages/blog/blog-page.html

@ -2,6 +2,30 @@
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="mb-3 text-center" i18n>Blog</h3> <h3 class="mb-3 text-center" i18n>Blog</h3>
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
href="../en/blog/2022/10/hacktoberfest-2022"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">Hacktoberfest 2022</div>
<div class="d-flex text-muted">2022-10-01</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
></ion-icon>
</div>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-content> <mat-card-content>
<div class="container p-0"> <div class="container p-0">

11
apps/client/src/app/pages/features/features-page.html

@ -192,6 +192,17 @@
</div> </div>
</mat-card> </mat-card>
</div> </div>
<div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1">
<h4>Multi-Language</h4>
<p class="m-0">
Use Ghostfolio in multiple languages: English, Dutch, German,
Italian and Spanish are currently supported.
</p>
</div>
</mat-card>
</div>
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">

16
apps/client/src/app/pages/landing/landing-page.component.ts

@ -1,4 +1,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -11,6 +14,8 @@ import { Subject } from 'rxjs';
export class LandingPageComponent implements OnDestroy, OnInit { export class LandingPageComponent implements OnDestroy, OnInit {
public currentYear = format(new Date(), 'yyyy'); public currentYear = format(new Date(), 'yyyy');
public demoAuthToken: string; public demoAuthToken: string;
public hasPermissionForStatistics: boolean;
public statistics: Statistics;
public testimonials = [ public testimonials = [
{ {
author: 'Philipp', author: 'Philipp',
@ -36,7 +41,16 @@ export class LandingPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor() {} public constructor(private dataService: DataService) {
const { globalPermissions, statistics } = this.dataService.fetchInfo();
this.hasPermissionForStatistics = hasPermission(
globalPermissions,
permissions.enableStatistics
);
this.statistics = statistics;
}
public ngOnInit() {} public ngOnInit() {}

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

@ -42,6 +42,103 @@
</div> </div>
</div> </div>
<div *ngIf="hasPermissionForStatistics" class="row mb-5">
<div class="col-md-4 d-flex my-1">
<a
class="d-block"
title="Ghostfolio in Numbers: Monthly Active Users (MAU)"
[routerLink]="['/about']"
>
<gf-value
icon="people-outline"
size="large"
[value]="statistics?.activeUsers30d ?? '-'"
>Monthly Active Users</gf-value
>
</a>
</div>
<div class="col-md-4 d-flex my-1">
<a
class="d-block"
title="Ghostfolio in Numbers: Stars on GitHub"
[routerLink]="['/about']"
>
<gf-value
icon="star-outline"
size="large"
[value]="statistics?.gitHubStargazers ?? '-'"
>Stars on GitHub</gf-value
>
</a>
</div>
<div class="col-md-4 d-flex my-1">
<a
class="d-block"
title="Ghostfolio in Numbers: Pulls on Docker Hub"
[routerLink]="['/about']"
>
<gf-value
icon="cloud-download-outline"
size="large"
[value]="statistics?.dockerHubPulls ?? '-'"
>Pulls on Docker Hub</gf-value
>
</a>
</div>
</div>
<div class="row mb-5">
<div class="col-12 text-center text-muted"><small>As seen in</small></div>
<div class="col-md-2 d-flex justify-content-center my-1">
<a
class="d-block logo logo-alternative-to mask"
href="https://alternativeto.net"
target="_blank"
title="AlternativeTo - Crowdsourced software recommendations"
></a>
</div>
<div class="col-md-2 d-flex justify-content-center my-1">
<a
class="d-block logo logo-awesome"
href="https://github.com/awesome-selfhosted/awesome-selfhosted"
target="_blank"
title="Awesome-Selfhosted: A list of Free Software network services and web applications which can be hosted on your own servers"
></a>
</div>
<div class="col-md-2 d-flex justify-content-center my-1">
<a
class="d-block logo logo-openstartup"
href="https://openstartup.tm"
target="_blank"
title="Open Startup: The most complete list of open startups"
></a>
</div>
<div class="col-md-2 d-flex justify-content-center my-1">
<a
class="d-block logo logo-privacy-tools mask"
href="https://www.privacytools.io"
target="_blank"
title="Privacy Tools: Software Alternatives and Encryption"
></a>
</div>
<div class="col-md-2 d-flex justify-content-center my-1">
<a
class="d-block logo logo-product-hunt"
href="https://www.producthunt.com"
target="_blank"
title="Product Hunt – The best new products in tech."
></a>
</div>
<div class="col-md-2 d-flex justify-content-center my-1">
<a
class="d-block logo logo-unraid mask"
href="https://unraid.net"
target="_blank"
title="Unraid | Unleash Your Hardware"
></a>
</div>
</div>
<div class="row my-5"> <div class="row my-5">
<div class="col text-center"> <div class="col text-center">
<h2 class="h4 mb-1 text-center"> <h2 class="h4 mb-1 text-center">
@ -55,6 +152,28 @@
</div> </div>
</div> </div>
<div class="row my-3">
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title>360° View</mat-card-title>
Get the full picture of your personal finances across multiple
platforms.
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title>Web3 Ready</mat-card-title>
Use Ghostfolio anonymously and own your financial data.
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title>Open Source</mat-card-title>
Benefit from continuous improvements through a strong community.
</mat-card>
</div>
</div>
<div class="row my-5"> <div class="row my-5">
<div class="col-md-6 offset-md-3"> <div class="col-md-6 offset-md-3">
<h2 class="h4 mb-1 text-center">Why <strong>Ghostfolio</strong>?</h2> <h2 class="h4 mb-1 text-center">Why <strong>Ghostfolio</strong>?</h2>
@ -133,24 +252,48 @@
</div> </div>
</div> </div>
<div class="row my-5"> <div class="row my-3">
<div class="col-md-6 offset-md-3"> <div class="col-12">
<h2 class="h4 mb-1 text-center"> <h2 class="h4 mb-1 text-center">
How does <strong>Ghostfolio</strong> work? How does <strong>Ghostfolio</strong> work?
</h2> </h2>
<p class="lead mb-3 text-center">Get started in only 3 steps</p> <p class="lead mb-3 text-center">Get started in only 3 steps</p>
<ol class="m-0 pl-3"> </div>
<li class="mb-2"> <div class="col-md-4 my-2">
Sign up anonymously<br />(no e-mail address nor credit card required) <mat-card class="d-flex flex-row h-100">
</li> <div class="flex-grow-1">
<li class="mb-2">Add any of your historical transactions</li> <div class="font-weight-bold">Sign up anonymously*</div>
<li>Get valuable insights of your portfolio composition</li> <div class="text-muted">
</ol> <small>* no e-mail address nor credit card required</small>
</div>
</div>
<div class="pl-2 text-muted text-right">1</div>
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card class="d-flex flex-row h-100">
<div class="flex-grow-1">
<div class="font-weight-bold">
Add any of your historical transactions
</div>
</div>
<div class="pl-2 text-muted text-right">2</div>
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card class="d-flex flex-row h-100">
<div class="flex-grow-1">
<div class="font-weight-bold">
Get valuable insights of your portfolio composition
</div>
</div>
<div class="pl-2 text-muted text-right">3</div>
</mat-card>
</div> </div>
</div> </div>
<div class="row my-5"> <div class="row my-5">
<div class="col-md-6 offset-md-3"> <div class="col">
<h2 class="h4 mb-1 text-center">Are <strong>you</strong> ready?</h2> <h2 class="h4 mb-1 text-center">Are <strong>you</strong> ready?</h2>
<p class="lead mb-3 text-center"> <p class="lead mb-3 text-center">
Join now or check out the example account Join now or check out the example account
@ -194,7 +337,7 @@
<div class="row"> <div class="row">
<div class="align-items-center d-flex flex-column w-100"> <div class="align-items-center d-flex flex-column w-100">
<a <a
class="agplv3-logo" class="logo logo-agplv3 mask"
href="https://www.gnu.org/licenses/agpl-3.0.html" href="https://www.gnu.org/licenses/agpl-3.0.html"
target="_blank" target="_blank"
title="GNU Affero General Public License Version 3" title="GNU Affero General Public License Version 3"

4
apps/client/src/app/pages/landing/landing-page.module.ts

@ -1,8 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfLogoModule } from '@ghostfolio/ui/logo'; import { GfLogoModule } from '@ghostfolio/ui/logo';
import { GfValueModule } from '@ghostfolio/ui/value';
import { LandingPageRoutingModule } from './landing-page-routing.module'; import { LandingPageRoutingModule } from './landing-page-routing.module';
import { LandingPageComponent } from './landing-page.component'; import { LandingPageComponent } from './landing-page.component';
@ -12,8 +14,10 @@ import { LandingPageComponent } from './landing-page.component';
imports: [ imports: [
CommonModule, CommonModule,
GfLogoModule, GfLogoModule,
GfValueModule,
LandingPageRoutingModule, LandingPageRoutingModule,
MatButtonModule, MatButtonModule,
MatCardModule,
RouterModule RouterModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]

75
apps/client/src/app/pages/landing/landing-page.scss

@ -3,16 +3,6 @@
:host { :host {
display: block; display: block;
.agplv3-logo {
background-color: rgba(var(--dark-primary-text));
height: 3rem;
mask-image: url('/assets/images/AGPLv3-logo.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
width: 7.5rem;
}
.button-container { .button-container {
.mat-stroked-button { .mat-stroked-button {
background-color: var(--light-background); background-color: var(--light-background);
@ -34,6 +24,58 @@
} }
} }
.logo {
height: 3rem;
width: 7.5rem;
&.mask {
background-color: rgba(var(--dark-secondary-text));
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}
&.logo-agplv3 {
mask-image: url('/assets/images/logo-AGPLv3.svg');
}
&.logo-alternative-to {
mask-image: url('/assets/images/logo-alternative-to.svg');
}
&.logo-awesome {
background-image: url('/assets/images/logo-awesome.png');
background-position: center;
background-repeat: no-repeat;
background-size: contain;
filter: grayscale(1);
}
&.logo-openstartup {
background-image: url('/assets/images/logo-openstartup.png');
background-position: center;
background-repeat: no-repeat;
background-size: contain;
filter: grayscale(1);
}
&.logo-privacy-tools {
mask-image: url('/assets/images/logo-privacy-tools.svg');
}
&.logo-product-hunt {
background-image: url('/assets/images/logo-product-hunt.png');
background-position: center;
background-repeat: no-repeat;
background-size: contain;
filter: grayscale(1);
}
&.logo-unraid {
mask-image: url('/assets/images/logo-unraid.svg');
}
}
.outro-inner-container { .outro-inner-container {
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
max-height: 66vh; max-height: 66vh;
@ -56,16 +98,21 @@
} }
:host-context(.is-dark-theme) { :host-context(.is-dark-theme) {
.agplv3-logo {
background-color: rgba(var(--light-primary-text));
}
.button-container { .button-container {
.mat-stroked-button { .mat-stroked-button {
background-color: var(--dark-background); background-color: var(--dark-background);
} }
} }
.logo {
&.logo-agplv3,
&.logo-alternative-to,
&.logo-privacy-tools,
&.logo-unraid {
background-color: rgba(var(--light-primary-text));
}
}
.outro-inner-container { .outro-inner-container {
div { div {
background-image: url('/assets/intro-dark.jpg') !important; background-image: url('/assets/intro-dark.jpg') !important;

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

@ -13,8 +13,16 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-header> <mat-card-header class="mb-2 overflow-hidden w-100">
<mat-card-title i18n>Proportion of Net Worth</mat-card-title> <mat-card-title class="m-0 text-truncate" i18n
>Proportion of Net Worth</mat-card-title
>
<gf-value
class="flex-grow-1 justify-content-end l-2"
size="medium"
[isPercent]="true"
[value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage"
></gf-value>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<mat-progress-bar <mat-progress-bar

1
apps/client/src/app/pages/portfolio/allocations/allocations-page.scss

@ -20,6 +20,7 @@
::ng-deep { ::ng-deep {
.mat-card-header-text { .mat-card-header-text {
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden;
} }
} }

5
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -126,7 +126,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.isLoadingBenchmarkComparator = true; this.isLoadingBenchmarkComparator = true;
this.dataService this.dataService
.fetchChart({ range: this.user?.settings?.dateRange, version: 2 }) .fetchPortfolioPerformance({
range: this.user?.settings?.dateRange,
version: 2
})
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart }) => { .subscribe(({ chart }) => {
this.firstOrderDate = new Date(chart?.[0]?.date ?? new Date()); this.firstOrderDate = new Date(chart?.[0]?.date ?? new Date());

8
apps/client/src/app/pages/portfolio/fire/fire-page.component.ts

@ -37,14 +37,14 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.dataService this.dataService
.fetchPortfolioSummary() .fetchPortfolioDetails({})
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ cash, currentValue }) => { .subscribe(({ summary }) => {
if (cash === null || currentValue === null) { if (summary.cash === null || summary.currentValue === null) {
return; return;
} }
this.fireWealth = new Big(currentValue); this.fireWealth = new Big(summary.currentValue);
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100); this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12); this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);

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

@ -31,7 +31,6 @@ import {
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPublicDetails, PortfolioPublicDetails,
PortfolioReport, PortfolioReport,
PortfolioSummary,
UniqueAsset, UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -302,7 +301,11 @@ export class DataService {
); );
} }
public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) { public fetchPortfolioDetails({
filters
}: {
filters?: Filter[];
}): Observable<PortfolioDetails> {
let params = new HttpParams(); let params = new HttpParams();
if (filters?.length > 0) { if (filters?.length > 0) {
@ -348,17 +351,32 @@ export class DataService {
} }
} }
return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', { return this.http
params .get<any>('/api/v1/portfolio/details', {
}); params
})
.pipe(
map((response) => {
if (response.summary?.firstOrderDate) {
response.summary.firstOrderDate = parseISO(
response.summary.firstOrderDate
);
}
return response;
})
);
} }
public fetchPortfolioPerformance(params: { [param: string]: any }) { public fetchPortfolioPerformance({
range,
version
}: {
range: DateRange;
version: number;
}) {
return this.http.get<PortfolioPerformanceResponse>( return this.http.get<PortfolioPerformanceResponse>(
'/api/v1/portfolio/performance', `/api/v${version}/portfolio/performance`,
{ { params: { range } }
params
}
); );
} }
@ -372,18 +390,6 @@ export class DataService {
return this.http.get<PortfolioReport>('/api/v1/portfolio/report'); return this.http.get<PortfolioReport>('/api/v1/portfolio/report');
} }
public fetchPortfolioSummary(): Observable<PortfolioSummary> {
return this.http.get<any>('/api/v1/portfolio/summary').pipe(
map((summary) => {
if (summary.firstOrderDate) {
summary.firstOrderDate = parseISO(summary.firstOrderDate);
}
return summary;
})
);
}
public fetchPositionDetail({ public fetchPositionDetail({
dataSource, dataSource,
symbol symbol

BIN
apps/client/src/assets/images/blog/hacktoberfest-2022.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

0
apps/client/src/assets/images/AGPLv3-logo.svg → apps/client/src/assets/images/logo-AGPLv3.svg

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

1
apps/client/src/assets/images/logo-alternative-to.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

BIN
apps/client/src/assets/images/logo-awesome.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
apps/client/src/assets/images/logo-openstartup.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

35
apps/client/src/assets/images/logo-privacy-tools.svg

@ -0,0 +1,35 @@
<svg version="1.2" baseProfile="tiny-ps" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 508 101" width="508" height="101">
<title>horizontal-svg</title>
<style>
tspan { white-space:pre }
.shp0 { fill: #3498db }
.shp1 { fill: transparent }
.shp2 { fill: #8a9595 }
</style>
<g id="Logo">
<g id="Shield">
<g id="Layer">
<path id="Layer" class="shp0" d="M284.81 0C279.86 0.24 254.33 8.51 242.56 14.89C240.87 15.81 239.01 18.96 239 20.34C239 33 242.05 51.67 250.66 68.26L267.5 51.42C264.25 43.19 266.19 33.81 272.44 27.56C276.25 23.94 283.41 21.26 289.64 21.15C292.47 21.1 295.11 21.58 297.15 22.74C298.34 23.43 297.78 24.32 297.42 24.68L284.52 37.05C282.89 38.8 284.3 43.29 285.36 46.15C288.21 47.2 292.7 48.61 294.45 46.98L306.83 34.08C307.18 33.73 308.07 33.17 308.76 34.36C312.47 40.88 309.21 53.53 303.95 59.06C297.69 65.32 288.31 67.26 280.09 64L260.76 83.33C267.04 90.61 274.95 96.69 284.81 100.54C320.46 86.63 330.62 43.46 330.62 20.34C330.61 18.96 328.76 15.81 327.06 14.89C315.29 8.51 289.76 0.24 284.81 0L284.81 0Z" />
<g id="Layer">
<path id="Layer" class="shp1" d="M250.69 68.23C250.93 68.68 251.17 69.13 251.41 69.58C252.43 71.46 253.52 73.31 254.69 75.12C255.86 76.93 257.1 78.69 258.43 80.4C259.19 81.38 259.98 82.35 260.8 83.3L280.12 63.97C288.35 67.23 297.73 65.29 303.98 59.03C309.25 53.5 312.51 40.85 308.8 34.33C308.11 33.14 307.22 33.7 306.86 34.05L294.49 46.95C292.74 48.58 288.25 47.17 285.39 46.12C284.34 43.26 282.93 38.77 284.56 37.02L297.46 24.65C297.81 24.29 298.37 23.4 297.18 22.71C295.14 21.55 292.51 21.07 289.68 21.12C283.45 21.23 276.28 23.91 272.48 27.53C266.22 33.79 264.28 43.16 267.54 51.39L250.69 68.23Z" />
</g>
</g>
</g>
<g id="Tools">
<path id="Layer" class="shp2" d="M343.3 31.55L343.3 24.61L387.83 24.61L387.83 31.55L369.48 31.55L369.48 76.83L361.65 76.83L361.65 31.55L343.3 31.55Z" />
<path id="Layer" fill-rule="evenodd" class="shp2" d="M381.33 50.12C382.38 47.68 383.79 45.59 385.58 43.85C387.37 42.06 389.46 40.67 391.85 39.67C394.29 38.63 396.87 38.11 399.61 38.11C402.34 38.11 404.9 38.63 407.29 39.67C409.73 40.67 411.82 42.06 413.56 43.85C415.35 45.59 416.74 47.68 417.73 50.12C418.78 52.5 419.3 55.06 419.3 57.8C419.3 60.58 418.78 63.2 417.73 65.63C416.74 68.02 415.35 70.11 413.56 71.9C411.82 73.64 409.73 75.03 407.29 76.08C404.9 77.07 402.34 77.57 399.61 77.57C396.87 77.57 394.29 77.07 391.85 76.08C389.46 75.03 387.37 73.64 385.58 71.9C383.79 70.11 382.38 68.02 381.33 65.63C380.34 63.2 379.84 60.58 379.84 57.8C379.84 55.06 380.34 52.5 381.33 50.12ZM408.33 67.2C410.77 64.61 411.99 61.48 411.99 57.8C411.99 54.12 410.77 51.01 408.33 48.48C405.95 45.94 403.04 44.67 399.61 44.67C396.18 44.67 393.24 45.94 390.8 48.48C388.37 51.01 387.15 54.12 387.15 57.8C387.15 61.48 388.34 64.61 390.73 67.2C393.17 69.73 396.13 71 399.61 71C403.04 71 405.95 69.73 408.33 67.2Z" />
<path id="Layer" fill-rule="evenodd" class="shp2" d="M423.45 50.12C424.5 47.68 425.91 45.59 427.7 43.85C429.49 42.06 431.58 40.67 433.97 39.67C436.41 38.63 438.99 38.11 441.73 38.11C444.46 38.11 447.02 38.63 449.41 39.67C451.85 40.67 453.94 42.06 455.68 43.85C457.47 45.59 458.86 47.68 459.85 50.12C460.9 52.5 461.42 55.06 461.42 57.8C461.42 60.58 460.9 63.2 459.85 65.63C458.86 68.02 457.47 70.11 455.68 71.9C453.94 73.64 451.85 75.03 449.41 76.08C447.02 77.07 444.46 77.57 441.73 77.57C438.99 77.57 436.41 77.07 433.97 76.08C431.58 75.03 429.49 73.64 427.7 71.9C425.91 70.11 424.5 68.02 423.45 65.63C422.46 63.2 421.96 60.58 421.96 57.8C421.96 55.06 422.46 52.5 423.45 50.12ZM450.45 67.2C452.89 64.61 454.11 61.48 454.11 57.8C454.11 54.12 452.89 51.01 450.45 48.48C448.07 45.94 445.16 44.67 441.73 44.67C438.3 44.67 435.36 45.94 432.92 48.48C430.49 51.01 429.27 54.12 429.27 57.8C429.27 61.48 430.46 64.61 432.85 67.2C435.29 69.73 438.25 71 441.73 71C445.16 71 448.07 69.73 450.45 67.2Z" />
<path id="Layer" class="shp2" d="M473.1 22.97L473.1 76.83L465.64 76.83L465.64 24.61L473.1 22.97Z" />
<path id="Layer" class="shp2" d="M475.48 71.9L479.51 66.98C481.5 68.52 483.51 69.71 485.55 70.56C487.64 71.35 489.75 71.75 491.89 71.75C494.58 71.75 496.76 71.23 498.45 70.18C500.15 69.14 500.99 67.77 500.99 66.08C500.99 64.74 500.49 63.67 499.5 62.87C498.5 62.08 496.96 61.53 494.87 61.23L488.01 60.26C484.28 59.72 481.45 58.57 479.51 56.83C477.62 55.04 476.67 52.68 476.67 49.75C476.67 46.31 478.04 43.55 480.78 41.47C483.51 39.33 487.09 38.26 491.52 38.26C494.3 38.26 496.94 38.66 499.42 39.45C501.96 40.25 504.42 41.47 506.81 43.11L503 48.03C500.92 46.64 498.88 45.64 496.89 45.05C494.95 44.4 492.98 44.08 490.99 44.08C488.71 44.08 486.87 44.55 485.48 45.49C484.08 46.44 483.39 47.68 483.39 49.22C483.39 50.62 483.86 51.69 484.8 52.43C485.8 53.13 487.41 53.65 489.65 54L496.51 54.97C500.24 55.51 503.08 56.68 505.02 58.47C507.01 60.26 508 62.63 508 65.56C508 67.25 507.58 68.84 506.73 70.33C505.94 71.78 504.82 73.02 503.38 74.06C501.99 75.11 500.32 75.95 498.38 76.6C496.44 77.2 494.35 77.49 492.11 77.49C488.83 77.49 485.75 77.02 482.86 76.08C480.03 75.13 477.57 73.74 475.48 71.9L475.48 71.9Z" />
</g>
<g id="Privacy">
<path id="Layer" fill-rule="evenodd" class="shp2" d="M1.46 76.82L1.46 24.6L25.63 24.6C30.7 24.6 34.8 26.07 37.94 29.01C41.12 31.89 42.71 35.64 42.71 40.27C42.71 44.85 41.12 48.57 37.94 51.46C34.75 54.34 30.65 55.79 25.63 55.79L9.29 55.79L9.29 76.82L1.46 76.82ZM24.81 31.47L9.29 31.47L9.29 49.15L24.81 49.15C27.84 49.15 30.25 48.35 32.04 46.76C33.88 45.12 34.8 42.95 34.8 40.27C34.8 37.58 33.88 35.45 32.04 33.85C30.25 32.26 27.84 31.47 24.81 31.47L24.81 31.47Z" />
<path id="Layer" class="shp2" d="M46.91 76.82L46.91 38.85L54.37 38.85L54.37 43.7C55.51 41.86 56.98 40.47 58.77 39.52C60.56 38.53 62.57 38.03 64.81 38.03C65.61 38.03 66.3 38.08 66.9 38.18C67.5 38.28 68.07 38.43 68.62 38.63L68.62 45.34C67.92 45.09 67.2 44.89 66.45 44.74C65.71 44.6 64.96 44.52 64.22 44.52C62.03 44.52 60.06 45.12 58.32 46.31C56.63 47.46 55.31 49.17 54.37 51.46L54.37 76.82L46.91 76.82Z" />
<path id="Layer" class="shp2" d="M75.41 32.74C74.17 32.74 73.1 32.29 72.2 31.4C71.31 30.45 70.86 29.36 70.86 28.12C70.86 26.87 71.31 25.8 72.2 24.91C73.1 23.96 74.17 23.49 75.41 23.49C76.65 23.49 77.72 23.96 78.62 24.91C79.56 25.8 80.03 26.87 80.03 28.12C80.03 29.36 79.56 30.45 78.62 31.4C77.72 32.29 76.65 32.74 75.41 32.74L75.41 32.74ZM79.14 38.86L79.14 76.82L71.68 76.82L71.68 38.86L79.14 38.86Z" />
<path id="Layer" class="shp2" d="M97.33 76.82L80.92 38.85L89.12 38.85L100.98 67.27L112.84 38.85L120.83 38.85L104.41 76.82L97.33 76.82Z" />
<path id="Layer" fill-rule="evenodd" class="shp2" d="M134.56 77.5C130.43 77.5 127.08 76.43 124.49 74.29C121.9 72.15 120.61 69.37 120.61 65.94C120.61 62.36 121.98 59.55 124.71 57.51C127.45 55.42 131.2 54.38 135.98 54.38C137.82 54.38 139.61 54.57 141.35 54.97C143.09 55.32 144.75 55.82 146.35 56.46L146.35 52.44C146.35 49.75 145.55 47.74 143.96 46.39C142.37 45.05 140.08 44.38 137.1 44.38C135.36 44.38 133.54 44.65 131.65 45.2C129.76 45.7 127.65 46.49 125.31 47.59L122.55 41.99C125.39 40.65 128.07 39.68 130.61 39.08C133.14 38.44 135.65 38.11 138.14 38.11C143.06 38.11 146.87 39.31 149.55 41.7C152.29 44.03 153.66 47.36 153.66 51.69L153.66 76.83L146.35 76.83L146.35 73.55C144.66 74.89 142.84 75.88 140.9 76.53C138.96 77.18 136.85 77.5 134.56 77.5L134.56 77.5ZM127.77 65.79C127.77 67.63 128.54 69.12 130.08 70.26C131.68 71.41 133.74 71.98 136.28 71.98C138.26 71.98 140.1 71.68 141.8 71.09C143.49 70.49 145 69.59 146.35 68.4L146.35 61.84C144.9 61.04 143.39 60.47 141.8 60.12C140.2 59.72 138.46 59.52 136.57 59.52C133.89 59.52 131.75 60.09 130.16 61.24C128.57 62.38 127.77 63.9 127.77 65.79L127.77 65.79Z" />
<path id="Layer" class="shp2" d="M177.4 70.93C179.29 70.93 181.08 70.56 182.77 69.81C184.46 69.02 186.08 67.85 187.62 66.31L192.09 71.15C190.11 73.14 187.82 74.71 185.23 75.85C182.65 76.95 179.94 77.49 177.1 77.49C174.37 77.49 171.81 77 169.42 76C167.03 74.96 164.97 73.57 163.23 71.83C161.49 70.08 160.09 68.02 159.05 65.63C158.06 63.2 157.56 60.59 157.56 57.8C157.56 55.07 158.06 52.51 159.05 50.12C160.09 47.68 161.49 45.59 163.23 43.85C164.97 42.06 167.03 40.67 169.42 39.68C171.81 38.63 174.37 38.11 177.1 38.11C179.94 38.11 182.7 38.68 185.38 39.82C188.07 40.92 190.4 42.46 192.39 44.45L187.69 49.52C186.25 47.98 184.64 46.81 182.85 46.02C181.06 45.17 179.19 44.75 177.25 44.75C173.82 44.75 170.91 46.02 168.52 48.55C166.14 51.09 164.94 54.17 164.94 57.8C164.94 61.53 166.14 64.66 168.52 67.2C170.96 69.69 173.92 70.93 177.4 70.93L177.4 70.93Z" />
<path id="Layer" class="shp2" d="M204.75 80.4L206.24 76.97L191.1 38.85L199.31 38.85L210.42 67.87L222.65 38.85L230.71 38.85L212.36 81.37C210.62 85.4 208.63 88.28 206.39 90.02C204.15 91.81 201.29 92.71 197.81 92.71C197.02 92.71 196.27 92.66 195.57 92.56C194.93 92.51 194.36 92.41 193.86 92.26L193.86 85.7C194.36 85.8 194.85 85.87 195.35 85.92C195.9 85.97 196.52 85.99 197.22 85.99C198.91 85.99 200.37 85.52 201.62 84.58C202.91 83.68 203.95 82.29 204.75 80.4L204.75 80.4Z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
apps/client/src/assets/images/logo-product-hunt.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

9
apps/client/src/assets/images/logo-unraid.svg

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222.4 139.8">
<path fill="#ffffff" d="M146.70000000000002 130.2H135l-3 9h-6.5l13.4-38.5h8l13.4 38.5h-7.1l-10.6-31.6-5.8 16.9h8.2l1.7 5.7zM29.7 100.8v25.4c0 8.9-5.8 13.6-14.9 13.6-9 0-14.8-4.7-14.8-13.6v-25.4h6.5v25.4c0 5.2 3.2 7.9 8.2 7.9 5.2 0 8.4-2.7 8.4-7.9v-25.4h6.6zM50.9 112.7v26.5h-6.5v-38.5h6.1l17 26.5v-26.5H74v38.5h-6.1l-17-26.5zM171.3 100.8h6.5v38.5h-6.5v-38.5zM222.4 125.4c0 9-5.9 13.8-15.2 13.8h-14.5v-38.5h14.6c9.2 0 15.1 4.8 15.1 13.8v10.9zm-6.6-10.8c0-5.3-3.3-8.1-8.5-8.1h-8.1v27.1h8c5.3 0 8.6-2.8 8.6-8.1v-10.9zM108.3 124.7c4.3-1.6 6.9-5.3 6.9-11.5 0-8.7-5.1-12.4-12.8-12.4H88.8v38.5h6.5v-32.8h6.9c3.8 0 6.2 1.8 6.2 6.7s-2.4 6.8-6.2 6.8h-3.4l9.2 19.4h7.5l-7.2-14.7z" class="st0"></path>
<linearGradient id="SVGID_1_" x1="68.07" x2="154.073" y1="81.489" y2="-4.514" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#e32929"></stop>
<stop offset="1" stop-color="#ff8d30"></stop>
</linearGradient>
<path fill="url(#SVGID_1_)" d="M107.8 19.2h6.5v38.5h-6.5V19.2zM50.9 57.7h-6.5V19.2h6.5v38.5zm25.2 4.6h6.5V77h-6.5V62.3zM60.2 45.8h6.5v23.8h-6.5V45.8zm31.7 0h6.5v23.8h-6.5V45.8zm79.4-26.6h6.5v38.5h-6.5V19.2zm-25.2-4.5h-6.5V0h6.5v14.7zM162 31.1h-6.5V7.4h6.5v23.7zm-31.8 0h-6.5V7.4h6.5v23.7z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

38
apps/client/src/assets/sitemap.xml

@ -6,70 +6,74 @@
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url> <url>
<loc>https://ghostfol.io</loc> <loc>https://ghostfol.io</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc> <loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/about</loc> <loc>https://ghostfol.io/en/about</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/about/changelog</loc> <loc>https://ghostfol.io/en/about/changelog</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog</loc> <loc>https://ghostfol.io/en/blog</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc> <loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc> <loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc> <loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc> <loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc> <loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/10/hacktoberfest-2022</loc>
<lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/demo</loc> <loc>https://ghostfol.io/en/demo</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/faq</loc> <loc>https://ghostfol.io/en/faq</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/features</loc> <loc>https://ghostfol.io/en/features</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/markets</loc> <loc>https://ghostfol.io/en/markets</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/pricing</loc> <loc>https://ghostfol.io/en/pricing</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/register</loc> <loc>https://ghostfol.io/en/register</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/resources</loc> <loc>https://ghostfol.io/en/resources</loc>
<lastmod>2022-09-01T00:00:00+00:00</lastmod> <lastmod>2022-10-01T00:00:00+00:00</lastmod>
</url> </url>
</urlset> </urlset>

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

@ -38,7 +38,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">28</context> <context context-type="linenumber">31</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -86,7 +86,7 @@
<target state="translated">Aktivitäten</target> <target state="translated">Aktivitäten</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">33</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/accounts-table/accounts-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
@ -210,7 +210,7 @@
<target state="translated">Jobs löschen</target> <target state="translated">Jobs löschen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">24</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html"> <trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html">
@ -218,7 +218,7 @@
<target state="translated">Symbol</target> <target state="translated">Symbol</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">29</context> <context context-type="linenumber">32</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
@ -238,7 +238,7 @@
<target state="translated">Datenquelle</target> <target state="translated">Datenquelle</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">30</context> <context context-type="linenumber">33</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
@ -254,7 +254,7 @@
<target state="translated">Versuche</target> <target state="translated">Versuche</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">34</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html"> <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
@ -262,7 +262,7 @@
<target state="translated">Erstellt</target> <target state="translated">Erstellt</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">32</context> <context context-type="linenumber">35</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html"> <trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html">
@ -270,7 +270,7 @@
<target state="translated">Abgeschlossen</target> <target state="translated">Abgeschlossen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">33</context> <context context-type="linenumber">36</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html"> <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
@ -278,7 +278,7 @@
<target state="translated">Status</target> <target state="translated">Status</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">34</context> <context context-type="linenumber">37</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html"> <trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html">
@ -286,7 +286,7 @@
<target state="translated">Anlageprofil</target> <target state="translated">Anlageprofil</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">49</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html"> <trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html">
@ -294,7 +294,7 @@
<target state="translated">Historische Marktdaten</target> <target state="translated">Historische Marktdaten</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">54</context> <context context-type="linenumber">57</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html"> <trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html">
@ -302,7 +302,7 @@
<target state="translated">Daten anzeigen</target> <target state="translated">Daten anzeigen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">109</context> <context context-type="linenumber">112</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html"> <trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html">
@ -310,7 +310,7 @@
<target state="translated">Stacktrace anzeigen</target> <target state="translated">Stacktrace anzeigen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">116</context> <context context-type="linenumber">119</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html"> <trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html">
@ -318,7 +318,7 @@
<target state="translated">Job löschen</target> <target state="translated">Job löschen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">119</context> <context context-type="linenumber">122</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html"> <trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html">
@ -370,7 +370,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">66</context> <context context-type="linenumber">74</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context>
@ -394,7 +394,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">73</context> <context context-type="linenumber">81</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context>
@ -574,7 +574,7 @@
<target state="translated">Hinzufügen</target> <target state="translated">Hinzufügen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">183</context> <context context-type="linenumber">187</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html"> <trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html">
@ -582,7 +582,7 @@
<target state="translated">Verwaltung</target> <target state="translated">Verwaltung</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">190</context> <context context-type="linenumber">194</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html"> <trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html">
@ -590,7 +590,7 @@
<target state="translated">Cache leeren</target> <target state="translated">Cache leeren</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">194</context> <context context-type="linenumber">198</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2817099043823177227" datatype="html"> <trans-unit id="2817099043823177227" datatype="html">
@ -1034,7 +1034,7 @@
<target state="translated">Gesamtvermögen</target> <target state="translated">Gesamtvermögen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">179</context> <context context-type="linenumber">190</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html"> <trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html">
@ -1042,7 +1042,7 @@
<target state="translated">Performance pro Jahr</target> <target state="translated">Performance pro Jahr</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">190</context> <context context-type="linenumber">201</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html"> <trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html">
@ -1050,7 +1050,7 @@
<target state="translated">Dividenden</target> <target state="translated">Dividenden</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">206</context> <context context-type="linenumber">217</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6785405835169448749" datatype="html"> <trans-unit id="6785405835169448749" datatype="html">
@ -1172,6 +1172,10 @@
<trans-unit id="3041670542776846470" datatype="html"> <trans-unit id="3041670542776846470" datatype="html">
<source>This feature requires a subscription.</source> <source>This feature requires a subscription.</source>
<target state="translated">Diese Funktion erfordert ein Abonnement.</target> <target state="translated">Diese Funktion erfordert ein Abonnement.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-summary/home-summary.component.ts</context>
<context context-type="linenumber">112</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context> <context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
<context context-type="linenumber">67</context> <context context-type="linenumber">67</context>
@ -1180,6 +1184,10 @@
<trans-unit id="5499742151525073097" datatype="html"> <trans-unit id="5499742151525073097" datatype="html">
<source>Upgrade Plan</source> <source>Upgrade Plan</source>
<target state="translated">Abonnement abschliessen</target> <target state="translated">Abonnement abschliessen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-summary/home-summary.component.ts</context>
<context context-type="linenumber">114</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context> <context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
<context context-type="linenumber">69</context> <context context-type="linenumber">69</context>
@ -1382,7 +1390,7 @@
<target state="translated">Lokalität</target> <target state="translated">Lokalität</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">135</context> <context context-type="linenumber">144</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html"> <trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
@ -1390,7 +1398,7 @@
<target state="translated">Datums- und Zahlenformat</target> <target state="translated">Datums- und Zahlenformat</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">137</context> <context context-type="linenumber">146</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html"> <trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
@ -1398,7 +1406,7 @@
<target state="translated">Ansicht</target> <target state="translated">Ansicht</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">160</context> <context context-type="linenumber">172</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html"> <trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
@ -1406,7 +1414,7 @@
<target state="translated">Einloggen mit Fingerabdruck</target> <target state="translated">Einloggen mit Fingerabdruck</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">181</context> <context context-type="linenumber">196</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html"> <trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
@ -1414,7 +1422,7 @@
<target state="translated">Benutzer ID</target> <target state="translated">Benutzer ID</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">208</context> <context context-type="linenumber">223</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html"> <trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
@ -1422,7 +1430,7 @@
<target state="translated">Zugangsberechtigung</target> <target state="translated">Zugangsberechtigung</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">217</context> <context context-type="linenumber">232</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html"> <trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
@ -1516,6 +1524,10 @@
<trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html"> <trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html">
<source>Platform</source> <source>Platform</source>
<target state="translated">Plattform</target> <target state="translated">Plattform</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">29</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
<context context-type="linenumber">35</context> <context context-type="linenumber">35</context>
@ -1530,7 +1542,7 @@
<target state="translated">Konto ID</target> <target state="translated">Konto ID</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">63</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4979019387603946865" datatype="html"> <trans-unit id="4979019387603946865" datatype="html">
@ -1618,7 +1630,7 @@
<target state="translated">Nach Konto</target> <target state="translated">Nach Konto</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">33</context> <context context-type="linenumber">41</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html"> <trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html">
@ -1626,7 +1638,7 @@
<target state="translated">Nach Währung</target> <target state="translated">Nach Währung</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">58</context> <context context-type="linenumber">66</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html"> <trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html">
@ -1634,7 +1646,7 @@
<target state="translated">Nach Asset Class</target> <target state="translated">Nach Asset Class</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">86</context> <context context-type="linenumber">94</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html"> <trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html">
@ -1642,7 +1654,7 @@
<target state="translated">Nach Position</target> <target state="translated">Nach Position</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">114</context> <context context-type="linenumber">122</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html"> <trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html">
@ -1650,7 +1662,7 @@
<target state="translated">Nach Sektor</target> <target state="translated">Nach Sektor</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">142</context> <context context-type="linenumber">150</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html"> <trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html">
@ -1658,7 +1670,7 @@
<target state="translated">Nach Kontinent</target> <target state="translated">Nach Kontinent</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">171</context> <context context-type="linenumber">179</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html"> <trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html">
@ -1666,7 +1678,7 @@
<target state="translated">Nach Land</target> <target state="translated">Nach Land</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">199</context> <context context-type="linenumber">207</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html"> <trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html">
@ -1674,7 +1686,7 @@
<target state="translated">Regionen</target> <target state="translated">Regionen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">230</context> <context context-type="linenumber">238</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2038,7 +2050,7 @@
<target state="translated">Portfolio</target> <target state="translated">Portfolio</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">111</context> <context context-type="linenumber">107</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context>
@ -2278,7 +2290,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">116</context> <context context-type="linenumber">119</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html"> <trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html">
@ -2294,7 +2306,7 @@
<target state="translated">Sprache</target> <target state="translated">Sprache</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">115</context> <context context-type="linenumber">118</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html"> <trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html">
@ -2418,7 +2430,7 @@
<target state="translated">Entwickelte Länder</target> <target state="translated">Entwickelte Länder</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">256</context> <context context-type="linenumber">264</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2430,7 +2442,7 @@
<target state="translated">Schwellenländer</target> <target state="translated">Schwellenländer</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">265</context> <context context-type="linenumber">273</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2442,7 +2454,7 @@
<target state="translated">Andere Länder</target> <target state="translated">Andere Länder</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">274</context> <context context-type="linenumber">282</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2610,7 +2622,7 @@
<target state="translated">Experimentelle Funktionen</target> <target state="translated">Experimentelle Funktionen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">196</context> <context context-type="linenumber">211</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1b25c6e22f822e07a3e4d5aae4edc5b41fe083c2" datatype="html"> <trans-unit id="1b25c6e22f822e07a3e4d5aae4edc5b41fe083c2" datatype="html">
@ -2626,7 +2638,7 @@
<target state="translated">Vergleichen mit...</target> <target state="translated">Vergleichen mit...</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">18</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1931353503905413384" datatype="html"> <trans-unit id="1931353503905413384" datatype="html">
@ -2634,7 +2646,7 @@
<target state="translated">Benchmark</target> <target state="translated">Benchmark</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">120</context> <context context-type="linenumber">116</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4210837540bca56dca96fcc451518659d06ad02a" datatype="html"> <trans-unit id="4210837540bca56dca96fcc451518659d06ad02a" datatype="html">
@ -2642,7 +2654,23 @@
<target state="translated">Anteil am Gesamtvermögen</target> <target state="translated">Anteil am Gesamtvermögen</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">17</context> <context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="ac598d664f86ba5783915d65f2664a7f38a9d23a" datatype="html">
<source>Account Type</source>
<target state="new">Account Type</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="98fc3013bfcbf452b9f37bbfcdb77b9b882866e3" datatype="html">
<source>Excluded from Analysis</source>
<target state="new">Excluded from Analysis</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">176</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
</body> </body>

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

@ -2660,7 +2660,7 @@
</trans-unit> </trans-unit>
<trans-unit id="98fc3013bfcbf452b9f37bbfcdb77b9b882866e3" datatype="html"> <trans-unit id="98fc3013bfcbf452b9f37bbfcdb77b9b882866e3" datatype="html">
<source>Excluded from Analysis</source> <source>Excluded from Analysis</source>
<target state="translated">Excluido del análisis</target> <target state="new">Excluido del análisis</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">176</context> <context context-type="linenumber">176</context>
@ -2668,7 +2668,7 @@
</trans-unit> </trans-unit>
<trans-unit id="ac598d664f86ba5783915d65f2664a7f38a9d23a" datatype="html"> <trans-unit id="ac598d664f86ba5783915d65f2664a7f38a9d23a" datatype="html">
<source>Account Type</source> <source>Account Type</source>
<target state="translated">Tipo de cuenta</target> <target state="new">Tipo de cuenta</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">25</context>

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

@ -2672,6 +2672,20 @@
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">25</context>
<trans-unit id="ac598d664f86ba5783915d65f2664a7f38a9d23a" datatype="html">
<source>Account Type</source>
<target state="new">Account Type</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="98fc3013bfcbf452b9f37bbfcdb77b9b882866e3" datatype="html">
<source>Excluded from Analysis</source>
<target state="new">Excluded from Analysis</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">176</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
</body> </body>

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

File diff suppressed because it is too large

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

@ -35,7 +35,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">28</context> <context context-type="linenumber">31</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
@ -79,7 +79,7 @@
<source>Activities</source> <source>Activities</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">33</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/accounts-table/accounts-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
@ -196,14 +196,14 @@
<source>Delete Jobs</source> <source>Delete Jobs</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">24</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html"> <trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html">
<source>Symbol</source> <source>Symbol</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">29</context> <context context-type="linenumber">32</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
@ -222,7 +222,7 @@
<source>Data Source</source> <source>Data Source</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">30</context> <context context-type="linenumber">33</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
@ -237,63 +237,63 @@
<source>Attempts</source> <source>Attempts</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">34</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html"> <trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
<source>Created</source> <source>Created</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">32</context> <context context-type="linenumber">35</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html"> <trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html">
<source>Finished</source> <source>Finished</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">33</context> <context context-type="linenumber">36</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html"> <trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
<source>Status</source> <source>Status</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">34</context> <context context-type="linenumber">37</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html"> <trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html">
<source>Asset Profile</source> <source>Asset Profile</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">49</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html"> <trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html">
<source>Historical Market Data</source> <source>Historical Market Data</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">54</context> <context context-type="linenumber">57</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html"> <trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html">
<source>View Data</source> <source>View Data</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">109</context> <context context-type="linenumber">112</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html"> <trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html">
<source>View Stacktrace</source> <source>View Stacktrace</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">116</context> <context context-type="linenumber">119</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html"> <trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html">
<source>Delete Job</source> <source>Delete Job</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-jobs/admin-jobs.html</context>
<context context-type="linenumber">119</context> <context context-type="linenumber">122</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html"> <trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html">
@ -341,7 +341,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">66</context> <context context-type="linenumber">74</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context>
@ -364,7 +364,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">73</context> <context context-type="linenumber">81</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html</context>
@ -523,21 +523,21 @@
<source>Add</source> <source>Add</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">183</context> <context context-type="linenumber">187</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html"> <trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html">
<source>Housekeeping</source> <source>Housekeeping</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">190</context> <context context-type="linenumber">194</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html"> <trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html">
<source>Flush Cache</source> <source>Flush Cache</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">194</context> <context context-type="linenumber">198</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2817099043823177227" datatype="html"> <trans-unit id="2817099043823177227" datatype="html">
@ -936,21 +936,21 @@
<source>Net Worth</source> <source>Net Worth</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">179</context> <context context-type="linenumber">190</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html"> <trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html">
<source>Annualized Performance</source> <source>Annualized Performance</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">190</context> <context context-type="linenumber">201</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html"> <trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html">
<source>Dividend</source> <source>Dividend</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">206</context> <context context-type="linenumber">217</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6785405835169448749" datatype="html"> <trans-unit id="6785405835169448749" datatype="html">
@ -1058,6 +1058,10 @@
</trans-unit> </trans-unit>
<trans-unit id="3041670542776846470" datatype="html"> <trans-unit id="3041670542776846470" datatype="html">
<source>This feature requires a subscription.</source> <source>This feature requires a subscription.</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-summary/home-summary.component.ts</context>
<context context-type="linenumber">112</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context> <context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
<context context-type="linenumber">67</context> <context context-type="linenumber">67</context>
@ -1065,6 +1069,10 @@
</trans-unit> </trans-unit>
<trans-unit id="5499742151525073097" datatype="html"> <trans-unit id="5499742151525073097" datatype="html">
<source>Upgrade Plan</source> <source>Upgrade Plan</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-summary/home-summary.component.ts</context>
<context context-type="linenumber">114</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context> <context context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
<context context-type="linenumber">69</context> <context context-type="linenumber">69</context>
@ -1243,42 +1251,42 @@
<source>Locale</source> <source>Locale</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">135</context> <context context-type="linenumber">144</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html"> <trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
<source>Date and number format</source> <source>Date and number format</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">137</context> <context context-type="linenumber">146</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html"> <trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
<source>View Mode</source> <source>View Mode</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">160</context> <context context-type="linenumber">172</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html"> <trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
<source>Sign in with fingerprint</source> <source>Sign in with fingerprint</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">181</context> <context context-type="linenumber">196</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html"> <trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
<source>User ID</source> <source>User ID</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">208</context> <context context-type="linenumber">223</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html"> <trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
<source>Granted Access</source> <source>Granted Access</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">217</context> <context context-type="linenumber">232</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html"> <trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
@ -1362,6 +1370,10 @@
</trans-unit> </trans-unit>
<trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html"> <trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html">
<source>Platform</source> <source>Platform</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">29</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
<context context-type="linenumber">35</context> <context context-type="linenumber">35</context>
@ -1375,7 +1387,7 @@
<source>Account ID</source> <source>Account ID</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">63</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4979019387603946865" datatype="html"> <trans-unit id="4979019387603946865" datatype="html">
@ -1453,56 +1465,56 @@
<source>By Account</source> <source>By Account</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">33</context> <context context-type="linenumber">41</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html"> <trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html">
<source>By Currency</source> <source>By Currency</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">58</context> <context context-type="linenumber">66</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html"> <trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html">
<source>By Asset Class</source> <source>By Asset Class</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">86</context> <context context-type="linenumber">94</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html"> <trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html">
<source>By Holding</source> <source>By Holding</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">114</context> <context context-type="linenumber">122</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html"> <trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html">
<source>By Sector</source> <source>By Sector</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">142</context> <context context-type="linenumber">150</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html"> <trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html">
<source>By Continent</source> <source>By Continent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">171</context> <context context-type="linenumber">179</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html"> <trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html">
<source>By Country</source> <source>By Country</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">199</context> <context context-type="linenumber">207</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html"> <trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html">
<source>Regions</source> <source>Regions</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">230</context> <context context-type="linenumber">238</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -1826,7 +1838,7 @@
<source>Portfolio</source> <source>Portfolio</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">111</context> <context context-type="linenumber">107</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context>
@ -2037,7 +2049,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">116</context> <context context-type="linenumber">119</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html"> <trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html">
@ -2051,7 +2063,7 @@
<source>Language</source> <source>Language</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">115</context> <context context-type="linenumber">118</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html"> <trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html">
@ -2104,7 +2116,7 @@
<source>Developed Markets</source> <source>Developed Markets</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">256</context> <context context-type="linenumber">264</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2144,7 +2156,7 @@
<source>Other Markets</source> <source>Other Markets</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">274</context> <context context-type="linenumber">282</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2155,7 +2167,7 @@
<source>Emerging Markets</source> <source>Emerging Markets</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">265</context> <context context-type="linenumber">273</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
@ -2332,14 +2344,14 @@
<source>Experimental Features</source> <source>Experimental Features</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">196</context> <context context-type="linenumber">211</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1931353503905413384" datatype="html"> <trans-unit id="1931353503905413384" datatype="html">
<source>Benchmark</source> <source>Benchmark</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts</context>
<context context-type="linenumber">120</context> <context context-type="linenumber">116</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1b25c6e22f822e07a3e4d5aae4edc5b41fe083c2" datatype="html"> <trans-unit id="1b25c6e22f822e07a3e4d5aae4edc5b41fe083c2" datatype="html">
@ -2353,14 +2365,28 @@
<source>Compare with...</source> <source>Compare with...</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">18</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4210837540bca56dca96fcc451518659d06ad02a" datatype="html"> <trans-unit id="4210837540bca56dca96fcc451518659d06ad02a" datatype="html">
<source>Proportion of Net Worth</source> <source>Proportion of Net Worth</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/allocations/allocations-page.html</context>
<context context-type="linenumber">17</context> <context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="98fc3013bfcbf452b9f37bbfcdb77b9b882866e3" datatype="html">
<source>Excluded from Analysis</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">176</context>
</context-group>
</trans-unit>
<trans-unit id="ac598d664f86ba5783915d65f2664a7f38a9d23a" datatype="html">
<source>Account Type</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html</context>
<context context-type="linenumber">25</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
</body> </body>

51
apps/client/src/styles.scss

@ -18,6 +18,7 @@ $mat-css-light-theme-selector: '.is-light-theme';
:root { :root {
--dark-background: rgb(39, 39, 39); --dark-background: rgb(39, 39, 39);
--font-family-sans-serif: Roboto, 'Helvetica Neue', sans-serif;
--light-background: rgb(255, 255, 255); --light-background: rgb(255, 255, 255);
} }
@ -146,11 +147,8 @@ ngx-skeleton-loader {
@include gf-table; @include gf-table;
} }
.mat-fab, .lead {
.mat-flat-button { font-weight: unset;
&.mat-primary {
color: rgba(var(--light-primary-text)) !important;
}
} }
.mat-card { .mat-card {
@ -164,6 +162,49 @@ ngx-skeleton-loader {
margin: 0 !important; margin: 0 !important;
} }
.mat-fab,
.mat-flat-button {
&.mat-primary {
color: rgba(var(--light-primary-text)) !important;
}
}
.mat-form-field {
&.compact-with-outline {
.mat-form-field-wrapper {
margin: 0.5rem 0 0.25rem;
padding-bottom: 1rem;
.mat-form-field-infix {
border-top-width: 0;
padding: 1rem 0 0.75rem;
.mat-form-field-label {
margin-top: 0.1rem;
}
.mat-select-arrow-wrapper {
transform: none;
}
}
.mat-form-field-prefix {
top: 0;
}
.mat-form-field-suffix {
top: 0;
}
}
}
&.without-hint {
.mat-form-field-wrapper {
padding-bottom: 0;
}
}
}
.no-min-width { .no-min-width {
min-width: unset !important; min-width: unset !important;
} }

8
libs/common/src/lib/helper.ts

@ -1,7 +1,7 @@
import * as currencies from '@dinero.js/currencies'; import * as currencies from '@dinero.js/currencies';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns'; import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
import { de } from 'date-fns/locale'; import { de, es, it, nl } from 'date-fns/locale';
import { ghostfolioScraperApiSymbolPrefix, locale } from './config'; import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
import { Benchmark } from './interfaces'; import { Benchmark } from './interfaces';
@ -75,6 +75,12 @@ export function getCssVariable(aCssVariable: string) {
export function getDateFnsLocale(aLanguageCode: string) { export function getDateFnsLocale(aLanguageCode: string) {
if (aLanguageCode === 'de') { if (aLanguageCode === 'de') {
return de; return de;
} else if (aLanguageCode === 'es') {
return es;
} else if (aLanguageCode === 'it') {
return it;
} else if (aLanguageCode === 'nl') {
return nl;
} }
return undefined; return undefined;

2
libs/common/src/lib/interfaces/historical-data-item.interface.ts

@ -2,5 +2,7 @@ export interface HistoricalDataItem {
averagePrice?: number; averagePrice?: number;
date: string; date: string;
grossPerformancePercent?: number; grossPerformancePercent?: number;
netPerformance?: number;
netPerformanceInPercentage?: number;
value: number; value: number;
} }

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

@ -1,4 +1,7 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import {
PortfolioPosition,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
export interface PortfolioDetails { export interface PortfolioDetails {
accounts: { accounts: {
@ -13,5 +16,6 @@ export interface PortfolioDetails {
filteredValueInBaseCurrency?: number; filteredValueInBaseCurrency?: number;
filteredValueInPercentage: number; filteredValueInPercentage: number;
holdings: { [symbol: string]: PortfolioPosition }; holdings: { [symbol: string]: PortfolioPosition };
summary: PortfolioSummary;
totalValueInBaseCurrency?: number; totalValueInBaseCurrency?: number;
} }

3
libs/common/src/lib/interfaces/portfolio-summary.interface.ts

@ -3,9 +3,10 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
export interface PortfolioSummary extends PortfolioPerformance { export interface PortfolioSummary extends PortfolioPerformance {
annualizedPerformancePercent: number; annualizedPerformancePercent: number;
cash: number; cash: number;
dividend: number;
committedFunds: number; committedFunds: number;
dividend: number;
emergencyFund: number; emergencyFund: number;
excludedAccountsAndActivities: number;
fees: number; fees: number;
firstOrderDate: Date; firstOrderDate: Date;
items: number; items: number;

2
libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts

@ -1,6 +1,8 @@
import { HistoricalDataItem } from '../historical-data-item.interface';
import { PortfolioPerformance } from '../portfolio-performance.interface'; import { PortfolioPerformance } from '../portfolio-performance.interface';
import { ResponseError } from './errors.interface'; import { ResponseError } from './errors.interface';
export interface PortfolioPerformanceResponse extends ResponseError { export interface PortfolioPerformanceResponse extends ResponseError {
chart?: HistoricalDataItem[];
performance: PortfolioPerformance; performance: PortfolioPerformance;
} }

1
libs/common/src/lib/interfaces/statistics.interface.ts

@ -1,6 +1,7 @@
export interface Statistics { export interface Statistics {
activeUsers1d: number; activeUsers1d: number;
activeUsers30d: number; activeUsers30d: number;
dockerHubPulls: number;
gitHubContributors: number; gitHubContributors: number;
gitHubStargazers: number; gitHubStargazers: number;
newUsers30d: number; newUsers30d: number;

2
libs/ui/src/lib/line-chart/line-chart.component.ts

@ -93,7 +93,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
} }
public ngOnChanges() { public ngOnChanges() {
if (this.historicalDataItems) { if (this.historicalDataItems || this.historicalDataItems === null) {
setTimeout(() => { setTimeout(() => {
// Wait for the chartCanvas // Wait for the chartCanvas
this.initialize(); this.initialize();

128
libs/ui/src/lib/value/value.component.html

@ -1,67 +1,73 @@
<ng-template #label><ng-content></ng-content></ng-template> <div *ngIf="icon" class="align-self-center mr-3">
<ng-container *ngIf="value || value === 0 || value === null"> <ion-icon class="h3 m-0" [name]="icon"></ion-icon>
<div </div>
class="d-flex" <div>
[ngClass]="position === 'end' ? 'justify-content-end' : ''" <ng-template #label><ng-content></ng-content></ng-template>
> <ng-container *ngIf="value || value === 0 || value === null">
<ng-container *ngIf="isNumber || value === null"> <div
<ng-container *ngIf="colorizeSign && !useAbsoluteValue"> class="d-flex"
<div *ngIf="value > 0" class="mr-1 text-success">+</div> [ngClass]="position === 'end' ? 'justify-content-end' : ''"
<div *ngIf="value < 0" class="mr-1 text-danger">-</div> >
</ng-container> <ng-container *ngIf="isNumber || value === null">
<div <ng-container *ngIf="colorizeSign && !useAbsoluteValue">
*ngIf="isPercent" <div *ngIf="value > 0" class="mr-1 text-success">+</div>
class="mb-0 value" <div *ngIf="value < 0" class="mr-1 text-danger">-</div>
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
{{ formattedValue }}%
</div>
<div
*ngIf="!isPercent"
class="mb-0 value"
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
<ng-container *ngIf="value === null">
<span class="text-monospace text-muted">***</span>
</ng-container>
<ng-container *ngIf="value !== null">
{{ formattedValue }}
</ng-container> </ng-container>
<div
*ngIf="isPercent"
class="mb-0 value"
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
{{ formattedValue }}%
</div>
<div
*ngIf="!isPercent"
class="mb-0 value"
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
<ng-container *ngIf="value === null">
<span class="text-monospace text-muted">***</span>
</ng-container>
<ng-container *ngIf="value !== null">
{{ formattedValue }}
</ng-container>
</div>
<small *ngIf="currency && size === 'medium'" class="ml-1">
{{ currency }}
</small>
<div *ngIf="currency && size !== 'medium'" class="ml-1">
{{ currency }}
</div>
</ng-container>
<ng-container *ngIf="isString">
<div
class="mb-0 text-truncate value"
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
{{ formattedValue | titlecase }}
</div>
</ng-container>
</div>
<ng-container>
<div *ngIf="size === 'large'">
<span class="h6"
><ng-container *ngTemplateOutlet="label"></ng-container
></span>
<span *ngIf="subLabel" class="text-muted"> {{ subLabel }}</span>
</div> </div>
<small *ngIf="currency && size === 'medium'" class="ml-1"> <small *ngIf="size !== 'large'">
{{ currency }} <ng-container *ngTemplateOutlet="label"></ng-container>
</small> </small>
<div *ngIf="currency && size !== 'medium'" class="ml-1">
{{ currency }}
</div>
</ng-container> </ng-container>
<ng-container *ngIf="isString">
<div
class="mb-0 text-truncate value"
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
>
{{ formattedValue | titlecase }}
</div>
</ng-container>
</div>
<ng-container>
<div *ngIf="size === 'large'">
<span class="h6"
><ng-container *ngTemplateOutlet="label"></ng-container
></span>
<span *ngIf="subLabel" class="text-muted"> {{ subLabel }}</span>
</div>
<small *ngIf="size !== 'large'">
<ng-container *ngTemplateOutlet="label"></ng-container>
</small>
</ng-container> </ng-container>
</ng-container>
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="value === undefined" *ngIf="value === undefined"
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
height: size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem', height:
width: '5rem' size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem',
}" width: '5rem'
></ngx-skeleton-loader> }"
></ngx-skeleton-loader>
</div>

2
libs/ui/src/lib/value/value.component.scss

@ -1,6 +1,6 @@
:host { :host {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
.h2 { .h2 {

1
libs/ui/src/lib/value/value.component.ts

@ -16,6 +16,7 @@ import { isNumber } from 'lodash';
export class ValueComponent implements OnChanges { export class ValueComponent implements OnChanges {
@Input() colorizeSign = false; @Input() colorizeSign = false;
@Input() currency = ''; @Input() currency = '';
@Input() icon = '';
@Input() isAbsolute = false; @Input() isAbsolute = false;
@Input() isCurrency = false; @Input() isCurrency = false;
@Input() isDate = false; @Input() isDate = false;

4
libs/ui/src/lib/value/value.module.ts

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { ValueComponent } from './value.component'; import { ValueComponent } from './value.component';
@ -8,6 +8,6 @@ import { ValueComponent } from './value.component';
declarations: [ValueComponent], declarations: [ValueComponent],
exports: [ValueComponent], exports: [ValueComponent],
imports: [CommonModule, NgxSkeletonLoaderModule], imports: [CommonModule, NgxSkeletonLoaderModule],
providers: [] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfValueModule {} export class GfValueModule {}

6
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "1.195.0", "version": "1.201.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
@ -81,7 +81,7 @@
"@nestjs/schedule": "2.1.0", "@nestjs/schedule": "2.1.0",
"@nestjs/serve-static": "3.0.0", "@nestjs/serve-static": "3.0.0",
"@nrwl/angular": "14.6.4", "@nrwl/angular": "14.6.4",
"@prisma/client": "4.1.1", "@prisma/client": "4.4.0",
"@simplewebauthn/browser": "5.2.1", "@simplewebauthn/browser": "5.2.1",
"@simplewebauthn/server": "5.2.1", "@simplewebauthn/server": "5.2.1",
"@stripe/stripe-js": "1.22.0", "@stripe/stripe-js": "1.22.0",
@ -119,7 +119,7 @@
"passport": "0.6.0", "passport": "0.6.0",
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0", "passport-jwt": "4.0.0",
"prisma": "4.1.1", "prisma": "4.4.0",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rxjs": "7.5.6", "rxjs": "7.5.6",
"stripe": "8.199.0", "stripe": "8.199.0",

2
prisma/migrations/20210605161257_added_symbol_profile/migration.sql

@ -1,5 +1,5 @@
-- AlterTable -- AlterTable
ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT; ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT;
-- CreateTable -- CreateTable
CREATE TABLE "SymbolProfile" ( CREATE TABLE "SymbolProfile" (

2
prisma/migrations/20210612110542_added_auth_device/migration.sql

@ -1,5 +1,5 @@
-- AlterTable -- AlterTable
ALTER TABLE "User" ADD COLUMN "authChallenge" TEXT; ALTER TABLE "User" ADD COLUMN "authChallenge" TEXT;
-- CreateTable -- CreateTable
CREATE TABLE "AuthDevice" ( CREATE TABLE "AuthDevice" (

2
prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "sectors" JSONB; ALTER TABLE "SymbolProfile" ADD COLUMN "sectors" JSONB;

4
prisma/migrations/20210703194509_added_balance_to_account/migration.sql

@ -2,5 +2,5 @@
ALTER TYPE "AccountType" ADD VALUE 'CASH'; ALTER TYPE "AccountType" ADD VALUE 'CASH';
-- AlterTable -- AlterTable
ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0, ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0,
ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD'; ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD';

2
prisma/migrations/20210724160404_added_currency_to_symbol_profile/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "currency" "Currency"; ALTER TABLE "SymbolProfile" ADD COLUMN "currency" "Currency";

2
prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false; ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;

2
prisma/migrations/20210808075949_added_asset_class_to_symbol_profile/migration.sql

@ -2,4 +2,4 @@
CREATE TYPE "AssetClass" AS ENUM ('CASH', 'COMMODITY', 'EQUITY'); CREATE TYPE "AssetClass" AS ENUM ('CASH', 'COMMODITY', 'EQUITY');
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "assetClass" "AssetClass"; ALTER TABLE "SymbolProfile" ADD COLUMN "assetClass" "AssetClass";

2
prisma/migrations/20210815180121_added_settings_to_settings/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "Settings" ADD COLUMN "settings" JSONB; ALTER TABLE "Settings" ADD COLUMN "settings" JSONB;

2
prisma/migrations/20210822200534_added_asset_sub_class_to_symbol_profile/migration.sql

@ -2,4 +2,4 @@
CREATE TYPE "AssetSubClass" AS ENUM ('CRYPTOCURRENCY', 'ETF', 'STOCK'); CREATE TYPE "AssetSubClass" AS ENUM ('CRYPTOCURRENCY', 'ETF', 'STOCK');
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "assetSubClass" "AssetSubClass"; ALTER TABLE "SymbolProfile" ADD COLUMN "assetSubClass" "AssetSubClass";

2
prisma/migrations/20210916182355_added_data_source_to_market_data/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "MarketData" ADD COLUMN "dataSource" "DataSource" NOT NULL DEFAULT E'YAHOO'; ALTER TABLE "MarketData" ADD COLUMN "dataSource" "DataSource" NOT NULL DEFAULT E'YAHOO';

2
prisma/migrations/20211107082008_added_symbol_mapping_to_symbol_profile/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "symbolMapping" JSONB; ALTER TABLE "SymbolProfile" ADD COLUMN "symbolMapping" JSONB;

2
prisma/migrations/20211107171624_added_scraper_configuration_to_symbol_profile/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "scraperConfiguration" JSONB; ALTER TABLE "SymbolProfile" ADD COLUMN "scraperConfiguration" JSONB;

2
prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT; ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT;

2
prisma/migrations/20220924175215_added_is_excluded_to_account/migration.sql

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "isExcluded" BOOLEAN NOT NULL DEFAULT false;

1
prisma/schema.prisma

@ -27,6 +27,7 @@ model Account {
currency String? currency String?
id String @default(uuid()) id String @default(uuid())
isDefault Boolean @default(false) isDefault Boolean @default(false)
isExcluded Boolean @default(false)
name String? name String?
platformId String? platformId String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

36
yarn.lock

@ -3839,22 +3839,22 @@
schema-utils "^3.0.0" schema-utils "^3.0.0"
source-map "^0.7.3" source-map "^0.7.3"
"@prisma/client@4.1.1": "@prisma/client@4.4.0":
version "4.1.1" version "4.4.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.1.1.tgz#dcb1118397deb8247fbe39a1f3eee5606648adf8" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.4.0.tgz#45f59c172dd3621ecc92d7cf9bc765d85e6c7d56"
integrity sha512-2pXuIUYxHv5H9o6QTa1VIsl4yMgsAjKQOitlo8WVTB+vo73rmMJITBPavdGUZSWUc7adMkFzEV3y5rVTUQr77Q== integrity sha512-ciKOP246x1xwr04G9ajHlJ4pkmtu9Q6esVyqVBO0QJihaKQIUvbPjClp17IsRJyxqNpFm4ScbOc/s9DUzKHINQ==
dependencies: dependencies:
"@prisma/engines-version" "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8" "@prisma/engines-version" "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6"
"@prisma/engines-version@4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8": "@prisma/engines-version@4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6":
version "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8" version "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8.tgz#ce00e6377126e491a8b1e0e2039c97e2924bd6d9" resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6.tgz#00875863bb30b670a586a5b5794a000f7f3ad976"
integrity sha512-cRRJwpHFGFJZvtHbY3GZjMffNBEjjZk68ztn+S2hDgPCGB4H66IK26roK94GJxBodSehwRJ0wGyebC2GoIH1JQ== integrity sha512-P5v/PuEIJLYXZUZBvOLPqoyCW+m6StNqHdiR6te++gYVODpPdLakks5HVx3JaZIY+LwR02juJWFlwpc9Eog/ug==
"@prisma/engines@4.1.1": "@prisma/engines@4.4.0":
version "4.1.1" version "4.4.0"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.1.1.tgz#a6a75870618bbd19ff734c51af7dbe9f362c3265" resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.4.0.tgz#6ca7d3ce8eee08dcfa82311b0a02f5ccaac7dc0c"
integrity sha512-DCw8L/SS0IXqmj5IW/fMxOXiifnsfjBzDfRhf0j3NFWqvMCh9OtfjmXQZxVgI2mwvJLc/5jzXhkiWT39qS09dA== integrity sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==
"@rollup/plugin-babel@^5.3.0": "@rollup/plugin-babel@^5.3.0":
version "5.3.1" version "5.3.1"
@ -16763,12 +16763,12 @@ pretty-hrtime@^1.0.3:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A== integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
prisma@4.1.1: prisma@4.4.0:
version "4.1.1" version "4.4.0"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.1.1.tgz#41c2e19896357f484ef21567165d762908376fca" resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.4.0.tgz#0c53324bf6a29474636b3e1964e0d72e0277bf8f"
integrity sha512-yw50J8If2dKP4wYIi695zthsCASQFHiogGvUHHWd3falx/rpsD6Sb1LMLRV9nO3iGG3lozxNJ2PSINxK7xwdpg== integrity sha512-l/QKLmLcKJQFuc+X02LyICo0NWTUVaNNZ00jKJBqwDyhwMAhboD1FWwYV50rkH4Wls0RviAJSFzkC2ZrfawpfA==
dependencies: dependencies:
"@prisma/engines" "4.1.1" "@prisma/engines" "4.4.0"
prismjs@^1.27.0, prismjs@^1.28.0: prismjs@^1.27.0, prismjs@^1.28.0:
version "1.28.0" version "1.28.0"

Loading…
Cancel
Save