Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
6dcb0d8583 | |||
40b6777814 | |||
25deba16df | |||
be93ca8968 | |||
0436cc6487 | |||
857708dc4d | |||
1ca4f885b0 | |||
c9368c5cf2 | |||
29423efea3 | |||
f3ee99fb2b | |||
3df8810412 | |||
b8ca88c6df | |||
2c068c412d | |||
9fdbd22cb5 | |||
8f5f4c5875 | |||
50fb82a6e6 | |||
2c10cd7edf | |||
bbde86c66e | |||
73c0843d51 | |||
04fc2cd3e1 | |||
b39c97ab9f | |||
1dd5e9c787 | |||
a9985b65b8 |
38
CHANGELOG.md
38
CHANGELOG.md
@ -5,7 +5,43 @@ 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).
|
||||
|
||||
## 2.43.0 - 2024-01-23
|
||||
## 2.46.0 - 2024-01-28
|
||||
|
||||
### Added
|
||||
|
||||
- Added a button to reset the active filters in the assistant (experimental)
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated the portfolio allocations to work with the filters of the assistant (experimental)
|
||||
- Migrated the portfolio holdings to work with the filters of the assistant (experimental)
|
||||
|
||||
## 2.45.0 - 2024-01-27
|
||||
|
||||
### Added
|
||||
|
||||
- Extended the assistant by an account selector (experimental)
|
||||
- Added support to grant private access with permissions (experimental)
|
||||
- Added `permissions` to the `Access` model
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated the tag selector to a form group in the assistant (experimental)
|
||||
- Formatted the name in the _EOD Historical Data_ service
|
||||
- Improved the language localization for German (`de`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the import for activities with `MANUAL` data source and type `FEE`, `INTEREST`, `ITEM` or `LIABILITY`
|
||||
- Removed holdings with incomplete data from the _Top 3_ and _Bottom 3_ performers on the analysis page
|
||||
|
||||
## 2.44.0 - 2024-01-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved the validation for non-numeric results in the _EOD Historical Data_ service
|
||||
|
||||
## 2.43.1 - 2024-01-23
|
||||
|
||||
### Added
|
||||
|
||||
|
@ -13,7 +13,6 @@ COPY ./.yarnrc .yarnrc
|
||||
COPY ./prisma/schema.prisma prisma/schema.prisma
|
||||
|
||||
RUN apt update && apt install -y \
|
||||
curl \
|
||||
g++ \
|
||||
git \
|
||||
make \
|
||||
@ -53,6 +52,7 @@ RUN yarn database:generate-typings
|
||||
# Image to run, copy everything needed from builder
|
||||
FROM node:18-slim
|
||||
RUN apt update && apt install -y \
|
||||
curl \
|
||||
openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
@ -42,23 +42,27 @@ export class AccessController {
|
||||
where: { userId: this.request.user.id }
|
||||
});
|
||||
|
||||
return accessesWithGranteeUser.map((access) => {
|
||||
if (access.GranteeUser) {
|
||||
return accessesWithGranteeUser.map(
|
||||
({ alias, GranteeUser, id, permissions }) => {
|
||||
if (GranteeUser) {
|
||||
return {
|
||||
alias,
|
||||
id,
|
||||
permissions,
|
||||
grantee: GranteeUser?.id,
|
||||
type: 'PRIVATE'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
alias: access.alias,
|
||||
grantee: access.GranteeUser?.id,
|
||||
id: access.id,
|
||||
type: 'RESTRICTED_VIEW'
|
||||
alias,
|
||||
id,
|
||||
permissions,
|
||||
grantee: 'Public',
|
||||
type: 'PUBLIC'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
alias: access.alias,
|
||||
grantee: 'Public',
|
||||
id: access.id,
|
||||
type: 'PUBLIC'
|
||||
};
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
@HasPermission(permissions.createAccess)
|
||||
@ -83,6 +87,7 @@ export class AccessController {
|
||||
GranteeUser: data.granteeUserId
|
||||
? { connect: { id: data.granteeUserId } }
|
||||
: undefined,
|
||||
permissions: data.permissions,
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
});
|
||||
} catch {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { AccessPermission } from '@prisma/client';
|
||||
import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class CreateAccessDto {
|
||||
@IsOptional()
|
||||
@ -9,7 +10,7 @@ export class CreateAccessDto {
|
||||
@IsUUID()
|
||||
granteeUserId?: string;
|
||||
|
||||
@IsEnum(AccessPermission, { each: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
type?: 'PUBLIC';
|
||||
permissions?: AccessPermission[];
|
||||
}
|
||||
|
@ -575,7 +575,7 @@ export class ImportService {
|
||||
|
||||
for (const [
|
||||
index,
|
||||
{ currency, dataSource, symbol }
|
||||
{ currency, dataSource, symbol, type }
|
||||
] of uniqueActivitiesDto.entries()) {
|
||||
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
|
||||
throw new Error(
|
||||
@ -583,28 +583,33 @@ export class ImportService {
|
||||
);
|
||||
}
|
||||
|
||||
const assetProfile = (
|
||||
await this.dataProviderService.getAssetProfiles([
|
||||
{ dataSource, symbol }
|
||||
])
|
||||
)?.[symbol];
|
||||
const assetProfile = {
|
||||
currency,
|
||||
...(
|
||||
await this.dataProviderService.getAssetProfiles([
|
||||
{ dataSource, symbol }
|
||||
])
|
||||
)?.[symbol]
|
||||
};
|
||||
|
||||
if (!assetProfile?.name) {
|
||||
throw new Error(
|
||||
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||
);
|
||||
}
|
||||
if (type === 'BUY' || type === 'DIVIDEND' || type === 'SELL') {
|
||||
if (!assetProfile?.name) {
|
||||
throw new Error(
|
||||
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
assetProfile.currency !== currency &&
|
||||
!this.exchangeRateDataService.hasCurrencyPair(
|
||||
currency,
|
||||
assetProfile.currency
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"`
|
||||
);
|
||||
if (
|
||||
assetProfile.currency !== currency &&
|
||||
!this.exchangeRateDataService.hasCurrencyPair(
|
||||
currency,
|
||||
assetProfile.currency
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =
|
||||
|
@ -74,6 +74,11 @@ export class PortfolioController {
|
||||
): Promise<PortfolioDetails & { hasError: boolean }> {
|
||||
let hasDetails = true;
|
||||
let hasError = false;
|
||||
const hasReadRestrictedAccessPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user: this.request.user
|
||||
});
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||
hasDetails = this.request.user.subscription.type === 'Premium';
|
||||
@ -108,7 +113,7 @@ export class PortfolioController {
|
||||
let portfolioSummary = summary;
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
hasReadRestrictedAccessPermission ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
const totalInvestment = Object.values(holdings)
|
||||
@ -148,7 +153,7 @@ export class PortfolioController {
|
||||
|
||||
if (
|
||||
hasDetails === false ||
|
||||
impersonationId ||
|
||||
hasReadRestrictedAccessPermission ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
portfolioSummary = nullifyValuesInObject(summary, [
|
||||
@ -164,6 +169,7 @@ export class PortfolioController {
|
||||
'excludedAccountsAndActivities',
|
||||
'fees',
|
||||
'fireWealth',
|
||||
'interest',
|
||||
'items',
|
||||
'liabilities',
|
||||
'netWorth',
|
||||
@ -216,6 +222,12 @@ export class PortfolioController {
|
||||
@Query('range') dateRange: DateRange = 'max',
|
||||
@Query('tags') filterByTags?: string
|
||||
): Promise<PortfolioDividends> {
|
||||
const hasReadRestrictedAccessPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user: this.request.user
|
||||
});
|
||||
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
@ -230,7 +242,7 @@ export class PortfolioController {
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
hasReadRestrictedAccessPermission ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
const maxDividend = dividends.reduce(
|
||||
@ -266,6 +278,12 @@ export class PortfolioController {
|
||||
@Query('range') dateRange: DateRange = 'max',
|
||||
@Query('tags') filterByTags?: string
|
||||
): Promise<PortfolioInvestments> {
|
||||
const hasReadRestrictedAccessPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user: this.request.user
|
||||
});
|
||||
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
@ -281,7 +299,7 @@ export class PortfolioController {
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
hasReadRestrictedAccessPermission ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
const maxInvestment = investments.reduce(
|
||||
@ -329,6 +347,12 @@ export class PortfolioController {
|
||||
@Query('tags') filterByTags?: string,
|
||||
@Query('withExcludedAccounts') withExcludedAccounts = false
|
||||
): Promise<PortfolioPerformanceResponse> {
|
||||
const hasReadRestrictedAccessPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user: this.request.user
|
||||
});
|
||||
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
@ -344,7 +368,7 @@ export class PortfolioController {
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
hasReadRestrictedAccessPermission ||
|
||||
this.request.user.Settings.settings.viewMode === 'ZEN' ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
|
@ -38,6 +38,10 @@ export class UpdateUserSettingDto {
|
||||
@IsOptional()
|
||||
emergencyFund?: number;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
'filters.accounts'?: string[];
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
'filters.tags'?: string[];
|
||||
|
@ -105,6 +105,24 @@ export class UserService {
|
||||
return usersWithAdminRole.length > 0;
|
||||
}
|
||||
|
||||
public hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user
|
||||
}: {
|
||||
impersonationId: string;
|
||||
user: UserWithSettings;
|
||||
}) {
|
||||
if (!impersonationId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const access = user.Access?.find(({ id }) => {
|
||||
return id === impersonationId;
|
||||
});
|
||||
|
||||
return access?.permissions?.includes('READ_RESTRICTED') ?? true;
|
||||
}
|
||||
|
||||
public isRestrictedView(aUser: UserWithSettings) {
|
||||
return aUser.Settings.settings.isRestrictedView ?? false;
|
||||
}
|
||||
@ -113,6 +131,7 @@ export class UserService {
|
||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
||||
): Promise<UserWithSettings | null> {
|
||||
const {
|
||||
Access,
|
||||
accessToken,
|
||||
Account,
|
||||
Analytics,
|
||||
@ -127,6 +146,7 @@ export class UserService {
|
||||
updatedAt
|
||||
} = await this.prismaService.user.findUnique({
|
||||
include: {
|
||||
Access: true,
|
||||
Account: {
|
||||
include: { Platform: true }
|
||||
},
|
||||
@ -138,6 +158,7 @@ export class UserService {
|
||||
});
|
||||
|
||||
const user: UserWithSettings = {
|
||||
Access,
|
||||
accessToken,
|
||||
Account,
|
||||
authChallenge,
|
||||
@ -198,18 +219,18 @@ export class UserService {
|
||||
new Date(),
|
||||
user.createdAt
|
||||
);
|
||||
let frequency = 15;
|
||||
let frequency = 10;
|
||||
|
||||
if (daysSinceRegistration > 365) {
|
||||
frequency = 2;
|
||||
} else if (daysSinceRegistration > 180) {
|
||||
frequency = 3;
|
||||
} else if (daysSinceRegistration > 60) {
|
||||
frequency = 5;
|
||||
frequency = 4;
|
||||
} else if (daysSinceRegistration > 30) {
|
||||
frequency = 8;
|
||||
frequency = 6;
|
||||
} else if (daysSinceRegistration > 15) {
|
||||
frequency = 12;
|
||||
frequency = 8;
|
||||
}
|
||||
|
||||
if (Analytics?.activityCount % frequency === 1) {
|
||||
|
@ -230,6 +230,10 @@
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -568,6 +572,10 @@
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -926,6 +934,10 @@
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -1130,6 +1142,10 @@
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
|
||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
@ -22,13 +23,20 @@ export class RedactValuesInResponseInterceptor<T>
|
||||
): Observable<any> {
|
||||
return next.handle().pipe(
|
||||
map((data: any) => {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const hasImpersonationId =
|
||||
!!request.headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
|
||||
const { headers, user }: { headers: Headers; user: UserWithSettings } =
|
||||
context.switchToHttp().getRequest();
|
||||
|
||||
const impersonationId =
|
||||
headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
|
||||
const hasReadRestrictedPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
impersonationId,
|
||||
user
|
||||
});
|
||||
|
||||
if (
|
||||
hasImpersonationId ||
|
||||
this.userService.isRestrictedView(request.user)
|
||||
hasReadRestrictedPermission ||
|
||||
this.userService.isRestrictedView(user)
|
||||
) {
|
||||
data = redactAttributes({
|
||||
object: data,
|
||||
|
@ -24,7 +24,7 @@ export class ApiService {
|
||||
const searchQuery = filterBySearchQuery?.toLowerCase();
|
||||
const tagIds = filterByTags?.split(',') ?? [];
|
||||
|
||||
return [
|
||||
const filters = [
|
||||
...accountIds.map((accountId) => {
|
||||
return <Filter>{
|
||||
id: accountId,
|
||||
@ -43,10 +43,6 @@ export class ApiService {
|
||||
type: 'ASSET_SUB_CLASS'
|
||||
};
|
||||
}),
|
||||
{
|
||||
id: searchQuery,
|
||||
type: 'SEARCH_QUERY'
|
||||
},
|
||||
...tagIds.map((tagId) => {
|
||||
return <Filter>{
|
||||
id: tagId,
|
||||
@ -54,5 +50,14 @@ export class ApiService {
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
if (searchQuery) {
|
||||
filters.push({
|
||||
id: searchQuery,
|
||||
type: 'SEARCH_QUERY'
|
||||
});
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
}
|
||||
|
@ -62,9 +62,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
}, this.configurationService.get('REQUEST_TIMEOUT'));
|
||||
|
||||
return got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split(
|
||||
'.'
|
||||
)?.[0]}.json`,
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/funds/${
|
||||
symbol.split('.')?.[0]
|
||||
}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
@ -104,9 +104,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
}, this.configurationService.get('REQUEST_TIMEOUT'));
|
||||
|
||||
return got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split(
|
||||
'.'
|
||||
)?.[0]}.json`,
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${
|
||||
symbol.split('.')?.[0]
|
||||
}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||
import { DEFAULT_CURRENCY, UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
REPLACE_NAME_PARTS,
|
||||
UNKNOWN_KEY
|
||||
} from '@ghostfolio/common/config';
|
||||
import { isCurrency } from '@ghostfolio/common/helper';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
@ -137,18 +141,11 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
if (name) {
|
||||
name = name.replace('&', '&');
|
||||
|
||||
name = name.replace('Amundi Index Solutions - ', '');
|
||||
name = name.replace('iShares ETF (CH) - ', '');
|
||||
name = name.replace('iShares III Public Limited Company - ', '');
|
||||
name = name.replace('iShares V PLC - ', '');
|
||||
name = name.replace('iShares VI Public Limited Company - ', '');
|
||||
name = name.replace('iShares VII PLC - ', '');
|
||||
name = name.replace('Multi Units Luxembourg - ', '');
|
||||
name = name.replace('VanEck ETFs N.V. - ', '');
|
||||
name = name.replace('Vaneck Vectors Ucits Etfs Plc - ', '');
|
||||
name = name.replace('Vanguard Funds Public Limited Company - ', '');
|
||||
name = name.replace('Vanguard Index Funds - ', '');
|
||||
name = name.replace('Xtrackers (IE) Plc - ', '');
|
||||
for (const part of REPLACE_NAME_PARTS) {
|
||||
name = name.replace(part, '');
|
||||
}
|
||||
|
||||
name = name.trim();
|
||||
}
|
||||
|
||||
if (quoteType === 'FUTURE') {
|
||||
|
@ -11,7 +11,10 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
REPLACE_NAME_PARTS
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
@ -22,6 +25,7 @@ import {
|
||||
} from '@prisma/client';
|
||||
import { addDays, format, isSameDay, isToday } from 'date-fns';
|
||||
import got from 'got';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class EodHistoricalDataService implements DataProviderInterface {
|
||||
@ -144,10 +148,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
).json<any>();
|
||||
|
||||
return response.reduce(
|
||||
(result, historicalItem, index, array) => {
|
||||
result[this.convertFromEodSymbol(symbol)][historicalItem.date] = {
|
||||
marketPrice: historicalItem.close
|
||||
};
|
||||
(result, { close, date }, index, array) => {
|
||||
if (isNumber(close)) {
|
||||
result[this.convertFromEodSymbol(symbol)][date] = {
|
||||
marketPrice: close
|
||||
};
|
||||
} else {
|
||||
Logger.error(
|
||||
`Could not get historical market data for ${symbol} (${this.getName()}) at ${date}`,
|
||||
'EodHistoricalDataService'
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
@ -232,14 +243,23 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
return lookupItem.symbol === code;
|
||||
})?.currency;
|
||||
|
||||
result[this.convertFromEodSymbol(code)] = {
|
||||
currency:
|
||||
currency ??
|
||||
this.convertFromEodSymbol(code)?.replace(DEFAULT_CURRENCY, ''),
|
||||
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
||||
marketPrice: close,
|
||||
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
|
||||
};
|
||||
if (isNumber(close)) {
|
||||
result[this.convertFromEodSymbol(code)] = {
|
||||
currency:
|
||||
currency ??
|
||||
this.convertFromEodSymbol(code)?.replace(DEFAULT_CURRENCY, ''),
|
||||
dataSource: this.getName(),
|
||||
marketPrice: close,
|
||||
marketState: isToday(new Date(timestamp * 1000))
|
||||
? 'open'
|
||||
: 'closed'
|
||||
};
|
||||
} else {
|
||||
Logger.error(
|
||||
`Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`,
|
||||
'EodHistoricalDataService'
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
@ -345,6 +365,18 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
return aSymbol;
|
||||
}
|
||||
|
||||
private formatName({ name }: { name: string }) {
|
||||
if (name) {
|
||||
for (const part of REPLACE_NAME_PARTS) {
|
||||
name = name.replace(part, '');
|
||||
}
|
||||
|
||||
name = name.trim();
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private async getSearchResult(aQuery: string): Promise<
|
||||
(LookupItem & {
|
||||
assetClass: AssetClass;
|
||||
@ -380,9 +412,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
isin,
|
||||
name,
|
||||
currency: this.convertCurrency(Currency),
|
||||
dataSource: this.getName(),
|
||||
name: this.formatName({ name }),
|
||||
symbol: `${Code}.${Exchange}`
|
||||
};
|
||||
}
|
||||
|
@ -42,10 +42,21 @@ export class ManualService implements DataProviderInterface {
|
||||
public async getAssetProfile(
|
||||
aSymbol: string
|
||||
): Promise<Partial<SymbolProfile>> {
|
||||
return {
|
||||
const assetProfile: Partial<SymbolProfile> = {
|
||||
dataSource: this.getName(),
|
||||
symbol: aSymbol
|
||||
};
|
||||
|
||||
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([
|
||||
{ dataSource: this.getName(), symbol: aSymbol }
|
||||
]);
|
||||
|
||||
if (symbolProfile) {
|
||||
assetProfile.currency = symbolProfile.currency;
|
||||
assetProfile.name = symbolProfile.name;
|
||||
}
|
||||
|
||||
return assetProfile;
|
||||
}
|
||||
|
||||
public async getDividends({}: GetDividendsParams) {
|
||||
|
@ -38,7 +38,7 @@
|
||||
[pageTitle]="pageTitle"
|
||||
[user]="user"
|
||||
(signOut)="onSignOut()"
|
||||
></gf-header>
|
||||
/>
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
|
@ -17,8 +17,13 @@
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
|
||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon class="mr-1" name="lock-closed-outline" />
|
||||
<ng-container i18n>Restricted View</ng-container>
|
||||
@if (element.permissions.includes('READ')) {
|
||||
<ion-icon class="mr-1" name="lock-open-outline" />
|
||||
<ng-container i18n>View</ng-container>
|
||||
} @else if (element.permissions.includes('READ_RESTRICTED')) {
|
||||
<ion-icon class="mr-1" name="lock-closed-outline" />
|
||||
<ng-container i18n>Restricted view</ng-container>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
@ -4,7 +4,7 @@
|
||||
[deviceType]="data.deviceType"
|
||||
[title]="name"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-header>
|
||||
/>
|
||||
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div class="container p-0">
|
||||
@ -16,7 +16,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="valueInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[isLoading]="isLoadingChart"
|
||||
[locale]="user?.settings?.locale"
|
||||
></gf-investment-chart>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 row">
|
||||
@ -79,7 +79,7 @@
|
||||
[deviceType]="data.deviceType"
|
||||
[holdings]="holdings"
|
||||
[locale]="user?.settings?.locale"
|
||||
></gf-holdings-table>
|
||||
/>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
@ -102,7 +102,7 @@
|
||||
[totalItems]="totalItems"
|
||||
(export)="onExport()"
|
||||
(sortChanged)="onSortChanged($event)"
|
||||
></gf-activities-table-lazy>
|
||||
/>
|
||||
<gf-activities-table
|
||||
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
@ -115,7 +115,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="false"
|
||||
(export)="onExport()"
|
||||
></gf-activities-table>
|
||||
/>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
@ -128,7 +128,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView"
|
||||
(accountBalanceDeleted)="onDeleteAccountBalance($event)"
|
||||
></gf-account-balances>
|
||||
/>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
@ -138,4 +138,4 @@
|
||||
mat-dialog-actions
|
||||
[deviceType]="data.deviceType"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-footer>
|
||||
/>
|
||||
|
@ -39,7 +39,7 @@
|
||||
class="d-inline d-sm-none mr-1"
|
||||
[tooltip]="element.Platform?.name"
|
||||
[url]="element.Platform?.url"
|
||||
></gf-symbol-icon>
|
||||
/>
|
||||
<span>{{ element.name }} </span>
|
||||
<span
|
||||
*ngIf="element.isDefault"
|
||||
@ -83,7 +83,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="element.Platform?.name"
|
||||
[url]="element.Platform?.url"
|
||||
></gf-symbol-icon>
|
||||
/>
|
||||
<span>{{ element.Platform?.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
@ -131,7 +131,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.balance"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
@ -143,7 +143,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalBalanceInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -166,7 +166,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.value"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
@ -178,7 +178,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalValueInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -201,7 +201,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.valueInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
@ -213,7 +213,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalValueInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -296,4 +296,4 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
|
@ -8,7 +8,7 @@
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[symbol]="symbol"
|
||||
></gf-line-chart>
|
||||
/>
|
||||
<div
|
||||
*ngFor="let itemByMonth of marketDataByMonth | keyvalue"
|
||||
class="d-flex"
|
||||
|
@ -6,7 +6,7 @@
|
||||
[isLoading]="isLoading"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -213,7 +213,7 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -50,7 +50,7 @@
|
||||
[marketData]="marketDataDetails"
|
||||
[symbol]="data.symbol"
|
||||
(marketDataChanged)="onMarketDataChanged($event)"
|
||||
></gf-admin-market-data-detail>
|
||||
/>
|
||||
|
||||
<div class="mt-3" formGroupName="historicalData">
|
||||
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||
@ -162,7 +162,7 @@
|
||||
[keys]="['name']"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Countries</div>
|
||||
@ -172,7 +172,7 @@
|
||||
[keys]="['name']"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
@ -16,7 +16,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="0"
|
||||
[value]="userCount"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex my-3">
|
||||
@ -26,7 +26,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="0"
|
||||
[value]="transactionCount"
|
||||
></gf-value>
|
||||
/>
|
||||
<div *ngIf="transactionCount && userCount">
|
||||
{{ transactionCount / userCount | number : '1.2-2' }}
|
||||
<span i18n>per User</span>
|
||||
@ -39,10 +39,7 @@
|
||||
<table>
|
||||
<tr *ngFor="let exchangeRate of exchangeRates">
|
||||
<td>
|
||||
<gf-value
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="1"
|
||||
></gf-value>
|
||||
<gf-value [locale]="user?.settings?.locale" [value]="1" />
|
||||
</td>
|
||||
<td class="pl-1">{{ exchangeRate.label1 }}</td>
|
||||
<td class="px-1">=</td>
|
||||
@ -52,7 +49,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="4"
|
||||
[value]="exchangeRate.value"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
<td class="pl-1">{{ exchangeRate.label2 }}</td>
|
||||
<td>
|
||||
|
@ -35,7 +35,7 @@
|
||||
class="d-inline mr-1"
|
||||
[tooltip]="element.name"
|
||||
[url]="element.url"
|
||||
></gf-symbol-icon>
|
||||
/>
|
||||
<span>{{ element.name }}</span>
|
||||
</td></ng-container
|
||||
>
|
||||
|
@ -46,7 +46,7 @@
|
||||
class="ml-1"
|
||||
[enableLink]="false"
|
||||
[title]="'Expires ' + formatDistanceToNow(element.subscription.expiresAt) + ' (' + (element.subscription.expiresAt | date: defaultDateFormat) + ')'"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -107,7 +107,7 @@
|
||||
class="d-inline-block justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="element.accountCount"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -128,7 +128,7 @@
|
||||
class="d-inline-block justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="element.transactionCount"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -153,7 +153,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="0"
|
||||
[value]="element.engagement"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-12 d-flex justify-content-end">
|
||||
@ -50,7 +50,7 @@
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
<canvas
|
||||
#chartCanvas
|
||||
class="h-100"
|
||||
|
@ -19,5 +19,5 @@
|
||||
[theme]="{
|
||||
height: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@
|
||||
[ngClass]="{ 'w-100': hasTabs }"
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo class="px-2" [label]="pageTitle"></gf-logo>
|
||||
<gf-logo class="px-2" [label]="pageTitle" />
|
||||
</a>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
@ -141,7 +141,7 @@
|
||||
[user]="user"
|
||||
(closed)="closeAssistant()"
|
||||
(dateRangeChanged)="onDateRangeChange($event)"
|
||||
(selectedTagChanged)="onSelectedTagChanged($event)"
|
||||
(filtersChanged)="onFiltersChanged($event)"
|
||||
/>
|
||||
</mat-menu>
|
||||
</li>
|
||||
@ -165,6 +165,32 @@
|
||||
/>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
hasPermissionForSubscription &&
|
||||
user?.subscription?.type === 'Basic'
|
||||
"
|
||||
>
|
||||
<a class="d-flex" mat-menu-item [routerLink]="routerLinkPricing"
|
||||
><span class="align-items-center d-flex"
|
||||
><span
|
||||
><ng-container
|
||||
*ngIf="user.subscription.offer === 'default'"
|
||||
i18n
|
||||
>Upgrade Plan</ng-container
|
||||
>
|
||||
<ng-container
|
||||
*ngIf="user.subscription.offer === 'renewal'"
|
||||
i18n
|
||||
>Renew Plan</ng-container
|
||||
></span
|
||||
>
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false" /></span
|
||||
></a>
|
||||
<hr class="m-0" />
|
||||
</ng-container>
|
||||
<ng-container *ngIf="user?.access?.length > 0">
|
||||
<button mat-menu-item (click)="impersonateAccount(null)">
|
||||
<span class="align-items-center d-flex">
|
||||
@ -295,7 +321,7 @@
|
||||
class="px-2"
|
||||
[label]="pageTitle"
|
||||
[showLabel]="currentRoute !== 'register'"
|
||||
></gf-logo>
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
import { Router } from '@angular/router';
|
||||
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
||||
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
@ -20,11 +21,10 @@ import {
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { Filter, InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import { AssistantComponent } from '@ghostfolio/ui/assistant/assistant.component';
|
||||
import { Tag } from '@prisma/client';
|
||||
import { EMPTY, Subject } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -162,6 +162,34 @@ export class HeaderComponent implements OnChanges {
|
||||
});
|
||||
}
|
||||
|
||||
public onFiltersChanged(filters: Filter[]) {
|
||||
const userSetting: UpdateUserSettingDto = {};
|
||||
|
||||
for (const filter of filters) {
|
||||
let filtersType: string;
|
||||
|
||||
if (filter.type === 'ACCOUNT') {
|
||||
filtersType = 'accounts';
|
||||
} else if (filter.type === 'TAG') {
|
||||
filtersType = 'tags';
|
||||
}
|
||||
|
||||
userSetting[`filters.${filtersType}`] = filter.id ? [filter.id] : null;
|
||||
}
|
||||
|
||||
this.dataService
|
||||
.putUserSetting(userSetting)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.userService.remove();
|
||||
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
public onMenuClosed() {
|
||||
this.isMenuOpen = false;
|
||||
}
|
||||
@ -174,20 +202,6 @@ export class HeaderComponent implements OnChanges {
|
||||
this.assistantElement.initialize();
|
||||
}
|
||||
|
||||
public onSelectedTagChanged(tag: Tag) {
|
||||
this.dataService
|
||||
.putUserSetting({ 'filters.tags': tag ? [tag.id] : null })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.userService.remove();
|
||||
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
public onSignOut() {
|
||||
this.signOut.next();
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { RouterModule } from '@angular/router';
|
||||
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
||||
import { GfAssistantModule } from '@ghostfolio/ui/assistant';
|
||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
import { HeaderComponent } from './header.component';
|
||||
|
||||
@ -17,6 +18,7 @@ import { HeaderComponent } from './header.component';
|
||||
CommonModule,
|
||||
GfAssistantModule,
|
||||
GfLogoModule,
|
||||
GfPremiumIndicatorModule,
|
||||
LoginWithAccessTokenDialogModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
|
@ -5,7 +5,7 @@
|
||||
[isLoading]="positions === undefined"
|
||||
[options]="dateRangeOptions"
|
||||
(change)="onChangeDateRange($event.value)"
|
||||
></gf-toggle>
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
|
||||
@ -18,7 +18,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
[range]="user?.settings?.dateRange"
|
||||
></gf-positions>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
|
||||
|
@ -18,11 +18,11 @@
|
||||
[yMaxLabel]="greedLabel"
|
||||
[yMin]="0"
|
||||
[yMinLabel]="fearLabel"
|
||||
></gf-line-chart>
|
||||
/>
|
||||
<gf-fear-and-greed-index
|
||||
class="d-flex justify-content-center"
|
||||
[fearAndGreedIndex]="fearAndGreedIndex"
|
||||
></gf-fear-and-greed-index>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
[benchmarks]="benchmarks"
|
||||
[locale]="user?.settings?.locale"
|
||||
[user]="user"
|
||||
></gf-benchmark>
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
@ -41,7 +41,7 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -74,7 +74,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
|
||||
this.showDetails =
|
||||
!this.hasImpersonationId &&
|
||||
!this.user.settings.isRestrictedView &&
|
||||
this.user.settings.viewMode !== 'ZEN';
|
||||
|
||||
|
@ -78,7 +78,7 @@
|
||||
[showLoader]="false"
|
||||
[showXAxis]="false"
|
||||
[showYAxis]="false"
|
||||
></gf-line-chart>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,7 +95,7 @@
|
||||
[performance]="performance"
|
||||
[showDetails]="showDetails"
|
||||
[unit]="unit"
|
||||
></gf-portfolio-performance>
|
||||
/>
|
||||
<div
|
||||
*ngIf="showDetails && !user?.settings?.isExperimentalFeatures"
|
||||
class="text-center"
|
||||
@ -105,7 +105,7 @@
|
||||
[isLoading]="isLoadingPerformance"
|
||||
[options]="dateRangeOptions"
|
||||
(change)="onChangeDateRange($event.value)"
|
||||
></gf-toggle>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,7 +12,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[summary]="summary"
|
||||
(emergencyFundChanged)="onChangeEmergencyFund($event)"
|
||||
></gf-portfolio-summary>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
<canvas
|
||||
#chartCanvas
|
||||
class="h-100"
|
||||
|
@ -2,7 +2,7 @@
|
||||
mat-dialog-title
|
||||
[title]="data.title"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-header>
|
||||
/>
|
||||
|
||||
<div class="py-3" mat-dialog-content>
|
||||
<div class="align-items-center d-flex flex-column">
|
||||
|
@ -1,14 +1,12 @@
|
||||
<div class="container p-0">
|
||||
<div class="no-gutters row">
|
||||
<div
|
||||
class="status-container text-muted text-right"
|
||||
(click)="onShowErrors()"
|
||||
>
|
||||
<div class="status-container text-muted text-right">
|
||||
@if (errors?.length > 0 && !isLoading) {
|
||||
<ion-icon
|
||||
i18n-title
|
||||
name="time-outline"
|
||||
title="Oops! Our data provider partner is experiencing the hiccups."
|
||||
title="Oops! A data provider is experiencing the hiccups."
|
||||
(click)="onShowErrors()"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@ -20,7 +18,7 @@
|
||||
height: '4rem',
|
||||
width: '15rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="display-4 font-weight-bold m-0 text-center value-container"
|
||||
@ -43,7 +41,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<gf-value
|
||||
@ -53,7 +51,7 @@
|
||||
[value]="
|
||||
isLoading ? undefined : performance?.currentNetPerformancePercent
|
||||
"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -58,7 +58,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||
duration: 1,
|
||||
separator: getNumberFormatGroup(this.locale)
|
||||
}).start();
|
||||
} else if (this.performance?.currentValue === null) {
|
||||
} else if (this.showDetails === false) {
|
||||
new CountUp(
|
||||
'value',
|
||||
this.performance?.currentNetPerformancePercent * 100,
|
||||
@ -69,6 +69,8 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||
separator: getNumberFormatGroup(this.locale)
|
||||
}
|
||||
).start();
|
||||
} else {
|
||||
this.value.nativeElement.innerHTML = '*****';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Time in Market</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value class="justify-content-end" [value]="timeInMarket"></gf-value>
|
||||
<gf-value class="justify-content-end" [value]="timeInMarket" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -10,8 +10,8 @@
|
||||
[hidden]="summary?.ordersCount === null"
|
||||
>
|
||||
<div class="flex-grow-1 ml-3 text-truncate" i18n>
|
||||
{{ summary?.ordersCount }} {summary?.ordersCount, plural, =1 {transaction}
|
||||
other {transactions}}
|
||||
{{ summary?.ordersCount }}
|
||||
{summary?.ordersCount, plural, =1 {transaction} other {transactions}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -26,7 +26,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.totalBuy"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -38,7 +38,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.totalSell"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -53,7 +53,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.committedFunds"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -65,13 +65,17 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentGrossPerformance"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="align-items-center d-flex flex-grow-1 ml-3 text-truncate">
|
||||
<ng-container i18n>Gross Performance</ng-container>
|
||||
<abbr class="initialism ml-2 text-muted" title="Time-Weighted Rate of Return">(TWR)</abbr>
|
||||
<abbr
|
||||
class="initialism ml-2 text-muted"
|
||||
title="Time-Weighted Rate of Return"
|
||||
>(TWR)</abbr
|
||||
>
|
||||
</div>
|
||||
<div class="flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
@ -83,7 +87,7 @@
|
||||
[value]="
|
||||
isLoading ? undefined : summary?.currentGrossPerformancePercent
|
||||
"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -96,7 +100,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.fees"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -111,13 +115,17 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentNetPerformance"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate ml-3">
|
||||
<ng-container i18n>Net Performance</ng-container>
|
||||
<abbr class="initialism ml-2 text-muted" title="Time-Weighted Rate of Return">(TWR)</abbr>
|
||||
<abbr
|
||||
class="initialism ml-2 text-muted"
|
||||
title="Time-Weighted Rate of Return"
|
||||
>(TWR)</abbr
|
||||
>
|
||||
</div>
|
||||
<div class="flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
@ -127,7 +135,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : summary?.currentNetPerformancePercent"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -143,7 +151,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentValue"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -155,7 +163,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.items"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -176,7 +184,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.emergencyFund?.total"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -189,7 +197,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.emergencyFund?.cash"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -202,7 +210,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.emergencyFund?.assets"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -214,7 +222,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.cash"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -226,7 +234,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.excludedAccountsAndActivities"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -246,7 +254,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.liabilities"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -261,7 +269,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.netWorth"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -276,7 +284,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : summary?.annualizedPerformancePercent"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -291,7 +299,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.interest"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
@ -303,7 +311,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.dividend"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@
|
||||
[deviceType]="data.deviceType"
|
||||
[title]="SymbolProfile?.name ?? SymbolProfile?.symbol"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-header>
|
||||
/>
|
||||
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div class="container p-0">
|
||||
@ -16,7 +16,7 @@
|
||||
[locale]="data.locale"
|
||||
[unit]="data.baseCurrency"
|
||||
[value]="value"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[symbol]="data.symbol"
|
||||
></gf-line-chart>
|
||||
/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
@ -222,7 +222,7 @@
|
||||
[locale]="data.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Countries</div>
|
||||
@ -234,7 +234,7 @@
|
||||
[locale]="data.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@ -266,7 +266,7 @@
|
||||
[sortDisabled]="true"
|
||||
[totalItems]="totalItems"
|
||||
(export)="onExport()"
|
||||
></gf-activities-table-lazy>
|
||||
/>
|
||||
<gf-activities-table
|
||||
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
@ -280,7 +280,7 @@
|
||||
[showActions]="false"
|
||||
[showNameColumn]="false"
|
||||
(export)="onExport()"
|
||||
></gf-activities-table>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -314,4 +314,4 @@
|
||||
mat-dialog-actions
|
||||
[deviceType]="data.deviceType"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-footer>
|
||||
/>
|
||||
|
@ -18,7 +18,7 @@
|
||||
[marketState]="position?.marketState"
|
||||
[range]="range"
|
||||
[value]="position?.netPerformancePercentage"
|
||||
></gf-trend-indicator>
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="isLoading" class="flex-grow-1">
|
||||
<ngx-skeleton-loader
|
||||
@ -28,14 +28,14 @@
|
||||
height: '1.2rem',
|
||||
width: '12rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1rem',
|
||||
width: '8rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!isLoading" class="flex-grow-1 text-truncate">
|
||||
<div class="h6 m-0 text-truncate">{{ position?.name }}</div>
|
||||
@ -50,13 +50,13 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="position?.netPerformance"
|
||||
></gf-value>
|
||||
/>
|
||||
<gf-value
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="position?.netPerformancePercentage"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutters">
|
||||
<div class="col">
|
||||
<ng-container *ngIf="positions === undefined">
|
||||
<gf-position [isLoading]="true"></gf-position>
|
||||
<gf-position [isLoading]="true" />
|
||||
</ng-container>
|
||||
<ng-container *ngIf="positions !== undefined">
|
||||
<ng-container *ngIf="hasPositions">
|
||||
@ -13,7 +13,7 @@
|
||||
[locale]="locale"
|
||||
[position]="position"
|
||||
[range]="range"
|
||||
></gf-position>
|
||||
/>
|
||||
<gf-position
|
||||
*ngFor="let position of positionsRest"
|
||||
[baseCurrency]="baseCurrency"
|
||||
@ -21,15 +21,13 @@
|
||||
[locale]="locale"
|
||||
[position]="position"
|
||||
[range]="range"
|
||||
></gf-position>
|
||||
/>
|
||||
</ng-container>
|
||||
<div
|
||||
*ngIf="hasPermissionToCreateOrder && !hasPositions"
|
||||
class="p-3 text-center"
|
||||
>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator>
|
||||
<gf-no-transactions-info-indicator [hasBorder]="false" />
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@
|
||||
height: '2rem',
|
||||
width: '2rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="!isLoading"
|
||||
@ -26,14 +26,14 @@
|
||||
height: '1rem',
|
||||
width: '10rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1rem',
|
||||
width: '15rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!isLoading" class="flex-grow-1">
|
||||
<div class="h6 my-1">{{ rule?.name }}</div>
|
||||
|
@ -7,15 +7,13 @@
|
||||
class="my-2 text-center"
|
||||
>
|
||||
<mat-card-content>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator
|
||||
></mat-card-content>
|
||||
<gf-no-transactions-info-indicator [hasBorder]="false" />
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true"></gf-rule>
|
||||
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true" />
|
||||
<ng-container *ngIf="rules !== null && rules !== undefined">
|
||||
<gf-rule *ngFor="let rule of rules" [rule]="rule"></gf-rule>
|
||||
<gf-rule *ngFor="let rule of rules" [rule]="rule" />
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,10 +7,7 @@
|
||||
<div>
|
||||
<h5 class="align-items-center d-flex justify-content-center mb-3">
|
||||
<span>Ghostfolio Premium</span>
|
||||
<gf-premium-indicator
|
||||
class="ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator>
|
||||
<gf-premium-indicator class="ml-1" [enableLink]="false" />
|
||||
</h5>
|
||||
<div class="font-weight-normal h5 mb-3 text-center" i18n>
|
||||
Are you an ambitious investor who needs the full picture?
|
||||
|
@ -37,19 +37,23 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
|
||||
ngOnInit() {
|
||||
this.accessForm = this.formBuilder.group({
|
||||
alias: [this.data.access.alias],
|
||||
permissions: [this.data.access.permissions[0], Validators.required],
|
||||
type: [this.data.access.type, Validators.required],
|
||||
userId: [this.data.access.grantee, Validators.required]
|
||||
});
|
||||
|
||||
this.accessForm.get('type').valueChanges.subscribe((value) => {
|
||||
this.accessForm.get('type').valueChanges.subscribe((accessType) => {
|
||||
const permissionsControl = this.accessForm.get('permissions');
|
||||
const userIdControl = this.accessForm.get('userId');
|
||||
|
||||
if (value === 'PRIVATE') {
|
||||
if (accessType === 'PRIVATE') {
|
||||
permissionsControl.setValidators(Validators.required);
|
||||
userIdControl.setValidators(Validators.required);
|
||||
} else {
|
||||
userIdControl.clearValidators();
|
||||
}
|
||||
|
||||
permissionsControl.updateValueAndValidity();
|
||||
userIdControl.updateValueAndValidity();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
@ -64,7 +68,7 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
|
||||
const access: CreateAccessDto = {
|
||||
alias: this.accessForm.controls['alias'].value,
|
||||
granteeUserId: this.accessForm.controls['userId'].value,
|
||||
type: this.accessForm.controls['type'].value
|
||||
permissions: [this.accessForm.controls['permissions'].value]
|
||||
};
|
||||
|
||||
this.dataService
|
||||
|
@ -30,9 +30,20 @@
|
||||
@if (accessForm.controls['type'].value === 'PRIVATE') {
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label
|
||||
>Ghostfolio <ng-container i18n>User ID</ng-container></mat-label
|
||||
>
|
||||
<mat-label i18n>Permission</mat-label>
|
||||
<mat-select formControlName="permissions">
|
||||
<mat-option i18n value="READ_RESTRICTED">Restricted view</mat-option>
|
||||
@if(data?.user?.settings?.isExperimentalFeatures) {
|
||||
<mat-option i18n value="READ">View</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label>
|
||||
Ghostfolio <ng-container i18n>User ID</ng-container>
|
||||
</mat-label>
|
||||
<input
|
||||
formControlName="userId"
|
||||
matInput
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Access } from '@ghostfolio/common/interfaces';
|
||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface CreateOrUpdateAccessDialogParams {
|
||||
access: Access;
|
||||
user: User;
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||
@ -105,8 +104,10 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
|
||||
data: {
|
||||
access: {
|
||||
alias: '',
|
||||
permissions: ['READ_RESTRICTED'],
|
||||
type: 'PRIVATE'
|
||||
}
|
||||
},
|
||||
user: this.user
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
|
@ -6,13 +6,13 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h1>
|
||||
<gf-access-table
|
||||
[accesses]="accesses"
|
||||
[showActions]="hasPermissionToDeleteAccess"
|
||||
(accessDeleted)="onDeleteAccess($event)"
|
||||
></gf-access-table>
|
||||
/>
|
||||
<div *ngIf="hasPermissionToCreateAccess" class="fab-container">
|
||||
<a
|
||||
class="align-items-center d-flex justify-content-center"
|
||||
|
@ -5,7 +5,7 @@
|
||||
<gf-membership-card
|
||||
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
|
||||
[name]="user?.subscription?.type"
|
||||
></gf-membership-card>
|
||||
/>
|
||||
<div
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="d-flex flex-column mt-5"
|
||||
@ -15,10 +15,10 @@
|
||||
>
|
||||
<button color="primary" mat-flat-button (click)="onCheckout()">
|
||||
<ng-container *ngIf="user.subscription.offer === 'default'" i18n
|
||||
>Upgrade</ng-container
|
||||
>Upgrade Plan</ng-container
|
||||
>
|
||||
<ng-container *ngIf="user.subscription.offer === 'renewal'" i18n
|
||||
>Renew</ng-container
|
||||
>Renew Plan</ng-container
|
||||
>
|
||||
</button>
|
||||
<div *ngIf="price" class="mt-1 text-center">
|
||||
@ -43,8 +43,8 @@
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator
|
||||
></a>
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
*ngIf="hasPermissionToUpdateUserSettings"
|
||||
class="mx-1"
|
||||
|
@ -5,6 +5,6 @@
|
||||
[theme]="{
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
|
||||
<div class="align-items-center d-flex h-100 w-100" id="svgMap"></div>
|
||||
|
@ -15,7 +15,7 @@
|
||||
(accountDeleted)="onDeleteAccount($event)"
|
||||
(accountToUpdate)="onUpdateAccount($event)"
|
||||
(transferBalance)="onTransferBalance()"
|
||||
></gf-accounts-table>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -61,7 +61,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="platformEntry.name"
|
||||
[url]="platformEntry.url"
|
||||
></gf-symbol-icon>
|
||||
/>
|
||||
<span>{{ platformEntry.name }}</span>
|
||||
</span>
|
||||
</mat-option>
|
||||
|
@ -17,8 +17,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="account.Platform?.name"
|
||||
[url]="account.Platform?.url"
|
||||
></gf-symbol-icon
|
||||
><span>{{ account.name }}</span>
|
||||
/><span>{{ account.name }}</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
@ -35,8 +34,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="account.Platform?.name"
|
||||
[url]="account.Platform?.url"
|
||||
></gf-symbol-icon
|
||||
><span>{{ account.name }}</span>
|
||||
/><span>{{ account.name }}</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
|
@ -20,8 +20,8 @@
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
annual plan for ambitious investors who need the full picture of
|
||||
their financial assets.
|
||||
</p>
|
||||
|
@ -21,8 +21,8 @@
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
annual plan with our exclusive Black Week deal. Elevate your
|
||||
financial strategy with the power of Ghostfolio designed to give you
|
||||
the full picture of your assets.
|
||||
|
@ -176,6 +176,19 @@
|
||||
your university e-mail address.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Does the Ghostfolio Premium subscription renew
|
||||
automatically?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>No, <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> does
|
||||
not include auto-renewal. Upon expiration, you can choose whether to
|
||||
start a new subscription.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Which devices are supported?</mat-card-title>
|
||||
|
@ -142,7 +142,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<p class="m-0">
|
||||
Check the rate of return of your portfolio for
|
||||
@ -162,7 +162,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<p class="m-0">
|
||||
Check the allocations of your portfolio by account, asset
|
||||
@ -207,7 +207,7 @@
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span i18n>Market Mood</span>
|
||||
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
|
||||
<gf-premium-indicator class="ml-1" />
|
||||
</h4>
|
||||
<p class="m-0">
|
||||
Check the current market mood (<a
|
||||
@ -228,7 +228,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<p class="m-0">
|
||||
Identify potential risks in your portfolio with Ghostfolio
|
||||
|
@ -328,11 +328,7 @@
|
||||
<gf-carousel [aria-label]="'Testimonials'">
|
||||
<div *ngFor="let testimonial of testimonials" gf-carousel-item>
|
||||
<div class="d-flex px-4">
|
||||
<gf-logo
|
||||
class="mr-3 mt-2 pt-1"
|
||||
size="medium"
|
||||
[showLabel]="false"
|
||||
></gf-logo>
|
||||
<gf-logo class="mr-3 mt-2 pt-1" size="medium" [showLabel]="false" />
|
||||
<div>
|
||||
<div>{{ testimonial.quote }}</div>
|
||||
<div class="mt-2 text-muted">
|
||||
@ -361,10 +357,7 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-md-8 customer-map-container offset-md-2">
|
||||
<gf-world-map-chart
|
||||
format="👻"
|
||||
[countries]="countriesOfSubscribersMap"
|
||||
></gf-world-map-chart>
|
||||
<gf-world-map-chart format="👻" [countries]="countriesOfSubscribersMap" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -450,7 +443,7 @@
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center w-100"
|
||||
>
|
||||
<gf-logo size="medium"></gf-logo>
|
||||
<gf-logo size="medium" />
|
||||
<div>Wealth Management Software</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<gf-home-market></gf-home-market>
|
||||
<gf-home-market />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
|
||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import { Filter, User } from '@ghostfolio/common/interfaces';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
|
@ -26,7 +26,7 @@
|
||||
(importDividends)="onImportDividends()"
|
||||
(pageChanged)="onChangePage($event)"
|
||||
(sortChanged)="onSortChanged($event)"
|
||||
></gf-activities-table-lazy>
|
||||
/>
|
||||
<gf-activities-table
|
||||
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
@ -44,7 +44,7 @@
|
||||
(exportDrafts)="onExportDrafts($event)"
|
||||
(import)="onImport()"
|
||||
(importDividends)="onImportDividends()"
|
||||
></gf-activities-table>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -82,8 +82,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="account.Platform?.name"
|
||||
[url]="account.Platform?.url"
|
||||
></gf-symbol-icon
|
||||
><span>{{ account.name }}</span>
|
||||
/><span>{{ account.name }}</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
@ -357,7 +356,7 @@
|
||||
[locale]="data.user?.settings?.locale"
|
||||
[unit]="activityForm.controls['currency']?.value ?? data.user?.settings?.baseCurrency"
|
||||
[value]="total"
|
||||
></gf-value>
|
||||
/>
|
||||
<div>
|
||||
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||
<button
|
||||
|
@ -3,7 +3,7 @@
|
||||
[deviceType]="data.deviceType"
|
||||
[title]="dialogTitle"
|
||||
(closeButtonClicked)="onCancel()"
|
||||
></gf-dialog-header>
|
||||
/>
|
||||
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<mat-stepper
|
||||
@ -136,7 +136,7 @@
|
||||
[sortDisabled]="true"
|
||||
[totalItems]="totalItems"
|
||||
(selectedActivities)="updateSelection($event)"
|
||||
></gf-activities-table-lazy>
|
||||
/>
|
||||
<gf-activities-table
|
||||
*ngIf="importStep === 1 && data?.user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
@ -153,7 +153,7 @@
|
||||
[showFooter]="false"
|
||||
[showSymbolColumn]="false"
|
||||
(selectedActivities)="updateSelection($event)"
|
||||
></gf-activities-table>
|
||||
/>
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<button mat-button (click)="onReset(stepper)">
|
||||
<ng-container i18n>Back</ng-container>
|
||||
@ -214,4 +214,4 @@
|
||||
mat-dialog-actions
|
||||
[deviceType]="data.deviceType"
|
||||
(closeButtonClicked)="onCancel()"
|
||||
></gf-dialog-footer>
|
||||
/>
|
||||
|
@ -148,9 +148,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.initialize();
|
||||
|
||||
return this.dataService.fetchPortfolioDetails({
|
||||
filters: this.activeFilters
|
||||
});
|
||||
return this.fetchPortfolioDetails();
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
@ -159,7 +157,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.portfolioDetails = portfolioDetails;
|
||||
|
||||
this.initializeAnalysisData();
|
||||
this.initializeAllocationsData();
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
@ -210,6 +208,26 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
? `{0}%`
|
||||
: `{0} ${this.user?.settings?.baseCurrency}`;
|
||||
|
||||
if (this.user?.settings?.isExperimentalFeatures === true) {
|
||||
this.isLoading = true;
|
||||
|
||||
this.initialize();
|
||||
|
||||
this.fetchPortfolioDetails()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((portfolioDetails) => {
|
||||
this.initialize();
|
||||
|
||||
this.portfolioDetails = portfolioDetails;
|
||||
|
||||
this.initializeAllocationsData();
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
@ -217,7 +235,52 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
public initialize() {
|
||||
public onAccountChartClicked({ symbol }: UniqueAsset) {
|
||||
if (symbol && symbol !== UNKNOWN_KEY) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { accountId: symbol, accountDetailDialog: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
|
||||
if (dataSource && symbol) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private extractEtfProvider({
|
||||
assetSubClass,
|
||||
name
|
||||
}: {
|
||||
assetSubClass: PortfolioPosition['assetSubClass'];
|
||||
name: string;
|
||||
}) {
|
||||
if (assetSubClass === 'ETF') {
|
||||
const [firstWord] = name.split(' ');
|
||||
return firstWord;
|
||||
}
|
||||
|
||||
return UNKNOWN_KEY;
|
||||
}
|
||||
|
||||
private fetchPortfolioDetails() {
|
||||
return this.dataService.fetchPortfolioDetails({
|
||||
filters:
|
||||
this.activeFilters.length > 0
|
||||
? this.activeFilters
|
||||
: this.userService.getFilters()
|
||||
});
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.accounts = {};
|
||||
this.continents = {
|
||||
[UNKNOWN_KEY]: {
|
||||
@ -310,7 +373,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
public initializeAnalysisData() {
|
||||
private initializeAllocationsData() {
|
||||
for (const [
|
||||
id,
|
||||
{ name, valueInBaseCurrency, valueInPercentage }
|
||||
@ -540,27 +603,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
this.markets[UNKNOWN_KEY].value / marketsTotal;
|
||||
}
|
||||
|
||||
public onAccountChartClicked({ symbol }: UniqueAsset) {
|
||||
if (symbol && symbol !== UNKNOWN_KEY) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { accountId: symbol, accountDetailDialog: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
|
||||
if (dataSource && symbol) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openAccountDetailDialog(aAccountId: string) {
|
||||
const dialogRef = this.dialog.open(AccountDetailDialog, {
|
||||
autoFocus: false,
|
||||
@ -621,19 +663,4 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private extractEtfProvider({
|
||||
assetSubClass,
|
||||
name
|
||||
}: {
|
||||
assetSubClass: PortfolioPosition['assetSubClass'];
|
||||
name: string;
|
||||
}) {
|
||||
if (assetSubClass === 'ETF') {
|
||||
const [firstWord] = name.split(' ');
|
||||
return firstWord;
|
||||
}
|
||||
|
||||
return UNKNOWN_KEY;
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,14 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Allocations</h1>
|
||||
@if (!user?.settings?.isExperimentalFeatures) {
|
||||
<gf-activities-filter
|
||||
[allFilters]="allFilters"
|
||||
[isLoading]="isLoading"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -22,7 +24,7 @@
|
||||
size="medium"
|
||||
[isPercent]="true"
|
||||
[value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage"
|
||||
></gf-value>
|
||||
/>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-progress-bar
|
||||
@ -50,7 +52,7 @@
|
||||
[keys]="['id']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="platforms"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -62,8 +64,8 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -73,7 +75,7 @@
|
||||
[keys]="['currency']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -85,8 +87,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -96,7 +98,7 @@
|
||||
[keys]="['assetClassLabel', 'assetSubClassLabel']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -119,7 +121,7 @@
|
||||
[positions]="symbols"
|
||||
[showLabels]="deviceType !== 'mobile'"
|
||||
(proportionChartClicked)="onSymbolChartClicked($event)"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -131,8 +133,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -143,7 +145,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -155,8 +157,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -166,7 +168,7 @@
|
||||
[keys]="['name']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="continents"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -178,8 +180,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -188,7 +190,7 @@
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="marketsAdvanced"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -202,8 +204,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="world-map-chart-container">
|
||||
@ -212,7 +214,7 @@
|
||||
[format]="worldMapChartFormat"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[locale]="user?.settings?.locale"
|
||||
></gf-world-map-chart>
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md my-2">
|
||||
@ -275,7 +277,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="accounts"
|
||||
(proportionChartClicked)="onAccountChartClicked($event)"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -287,8 +289,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -298,7 +300,7 @@
|
||||
[keys]="['etfProvider']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -310,8 +312,8 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator
|
||||
></mat-card-title>
|
||||
/>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
@ -322,7 +324,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
@ -379,7 +379,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ positions }) => {
|
||||
const positionsSorted = sortBy(
|
||||
positions,
|
||||
positions.filter(({ netPerformancePercentage }) => {
|
||||
return isNumber(netPerformancePercentage);
|
||||
}),
|
||||
'netPerformancePercentage'
|
||||
).reverse();
|
||||
|
||||
|
@ -7,14 +7,14 @@
|
||||
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
|
||||
[options]="dateRangeOptions"
|
||||
(change)="onChangeDateRange($event.value)"
|
||||
></gf-toggle>
|
||||
/>
|
||||
</div>
|
||||
<gf-activities-filter
|
||||
[allFilters]="allFilters"
|
||||
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
/>
|
||||
}
|
||||
<div class="mb-5 row">
|
||||
<div class="col-lg">
|
||||
@ -30,7 +30,7 @@
|
||||
[performanceDataItems]="performanceDataItemsInPercentage"
|
||||
[user]="user"
|
||||
(benchmarkChanged)="onChangeBenchmark($event)"
|
||||
></gf-benchmark-comparator>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformance"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex mb-3 ml-3 py-1">
|
||||
@ -66,7 +66,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercent"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex py-1">
|
||||
@ -81,7 +81,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="isLoadingInvestmentChart ? undefined : (performance?.currentNetPerformanceWithCurrencyEffect === null ? null : performance?.currentNetPerformanceWithCurrencyEffect - performance?.currentNetPerformance)"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex ml-3 py-1">
|
||||
@ -96,7 +96,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect - performance?.currentNetPerformancePercent"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div><hr /></div>
|
||||
@ -112,7 +112,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformanceWithCurrencyEffect"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex ml-3 py-1">
|
||||
@ -127,7 +127,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
@ -167,7 +167,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="position.netPerformancePercentage"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@ -180,7 +180,7 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@ -215,7 +215,7 @@
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="position.netPerformancePercentage"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@ -228,7 +228,7 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@ -245,7 +245,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
@ -260,7 +260,7 @@
|
||||
[isLoading]="isLoadingInvestmentChart"
|
||||
[locale]="user?.settings?.locale"
|
||||
[range]="user?.settings?.dateRange"
|
||||
></gf-investment-chart>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -275,7 +275,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</div>
|
||||
<gf-toggle
|
||||
class="d-none d-lg-block"
|
||||
@ -283,7 +283,7 @@
|
||||
[isLoading]="false"
|
||||
[options]="modeOptions"
|
||||
(change)="onChangeGroupBy($event.value)"
|
||||
></gf-toggle>
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="streaks" class="row">
|
||||
<div class="col-md-6 col-xs-12 my-2">
|
||||
@ -317,7 +317,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[range]="user?.settings?.dateRange"
|
||||
[savingsRate]="savingsRate"
|
||||
></gf-investment-chart>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -332,7 +332,7 @@
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</div>
|
||||
<gf-toggle
|
||||
class="d-none d-lg-block"
|
||||
@ -340,7 +340,7 @@
|
||||
[isLoading]="false"
|
||||
[options]="modeOptions"
|
||||
(change)="onChangeGroupBy($event.value)"
|
||||
></gf-toggle>
|
||||
/>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<gf-investment-chart
|
||||
@ -353,7 +353,7 @@
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[locale]="user?.settings?.locale"
|
||||
[range]="user?.settings?.dateRange"
|
||||
></gf-investment-chart>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<gf-fire-calculator
|
||||
[annualInterestRate]="user?.settings?.annualInterestRate"
|
||||
@ -25,7 +25,7 @@
|
||||
(projectedTotalAmountChanged)="onProjectedTotalAmountChange($event)"
|
||||
(retirementDateChanged)="onRetirementDateChange($event)"
|
||||
(savingsRateChanged)="onSavingsRateChange($event)"
|
||||
></gf-fire-calculator>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -35,7 +35,7 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<div *ngIf="isLoading">
|
||||
<ngx-skeleton-loader
|
||||
@ -45,14 +45,14 @@
|
||||
height: '1rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1rem',
|
||||
width: '10rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!isLoading" i18n>
|
||||
If you retire today, you would be able to withdraw
|
||||
@ -63,7 +63,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="withdrawalRatePerYear?.toNumber()"
|
||||
></gf-value>
|
||||
/>
|
||||
per year</span
|
||||
>
|
||||
or
|
||||
@ -74,7 +74,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="withdrawalRatePerMonth?.toNumber()"
|
||||
></gf-value>
|
||||
/>
|
||||
per month</span
|
||||
>, based on your total assets of
|
||||
<span class="font-weight-bold"
|
||||
@ -84,8 +84,8 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="fireWealth?.toNumber()"
|
||||
></gf-value
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
and a withdrawal rate of 4%.
|
||||
</div>
|
||||
</div>
|
||||
@ -112,12 +112,12 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="emergencyFundRules"
|
||||
></gf-rules>
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h4 class="align-items-center d-flex m-0">
|
||||
@ -125,12 +125,12 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="currencyClusterRiskRules"
|
||||
></gf-rules>
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h4 class="align-items-center d-flex m-0">
|
||||
@ -138,12 +138,12 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="accountClusterRiskRules"
|
||||
></gf-rules>
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="align-items-center d-flex m-0">
|
||||
@ -151,12 +151,12 @@
|
||||
><gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
/>
|
||||
</h4>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="feeRules"
|
||||
></gf-rules>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -86,16 +86,14 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
||||
? $localize`Filter by account or tag...`
|
||||
: '';
|
||||
|
||||
return this.dataService.fetchPortfolioDetails({
|
||||
filters: this.activeFilters
|
||||
});
|
||||
return this.fetchPortfolioDetails();
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
.subscribe((portfolioDetails) => {
|
||||
this.portfolioDetails = portfolioDetails;
|
||||
|
||||
this.initializeAnalysisData();
|
||||
this.initialize();
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
@ -146,17 +144,41 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
||||
...tagFilters
|
||||
];
|
||||
|
||||
if (this.user?.settings?.isExperimentalFeatures === true) {
|
||||
this.holdings = undefined;
|
||||
|
||||
this.fetchPortfolioDetails()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((portfolioDetails) => {
|
||||
this.portfolioDetails = portfolioDetails;
|
||||
|
||||
this.initialize();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public initialize() {
|
||||
this.holdings = [];
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
public initializeAnalysisData() {
|
||||
this.initialize();
|
||||
private fetchPortfolioDetails() {
|
||||
return this.dataService.fetchPortfolioDetails({
|
||||
filters:
|
||||
this.activeFilters.length > 0
|
||||
? this.activeFilters
|
||||
: this.userService.getFilters()
|
||||
});
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.holdings = [];
|
||||
|
||||
for (const [symbol, holding] of Object.entries(
|
||||
this.portfolioDetails.holdings
|
||||
@ -165,11 +187,6 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openPositionDialog({
|
||||
dataSource,
|
||||
symbol
|
||||
|
@ -2,12 +2,14 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
|
||||
@if (!user?.settings?.isExperimentalFeatures) {
|
||||
<gf-activities-filter
|
||||
[allFilters]="allFilters"
|
||||
[isLoading]="isLoading"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -18,7 +20,7 @@
|
||||
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
||||
[holdings]="holdings"
|
||||
[locale]="user?.settings?.locale"
|
||||
></gf-holdings-table>
|
||||
/>
|
||||
<div
|
||||
*ngIf="hasPermissionToCreateOrder && holdings?.length > 0"
|
||||
class="text-center"
|
||||
|
@ -171,10 +171,7 @@
|
||||
<div class="align-items-center d-flex mb-2">
|
||||
<h4 class="align-items-center d-flex flex-grow-1 m-0">
|
||||
<span>Premium</span>
|
||||
<gf-premium-indicator
|
||||
class="ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator>
|
||||
<gf-premium-indicator class="ml-1" [enableLink]="false" />
|
||||
</h4>
|
||||
<div *ngIf="user?.subscription?.type === 'Premium'">
|
||||
<ion-icon class="mr-1" name="checkmark-outline" />
|
||||
|
@ -20,7 +20,7 @@
|
||||
[keys]="['symbol']"
|
||||
[positions]="symbols"
|
||||
[showLabels]="deviceType !== 'mobile'"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -35,7 +35,7 @@
|
||||
[keys]="['currency']"
|
||||
[maxItems]="10"
|
||||
[positions]="positions"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -50,7 +50,7 @@
|
||||
[keys]="['name']"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -64,7 +64,7 @@
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[positions]="continents"
|
||||
></gf-portfolio-proportion-chart>
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -81,7 +81,7 @@
|
||||
format="{0}%"
|
||||
[countries]="countries"
|
||||
[isInPercent]="true"
|
||||
></gf-world-map-chart>
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md my-2">
|
||||
@ -135,7 +135,7 @@
|
||||
[hasPermissionToShowValues]="false"
|
||||
[holdings]="holdings"
|
||||
[pageSize]="7"
|
||||
></gf-holdings-table>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-5">
|
||||
|
@ -9,7 +9,7 @@
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center w-100"
|
||||
>
|
||||
<gf-logo size="large"></gf-logo>
|
||||
<gf-logo size="large" />
|
||||
<p class="lead m-0">Wealth Management Software</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,6 +44,7 @@ import { SumioPageComponent } from './products/sumio-page.component';
|
||||
import { TillerPageComponent } from './products/tiller-page.component';
|
||||
import { UtlunaPageComponent } from './products/utluna-page.component';
|
||||
import { VyzerPageComponent } from './products/vyzer-page.component';
|
||||
import { WealthfolioPageComponent } from './products/wealthfolio-page.component';
|
||||
import { WealthicaPageComponent } from './products/wealthica-page.component';
|
||||
import { WhalPageComponent } from './products/whal-page.component';
|
||||
import { YeekateePageComponent } from './products/yeekatee-page.component';
|
||||
@ -528,6 +529,14 @@ export const products: Product[] = [
|
||||
pricingPerYear: '$348',
|
||||
slogan: 'Virtual Family Office for Smart Wealth Management'
|
||||
},
|
||||
{
|
||||
component: WealthfolioPageComponent,
|
||||
hasSelfHostingAbility: true,
|
||||
key: 'wealthfolio',
|
||||
languages: ['English'],
|
||||
name: 'Wealthfolio',
|
||||
slogan: 'Desktop Investment Tracker'
|
||||
},
|
||||
{
|
||||
component: WealthicaPageComponent,
|
||||
founded: 2015,
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { products } from '../products';
|
||||
import { BaseProductPageComponent } from './base-page.component';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||
selector: 'gf-wealthfolio-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class WealthfolioPageComponent extends BaseProductPageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'wealthfolio';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center mb-4 w-100"
|
||||
>
|
||||
<gf-logo size="medium"></gf-logo>
|
||||
<gf-logo size="medium" />
|
||||
</div>
|
||||
|
||||
<div *ngIf="!hasError" class="col d-flex justify-content-center">
|
||||
|
@ -47,18 +47,26 @@ export class UserService extends ObservableStore<UserStoreState> {
|
||||
}
|
||||
|
||||
public getFilters() {
|
||||
const filters: Filter[] = [];
|
||||
const user = this.getState().user;
|
||||
|
||||
return user?.settings?.isExperimentalFeatures === true
|
||||
? user.settings['filters.tags']
|
||||
? <Filter[]>[
|
||||
{
|
||||
id: user.settings['filters.tags'][0],
|
||||
type: 'TAG'
|
||||
}
|
||||
]
|
||||
: []
|
||||
: [];
|
||||
if (user?.settings?.isExperimentalFeatures === true) {
|
||||
if (user.settings['filters.accounts']) {
|
||||
filters.push({
|
||||
id: user.settings['filters.accounts'][0],
|
||||
type: 'ACCOUNT'
|
||||
});
|
||||
}
|
||||
|
||||
if (user.settings['filters.tags']) {
|
||||
filters.push({
|
||||
id: user.settings['filters.tags'][0],
|
||||
type: 'TAG'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
public remove() {
|
||||
|
@ -48,7 +48,7 @@
|
||||
<link href="../assets/site.webmanifest" rel="manifest" />
|
||||
</head>
|
||||
<body>
|
||||
<gf-root></gf-root>
|
||||
<gf-root />
|
||||
|
||||
<script src="../ionicons/ionicons.esm.js" type="module"></script>
|
||||
<script nomodule="" src="ionicons.js"></script>
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -112,6 +112,21 @@ export const QUEUE_JOB_STATUS_LIST = <JobStatus[]>[
|
||||
'waiting'
|
||||
];
|
||||
|
||||
export const REPLACE_NAME_PARTS = [
|
||||
'Amundi Index Solutions -',
|
||||
'iShares ETF (CH) -',
|
||||
'iShares III Public Limited Company -',
|
||||
'iShares V PLC -',
|
||||
'iShares VI Public Limited Company -',
|
||||
'iShares VII PLC -',
|
||||
'Multi Units Luxembourg -',
|
||||
'VanEck ETFs N.V. -',
|
||||
'Vaneck Vectors Ucits Etfs Plc -',
|
||||
'Vanguard Funds Public Limited Company -',
|
||||
'Vanguard Index Funds -',
|
||||
'Xtrackers (IE) Plc -'
|
||||
];
|
||||
|
||||
export const SUPPORTED_LANGUAGE_CODES = [
|
||||
'de',
|
||||
'en',
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { AccessType } from '@ghostfolio/common/types';
|
||||
import { AccessPermission } from '@prisma/client';
|
||||
|
||||
export interface Access {
|
||||
alias?: string;
|
||||
grantee?: string;
|
||||
id: string;
|
||||
type: 'PRIVATE' | 'PUBLIC' | 'RESTRICTED_VIEW';
|
||||
permissions: AccessPermission[];
|
||||
type: AccessType;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export interface UserSettings {
|
||||
colorScheme?: ColorScheme;
|
||||
dateRange?: DateRange;
|
||||
emergencyFund?: number;
|
||||
'filters.accounts'?: string[];
|
||||
'filters.tags'?: string[];
|
||||
isExperimentalFeatures?: boolean;
|
||||
isRestrictedView?: boolean;
|
||||
|
1
libs/common/src/lib/types/access-type.type.ts
Normal file
1
libs/common/src/lib/types/access-type.type.ts
Normal file
@ -0,0 +1 @@
|
||||
export type AccessType = 'PRIVATE' | 'PUBLIC';
|
@ -1,3 +1,4 @@
|
||||
import type { AccessType } from './access-type.type';
|
||||
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
||||
import type { AccountWithPlatform } from './account-with-platform.type';
|
||||
import type { AccountWithValue } from './account-with-value.type';
|
||||
@ -18,6 +19,7 @@ import type { UserWithSettings } from './user-with-settings.type';
|
||||
import type { ViewMode } from './view-mode.type';
|
||||
|
||||
export type {
|
||||
AccessType,
|
||||
AccessWithGranteeUser,
|
||||
AccountWithPlatform,
|
||||
AccountWithValue,
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
import { SubscriptionOffer } from '@ghostfolio/common/types';
|
||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
|
||||
import { Account, Settings, User } from '@prisma/client';
|
||||
import { Access, Account, Settings, User } from '@prisma/client';
|
||||
|
||||
// TODO: Compare with User interface
|
||||
export type UserWithSettings = User & {
|
||||
Access: Access[];
|
||||
Account: Account[];
|
||||
activityCount: number;
|
||||
permissions?: string[];
|
||||
|
@ -26,7 +26,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="element?.Account?.currency"
|
||||
[value]="element?.value"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
@ -120,7 +120,7 @@
|
||||
[dataSource]="element.SymbolProfile?.dataSource"
|
||||
[symbol]="element.SymbolProfile?.symbol"
|
||||
[tooltip]="element.SymbolProfile?.name"
|
||||
></gf-symbol-icon>
|
||||
/>
|
||||
<div>{{ element.dataSource }}</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -154,7 +154,7 @@
|
||||
<ng-container i18n>Type</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<gf-activity-type [activityType]="element.type"></gf-activity-type>
|
||||
<gf-activity-type [activityType]="element.type" />
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -188,7 +188,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.quantity"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -212,7 +212,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.unitPrice"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -236,7 +236,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.fee"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -259,7 +259,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.value"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -291,7 +291,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.valueInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -307,7 +307,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="element.Account?.Platform?.name"
|
||||
[url]="element.Account?.Platform?.url"
|
||||
></gf-symbol-icon>
|
||||
/>
|
||||
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
@ -467,7 +467,7 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
|
||||
<mat-paginator
|
||||
[length]="totalItems"
|
||||
@ -486,7 +486,5 @@
|
||||
"
|
||||
class="p-3 text-center"
|
||||
>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator>
|
||||
<gf-no-transactions-info-indicator [hasBorder]="false" />
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@
|
||||
[ngClass]="{ 'd-none': !hasPermissionToFilter }"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
/>
|
||||
|
||||
<div *ngIf="hasPermissionToCreateActivity" class="d-flex justify-content-end">
|
||||
<button
|
||||
@ -164,7 +164,7 @@
|
||||
<ng-container i18n>Type</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<gf-activity-type [activityType]="element.type"></gf-activity-type>
|
||||
<gf-activity-type [activityType]="element.type" />
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||
</ng-container>
|
||||
@ -239,7 +239,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.quantity"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
@ -268,7 +268,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.unitPrice"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
@ -297,7 +297,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.fee"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||
@ -306,7 +306,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : totalFees"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -330,7 +330,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.value"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||
@ -341,7 +341,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : totalValue"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -361,7 +361,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.valueInBaseCurrency"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td *matFooterCellDef class="d-lg-none d-xl-none px-1" mat-footer-cell>
|
||||
@ -372,7 +372,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : totalValue"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -393,7 +393,7 @@
|
||||
class="mr-1"
|
||||
[tooltip]="element.Account?.Platform?.name"
|
||||
[url]="element.Account?.Platform?.url"
|
||||
></gf-symbol-icon>
|
||||
/>
|
||||
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
@ -579,7 +579,7 @@
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
/>
|
||||
|
||||
<div
|
||||
*ngIf="
|
||||
@ -587,7 +587,5 @@
|
||||
"
|
||||
class="p-3 text-center"
|
||||
>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator>
|
||||
<gf-no-transactions-info-indicator [hasBorder]="false" />
|
||||
</div>
|
||||
|
@ -15,14 +15,14 @@ import {
|
||||
ViewChild,
|
||||
ViewChildren
|
||||
} from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { FormBuilder, FormControl } from '@angular/forms';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { Filter, User } from '@ghostfolio/common/interfaces';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
import { Tag } from '@prisma/client';
|
||||
import { Account, Tag } from '@prisma/client';
|
||||
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
@ -81,7 +81,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
|
||||
@Output() closed = new EventEmitter<void>();
|
||||
@Output() dateRangeChanged = new EventEmitter<DateRange>();
|
||||
@Output() selectedTagChanged = new EventEmitter<Tag>();
|
||||
@Output() filtersChanged = new EventEmitter<Filter[]>();
|
||||
|
||||
@ViewChild('menuTrigger') menuTriggerElement: MatMenuTrigger;
|
||||
@ViewChild('search', { static: true }) searchElement: ElementRef;
|
||||
@ -91,6 +91,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
|
||||
public static readonly SEARCH_RESULTS_DEFAULT_LIMIT = 5;
|
||||
|
||||
public accounts: Account[] = [];
|
||||
public dateRangeFormControl = new FormControl<string>(undefined);
|
||||
public readonly dateRangeOptions = [
|
||||
{ label: $localize`Today`, value: '1d' },
|
||||
@ -110,6 +111,10 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
{ label: $localize`5Y`, value: '5y' },
|
||||
{ label: $localize`Max`, value: 'max' }
|
||||
];
|
||||
public filterForm = this.formBuilder.group({
|
||||
account: new FormControl<string>(undefined),
|
||||
tag: new FormControl<string>(undefined)
|
||||
});
|
||||
public isLoading = false;
|
||||
public isOpen = false;
|
||||
public placeholder = $localize`Find holding...`;
|
||||
@ -119,7 +124,6 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
holdings: []
|
||||
};
|
||||
public tags: Tag[] = [];
|
||||
public tagsFormControl = new FormControl<string>(undefined);
|
||||
|
||||
private keyManager: FocusKeyManager<AssistantListItemComponent>;
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -127,12 +131,14 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
public constructor(
|
||||
private adminService: AdminService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService
|
||||
private dataService: DataService,
|
||||
private formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
public ngOnInit() {
|
||||
const { tags } = this.dataService.fetchInfo();
|
||||
|
||||
this.accounts = this.user?.accounts;
|
||||
this.tags = tags.map(({ id, name }) => {
|
||||
return {
|
||||
id,
|
||||
@ -140,6 +146,23 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
};
|
||||
});
|
||||
|
||||
this.filterForm.valueChanges
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ account, tag }) => {
|
||||
this.filtersChanged.emit([
|
||||
{
|
||||
id: account,
|
||||
type: 'ACCOUNT'
|
||||
},
|
||||
{
|
||||
id: tag,
|
||||
type: 'TAG'
|
||||
}
|
||||
]);
|
||||
|
||||
this.onCloseAssistant();
|
||||
});
|
||||
|
||||
this.searchFormControl.valueChanges
|
||||
.pipe(
|
||||
map((searchTerm) => {
|
||||
@ -181,11 +204,24 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
|
||||
public ngOnChanges() {
|
||||
this.dateRangeFormControl.setValue(this.user?.settings?.dateRange ?? null);
|
||||
this.tagsFormControl.setValue(
|
||||
this.user?.settings?.['filters.tags']?.[0] ?? null
|
||||
|
||||
this.filterForm.setValue(
|
||||
{
|
||||
account: this.user?.settings?.['filters.accounts']?.[0] ?? null,
|
||||
tag: this.user?.settings?.['filters.tags']?.[0] ?? null
|
||||
},
|
||||
{
|
||||
emitEvent: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public hasFilter(aFormValue: { [key: string]: string }) {
|
||||
return Object.values(aFormValue).some((value) => {
|
||||
return !!value;
|
||||
});
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.isLoading = true;
|
||||
this.keyManager = new FocusKeyManager(this.assistantListItems).withWrap();
|
||||
@ -219,12 +255,17 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
this.closed.emit();
|
||||
}
|
||||
|
||||
public onTagChange() {
|
||||
const selectedTag = this.tags.find(({ id }) => {
|
||||
return id === this.tagsFormControl.value;
|
||||
});
|
||||
|
||||
this.selectedTagChanged.emit(selectedTag);
|
||||
public onResetFilters() {
|
||||
this.filtersChanged.emit([
|
||||
{
|
||||
id: null,
|
||||
type: 'ACCOUNT'
|
||||
},
|
||||
{
|
||||
id: null,
|
||||
type: 'TAG'
|
||||
}
|
||||
]);
|
||||
|
||||
this.onCloseAssistant();
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user