Compare commits

...

23 Commits

Author SHA1 Message Date
6dcb0d8583 Release 2.46.0 (#2938) 2024-01-28 10:00:07 +01:00
40b6777814 Add upgrade plan button (#2937) 2024-01-28 09:58:38 +01:00
25deba16df Feature/add reset filters button to assistant (#2936)
* Add reset filters button

* Update changelog
2024-01-28 09:47:28 +01:00
be93ca8968 Feature/migrate allocations page to work with filters of assistant (#2933)
* Migrate portfolio allocations to work with filters of assistant

* Update changelog
2024-01-28 09:20:32 +01:00
0436cc6487 Migrate ngx-skeleton-loader components to self-closing tags (#2935) 2024-01-28 08:51:02 +01:00
857708dc4d Migrate gf-* components to self-closing tags (#2934) 2024-01-28 08:50:43 +01:00
1ca4f885b0 Feature/migrate holdings page to work with filters of assistant (#2932)
* Migrate portfolio holdings to work with filters of assistant

* Update changelog
2024-01-27 19:28:13 +01:00
c9368c5cf2 Release 2.45.0 (#2931) 2024-01-27 10:54:35 +01:00
29423efea3 Update translations (#2930) 2024-01-27 10:53:19 +01:00
f3ee99fb2b Feature/extend assistant by account selector (#2929)
* Add account selector to assistant

* Update changelog
2024-01-27 10:48:46 +01:00
3df8810412 Feature/Add support to grant private access with permissions (#2870)
* Add support to grant private access with permissions

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-01-27 09:44:13 +01:00
b8ca88c6df Add auto-renewal (#2920) 2024-01-27 08:46:27 +01:00
2c068c412d Feature/migrate tag selector to form group in assistant (#2926)
* Introduce filter form group

* Update changelog
2024-01-27 08:45:55 +01:00
9fdbd22cb5 Bugfix/fix activities import for manual data source (#2923)
* Fix import

* Update changelog
2024-01-27 08:41:45 +01:00
8f5f4c5875 Feature/format name in eod historical data service (#2922)
* Format name

* Update changelog
2024-01-26 22:37:47 +01:00
50fb82a6e6 Extend personal finance tools (#2925) 2024-01-26 22:37:26 +01:00
2c10cd7edf Bugfix/remove holdings with incomplete data from top3 bottom3 performers (#2921)
* Remove holdings with incomplete data

* Update changelog
2024-01-26 08:35:23 +01:00
bbde86c66e Feature/improve language localization for de 20240124 (#2918)
* Update translations

* Update changelog
2024-01-25 21:11:07 +01:00
73c0843d51 Feature/add permissions to access model (#2833)
* Add permissions to Access model

* Update changelog
2024-01-24 19:23:58 +01:00
04fc2cd3e1 Release 2.44.0 (#2917) 2024-01-24 12:26:36 +01:00
b39c97ab9f Bugfix/improve validation for non numeric results in eod service (#2916)
* Improve validation of non-numeric numbers

* Update changelog
2024-01-24 12:24:38 +01:00
1dd5e9c787 Release 2.43.1 (#2914) 2024-01-23 20:46:37 +01:00
a9985b65b8 Adjust Dockerfile to enable healthcheck (#2913) 2024-01-23 20:44:57 +01:00
113 changed files with 4177 additions and 1418 deletions

View File

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

View File

@ -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/*

View File

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

View File

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

View File

@ -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 })] =

View File

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

View File

@ -38,6 +38,10 @@ export class UpdateUserSettingDto {
@IsOptional()
emergencyFund?: number;
@IsArray()
@IsOptional()
'filters.accounts'?: string[];
@IsArray()
@IsOptional()
'filters.tags'?: string[];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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('&amp;', '&');
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') {

View File

@ -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}`
};
}

View File

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

View File

@ -38,7 +38,7 @@
[pageTitle]="pageTitle"
[user]="user"
(signOut)="onSignOut()"
></gf-header>
/>
</header>
<main role="main">

View File

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

View File

@ -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>
/>

View File

@ -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>
/>

View File

@ -8,7 +8,7 @@
[showXAxis]="true"
[showYAxis]="true"
[symbol]="symbol"
></gf-line-chart>
/>
<div
*ngFor="let itemByMonth of marketDataByMonth | keyvalue"
class="d-flex"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,5 +19,5 @@
[theme]="{
height: '100%'
}"
></ngx-skeleton-loader>
/>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -74,7 +74,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
});
this.showDetails =
!this.hasImpersonationId &&
!this.user.settings.isRestrictedView &&
this.user.settings.viewMode !== 'ZEN';

View File

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

View File

@ -12,7 +12,7 @@
[locale]="user?.settings?.locale"
[summary]="summary"
(emergencyFundChanged)="onChangeEmergencyFund($event)"
></gf-portfolio-summary>
/>
</mat-card-content>
</mat-card>
</div>

View File

@ -5,7 +5,7 @@
height: '100%',
width: '100%'
}"
></ngx-skeleton-loader>
/>
<canvas
#chartCanvas
class="h-100"

View File

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

View File

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

View File

@ -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 = '*****';
}
}
}

View File

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

View File

@ -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>
/>

View File

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

View File

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

View File

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

View File

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

View File

@ -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?

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { Access } from '@ghostfolio/common/interfaces';
import { Access, User } from '@ghostfolio/common/interfaces';
export interface CreateOrUpdateAccessDialogParams {
access: Access;
user: User;
}

View File

@ -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'

View File

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

View File

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

View File

@ -5,6 +5,6 @@
[theme]="{
width: '100%'
}"
></ngx-skeleton-loader>
/>
<div class="align-items-center d-flex h-100 w-100" id="svgMap"></div>

View File

@ -15,7 +15,7 @@
(accountDeleted)="onDeleteAccount($event)"
(accountToUpdate)="onUpdateAccount($event)"
(transferBalance)="onTransferBalance()"
></gf-accounts-table>
/>
</div>
</div>
</div>

View File

@ -61,7 +61,7 @@
class="mr-1"
[tooltip]="platformEntry.name"
[url]="platformEntry.url"
></gf-symbol-icon>
/>
<span>{{ platformEntry.name }}</span>
</span>
</mat-option>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'
];
}

View File

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

View File

@ -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() {

View File

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

View File

@ -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',

View File

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

View File

@ -7,6 +7,7 @@ export interface UserSettings {
colorScheme?: ColorScheme;
dateRange?: DateRange;
emergencyFund?: number;
'filters.accounts'?: string[];
'filters.tags'?: string[];
isExperimentalFeatures?: boolean;
isRestrictedView?: boolean;

View File

@ -0,0 +1 @@
export type AccessType = 'PRIVATE' | 'PUBLIC';

View File

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

View File

@ -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[];

View File

@ -26,7 +26,7 @@
[locale]="locale"
[unit]="element?.Account?.currency"
[value]="element?.value"
></gf-value>
/>
</div>
</td>
</ng-container>

View File

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

View File

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

View File

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