Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
0f637a5d0f | |||
3f85c327f5 | |||
c2df99072d | |||
e8afbcad9c | |||
e6d8de781b | |||
6e1935899f | |||
169cb85b66 | |||
fe6658d0ac | |||
1f0381228e | |||
f4b63b5de5 | |||
e45a0ad068 | |||
81c6cc021d | |||
859b24aa5b | |||
2bc325f182 | |||
a6186c23e2 | |||
cf234003ec | |||
8d3954304e | |||
9562139fa6 | |||
c857ea9a8f | |||
5c9fa71d95 | |||
fefbfa31d1 | |||
93a1fae51c | |||
3715edd9ba | |||
e3916e1ba3 | |||
76ceac4edc | |||
333b63bfe2 | |||
3006c21b12 | |||
f01a3f893d | |||
72974e888f | |||
0cee7a0b35 | |||
f3d337b044 | |||
7667af059c | |||
1095b47f45 | |||
dacd7271eb | |||
e093041184 | |||
8f2caa508a | |||
862f670ccf | |||
54bf4c7a43 |
76
CHANGELOG.md
@ -5,6 +5,80 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.202.0 - 07.10.2022
|
||||
|
||||
- Added support for a translated 4% rule in the _FIRE_ section
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the caching of the benchmarks in the markets overview (only cache if fetching was successful)
|
||||
- Improved the wording in the twitter bot service
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the support for cryptocurrencies having a symbol with less than 3 characters (e.g. `SC-USD`)
|
||||
- Fixed the text truncation in the value component
|
||||
|
||||
## 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
|
||||
|
||||
### Changed
|
||||
@ -195,7 +269,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- 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
|
||||
|
||||
### Changed
|
||||
|
29
angular.json
@ -136,10 +136,18 @@
|
||||
"baseHref": "/en/",
|
||||
"localize": ["en"]
|
||||
},
|
||||
"development-es": {
|
||||
"baseHref": "/es/",
|
||||
"localize": ["es"]
|
||||
},
|
||||
"development-it": {
|
||||
"baseHref": "/it/",
|
||||
"localize": ["it"]
|
||||
},
|
||||
"development-nl": {
|
||||
"baseHref": "/nl/",
|
||||
"localize": ["nl"]
|
||||
},
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
@ -184,9 +192,15 @@
|
||||
"development-en": {
|
||||
"browserTarget": "client:build:development-en"
|
||||
},
|
||||
"development-es": {
|
||||
"browserTarget": "client:build:development-es"
|
||||
},
|
||||
"development-it": {
|
||||
"browserTarget": "client:build:development-it"
|
||||
},
|
||||
"development-nl": {
|
||||
"browserTarget": "client:build:development-nl"
|
||||
},
|
||||
"production": {
|
||||
"browserTarget": "client:build:production"
|
||||
}
|
||||
@ -198,7 +212,12 @@
|
||||
"browserTarget": "client:build",
|
||||
"includeContext": true,
|
||||
"outputPath": "src/locales",
|
||||
"targetFiles": ["messages.de.xlf", "messages.it.xlf"]
|
||||
"targetFiles": [
|
||||
"messages.de.xlf",
|
||||
"messages.es.xlf",
|
||||
"messages.it.xlf",
|
||||
"messages.nl.xlf"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
@ -222,9 +241,17 @@
|
||||
"baseHref": "/de/",
|
||||
"translation": "apps/client/src/locales/messages.de.xlf"
|
||||
},
|
||||
"es": {
|
||||
"baseHref": "/es/",
|
||||
"translation": "apps/client/src/locales/messages.es.xlf"
|
||||
},
|
||||
"it": {
|
||||
"baseHref": "/it/",
|
||||
"translation": "apps/client/src/locales/messages.it.xlf"
|
||||
},
|
||||
"nl": {
|
||||
"baseHref": "/nl/",
|
||||
"translation": "apps/client/src/locales/messages.nl.xlf"
|
||||
}
|
||||
},
|
||||
"sourceLocale": "en"
|
||||
|
@ -95,9 +95,10 @@ export class AccountController {
|
||||
);
|
||||
|
||||
let accountsWithAggregations =
|
||||
await this.portfolioService.getAccountsWithAggregations(
|
||||
impersonationUserId || this.request.user.id
|
||||
);
|
||||
await this.portfolioService.getAccountsWithAggregations({
|
||||
userId: impersonationUserId || this.request.user.id,
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationUserId ||
|
||||
@ -137,10 +138,11 @@ export class AccountController {
|
||||
);
|
||||
|
||||
let accountsWithAggregations =
|
||||
await this.portfolioService.getAccountsWithAggregations(
|
||||
impersonationUserId || this.request.user.id,
|
||||
[{ id, type: 'ACCOUNT' }]
|
||||
);
|
||||
await this.portfolioService.getAccountsWithAggregations({
|
||||
filters: [{ id, type: 'ACCOUNT' }],
|
||||
userId: impersonationUserId || this.request.user.id,
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationUserId ||
|
||||
|
@ -107,15 +107,23 @@ export class AccountService {
|
||||
public async getCashDetails({
|
||||
currency,
|
||||
filters = [],
|
||||
userId
|
||||
userId,
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
currency: string;
|
||||
filters?: Filter[];
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<CashDetails> {
|
||||
let totalCashBalanceInBaseCurrency = new Big(0);
|
||||
|
||||
const where: Prisma.AccountWhereInput = { userId };
|
||||
const where: Prisma.AccountWhereInput = {
|
||||
userId
|
||||
};
|
||||
|
||||
if (withExcludedAccounts === false) {
|
||||
where.isExcluded = false;
|
||||
}
|
||||
|
||||
const {
|
||||
ACCOUNT: filtersByAccount,
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { AccountType } from '@prisma/client';
|
||||
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateIf
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateAccountDto {
|
||||
@IsString()
|
||||
@ -11,6 +17,10 @@ export class CreateAccountDto {
|
||||
@IsString()
|
||||
currency: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isExcluded?: boolean;
|
||||
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { AccountType } from '@prisma/client';
|
||||
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateIf
|
||||
} from 'class-validator';
|
||||
|
||||
export class UpdateAccountDto {
|
||||
@IsString()
|
||||
@ -14,6 +20,10 @@ export class UpdateAccountDto {
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isExcluded?: boolean;
|
||||
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
|
@ -73,6 +73,7 @@ export class BenchmarkService {
|
||||
}
|
||||
|
||||
const allTimeHighs = await Promise.all(promises);
|
||||
let storeInCache = true;
|
||||
|
||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||
const { marketPrice } =
|
||||
@ -85,6 +86,8 @@ export class BenchmarkService {
|
||||
allTimeHigh,
|
||||
marketPrice
|
||||
);
|
||||
} else {
|
||||
storeInCache = false;
|
||||
}
|
||||
|
||||
return {
|
||||
@ -100,11 +103,13 @@ export class BenchmarkService {
|
||||
};
|
||||
});
|
||||
|
||||
await this.redisCacheService.set(
|
||||
this.CACHE_KEY_BENCHMARKS,
|
||||
JSON.stringify(benchmarks),
|
||||
ms('4 hours') / 1000
|
||||
);
|
||||
if (storeInCache) {
|
||||
await this.redisCacheService.set(
|
||||
this.CACHE_KEY_BENCHMARKS,
|
||||
JSON.stringify(benchmarks),
|
||||
ms('4 hours') / 1000
|
||||
);
|
||||
}
|
||||
|
||||
return benchmarks;
|
||||
}
|
||||
@ -164,7 +169,7 @@ export class BenchmarkService {
|
||||
);
|
||||
|
||||
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
|
||||
return {
|
||||
const response = {
|
||||
marketData: [
|
||||
...marketDataItems
|
||||
.filter((marketDataItem, index) => {
|
||||
@ -181,17 +186,22 @@ export class BenchmarkService {
|
||||
marketDataItem.marketPrice
|
||||
) * 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) {
|
||||
|
@ -11,6 +11,9 @@ import { NextFunction, Request, Response } from 'express';
|
||||
export class FrontendMiddleware implements NestMiddleware {
|
||||
public indexHtmlDe = '';
|
||||
public indexHtmlEn = '';
|
||||
public indexHtmlEs = '';
|
||||
public indexHtmlIt = '';
|
||||
public indexHtmlNl = '';
|
||||
public isProduction: boolean;
|
||||
|
||||
public constructor(
|
||||
@ -32,6 +35,18 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
|
||||
'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 {}
|
||||
}
|
||||
|
||||
@ -43,6 +58,11 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
req.path === '/en/blog/2022/08/500-stars-on-github/'
|
||||
) {
|
||||
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 (
|
||||
@ -61,6 +81,33 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
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 {
|
||||
res.send(
|
||||
this.interpolate(this.indexHtmlEn, {
|
||||
|
@ -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> {
|
||||
try {
|
||||
const get = bent(
|
||||
@ -245,6 +266,8 @@ export class InfoService {
|
||||
const activeUsers1d = await this.countActiveUsers(1);
|
||||
const activeUsers30d = await this.countActiveUsers(30);
|
||||
const newUsers30d = await this.countNewUsers(30);
|
||||
|
||||
const dockerHubPulls = await this.countDockerHubPulls();
|
||||
const gitHubContributors = await this.countGitHubContributors();
|
||||
const gitHubStargazers = await this.countGitHubStargazers();
|
||||
const slackCommunityUsers = await this.countSlackCommunityUsers();
|
||||
@ -252,6 +275,7 @@ export class InfoService {
|
||||
statistics = {
|
||||
activeUsers1d,
|
||||
activeUsers30d,
|
||||
dockerHubPulls,
|
||||
gitHubContributors,
|
||||
gitHubStargazers,
|
||||
newUsers30d,
|
||||
|
@ -3,8 +3,8 @@ import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { Filter } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
@ -36,6 +36,7 @@ import { UpdateOrderDto } from './update-order.dto';
|
||||
@Controller('order')
|
||||
export class OrderController {
|
||||
public constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly impersonationService: ImpersonationService,
|
||||
private readonly orderService: OrderService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||
@ -73,30 +74,11 @@ export class OrderController {
|
||||
@Query('assetClasses') filterByAssetClasses?: string,
|
||||
@Query('tags') filterByTags?: string
|
||||
): Promise<Activities> {
|
||||
const accountIds = filterByAccounts?.split(',') ?? [];
|
||||
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
||||
const tagIds = filterByTags?.split(',') ?? [];
|
||||
|
||||
const filters: Filter[] = [
|
||||
...accountIds.map((accountId) => {
|
||||
return <Filter>{
|
||||
id: accountId,
|
||||
type: 'ACCOUNT'
|
||||
};
|
||||
}),
|
||||
...assetClasses.map((assetClass) => {
|
||||
return <Filter>{
|
||||
id: assetClass,
|
||||
type: 'ASSET_CLASS'
|
||||
};
|
||||
}),
|
||||
...tagIds.map((tagId) => {
|
||||
return <Filter>{
|
||||
id: tagId,
|
||||
type: 'TAG'
|
||||
};
|
||||
})
|
||||
];
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
filterByTags
|
||||
});
|
||||
|
||||
const impersonationUserId =
|
||||
await this.impersonationService.validateImpersonationId(
|
||||
@ -109,7 +91,8 @@ export class OrderController {
|
||||
filters,
|
||||
userCurrency,
|
||||
includeDrafts: true,
|
||||
userId: impersonationUserId || this.request.user.id
|
||||
userId: impersonationUserId || this.request.user.id,
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
|
||||
if (
|
||||
|
@ -2,6 +2,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
@ -18,6 +19,7 @@ import { OrderService } from './order.service';
|
||||
controllers: [OrderController],
|
||||
exports: [OrderService],
|
||||
imports: [
|
||||
ApiModule,
|
||||
CacheModule,
|
||||
ConfigurationModule,
|
||||
DataGatheringModule,
|
||||
|
@ -189,13 +189,15 @@ export class OrderService {
|
||||
includeDrafts = false,
|
||||
types,
|
||||
userCurrency,
|
||||
userId
|
||||
userId,
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
includeDrafts?: boolean;
|
||||
types?: TypeOfOrder[];
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<Activity[]> {
|
||||
const where: Prisma.OrderWhereInput = { userId };
|
||||
|
||||
@ -284,24 +286,28 @@ export class OrderService {
|
||||
},
|
||||
orderBy: { date: 'asc' }
|
||||
})
|
||||
).map((order) => {
|
||||
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
||||
)
|
||||
.filter((order) => {
|
||||
return withExcludedAccounts || order.Account?.isExcluded === false;
|
||||
})
|
||||
.map((order) => {
|
||||
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
||||
|
||||
return {
|
||||
...order,
|
||||
value,
|
||||
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
order.fee,
|
||||
order.SymbolProfile.currency,
|
||||
userCurrency
|
||||
),
|
||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
return {
|
||||
...order,
|
||||
value,
|
||||
order.SymbolProfile.currency,
|
||||
userCurrency
|
||||
)
|
||||
};
|
||||
});
|
||||
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
order.fee,
|
||||
order.SymbolProfile.currency,
|
||||
userCurrency
|
||||
),
|
||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
value,
|
||||
order.SymbolProfile.currency,
|
||||
userCurrency
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async updateOrder({
|
||||
|
@ -272,23 +272,20 @@ export class PortfolioCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
const isInPercentage = true;
|
||||
|
||||
return Object.keys(totalNetPerformanceValues).map((date) => {
|
||||
return isInPercentage
|
||||
? {
|
||||
date,
|
||||
value: totalInvestmentValues[date].eq(0)
|
||||
? 0
|
||||
: totalNetPerformanceValues[date]
|
||||
.div(totalInvestmentValues[date])
|
||||
.mul(100)
|
||||
.toNumber()
|
||||
}
|
||||
: {
|
||||
date,
|
||||
value: totalNetPerformanceValues[date].toNumber()
|
||||
};
|
||||
const netPerformanceInPercentage = totalInvestmentValues[date].eq(0)
|
||||
? 0
|
||||
: totalNetPerformanceValues[date]
|
||||
.div(totalInvestmentValues[date])
|
||||
.mul(100)
|
||||
.toNumber();
|
||||
|
||||
return {
|
||||
date,
|
||||
netPerformanceInPercentage,
|
||||
netPerformance: totalNetPerformanceValues[date].toNumber(),
|
||||
value: netPerformanceInPercentage
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -7,18 +7,17 @@ import {
|
||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Filter,
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
PortfolioInvestments,
|
||||
PortfolioPerformanceResponse,
|
||||
PortfolioPublicDetails,
|
||||
PortfolioReport,
|
||||
PortfolioSummary
|
||||
PortfolioReport
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import type {
|
||||
@ -52,6 +51,7 @@ export class PortfolioController {
|
||||
|
||||
public constructor(
|
||||
private readonly accessService: AccessService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly portfolioService: PortfolioService,
|
||||
@ -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')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||
@ -143,30 +123,11 @@ export class PortfolioController {
|
||||
): Promise<PortfolioDetails & { hasError: boolean }> {
|
||||
let hasError = false;
|
||||
|
||||
const accountIds = filterByAccounts?.split(',') ?? [];
|
||||
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
||||
const tagIds = filterByTags?.split(',') ?? [];
|
||||
|
||||
const filters: Filter[] = [
|
||||
...accountIds.map((accountId) => {
|
||||
return <Filter>{
|
||||
id: accountId,
|
||||
type: 'ACCOUNT'
|
||||
};
|
||||
}),
|
||||
...assetClasses.map((assetClass) => {
|
||||
return <Filter>{
|
||||
id: assetClass,
|
||||
type: 'ASSET_CLASS'
|
||||
};
|
||||
}),
|
||||
...tagIds.map((tagId) => {
|
||||
return <Filter>{
|
||||
id: tagId,
|
||||
type: 'TAG'
|
||||
};
|
||||
})
|
||||
];
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
filterByTags
|
||||
});
|
||||
|
||||
const {
|
||||
accounts,
|
||||
@ -174,18 +135,21 @@ export class PortfolioController {
|
||||
filteredValueInPercentage,
|
||||
hasErrors,
|
||||
holdings,
|
||||
summary,
|
||||
totalValueInBaseCurrency
|
||||
} = await this.portfolioService.getDetails(
|
||||
} = await this.portfolioService.getDetails({
|
||||
filters,
|
||||
impersonationId,
|
||||
this.request.user.id,
|
||||
range,
|
||||
filters
|
||||
);
|
||||
dateRange: range,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
||||
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
let portfolioSummary = summary;
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
@ -219,6 +183,22 @@ export class PortfolioController {
|
||||
accounts[name].current = current / totalValue;
|
||||
accounts[name].original = original / totalInvestment;
|
||||
}
|
||||
|
||||
portfolioSummary = nullifyValuesInObject(summary, [
|
||||
'cash',
|
||||
'committedFunds',
|
||||
'currentGrossPerformance',
|
||||
'currentNetPerformance',
|
||||
'currentValue',
|
||||
'dividend',
|
||||
'emergencyFund',
|
||||
'excludedAccountsAndActivities',
|
||||
'fees',
|
||||
'items',
|
||||
'netWorth',
|
||||
'totalBuy',
|
||||
'totalSell'
|
||||
]);
|
||||
}
|
||||
|
||||
let hasDetails = true;
|
||||
@ -244,7 +224,8 @@ export class PortfolioController {
|
||||
filteredValueInPercentage,
|
||||
hasError,
|
||||
holdings,
|
||||
totalValueInBaseCurrency
|
||||
totalValueInBaseCurrency,
|
||||
summary: hasDetails ? portfolioSummary : undefined
|
||||
};
|
||||
}
|
||||
|
||||
@ -319,6 +300,35 @@ export class PortfolioController {
|
||||
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')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
@ -369,12 +379,12 @@ export class PortfolioController {
|
||||
hasDetails = user.subscription.type === 'Premium';
|
||||
}
|
||||
|
||||
const { holdings } = await this.portfolioService.getDetails(
|
||||
access.userId,
|
||||
access.userId,
|
||||
'max',
|
||||
[{ id: 'EQUITY', type: 'ASSET_CLASS' }]
|
||||
);
|
||||
const { holdings } = await this.portfolioService.getDetails({
|
||||
dateRange: 'max',
|
||||
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
|
||||
impersonationId: access.userId,
|
||||
userId: access.userId
|
||||
});
|
||||
|
||||
const portfolioPublicDetails: PortfolioPublicDetails = {
|
||||
hasDetails,
|
||||
@ -411,46 +421,6 @@ export class PortfolioController {
|
||||
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')
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
|
@ -2,6 +2,7 @@ import { AccessModule } from '@ghostfolio/api/app/access/access.module';
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
@ -22,6 +23,7 @@ import { RulesService } from './rules.service';
|
||||
exports: [PortfolioService],
|
||||
imports: [
|
||||
AccessModule,
|
||||
ApiModule,
|
||||
ConfigurationModule,
|
||||
DataGatheringModule,
|
||||
DataProviderModule,
|
||||
|
@ -50,8 +50,11 @@ import type {
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import {
|
||||
Account,
|
||||
AssetClass,
|
||||
DataSource,
|
||||
Order,
|
||||
Platform,
|
||||
Prisma,
|
||||
Tag,
|
||||
Type as TypeOfOrder
|
||||
@ -104,14 +107,19 @@ export class PortfolioService {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
|
||||
public async getAccounts(
|
||||
aUserId: string,
|
||||
aFilters?: Filter[]
|
||||
): Promise<AccountWithValue[]> {
|
||||
const where: Prisma.AccountWhereInput = { userId: aUserId };
|
||||
public async getAccounts({
|
||||
filters,
|
||||
userId,
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<AccountWithValue[]> {
|
||||
const where: Prisma.AccountWhereInput = { userId: userId };
|
||||
|
||||
if (aFilters?.[0].id && aFilters?.[0].type === 'ACCOUNT') {
|
||||
where.id = aFilters[0].id;
|
||||
if (filters?.[0].id && filters?.[0].type === 'ACCOUNT') {
|
||||
where.id = filters[0].id;
|
||||
}
|
||||
|
||||
const [accounts, details] = await Promise.all([
|
||||
@ -120,7 +128,12 @@ export class PortfolioService {
|
||||
include: { Order: true, Platform: true },
|
||||
orderBy: { name: 'asc' }
|
||||
}),
|
||||
this.getDetails(aUserId, aUserId, undefined, aFilters)
|
||||
this.getDetails({
|
||||
filters,
|
||||
userId,
|
||||
withExcludedAccounts,
|
||||
impersonationId: userId
|
||||
})
|
||||
]);
|
||||
|
||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||
@ -158,11 +171,20 @@ export class PortfolioService {
|
||||
});
|
||||
}
|
||||
|
||||
public async getAccountsWithAggregations(
|
||||
aUserId: string,
|
||||
aFilters?: Filter[]
|
||||
): Promise<Accounts> {
|
||||
const accounts = await this.getAccounts(aUserId, aFilters);
|
||||
public async getAccountsWithAggregations({
|
||||
filters,
|
||||
userId,
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<Accounts> {
|
||||
const accounts = await this.getAccounts({
|
||||
filters,
|
||||
userId,
|
||||
withExcludedAccounts
|
||||
});
|
||||
let totalBalanceInBaseCurrency = new Big(0);
|
||||
let totalValueInBaseCurrency = new Big(0);
|
||||
let transactionCount = 0;
|
||||
@ -355,11 +377,14 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
public async getChartV2(
|
||||
aImpersonationId: string,
|
||||
aDateRange: DateRange = 'max'
|
||||
): Promise<HistoricalDataContainer> {
|
||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||
public async getChartV2({
|
||||
dateRange = 'max',
|
||||
impersonationId
|
||||
}: {
|
||||
dateRange?: DateRange;
|
||||
impersonationId: string;
|
||||
}): Promise<HistoricalDataContainer> {
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
|
||||
const { portfolioOrders, transactionPoints } =
|
||||
await this.getTransactionPoints({
|
||||
@ -383,7 +408,7 @@ export class PortfolioService {
|
||||
const endDate = new 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 step = Math.round(
|
||||
@ -403,13 +428,21 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
public async getDetails(
|
||||
aImpersonationId: string,
|
||||
aUserId: string,
|
||||
aDateRange: DateRange = 'max',
|
||||
aFilters?: Filter[]
|
||||
): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
||||
const userId = await this.getUserId(aImpersonationId, aUserId);
|
||||
public async getDetails({
|
||||
impersonationId,
|
||||
userId,
|
||||
dateRange = 'max',
|
||||
filters,
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
impersonationId: string;
|
||||
userId: string;
|
||||
dateRange?: DateRange;
|
||||
filters?: Filter[];
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
||||
// TODO:
|
||||
userId = await this.getUserId(impersonationId, userId);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
|
||||
const emergencyFund = new Big(
|
||||
@ -422,8 +455,9 @@ export class PortfolioService {
|
||||
|
||||
const { orders, portfolioOrders, transactionPoints } =
|
||||
await this.getTransactionPoints({
|
||||
filters,
|
||||
userId,
|
||||
filters: aFilters
|
||||
withExcludedAccounts
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
@ -437,15 +471,15 @@ export class PortfolioService {
|
||||
const portfolioStart = parseDate(
|
||||
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
|
||||
);
|
||||
const startDate = this.getStartDate(aDateRange, portfolioStart);
|
||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
startDate
|
||||
);
|
||||
|
||||
const cashDetails = await this.accountService.getCashDetails({
|
||||
filters,
|
||||
userId,
|
||||
currency: userCurrency,
|
||||
filters: aFilters
|
||||
currency: userCurrency
|
||||
});
|
||||
|
||||
const holdings: PortfolioDetails['holdings'] = {};
|
||||
@ -455,10 +489,10 @@ export class PortfolioService {
|
||||
let filteredValueInBaseCurrency = currentPositions.currentValue;
|
||||
|
||||
if (
|
||||
aFilters?.length === 0 ||
|
||||
(aFilters?.length === 1 &&
|
||||
aFilters[0].type === 'ASSET_CLASS' &&
|
||||
aFilters[0].id === 'CASH')
|
||||
filters?.length === 0 ||
|
||||
(filters?.length === 1 &&
|
||||
filters[0].type === 'ASSET_CLASS' &&
|
||||
filters[0].id === 'CASH')
|
||||
) {
|
||||
filteredValueInBaseCurrency = filteredValueInBaseCurrency.plus(
|
||||
cashDetails.balanceInBaseCurrency
|
||||
@ -554,10 +588,10 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
if (
|
||||
aFilters?.length === 0 ||
|
||||
(aFilters?.length === 1 &&
|
||||
aFilters[0].type === 'ASSET_CLASS' &&
|
||||
aFilters[0].id === 'CASH')
|
||||
filters?.length === 0 ||
|
||||
(filters?.length === 1 &&
|
||||
filters[0].type === 'ASSET_CLASS' &&
|
||||
filters[0].id === 'CASH')
|
||||
) {
|
||||
const cashPositions = await this.getCashPositions({
|
||||
cashDetails,
|
||||
@ -573,18 +607,20 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
const accounts = await this.getValueOfAccounts({
|
||||
filters,
|
||||
orders,
|
||||
portfolioItemsNow,
|
||||
userCurrency,
|
||||
userId,
|
||||
filters: aFilters
|
||||
withExcludedAccounts
|
||||
});
|
||||
|
||||
const summary = await this.getSummary(aImpersonationId);
|
||||
const summary = await this.getSummary(impersonationId);
|
||||
|
||||
return {
|
||||
accounts,
|
||||
holdings,
|
||||
summary,
|
||||
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
|
||||
filteredValueInPercentage: summary.netWorth
|
||||
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
|
||||
@ -603,7 +639,11 @@ export class PortfolioService {
|
||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||
|
||||
const orders = (
|
||||
await this.orderService.getOrders({ userCurrency, userId })
|
||||
await this.orderService.getOrders({
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts: true
|
||||
})
|
||||
).filter(({ SymbolProfile }) => {
|
||||
return (
|
||||
SymbolProfile.dataSource === aDataSource &&
|
||||
@ -987,6 +1027,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> {
|
||||
const currency = this.request.user.Settings.settings.baseCurrency;
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
@ -1079,74 +1218,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({
|
||||
cashDetails,
|
||||
emergencyFund,
|
||||
@ -1322,14 +1393,117 @@ export class PortfolioService {
|
||||
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({
|
||||
filters,
|
||||
includeDrafts = false,
|
||||
userId
|
||||
userId,
|
||||
withExcludedAccounts
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
includeDrafts?: boolean;
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<{
|
||||
transactionPoints: TransactionPoint[];
|
||||
orders: OrderWithAccount[];
|
||||
@ -1343,6 +1517,7 @@ export class PortfolioService {
|
||||
includeDrafts,
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts,
|
||||
types: ['BUY', 'SELL']
|
||||
});
|
||||
|
||||
@ -1394,17 +1569,22 @@ export class PortfolioService {
|
||||
orders,
|
||||
portfolioItemsNow,
|
||||
userCurrency,
|
||||
userId
|
||||
userId,
|
||||
withExcludedAccounts
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
orders: OrderWithAccount[];
|
||||
portfolioItemsNow: { [p: string]: TimelinePosition };
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}) {
|
||||
const accounts: PortfolioDetails['accounts'] = {};
|
||||
|
||||
let currentAccounts = [];
|
||||
let currentAccounts: (Account & {
|
||||
Order?: Order[];
|
||||
Platform?: Platform;
|
||||
})[] = [];
|
||||
|
||||
if (filters.length === 0) {
|
||||
currentAccounts = await this.accountService.getAccounts(userId);
|
||||
@ -1424,6 +1604,10 @@ export class PortfolioService {
|
||||
});
|
||||
}
|
||||
|
||||
currentAccounts = currentAccounts.filter((account) => {
|
||||
return withExcludedAccounts || account.isExcluded === false;
|
||||
});
|
||||
|
||||
for (const account of currentAccounts) {
|
||||
const ordersByAccount = orders.filter(({ accountId }) => {
|
||||
return accountId === account.id;
|
||||
|
9
apps/api/src/services/api/api.module.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
@Module({
|
||||
exports: [ApiService],
|
||||
providers: [ApiService]
|
||||
})
|
||||
export class ApiModule {}
|
42
apps/api/src/services/api/api.service.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Filter } from '@ghostfolio/common/interfaces';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ApiService {
|
||||
public constructor() {}
|
||||
|
||||
public buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
filterByTags
|
||||
}: {
|
||||
filterByAccounts?: string;
|
||||
filterByAssetClasses?: string;
|
||||
filterByTags?: string;
|
||||
}): Filter[] {
|
||||
const accountIds = filterByAccounts?.split(',') ?? [];
|
||||
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
||||
const tagIds = filterByTags?.split(',') ?? [];
|
||||
|
||||
return [
|
||||
...accountIds.map((accountId) => {
|
||||
return <Filter>{
|
||||
id: accountId,
|
||||
type: 'ACCOUNT'
|
||||
};
|
||||
}),
|
||||
...assetClasses.map((assetClass) => {
|
||||
return <Filter>{
|
||||
id: assetClass,
|
||||
type: 'ASSET_CLASS'
|
||||
};
|
||||
}),
|
||||
...tagIds.map((tagId) => {
|
||||
return <Filter>{
|
||||
id: tagId,
|
||||
type: 'TAG'
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
@ -57,8 +58,15 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
* DOGEUSD -> DOGE-USD
|
||||
*/
|
||||
public convertToYahooFinanceSymbol(aSymbol: string) {
|
||||
if (aSymbol.includes(this.baseCurrency) && aSymbol.length >= 6) {
|
||||
if (isCurrency(aSymbol.substring(0, aSymbol.length - 3))) {
|
||||
if (
|
||||
aSymbol.includes(this.baseCurrency) &&
|
||||
aSymbol.length > this.baseCurrency.length
|
||||
) {
|
||||
if (
|
||||
isCurrency(
|
||||
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
|
||||
)
|
||||
) {
|
||||
return `${aSymbol}=X`;
|
||||
} else if (
|
||||
this.cryptocurrencyService.isCryptocurrency(
|
||||
@ -90,7 +98,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
try {
|
||||
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
|
||||
const assetProfile = await yahooFinance.quoteSummary(symbol, {
|
||||
modules: ['price', 'summaryProfile']
|
||||
modules: ['price', 'summaryProfile', 'topHoldings']
|
||||
});
|
||||
|
||||
const { assetClass, assetSubClass } = this.parseAssetClass(
|
||||
@ -109,7 +117,16 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
});
|
||||
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 &&
|
||||
assetProfile.summaryProfile?.country
|
||||
) {
|
||||
@ -437,4 +454,46 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -53,13 +53,15 @@ export class TwitterBotService {
|
||||
symbolItem.marketPrice
|
||||
);
|
||||
|
||||
let status = `Current Market Mood: ${emoji} ${text} (${symbolItem.marketPrice}/100)`;
|
||||
let status = `Current market mood is ${emoji} ${text.toLowerCase()} (${
|
||||
symbolItem.marketPrice
|
||||
}/100)`;
|
||||
|
||||
const benchmarkListing = await this.getBenchmarkListing(3);
|
||||
|
||||
if (benchmarkListing?.length > 1) {
|
||||
status += '\n\n';
|
||||
status += '±% from ATH\n';
|
||||
status += '± from ATH in %\n';
|
||||
status += benchmarkListing;
|
||||
}
|
||||
|
||||
|
@ -95,6 +95,13 @@ const routes: Routes = [
|
||||
'./pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.module'
|
||||
).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',
|
||||
loadChildren: () =>
|
||||
|
@ -61,7 +61,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
|
||||
this.accountType = accountType;
|
||||
this.name = name;
|
||||
this.platformName = Platform?.name;
|
||||
this.platformName = Platform?.name ?? '-';
|
||||
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
@ -21,10 +21,12 @@
|
||||
|
||||
<div class="row">
|
||||
<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 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>
|
||||
|
||||
|
@ -2,7 +2,10 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<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-option></mat-option>
|
||||
<mat-option
|
||||
@ -13,7 +16,7 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button
|
||||
class="ml-1"
|
||||
class="mt-1"
|
||||
color="warn"
|
||||
mat-flat-button
|
||||
(click)="onDeleteJobs()"
|
||||
|
@ -5,15 +5,26 @@
|
||||
<mat-card-content>
|
||||
<div class="d-flex my-3">
|
||||
<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 class="d-flex my-3">
|
||||
<div class="w-50" i18n>Activity Count</div>
|
||||
<div class="w-50">
|
||||
<ng-container *ngIf="transactionCount">
|
||||
{{ transactionCount }} ({{ transactionCount / userCount | number
|
||||
: '1.2-2' }} <span i18n>per User</span>)
|
||||
</ng-container>
|
||||
<gf-value
|
||||
precision="0"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="transactionCount"
|
||||
></gf-value>
|
||||
<div *ngIf="transactionCount && userCount">
|
||||
{{ transactionCount / userCount | number : '1.2-2' }}
|
||||
<span i18n>per User</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex my-3">
|
||||
@ -162,8 +173,11 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<form #couponForm="ngForm">
|
||||
<mat-form-field appearance="outline" class="mr-2">
|
||||
<form #couponForm="ngForm" class="align-items-center d-flex">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="compact-with-outline mr-2 without-hint"
|
||||
>
|
||||
<mat-select
|
||||
name="duration"
|
||||
[value]="couponDuration"
|
||||
@ -176,6 +190,7 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button
|
||||
class="mt-1"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
(click)="onAddCoupon()"
|
||||
|
@ -45,21 +45,21 @@
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<gf-value
|
||||
class="align-items-end"
|
||||
class="justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="userItem.accountCount"
|
||||
></gf-value>
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<gf-value
|
||||
class="align-items-end"
|
||||
class="justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="userItem.transactionCount"
|
||||
></gf-value>
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<gf-value
|
||||
class="align-items-end"
|
||||
class="justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="0"
|
||||
[value]="userItem.engagement"
|
||||
|
@ -10,7 +10,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<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-select
|
||||
name="benchmark"
|
||||
@ -26,7 +30,7 @@
|
||||
</mat-form-field>
|
||||
</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
|
||||
[defaultValue]="user?.settings?.dateRange"
|
||||
[isLoading]="isLoading"
|
||||
|
@ -76,8 +76,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
!this.hasImpersonationId &&
|
||||
!this.user.settings.isRestrictedView &&
|
||||
this.user.settings.viewMode !== 'ZEN';
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
public onChangeDateRange(dateRange: DateRange) {
|
||||
@ -104,36 +102,51 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.historicalDataItems = null;
|
||||
this.isLoadingPerformance = true;
|
||||
|
||||
this.dataService
|
||||
.fetchChart({
|
||||
.fetchPortfolioPerformance({
|
||||
range: this.user?.settings?.dateRange,
|
||||
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
|
||||
})
|
||||
.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) => {
|
||||
this.errors = response.errors;
|
||||
this.hasError = response.hasErrors;
|
||||
this.performance = response.performance;
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -1,8 +1,18 @@
|
||||
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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.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 { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -14,8 +24,11 @@ import { takeUntil } from 'rxjs/operators';
|
||||
})
|
||||
export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public hasPermissionToUpdateUserSettings: boolean;
|
||||
public info: InfoItem;
|
||||
public isLoading = true;
|
||||
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
||||
public summary: PortfolioSummary;
|
||||
public user: User;
|
||||
|
||||
@ -25,8 +38,17 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.info = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForSubscription = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.enableSubscription
|
||||
);
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
@ -50,8 +72,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
.subscribe((aId) => {
|
||||
this.hasImpersonationId = !!aId;
|
||||
});
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
public onChangeEmergencyFund(emergencyFund: number) {
|
||||
@ -81,12 +101,30 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
this.isLoading = true;
|
||||
|
||||
this.dataService
|
||||
.fetchPortfolioSummary()
|
||||
.fetchPortfolioDetails({})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.summary = response;
|
||||
.subscribe(({ summary }) => {
|
||||
this.summary = summary;
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -172,6 +172,17 @@
|
||||
></gf-value>
|
||||
</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="col"><hr /></div>
|
||||
</div>
|
||||
|
@ -54,7 +54,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
public hasPermissionToUpdateViewMode: boolean;
|
||||
public hasPermissionToUpdateUserSettings: boolean;
|
||||
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 priceId: string;
|
||||
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
||||
|
@ -94,7 +94,10 @@
|
||||
<ng-container i18n>Base Currency</ng-container>
|
||||
</div>
|
||||
<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
|
||||
name="baseCurrency"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
@ -116,7 +119,10 @@
|
||||
<div class="hint-text text-muted" i18n>Beta</div>
|
||||
</div>
|
||||
<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
|
||||
name="language"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
@ -126,6 +132,9 @@
|
||||
<mat-option [value]="null"></mat-option>
|
||||
<mat-option value="de">Deutsch</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-form-field>
|
||||
</div>
|
||||
@ -138,7 +147,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
name="locale"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
@ -161,7 +173,10 @@
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<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
|
||||
name="viewMode"
|
||||
[disabled]="!hasPermissionToUpdateViewMode"
|
||||
|
@ -59,8 +59,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
this.openCreateAccountDialog();
|
||||
} else if (params['editDialog']) {
|
||||
if (this.accounts) {
|
||||
const account = this.accounts.find((account) => {
|
||||
return account.id === params['accountId'];
|
||||
const account = this.accounts.find(({ id }) => {
|
||||
return id === params['accountId'];
|
||||
});
|
||||
|
||||
this.openUpdateAccountDialog(account);
|
||||
@ -155,6 +155,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
balance,
|
||||
currency,
|
||||
id,
|
||||
isExcluded,
|
||||
name,
|
||||
platformId
|
||||
}: AccountModel): void {
|
||||
@ -165,6 +166,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
balance,
|
||||
currency,
|
||||
id,
|
||||
isExcluded,
|
||||
name,
|
||||
platformId
|
||||
}
|
||||
@ -231,6 +233,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
accountType: AccountType.SECURITIES,
|
||||
balance: 0,
|
||||
currency: this.user?.settings?.baseCurrency,
|
||||
isExcluded: false,
|
||||
name: null,
|
||||
platformId: null
|
||||
}
|
||||
|
@ -50,6 +50,14 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</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">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Account ID</mat-label>
|
||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
@ -15,6 +16,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
|
@ -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 {}
|
@ -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 {}
|
@ -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>
|
@ -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 {}
|
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -2,6 +2,30 @@
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<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-content>
|
||||
<div class="container p-0">
|
||||
|
@ -192,6 +192,17 @@
|
||||
</div>
|
||||
</mat-card>
|
||||
</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">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
|
@ -1,4 +1,7 @@
|
||||
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 { Subject } from 'rxjs';
|
||||
|
||||
@ -11,6 +14,8 @@ import { Subject } from 'rxjs';
|
||||
export class LandingPageComponent implements OnDestroy, OnInit {
|
||||
public currentYear = format(new Date(), 'yyyy');
|
||||
public demoAuthToken: string;
|
||||
public hasPermissionForStatistics: boolean;
|
||||
public statistics: Statistics;
|
||||
public testimonials = [
|
||||
{
|
||||
author: 'Philipp',
|
||||
@ -36,7 +41,16 @@ export class LandingPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
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() {}
|
||||
|
||||
|
@ -42,6 +42,103 @@
|
||||
</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="col text-center">
|
||||
<h2 class="h4 mb-1 text-center">
|
||||
@ -55,6 +152,28 @@
|
||||
</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="col-md-6 offset-md-3">
|
||||
<h2 class="h4 mb-1 text-center">Why <strong>Ghostfolio</strong>?</h2>
|
||||
@ -133,24 +252,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
<div class="row my-3">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 mb-1 text-center">
|
||||
How does <strong>Ghostfolio</strong> work?
|
||||
</h2>
|
||||
<p class="lead mb-3 text-center">Get started in only 3 steps</p>
|
||||
<ol class="m-0 pl-3">
|
||||
<li class="mb-2">
|
||||
Sign up anonymously<br />(no e-mail address nor credit card required)
|
||||
</li>
|
||||
<li class="mb-2">Add any of your historical transactions</li>
|
||||
<li>Get valuable insights of your portfolio composition</li>
|
||||
</ol>
|
||||
</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">Sign up anonymously*</div>
|
||||
<div class="text-muted">
|
||||
<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 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>
|
||||
<p class="lead mb-3 text-center">
|
||||
Join now or check out the example account
|
||||
@ -194,7 +337,7 @@
|
||||
<div class="row">
|
||||
<div class="align-items-center d-flex flex-column w-100">
|
||||
<a
|
||||
class="agplv3-logo"
|
||||
class="logo logo-agplv3 mask"
|
||||
href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
target="_blank"
|
||||
title="GNU Affero General Public License Version 3"
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
import { LandingPageRoutingModule } from './landing-page-routing.module';
|
||||
import { LandingPageComponent } from './landing-page.component';
|
||||
@ -12,8 +14,10 @@ import { LandingPageComponent } from './landing-page.component';
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfLogoModule,
|
||||
GfValueModule,
|
||||
LandingPageRoutingModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
|
@ -3,16 +3,6 @@
|
||||
:host {
|
||||
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 {
|
||||
.mat-stroked-button {
|
||||
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 {
|
||||
aspect-ratio: 16 / 9;
|
||||
max-height: 66vh;
|
||||
@ -56,16 +98,21 @@
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.agplv3-logo {
|
||||
background-color: rgba(var(--light-primary-text));
|
||||
}
|
||||
|
||||
.button-container {
|
||||
.mat-stroked-button {
|
||||
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 {
|
||||
div {
|
||||
background-image: url('/assets/intro-dark.jpg') !important;
|
||||
|
@ -13,8 +13,16 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title i18n>Proportion of Net Worth</mat-card-title>
|
||||
<mat-card-header class="mb-2 overflow-hidden w-100">
|
||||
<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-content>
|
||||
<mat-progress-bar
|
||||
|
@ -20,6 +20,7 @@
|
||||
::ng-deep {
|
||||
.mat-card-header-text {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,7 +126,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
this.isLoadingBenchmarkComparator = true;
|
||||
|
||||
this.dataService
|
||||
.fetchChart({ range: this.user?.settings?.dateRange, version: 2 })
|
||||
.fetchPortfolioPerformance({
|
||||
range: this.user?.settings?.dateRange,
|
||||
version: 2
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ chart }) => {
|
||||
this.firstOrderDate = new Date(chart?.[0]?.date ?? new Date());
|
||||
|
@ -37,14 +37,14 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.dataService
|
||||
.fetchPortfolioSummary()
|
||||
.fetchPortfolioDetails({})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ cash, currentValue }) => {
|
||||
if (cash === null || currentValue === null) {
|
||||
.subscribe(({ summary }) => {
|
||||
if (summary.cash === null || summary.currentValue === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fireWealth = new Big(currentValue);
|
||||
this.fireWealth = new Big(summary.currentValue);
|
||||
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
|
||||
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);
|
||||
|
||||
|
@ -35,7 +35,7 @@
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
<div *ngIf="!isLoading">
|
||||
<div *ngIf="!isLoading" i18n>
|
||||
If you retire today, you would be able to withdraw
|
||||
<span class="font-weight-bold"
|
||||
><gf-value
|
||||
|
@ -31,7 +31,6 @@ import {
|
||||
PortfolioPerformanceResponse,
|
||||
PortfolioPublicDetails,
|
||||
PortfolioReport,
|
||||
PortfolioSummary,
|
||||
UniqueAsset,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
@ -75,60 +74,19 @@ export class DataService {
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
}): Observable<Activities> {
|
||||
let params = new HttpParams();
|
||||
|
||||
if (filters?.length > 0) {
|
||||
const {
|
||||
ACCOUNT: filtersByAccount,
|
||||
ASSET_CLASS: filtersByAssetClass,
|
||||
TAG: filtersByTag
|
||||
} = groupBy(filters, (filter) => {
|
||||
return filter.type;
|
||||
});
|
||||
|
||||
if (filtersByAccount) {
|
||||
params = params.append(
|
||||
'accounts',
|
||||
filtersByAccount
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
|
||||
if (filtersByAssetClass) {
|
||||
params = params.append(
|
||||
'assetClasses',
|
||||
filtersByAssetClass
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
|
||||
if (filtersByTag) {
|
||||
params = params.append(
|
||||
'tags',
|
||||
filtersByTag
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.http.get<any>('/api/v1/order', { params }).pipe(
|
||||
map(({ activities }) => {
|
||||
for (const activity of activities) {
|
||||
activity.createdAt = parseISO(activity.createdAt);
|
||||
activity.date = parseISO(activity.date);
|
||||
}
|
||||
return { activities };
|
||||
return this.http
|
||||
.get<any>('/api/v1/order', {
|
||||
params: this.buildFiltersAsQueryParams({ filters })
|
||||
})
|
||||
);
|
||||
.pipe(
|
||||
map(({ activities }) => {
|
||||
for (const activity of activities) {
|
||||
activity.createdAt = parseISO(activity.createdAt);
|
||||
activity.date = parseISO(activity.date);
|
||||
}
|
||||
return { activities };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public fetchAdminData() {
|
||||
@ -136,30 +94,8 @@ export class DataService {
|
||||
}
|
||||
|
||||
public fetchAdminMarketData({ filters }: { filters?: Filter[] }) {
|
||||
let params = new HttpParams();
|
||||
|
||||
if (filters?.length > 0) {
|
||||
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
|
||||
filters,
|
||||
(filter) => {
|
||||
return filter.type;
|
||||
}
|
||||
);
|
||||
|
||||
if (filtersByAssetSubClass) {
|
||||
params = params.append(
|
||||
'assetSubClasses',
|
||||
filtersByAssetSubClass
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.http.get<AdminMarketData>('/api/v1/admin/market-data', {
|
||||
params
|
||||
params: this.buildFiltersAsQueryParams({ filters })
|
||||
});
|
||||
}
|
||||
|
||||
@ -302,63 +238,37 @@ export class DataService {
|
||||
);
|
||||
}
|
||||
|
||||
public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) {
|
||||
let params = new HttpParams();
|
||||
|
||||
if (filters?.length > 0) {
|
||||
const {
|
||||
ACCOUNT: filtersByAccount,
|
||||
ASSET_CLASS: filtersByAssetClass,
|
||||
TAG: filtersByTag
|
||||
} = groupBy(filters, (filter) => {
|
||||
return filter.type;
|
||||
});
|
||||
|
||||
if (filtersByAccount) {
|
||||
params = params.append(
|
||||
'accounts',
|
||||
filtersByAccount
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
|
||||
if (filtersByAssetClass) {
|
||||
params = params.append(
|
||||
'assetClasses',
|
||||
filtersByAssetClass
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
|
||||
if (filtersByTag) {
|
||||
params = params.append(
|
||||
'tags',
|
||||
filtersByTag
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', {
|
||||
params
|
||||
});
|
||||
public fetchPortfolioDetails({
|
||||
filters
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
}): Observable<PortfolioDetails> {
|
||||
return this.http
|
||||
.get<any>('/api/v1/portfolio/details', {
|
||||
params: this.buildFiltersAsQueryParams({ filters })
|
||||
})
|
||||
.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>(
|
||||
'/api/v1/portfolio/performance',
|
||||
{
|
||||
params
|
||||
}
|
||||
`/api/v${version}/portfolio/performance`,
|
||||
{ params: { range } }
|
||||
);
|
||||
}
|
||||
|
||||
@ -372,18 +282,6 @@ export class DataService {
|
||||
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({
|
||||
dataSource,
|
||||
symbol
|
||||
@ -452,4 +350,53 @@ export class DataService {
|
||||
couponCode
|
||||
});
|
||||
}
|
||||
|
||||
private buildFiltersAsQueryParams({ filters }: { filters?: Filter[] }) {
|
||||
let params = new HttpParams();
|
||||
|
||||
if (filters?.length > 0) {
|
||||
const {
|
||||
ACCOUNT: filtersByAccount,
|
||||
ASSET_CLASS: filtersByAssetClass,
|
||||
TAG: filtersByTag
|
||||
} = groupBy(filters, (filter) => {
|
||||
return filter.type;
|
||||
});
|
||||
|
||||
if (filtersByAccount) {
|
||||
params = params.append(
|
||||
'accounts',
|
||||
filtersByAccount
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
|
||||
if (filtersByAssetClass) {
|
||||
params = params.append(
|
||||
'assetClasses',
|
||||
filtersByAssetClass
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
|
||||
if (filtersByTag) {
|
||||
params = params.append(
|
||||
'tags',
|
||||
filtersByTag
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
BIN
apps/client/src/assets/images/blog/hacktoberfest-2022.png
Normal file
After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
1
apps/client/src/assets/images/logo-alternative-to.svg
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
apps/client/src/assets/images/logo-awesome.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
apps/client/src/assets/images/logo-openstartup.png
Normal file
After Width: | Height: | Size: 56 KiB |
35
apps/client/src/assets/images/logo-privacy-tools.svg
Normal file
@ -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
Normal file
After Width: | Height: | Size: 21 KiB |
9
apps/client/src/assets/images/logo-unraid.svg
Normal file
@ -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 |
@ -6,70 +6,74 @@
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<url>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</urlset>
|
||||
|
@ -15,7 +15,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="ccb2a809018b32a96c813ae69126ce05976109ce" datatype="html">
|
||||
<source>The risk of loss in trading can be substantial. It is not advisable to invest money you may need in the short term.</source>
|
||||
<target state="translated">Das Ausfallrisiko beim Börsenhandel kann erheblich sein. Es ist nicht ratsam, Geld zu investieren, welches Sie kurzfristig benötigen.</target>
|
||||
<target state="translated">Das Ausfallrisiko beim Börsenhandel kann erheblich sein. Es ist nicht ratsam, Geld zu investieren, welches du kurzfristig benötigst.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/app.component.html</context>
|
||||
<context context-type="linenumber">55,56</context>
|
||||
@ -38,7 +38,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<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">33</context>
|
||||
<context context-type="linenumber">35</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html">
|
||||
@ -218,7 +218,7 @@
|
||||
<target state="translated">Symbol</target>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
|
||||
@ -262,7 +262,7 @@
|
||||
<target state="translated">Erstellt</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html">
|
||||
@ -270,7 +270,7 @@
|
||||
<target state="translated">Abgeschlossen</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
|
||||
@ -278,7 +278,7 @@
|
||||
<target state="translated">Status</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html">
|
||||
@ -286,7 +286,7 @@
|
||||
<target state="translated">Anlageprofil</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html">
|
||||
@ -294,7 +294,7 @@
|
||||
<target state="translated">Historische Marktdaten</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html">
|
||||
@ -302,7 +302,7 @@
|
||||
<target state="translated">Daten anzeigen</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html">
|
||||
@ -310,7 +310,7 @@
|
||||
<target state="translated">Stacktrace anzeigen</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html">
|
||||
@ -318,7 +318,7 @@
|
||||
<target state="translated">Job löschen</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html">
|
||||
@ -370,7 +370,7 @@
|
||||
</context-group>
|
||||
<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="linenumber">66</context>
|
||||
<context context-type="linenumber">74</context>
|
||||
</context-group>
|
||||
<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>
|
||||
@ -394,7 +394,7 @@
|
||||
</context-group>
|
||||
<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="linenumber">73</context>
|
||||
<context context-type="linenumber">81</context>
|
||||
</context-group>
|
||||
<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>
|
||||
@ -418,7 +418,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5eebcc824c2d34b17abba8c9ef718c5ce118a5c8" datatype="html">
|
||||
@ -490,7 +490,7 @@
|
||||
<target state="translated">pro Benutzer</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
<context context-type="linenumber">26</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5067ede95804e0e0e2b078747848367a7373cc9b" datatype="html">
|
||||
@ -498,7 +498,7 @@
|
||||
<target state="translated">Letzte Daten einholen</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">33</context>
|
||||
<context context-type="linenumber">44</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="27e27f3f7338baefacfd5668bad03ece9c5955df" datatype="html">
|
||||
@ -506,7 +506,7 @@
|
||||
<target state="translated">Alle Daten einholen</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">46</context>
|
||||
<context context-type="linenumber">57</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="cc65b67b46b69cf06ff1f16a909e61612c9d57b8" datatype="html">
|
||||
@ -518,7 +518,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">60</context>
|
||||
<context context-type="linenumber">71</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="37ff0697aa81b2777250c483d2ce1ebdaa5ab042" datatype="html">
|
||||
@ -526,7 +526,7 @@
|
||||
<target state="translated">Wechselkurse</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">67</context>
|
||||
<context context-type="linenumber">78</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4567a660a7f67e254bbf13219906843e34d9f807" datatype="html">
|
||||
@ -534,7 +534,7 @@
|
||||
<target state="translated">Währung hinzufügen</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">106</context>
|
||||
<context context-type="linenumber">117</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="860e5f056b59410ec8db65cb53955505c6931752" datatype="html">
|
||||
@ -542,7 +542,7 @@
|
||||
<target state="translated">Systemmeldung</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">112</context>
|
||||
<context context-type="linenumber">123</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="657028d5fc9c3da8f2d667b6b15cd0df8b9a3729" datatype="html">
|
||||
@ -550,7 +550,7 @@
|
||||
<target state="translated">Systemmeldung setzen</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">134</context>
|
||||
<context context-type="linenumber">145</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7fd64c34428887e4cd56d05534b89c100b8544ad" datatype="html">
|
||||
@ -558,7 +558,7 @@
|
||||
<target state="translated">Lese-Modus</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">139</context>
|
||||
<context context-type="linenumber">150</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="e698b03c34b459b1b006d7f0473a49b9fcf5dfc1" datatype="html">
|
||||
@ -566,7 +566,7 @@
|
||||
<target state="translated">Gutscheincodes</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">152</context>
|
||||
<context context-type="linenumber">163</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
|
||||
@ -574,7 +574,7 @@
|
||||
<target state="translated">Hinzufügen</target>
|
||||
<context-group purpose="location">
|
||||
<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">198</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html">
|
||||
@ -582,7 +582,7 @@
|
||||
<target state="translated">Verwaltung</target>
|
||||
<context-group purpose="location">
|
||||
<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">205</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html">
|
||||
@ -590,7 +590,7 @@
|
||||
<target state="translated">Cache leeren</target>
|
||||
<context-group purpose="location">
|
||||
<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">209</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2817099043823177227" datatype="html">
|
||||
@ -1034,7 +1034,7 @@
|
||||
<target state="translated">Gesamtvermögen</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">179</context>
|
||||
<context context-type="linenumber">190</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html">
|
||||
@ -1042,7 +1042,7 @@
|
||||
<target state="translated">Performance pro Jahr</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">190</context>
|
||||
<context context-type="linenumber">201</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html">
|
||||
@ -1050,7 +1050,7 @@
|
||||
<target state="translated">Dividenden</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">206</context>
|
||||
<context context-type="linenumber">217</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6785405835169448749" datatype="html">
|
||||
@ -1172,6 +1172,10 @@
|
||||
<trans-unit id="3041670542776846470" datatype="html">
|
||||
<source>This feature requires a subscription.</source>
|
||||
<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 context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
||||
<context context-type="linenumber">67</context>
|
||||
@ -1180,6 +1184,10 @@
|
||||
<trans-unit id="5499742151525073097" datatype="html">
|
||||
<source>Upgrade Plan</source>
|
||||
<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 context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
||||
<context context-type="linenumber">69</context>
|
||||
@ -1382,7 +1390,7 @@
|
||||
<target state="translated">Lokalität</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
|
||||
@ -1390,7 +1398,7 @@
|
||||
<target state="translated">Datums- und Zahlenformat</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
|
||||
@ -1398,7 +1406,7 @@
|
||||
<target state="translated">Ansicht</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
|
||||
@ -1406,7 +1414,7 @@
|
||||
<target state="translated">Einloggen mit Fingerabdruck</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
|
||||
@ -1414,7 +1422,7 @@
|
||||
<target state="translated">Benutzer ID</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
|
||||
@ -1422,7 +1430,7 @@
|
||||
<target state="translated">Zugangsberechtigung</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
|
||||
@ -1516,6 +1524,10 @@
|
||||
<trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html">
|
||||
<source>Platform</source>
|
||||
<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 context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
|
||||
<context context-type="linenumber">35</context>
|
||||
@ -1530,7 +1542,7 @@
|
||||
<target state="translated">Konto ID</target>
|
||||
<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="linenumber">55</context>
|
||||
<context context-type="linenumber">63</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4979019387603946865" datatype="html">
|
||||
@ -1618,7 +1630,7 @@
|
||||
<target state="translated">Nach Konto</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html">
|
||||
@ -1626,7 +1638,7 @@
|
||||
<target state="translated">Nach Währung</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html">
|
||||
@ -1634,7 +1646,7 @@
|
||||
<target state="translated">Nach Asset Class</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html">
|
||||
@ -1642,7 +1654,7 @@
|
||||
<target state="translated">Nach Position</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html">
|
||||
@ -1650,7 +1662,7 @@
|
||||
<target state="translated">Nach Sektor</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html">
|
||||
@ -1658,7 +1670,7 @@
|
||||
<target state="translated">Nach Kontinent</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html">
|
||||
@ -1666,7 +1678,7 @@
|
||||
<target state="translated">Nach Land</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html">
|
||||
@ -1674,7 +1686,7 @@
|
||||
<target state="translated">Regionen</target>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||
@ -2038,7 +2050,7 @@
|
||||
<target state="translated">Portfolio</target>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context>
|
||||
@ -2278,7 +2290,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html">
|
||||
@ -2294,7 +2306,7 @@
|
||||
<target state="translated">Sprache</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html">
|
||||
@ -2302,7 +2314,7 @@
|
||||
<target state="translated">Datenverwaltung</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
<context context-type="linenumber">31</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3ccabfc3dc288eaa2355ba43298c739f85951ec3" datatype="html">
|
||||
@ -2418,7 +2430,7 @@
|
||||
<target state="translated">Entwickelte Länder</target>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||
@ -2430,7 +2442,7 @@
|
||||
<target state="translated">Schwellenländer</target>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||
@ -2610,7 +2622,7 @@
|
||||
<target state="translated">Experimentelle Funktionen</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="1b25c6e22f822e07a3e4d5aae4edc5b41fe083c2" datatype="html">
|
||||
@ -2626,7 +2638,7 @@
|
||||
<target state="translated">Vergleichen mit...</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="1931353503905413384" datatype="html">
|
||||
@ -2634,7 +2646,7 @@
|
||||
<target state="translated">Benchmark</target>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4210837540bca56dca96fcc451518659d06ad02a" datatype="html">
|
||||
@ -2642,7 +2654,31 @@
|
||||
<target state="translated">Anteil am Gesamtvermögen</target>
|
||||
<context-group purpose="location">
|
||||
<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="translated">Kontotyp</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="translated">Von der Analyse ausgenommen</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>
|
||||
</trans-unit>
|
||||
<trans-unit id="458363f8e413759aa9e3235a53fd0f64cc916395" datatype="html">
|
||||
<source> If you retire today, you would be able to withdraw <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="font-weight-bold" >"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="<gf-value class="d-inline-block" [currency]="user?.settings?.baseCurrency" [locale]="user?.settings?.locale" [value]="withdrawalRatePerYear?.toNumber()" >"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="</gf-value>"/> per year<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span >"/> or <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="font-weight-bold" >"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="<gf-value class="d-inline-block" [currency]="user?.settings?.baseCurrency" [locale]="user?.settings?.locale" [value]="withdrawalRatePerMonth?.toNumber()" >"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="</gf-value>"/> per month<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span >"/>, based on your total assets of <x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="<gf-value class="d-inline-block" [currency]="user?.settings?.baseCurrency" [locale]="user?.settings?.locale" [value]="fireWealth?.toNumber()" >"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="</gf-value>"/> and a withdrawal rate of 4%. </source>
|
||||
<target state="translated"> Wenn du heute in den Ruhestand gehen würdest, könnest du <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="font-weight-bold" >"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="<gf-value class="d-inline-block" [currency]="user?.settings?.baseCurrency" [locale]="user?.settings?.locale" [value]="withdrawalRatePerYear?.toNumber()" >"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="</gf-value>"/> pro Jahr<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span >"/> oder <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="font-weight-bold" >"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="<gf-value class="d-inline-block" [currency]="user?.settings?.baseCurrency" [locale]="user?.settings?.locale" [value]="withdrawalRatePerMonth?.toNumber()" >"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="</gf-value>"/> pro Monat<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span >"/> entnehmen, bezogen auf dein Gesamtanlagevermögen von <x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="<gf-value class="d-inline-block" [currency]="user?.settings?.baseCurrency" [locale]="user?.settings?.locale" [value]="fireWealth?.toNumber()" >"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="</gf-value>"/> und einer Entnahmerate von 4%. </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
|
||||
<context context-type="linenumber">38,66</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
</body>
|
||||
|
2687
apps/client/src/locales/messages.es.xlf
Normal file
2686
apps/client/src/locales/messages.nl.xlf
Normal file
@ -35,7 +35,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<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">33</context>
|
||||
<context context-type="linenumber">35</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
|
||||
@ -196,14 +196,14 @@
|
||||
<source>Delete Jobs</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7cd2168068d1fd50772c493d493f83e4e412ebc8" datatype="html">
|
||||
<source>Symbol</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.html</context>
|
||||
@ -237,63 +237,63 @@
|
||||
<source>Attempts</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="1b051734b0ee9021991c91b3ed4e81c244322462" datatype="html">
|
||||
<source>Created</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="edcc19a49c950289ffe5d38be4843cdf194e5622" datatype="html">
|
||||
<source>Finished</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="81b97b8ea996ad1e4f9fca8415021850214884b1" datatype="html">
|
||||
<source>Status</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="779aa6949e9d62c58ad44357d11a3157ef6780f5" datatype="html">
|
||||
<source>Asset Profile</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="8ea23a2cc3e9549fa71e7724870038a17216b210" datatype="html">
|
||||
<source>Historical Market Data</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="30afc50625f30e4ac97acc23fd7e77031a341a8f" datatype="html">
|
||||
<source>View Data</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="94e6ec0f0d021b88dfa4ef191447315fc1898f00" datatype="html">
|
||||
<source>View Stacktrace</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="de50f7bcb18ed16c00012741202155acb5c61acf" datatype="html">
|
||||
<source>Delete Job</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="bfb6a28329c452254e363723ef9718b5178dfd1d" datatype="html">
|
||||
@ -341,7 +341,7 @@
|
||||
</context-group>
|
||||
<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="linenumber">66</context>
|
||||
<context context-type="linenumber">74</context>
|
||||
</context-group>
|
||||
<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>
|
||||
@ -364,7 +364,7 @@
|
||||
</context-group>
|
||||
<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="linenumber">73</context>
|
||||
<context context-type="linenumber">81</context>
|
||||
</context-group>
|
||||
<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>
|
||||
@ -386,7 +386,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5eebcc824c2d34b17abba8c9ef718c5ce118a5c8" datatype="html">
|
||||
@ -449,21 +449,21 @@
|
||||
<source>per User</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
<context context-type="linenumber">26</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5067ede95804e0e0e2b078747848367a7373cc9b" datatype="html">
|
||||
<source>Gather Recent Data</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">33</context>
|
||||
<context context-type="linenumber">44</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="27e27f3f7338baefacfd5668bad03ece9c5955df" datatype="html">
|
||||
<source>Gather All Data</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">46</context>
|
||||
<context context-type="linenumber">57</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="cc65b67b46b69cf06ff1f16a909e61612c9d57b8" datatype="html">
|
||||
@ -474,70 +474,70 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">60</context>
|
||||
<context context-type="linenumber">71</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="37ff0697aa81b2777250c483d2ce1ebdaa5ab042" datatype="html">
|
||||
<source>Exchange Rates</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">67</context>
|
||||
<context context-type="linenumber">78</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4567a660a7f67e254bbf13219906843e34d9f807" datatype="html">
|
||||
<source>Add Currency</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">106</context>
|
||||
<context context-type="linenumber">117</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="860e5f056b59410ec8db65cb53955505c6931752" datatype="html">
|
||||
<source>System Message</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">112</context>
|
||||
<context context-type="linenumber">123</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="657028d5fc9c3da8f2d667b6b15cd0df8b9a3729" datatype="html">
|
||||
<source>Set Message</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">134</context>
|
||||
<context context-type="linenumber">145</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7fd64c34428887e4cd56d05534b89c100b8544ad" datatype="html">
|
||||
<source>Read-only Mode</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">139</context>
|
||||
<context context-type="linenumber">150</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="e698b03c34b459b1b006d7f0473a49b9fcf5dfc1" datatype="html">
|
||||
<source>Coupons</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">152</context>
|
||||
<context context-type="linenumber">163</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="f6755cff4957d5c3c89bafce5651f1b6fa2b1fd9" datatype="html">
|
||||
<source>Add</source>
|
||||
<context-group purpose="location">
|
||||
<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">198</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="e799e6b926557f0098f41888cdf8df868eff3d47" datatype="html">
|
||||
<source>Housekeeping</source>
|
||||
<context-group purpose="location">
|
||||
<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">205</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="c7ac907e52a7ce2ac70b1786eb5f403ce306ce1f" datatype="html">
|
||||
<source>Flush Cache</source>
|
||||
<context-group purpose="location">
|
||||
<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">209</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2817099043823177227" datatype="html">
|
||||
@ -936,21 +936,21 @@
|
||||
<source>Net Worth</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">179</context>
|
||||
<context context-type="linenumber">190</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="e1b20ce1622d86ae0a75a3555a1a9ae7c2c60e58" datatype="html">
|
||||
<source>Annualized Performance</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">190</context>
|
||||
<context context-type="linenumber">201</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="d3aa83bd247983dd056a62f56ffb25269bd98d47" datatype="html">
|
||||
<source>Dividend</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">206</context>
|
||||
<context context-type="linenumber">217</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6785405835169448749" datatype="html">
|
||||
@ -1058,6 +1058,10 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="3041670542776846470" datatype="html">
|
||||
<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 context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
||||
<context context-type="linenumber">67</context>
|
||||
@ -1065,6 +1069,10 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="5499742151525073097" datatype="html">
|
||||
<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 context-type="sourcefile">apps/client/src/app/core/http-response.interceptor.ts</context>
|
||||
<context context-type="linenumber">69</context>
|
||||
@ -1243,42 +1251,42 @@
|
||||
<source>Locale</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
|
||||
<source>Date and number format</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
|
||||
<source>View Mode</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
|
||||
<source>Sign in with fingerprint</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
|
||||
<source>User ID</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
|
||||
<source>Granted Access</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
|
||||
@ -1362,6 +1370,10 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="f53dff66901984e217d461bf10fde4e26612c3d3" datatype="html">
|
||||
<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 context-type="sourcefile">apps/client/src/app/components/accounts-table/accounts-table.component.html</context>
|
||||
<context context-type="linenumber">35</context>
|
||||
@ -1375,7 +1387,7 @@
|
||||
<source>Account ID</source>
|
||||
<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="linenumber">55</context>
|
||||
<context context-type="linenumber">63</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4979019387603946865" datatype="html">
|
||||
@ -1453,56 +1465,56 @@
|
||||
<source>By Account</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="b79f5520c0cb9a00bd589e8a4c86ffcf5ae439d7" datatype="html">
|
||||
<source>By Currency</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="8288ff761f2d259625d2e5a3d96db727926d9cd4" datatype="html">
|
||||
<source>By Asset Class</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="b64539bb7815eb3275b55ad723d3897fc6ba8d23" datatype="html">
|
||||
<source>By Holding</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="9f86714c9a6b74e13c96ab02102ce40c34fe13b9" datatype="html">
|
||||
<source>By Sector</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7017e0e26b53ef322c3e3bbf95f06a85487a12b2" datatype="html">
|
||||
<source>By Continent</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="f27e9dd8de80176286e02312e694cb8d1e485a5d" datatype="html">
|
||||
<source>By Country</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="85780db87ac6c9f202615ac63754551c061e7236" datatype="html">
|
||||
<source>Regions</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||
@ -1826,7 +1838,7 @@
|
||||
<source>Portfolio</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page-routing.module.ts</context>
|
||||
@ -2037,7 +2049,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="c004f99bac91f7dc28e87d458f80e5035ae99884" datatype="html">
|
||||
@ -2051,14 +2063,14 @@
|
||||
<source>Language</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="22b6da584a3402f5d6bc028dcca0975ac27ad830" datatype="html">
|
||||
<source>Data Management</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
<context context-type="linenumber">31</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3ccabfc3dc288eaa2355ba43298c739f85951ec3" datatype="html">
|
||||
@ -2104,7 +2116,7 @@
|
||||
<source>Developed Markets</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||
@ -2144,7 +2156,7 @@
|
||||
<source>Other Markets</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||
@ -2155,7 +2167,7 @@
|
||||
<source>Emerging Markets</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||
@ -2332,14 +2344,14 @@
|
||||
<source>Experimental Features</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="1931353503905413384" datatype="html">
|
||||
<source>Benchmark</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="1b25c6e22f822e07a3e4d5aae4edc5b41fe083c2" datatype="html">
|
||||
@ -2353,16 +2365,37 @@
|
||||
<source>Compare with...</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4210837540bca56dca96fcc451518659d06ad02a" datatype="html">
|
||||
<source>Proportion of Net Worth</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="458363f8e413759aa9e3235a53fd0f64cc916395" datatype="html">
|
||||
<source> If you retire today, you would be able to withdraw <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="font-weight-bold" >"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="<gf-value class="d-inline-block" [currency]="user?.settings?.baseCurrency" [locale]="user?.settings?.locale" [value]="withdrawalRatePerYear?.toNumber()" >"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="</gf-value>"/> per year<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span >"/> or <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="font-weight-bold" >"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="<gf-value class="d-inline-block" [currency]="user?.settings?.baseCurrency" [locale]="user?.settings?.locale" [value]="withdrawalRatePerMonth?.toNumber()" >"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="</gf-value>"/> per month<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span >"/>, based on your total assets of <x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="<gf-value class="d-inline-block" [currency]="user?.settings?.baseCurrency" [locale]="user?.settings?.locale" [value]="fireWealth?.toNumber()" >"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="</gf-value>"/> and a withdrawal rate of 4%. </source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
|
||||
<context context-type="linenumber">38,66</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
</xliff>
|
||||
|
@ -18,6 +18,7 @@ $mat-css-light-theme-selector: '.is-light-theme';
|
||||
|
||||
:root {
|
||||
--dark-background: rgb(39, 39, 39);
|
||||
--font-family-sans-serif: Roboto, 'Helvetica Neue', sans-serif;
|
||||
--light-background: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
@ -146,11 +147,8 @@ ngx-skeleton-loader {
|
||||
@include gf-table;
|
||||
}
|
||||
|
||||
.mat-fab,
|
||||
.mat-flat-button {
|
||||
&.mat-primary {
|
||||
color: rgba(var(--light-primary-text)) !important;
|
||||
}
|
||||
.lead {
|
||||
font-weight: unset;
|
||||
}
|
||||
|
||||
.mat-card {
|
||||
@ -164,6 +162,49 @@ ngx-skeleton-loader {
|
||||
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 {
|
||||
min-width: unset !important;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as currencies from '@dinero.js/currencies';
|
||||
import { DataSource } from '@prisma/client';
|
||||
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 { Benchmark } from './interfaces';
|
||||
@ -75,6 +75,12 @@ export function getCssVariable(aCssVariable: string) {
|
||||
export function getDateFnsLocale(aLanguageCode: string) {
|
||||
if (aLanguageCode === 'de') {
|
||||
return de;
|
||||
} else if (aLanguageCode === 'es') {
|
||||
return es;
|
||||
} else if (aLanguageCode === 'it') {
|
||||
return it;
|
||||
} else if (aLanguageCode === 'nl') {
|
||||
return nl;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
@ -2,5 +2,7 @@ export interface HistoricalDataItem {
|
||||
averagePrice?: number;
|
||||
date: string;
|
||||
grossPerformancePercent?: number;
|
||||
netPerformance?: number;
|
||||
netPerformanceInPercentage?: number;
|
||||
value: number;
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
PortfolioPosition,
|
||||
PortfolioSummary
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface PortfolioDetails {
|
||||
accounts: {
|
||||
@ -13,5 +16,6 @@ export interface PortfolioDetails {
|
||||
filteredValueInBaseCurrency?: number;
|
||||
filteredValueInPercentage: number;
|
||||
holdings: { [symbol: string]: PortfolioPosition };
|
||||
summary: PortfolioSummary;
|
||||
totalValueInBaseCurrency?: number;
|
||||
}
|
||||
|
@ -3,9 +3,10 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
|
||||
export interface PortfolioSummary extends PortfolioPerformance {
|
||||
annualizedPerformancePercent: number;
|
||||
cash: number;
|
||||
dividend: number;
|
||||
committedFunds: number;
|
||||
dividend: number;
|
||||
emergencyFund: number;
|
||||
excludedAccountsAndActivities: number;
|
||||
fees: number;
|
||||
firstOrderDate: Date;
|
||||
items: number;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { HistoricalDataItem } from '../historical-data-item.interface';
|
||||
import { PortfolioPerformance } from '../portfolio-performance.interface';
|
||||
import { ResponseError } from './errors.interface';
|
||||
|
||||
export interface PortfolioPerformanceResponse extends ResponseError {
|
||||
chart?: HistoricalDataItem[];
|
||||
performance: PortfolioPerformance;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
export interface Statistics {
|
||||
activeUsers1d: number;
|
||||
activeUsers30d: number;
|
||||
dockerHubPulls: number;
|
||||
gitHubContributors: number;
|
||||
gitHubStargazers: number;
|
||||
newUsers30d: number;
|
||||
|
@ -93,7 +93,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
public ngOnChanges() {
|
||||
if (this.historicalDataItems) {
|
||||
if (this.historicalDataItems || this.historicalDataItems === null) {
|
||||
setTimeout(() => {
|
||||
// Wait for the chartCanvas
|
||||
this.initialize();
|
||||
|
@ -1,67 +1,73 @@
|
||||
<ng-template #label><ng-content></ng-content></ng-template>
|
||||
<ng-container *ngIf="value || value === 0 || value === null">
|
||||
<div
|
||||
class="d-flex"
|
||||
[ngClass]="position === 'end' ? 'justify-content-end' : ''"
|
||||
>
|
||||
<ng-container *ngIf="isNumber || value === null">
|
||||
<ng-container *ngIf="colorizeSign && !useAbsoluteValue">
|
||||
<div *ngIf="value > 0" class="mr-1 text-success">+</div>
|
||||
<div *ngIf="value < 0" class="mr-1 text-danger">-</div>
|
||||
<div *ngIf="icon" class="align-self-center mr-3">
|
||||
<ion-icon class="h3 m-0" [name]="icon"></ion-icon>
|
||||
</div>
|
||||
<div class="w-100">
|
||||
<ng-template #label><ng-content></ng-content></ng-template>
|
||||
<ng-container *ngIf="value || value === 0 || value === null">
|
||||
<div
|
||||
class="d-flex"
|
||||
[ngClass]="position === 'end' ? 'justify-content-end' : ''"
|
||||
>
|
||||
<ng-container *ngIf="isNumber || value === null">
|
||||
<ng-container *ngIf="colorizeSign && !useAbsoluteValue">
|
||||
<div *ngIf="value > 0" class="mr-1 text-success">+</div>
|
||||
<div *ngIf="value < 0" class="mr-1 text-danger">-</div>
|
||||
</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
|
||||
*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>
|
||||
<small *ngIf="size !== 'large'">
|
||||
<ng-container *ngTemplateOutlet="label"></ng-container>
|
||||
</small>
|
||||
<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
|
||||
*ngIf="value === undefined"
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem',
|
||||
width: '5rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="value === undefined"
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height:
|
||||
size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem',
|
||||
width: '5rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
.h2 {
|
||||
|
@ -16,6 +16,7 @@ import { isNumber } from 'lodash';
|
||||
export class ValueComponent implements OnChanges {
|
||||
@Input() colorizeSign = false;
|
||||
@Input() currency = '';
|
||||
@Input() icon = '';
|
||||
@Input() isAbsolute = false;
|
||||
@Input() isCurrency = false;
|
||||
@Input() isDate = false;
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { ValueComponent } from './value.component';
|
||||
@ -8,6 +8,6 @@ import { ValueComponent } from './value.component';
|
||||
declarations: [ValueComponent],
|
||||
exports: [ValueComponent],
|
||||
imports: [CommonModule, NgxSkeletonLoaderModule],
|
||||
providers: []
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfValueModule {}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.195.0",
|
||||
"version": "1.202.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -81,7 +81,7 @@
|
||||
"@nestjs/schedule": "2.1.0",
|
||||
"@nestjs/serve-static": "3.0.0",
|
||||
"@nrwl/angular": "14.6.4",
|
||||
"@prisma/client": "4.1.1",
|
||||
"@prisma/client": "4.4.0",
|
||||
"@simplewebauthn/browser": "5.2.1",
|
||||
"@simplewebauthn/server": "5.2.1",
|
||||
"@stripe/stripe-js": "1.22.0",
|
||||
@ -119,7 +119,7 @@
|
||||
"passport": "0.6.0",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
"passport-jwt": "4.0.0",
|
||||
"prisma": "4.1.1",
|
||||
"prisma": "4.4.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "7.5.6",
|
||||
"stripe": "8.199.0",
|
||||
|
@ -1,5 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT;
|
||||
ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SymbolProfile" (
|
||||
|
@ -1,5 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "authChallenge" TEXT;
|
||||
ALTER TABLE "User" ADD COLUMN "authChallenge" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuthDevice" (
|
||||
|
@ -1,2 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "sectors" JSONB;
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "sectors" JSONB;
|
||||
|
@ -2,5 +2,5 @@
|
||||
ALTER TYPE "AccountType" ADD VALUE 'CASH';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD';
|
||||
ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD';
|
||||
|
@ -1,2 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "currency" "Currency";
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "currency" "Currency";
|
||||
|
@ -1,2 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
@ -2,4 +2,4 @@
|
||||
CREATE TYPE "AssetClass" AS ENUM ('CASH', 'COMMODITY', 'EQUITY');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "assetClass" "AssetClass";
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "assetClass" "AssetClass";
|
||||
|
@ -1,2 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Settings" ADD COLUMN "settings" JSONB;
|
||||
ALTER TABLE "Settings" ADD COLUMN "settings" JSONB;
|
||||
|
@ -2,4 +2,4 @@
|
||||
CREATE TYPE "AssetSubClass" AS ENUM ('CRYPTOCURRENCY', 'ETF', 'STOCK');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "assetSubClass" "AssetSubClass";
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "assetSubClass" "AssetSubClass";
|
||||
|
@ -1,2 +1,2 @@
|
||||
-- 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';
|
||||
|
@ -1,2 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "symbolMapping" JSONB;
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "symbolMapping" JSONB;
|
||||
|
@ -1,2 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "scraperConfiguration" JSONB;
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "scraperConfiguration" JSONB;
|
||||
|
@ -1,2 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT;
|
||||
ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT;
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "isExcluded" BOOLEAN NOT NULL DEFAULT false;
|
@ -27,6 +27,7 @@ model Account {
|
||||
currency String?
|
||||
id String @default(uuid())
|
||||
isDefault Boolean @default(false)
|
||||
isExcluded Boolean @default(false)
|
||||
name String?
|
||||
platformId String?
|
||||
updatedAt DateTime @updatedAt
|
||||
|
36
yarn.lock
@ -3839,22 +3839,22 @@
|
||||
schema-utils "^3.0.0"
|
||||
source-map "^0.7.3"
|
||||
|
||||
"@prisma/client@4.1.1":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.1.1.tgz#dcb1118397deb8247fbe39a1f3eee5606648adf8"
|
||||
integrity sha512-2pXuIUYxHv5H9o6QTa1VIsl4yMgsAjKQOitlo8WVTB+vo73rmMJITBPavdGUZSWUc7adMkFzEV3y5rVTUQr77Q==
|
||||
"@prisma/client@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.4.0.tgz#45f59c172dd3621ecc92d7cf9bc765d85e6c7d56"
|
||||
integrity sha512-ciKOP246x1xwr04G9ajHlJ4pkmtu9Q6esVyqVBO0QJihaKQIUvbPjClp17IsRJyxqNpFm4ScbOc/s9DUzKHINQ==
|
||||
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":
|
||||
version "4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.1.0-48.8d8414deb360336e4698a65aa45a1fbaf1ce13d8.tgz#ce00e6377126e491a8b1e0e2039c97e2924bd6d9"
|
||||
integrity sha512-cRRJwpHFGFJZvtHbY3GZjMffNBEjjZk68ztn+S2hDgPCGB4H66IK26roK94GJxBodSehwRJ0wGyebC2GoIH1JQ==
|
||||
"@prisma/engines-version@4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6":
|
||||
version "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6.tgz#00875863bb30b670a586a5b5794a000f7f3ad976"
|
||||
integrity sha512-P5v/PuEIJLYXZUZBvOLPqoyCW+m6StNqHdiR6te++gYVODpPdLakks5HVx3JaZIY+LwR02juJWFlwpc9Eog/ug==
|
||||
|
||||
"@prisma/engines@4.1.1":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.1.1.tgz#a6a75870618bbd19ff734c51af7dbe9f362c3265"
|
||||
integrity sha512-DCw8L/SS0IXqmj5IW/fMxOXiifnsfjBzDfRhf0j3NFWqvMCh9OtfjmXQZxVgI2mwvJLc/5jzXhkiWT39qS09dA==
|
||||
"@prisma/engines@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.4.0.tgz#6ca7d3ce8eee08dcfa82311b0a02f5ccaac7dc0c"
|
||||
integrity sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==
|
||||
|
||||
"@rollup/plugin-babel@^5.3.0":
|
||||
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"
|
||||
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
|
||||
|
||||
prisma@4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.1.1.tgz#41c2e19896357f484ef21567165d762908376fca"
|
||||
integrity sha512-yw50J8If2dKP4wYIi695zthsCASQFHiogGvUHHWd3falx/rpsD6Sb1LMLRV9nO3iGG3lozxNJ2PSINxK7xwdpg==
|
||||
prisma@4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.4.0.tgz#0c53324bf6a29474636b3e1964e0d72e0277bf8f"
|
||||
integrity sha512-l/QKLmLcKJQFuc+X02LyICo0NWTUVaNNZ00jKJBqwDyhwMAhboD1FWwYV50rkH4Wls0RviAJSFzkC2ZrfawpfA==
|
||||
dependencies:
|
||||
"@prisma/engines" "4.1.1"
|
||||
"@prisma/engines" "4.4.0"
|
||||
|
||||
prismjs@^1.27.0, prismjs@^1.28.0:
|
||||
version "1.28.0"
|
||||
|