Compare commits

...

50 Commits

Author SHA1 Message Date
00a2b60eb5 Release 2.49.0 (#2979) 2024-02-09 19:30:00 +01:00
fcbf2f1645 Feature/remove lazy from name of activities table (#2978)
* Remove lazy from name

* Update translations
2024-02-09 18:48:05 +01:00
460266a501 Feature/upgrade yahoo finance2 to version 2.9.1 (#2963)
* Upgrade yahoo-finance2 to version 2.9.1

* Update changelog
2024-02-09 18:41:18 +01:00
9fe90273c7 Feature/move assistant to general availability (#2977)
* Move assistant from experimental to general availability

* Update changelog
2024-02-09 18:25:15 +01:00
4078229fe6 Feature/add button to apply filters in assistant (#2971)
* Add apply filters button

* Update changelog
2024-02-09 09:45:54 +01:00
609c03f174 Add analytics image (#2970) 2024-02-08 08:00:04 +01:00
e7d4641d13 Feature/reload data on logo click (#2959)
* Reload data on logo click

* Update changelog
2024-02-07 21:03:28 +01:00
cc1d9811e0 Release 2.48.1 (#2968) 2024-02-06 17:00:40 +01:00
35450ac004 Bugfix/add missing data provider info to search results of coingecko (#2967)
* Add missing data provider info

* Update changelog
2024-02-06 16:58:54 +01:00
9c18f48a32 Release 2.48.0 (#2962) 2024-02-05 19:57:40 +01:00
87529490c3 Feature/refresh cryptocurrencies list 20240205 (#2961)
* Update cryptocurrencies.json

* Update changelog
2024-02-05 19:56:16 +01:00
893e76f83f Feature/provide data provider info in search (#2958)
* Provide data provider info in search

* Update changelog
2024-02-05 19:55:39 +01:00
06ba7a4b1b Feature/extend assistant by asset class selector (#2957)
* Remove tabs

* Add asset class selector

* Update changelog
2024-02-04 15:50:58 +01:00
c68d113d27 Feature/improve usability of account and tag selector of assistant (#2955)
* Change radio button to select

* account
* tag

* Update changelog
2024-02-04 12:11:01 +01:00
69e3bee52c Feature/upgrade prettier to version 3.2.5 (#2954)
* Upgrade prettier to version 3.2.5

* Update changelog
2024-02-04 12:10:33 +01:00
cea569c987 Add Fina (#2956) 2024-02-04 12:10:22 +01:00
Hey
2a38a16f6b Feature/Improve error logs for timeout in data provider services (#2953)
* Improve error logs for timeout in data provider services

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-02-04 11:56:00 +01:00
0f9455cf02 Release 2.47.0 (#2951) 2024-02-03 09:44:29 +01:00
d4afa03505 Bugfix/fix rendering issue with date range selector of assistant (#2950)
* Improve click handling

* Improve locales

* Update changelog
2024-02-03 09:42:50 +01:00
c9237146e2 Feature/add investment value to chart (#2948)
* Add investment value to chart

* Update changelog
2024-02-03 09:23:19 +01:00
faad65b6f3 Update translations (#2940)
* Update translations

* Update changelog
2024-02-02 20:42:12 +01:00
e459c72100 Update OSS friends (#2942) 2024-02-01 20:54:25 +01:00
a8add30125 Feature/upgrade prettier to version 3.2.4 (#2928)
* Upgrade prettier to version 3.2.4

* Update changelog
2024-01-31 18:12:09 +01:00
b535aee91d Remove reference to Internet Identity (#2946) 2024-01-31 17:19:05 +01:00
4434d0315f Remove unused timeline calculation (#2947) 2024-01-30 20:56:41 +01:00
8b10695353 Feature/only show used tags in tag selector of assistant (#2943)
* Only show used tags in tag selector

* Update changelog
2024-01-29 19:53:47 +01:00
e82dcc8ace Feature/fix export in lazy-loaded activities table (#2939)
* Fix export in lazy-loaded activities table

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-01-29 19:37:09 +01:00
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
170 changed files with 9418 additions and 6948 deletions

View File

@ -5,7 +5,90 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.43.0 - 2024-01-23 ## 2.49.0 - 2024-02-09
### Added
- Added a button to apply the active filters in the assistant
### Changed
- Moved the assistant from experimental to general availability
- Improved the usability by reloading the content with a logo click on the home page
- Upgraded `yahoo-finance2` from version `2.9.0` to `2.9.1`
## 2.48.1 - 2024-02-06
### Fixed
- Added the missing data provider information to the _CoinGecko_ service
## 2.48.0 - 2024-02-05
### Added
- Extended the assistant by an asset class selector (experimental)
- Added the data provider information to the search endpoint
### Changed
- Improved the usability of the account selector in the assistant (experimental)
- Improved the usability of the tag selector in the assistant (experimental)
- Improved the error logs for a timeout in the data provider services
- Refreshed the cryptocurrencies list
- Upgraded `prettier` from version `3.2.4` to `3.2.5`
## 2.47.0 - 2024-02-02
### Changed
- Improved the tag selector to only show used tags in the assistant (experimental)
- Improved the language localization for German (`de`)
- Upgraded `prettier` from version `3.2.1` to `3.2.4`
### Fixed
- Fixed a rendering issue caused by the date range selector in the assistant (experimental)
- Fixed an issue with the currency conversion in the investment timeline
- Fixed the export in the lazy-loaded activities table on the portfolio activities page (experimental)
## 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 ### Added

View File

@ -13,7 +13,6 @@ COPY ./.yarnrc .yarnrc
COPY ./prisma/schema.prisma prisma/schema.prisma COPY ./prisma/schema.prisma prisma/schema.prisma
RUN apt update && apt install -y \ RUN apt update && apt install -y \
curl \
g++ \ g++ \
git \ git \
make \ make \
@ -53,6 +52,7 @@ RUN yarn database:generate-typings
# Image to run, copy everything needed from builder # Image to run, copy everything needed from builder
FROM node:18-slim FROM node:18-slim
RUN apt update && apt install -y \ RUN apt update && apt install -y \
curl \
openssl \ openssl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View File

@ -280,6 +280,10 @@ Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Sl
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio). If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).
## Analytics
![Alt](https://repobeats.axiom.co/api/embed/281a80b2d0c4af1162866c24c803f1f18e5ed60e.svg 'Repobeats analytics image')
## License ## License
© 2021 - 2024 [Ghostfolio](https://ghostfol.io) © 2021 - 2024 [Ghostfolio](https://ghostfol.io)

View File

@ -42,23 +42,27 @@ export class AccessController {
where: { userId: this.request.user.id } where: { userId: this.request.user.id }
}); });
return accessesWithGranteeUser.map((access) => { return accessesWithGranteeUser.map(
if (access.GranteeUser) { ({ alias, GranteeUser, id, permissions }) => {
if (GranteeUser) {
return {
alias,
id,
permissions,
grantee: GranteeUser?.id,
type: 'PRIVATE'
};
}
return { return {
alias: access.alias, alias,
grantee: access.GranteeUser?.id, id,
id: access.id, permissions,
type: 'RESTRICTED_VIEW' grantee: 'Public',
type: 'PUBLIC'
}; };
} }
);
return {
alias: access.alias,
grantee: 'Public',
id: access.id,
type: 'PUBLIC'
};
});
} }
@HasPermission(permissions.createAccess) @HasPermission(permissions.createAccess)
@ -83,6 +87,7 @@ export class AccessController {
GranteeUser: data.granteeUserId GranteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } } ? { connect: { id: data.granteeUserId } }
: undefined, : undefined,
permissions: data.permissions,
User: { connect: { id: this.request.user.id } } User: { connect: { id: this.request.user.id } }
}); });
} catch { } 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 { export class CreateAccessDto {
@IsOptional() @IsOptional()
@ -9,7 +10,7 @@ export class CreateAccessDto {
@IsUUID() @IsUUID()
granteeUserId?: string; granteeUserId?: string;
@IsEnum(AccessPermission, { each: true })
@IsOptional() @IsOptional()
@IsString() permissions?: AccessPermission[];
type?: 'PUBLIC';
} }

View File

@ -1,4 +1,5 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { Export } from '@ghostfolio/common/interfaces'; import { Export } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common'; import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common';
@ -10,6 +11,7 @@ import { ExportService } from './export.service';
@Controller('export') @Controller('export')
export class ExportController { export class ExportController {
public constructor( public constructor(
private readonly apiService: ApiService,
private readonly exportService: ExportService, private readonly exportService: ExportService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@ -17,10 +19,20 @@ export class ExportController {
@Get() @Get()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async export( public async export(
@Query('activityIds') activityIds?: string[] @Query('accounts') filterByAccounts?: string,
@Query('activityIds') activityIds?: string[],
@Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string
): Promise<Export> { ): Promise<Export> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByTags
});
return this.exportService.export({ return this.exportService.export({
activityIds, activityIds,
filters,
userCurrency: this.request.user.Settings.settings.baseCurrency, userCurrency: this.request.user.Settings.settings.baseCurrency,
userId: this.request.user.id userId: this.request.user.id
}); });

View File

@ -1,6 +1,7 @@
import { AccountModule } from '@ghostfolio/api/app/account/account.module'; import { AccountModule } from '@ghostfolio/api/app/account/account.module';
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -12,6 +13,7 @@ import { ExportService } from './export.service';
@Module({ @Module({
imports: [ imports: [
AccountModule, AccountModule,
ApiModule,
ConfigurationModule, ConfigurationModule,
DataGatheringModule, DataGatheringModule,
DataProviderModule, DataProviderModule,

View File

@ -1,7 +1,7 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { environment } from '@ghostfolio/api/environments/environment'; import { environment } from '@ghostfolio/api/environments/environment';
import { Export } from '@ghostfolio/common/interfaces'; import { Filter, Export } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
@ -13,10 +13,12 @@ export class ExportService {
public async export({ public async export({
activityIds, activityIds,
filters,
userCurrency, userCurrency,
userId userId
}: { }: {
activityIds?: string[]; activityIds?: string[];
filters?: Filter[];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
}): Promise<Export> { }): Promise<Export> {
@ -42,6 +44,7 @@ export class ExportService {
); );
let { activities } = await this.orderService.getOrders({ let { activities } = await this.orderService.getOrders({
filters,
userCurrency, userCurrency,
userId, userId,
includeDrafts: true, includeDrafts: true,

View File

@ -64,16 +64,13 @@ export class ImportController {
maxActivitiesToImport = Number.MAX_SAFE_INTEGER; maxActivitiesToImport = Number.MAX_SAFE_INTEGER;
} }
const userCurrency = this.request.user.Settings.settings.baseCurrency;
try { try {
const activities = await this.importService.import({ const activities = await this.importService.import({
isDryRun, isDryRun,
maxActivitiesToImport, maxActivitiesToImport,
userCurrency,
accountsDto: importData.accounts ?? [], accountsDto: importData.accounts ?? [],
activitiesDto: importData.activities, activitiesDto: importData.activities,
userId: this.request.user.id user: this.request.user
}); });
return { activities }; return { activities };

View File

@ -21,7 +21,8 @@ import {
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { import {
AccountWithPlatform, AccountWithPlatform,
OrderWithAccount OrderWithAccount,
UserWithSettings
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
@ -138,17 +139,16 @@ export class ImportService {
activitiesDto, activitiesDto,
isDryRun = false, isDryRun = false,
maxActivitiesToImport, maxActivitiesToImport,
userCurrency, user
userId
}: { }: {
accountsDto: Partial<CreateAccountDto>[]; accountsDto: Partial<CreateAccountDto>[];
activitiesDto: Partial<CreateOrderDto>[]; activitiesDto: Partial<CreateOrderDto>[];
isDryRun?: boolean; isDryRun?: boolean;
maxActivitiesToImport: number; maxActivitiesToImport: number;
userCurrency: string; user: UserWithSettings;
userId: string;
}): Promise<Activity[]> { }): Promise<Activity[]> {
const accountIdMapping: { [oldAccountId: string]: string } = {}; const accountIdMapping: { [oldAccountId: string]: string } = {};
const userCurrency = user.Settings.settings.baseCurrency;
if (!isDryRun && accountsDto?.length) { if (!isDryRun && accountsDto?.length) {
const [existingAccounts, existingPlatforms] = await Promise.all([ const [existingAccounts, existingPlatforms] = await Promise.all([
@ -171,7 +171,7 @@ export class ImportService {
); );
// If there is no account or if the account belongs to a different user then create a new account // If there is no account or if the account belongs to a different user then create a new account
if (!accountWithSameId || accountWithSameId.userId !== userId) { if (!accountWithSameId || accountWithSameId.userId !== user.id) {
let oldAccountId: string; let oldAccountId: string;
const platformId = account.platformId; const platformId = account.platformId;
@ -184,7 +184,7 @@ export class ImportService {
let accountObject: Prisma.AccountCreateInput = { let accountObject: Prisma.AccountCreateInput = {
...account, ...account,
User: { connect: { id: userId } } User: { connect: { id: user.id } }
}; };
if ( if (
@ -200,7 +200,7 @@ export class ImportService {
const newAccount = await this.accountService.createAccount( const newAccount = await this.accountService.createAccount(
accountObject, accountObject,
userId user.id
); );
// Store the new to old account ID mappings for updating activities // Store the new to old account ID mappings for updating activities
@ -231,16 +231,17 @@ export class ImportService {
const assetProfiles = await this.validateActivities({ const assetProfiles = await this.validateActivities({
activitiesDto, activitiesDto,
maxActivitiesToImport maxActivitiesToImport,
user
}); });
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({ const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({
activitiesDto, activitiesDto,
userCurrency, userCurrency,
userId userId: user.id
}); });
const accounts = (await this.accountService.getAccounts(userId)).map( const accounts = (await this.accountService.getAccounts(user.id)).map(
({ id, name }) => { ({ id, name }) => {
return { id, name }; return { id, name };
} }
@ -345,7 +346,6 @@ export class ImportService {
quantity, quantity,
type, type,
unitPrice, unitPrice,
userId,
accountId: validatedAccount?.id, accountId: validatedAccount?.id,
accountUserId: undefined, accountUserId: undefined,
createdAt: new Date(), createdAt: new Date(),
@ -374,7 +374,8 @@ export class ImportService {
}, },
Account: validatedAccount, Account: validatedAccount,
symbolProfileId: undefined, symbolProfileId: undefined,
updatedAt: new Date() updatedAt: new Date(),
userId: user.id
}; };
} else { } else {
if (error) { if (error) {
@ -388,7 +389,6 @@ export class ImportService {
quantity, quantity,
type, type,
unitPrice, unitPrice,
userId,
accountId: validatedAccount?.id, accountId: validatedAccount?.id,
SymbolProfile: { SymbolProfile: {
connectOrCreate: { connectOrCreate: {
@ -406,7 +406,8 @@ export class ImportService {
} }
}, },
updateAccountBalance: false, updateAccountBalance: false,
User: { connect: { id: userId } } User: { connect: { id: user.id } },
userId: user.id
}); });
} }
@ -553,10 +554,12 @@ export class ImportService {
private async validateActivities({ private async validateActivities({
activitiesDto, activitiesDto,
maxActivitiesToImport maxActivitiesToImport,
user
}: { }: {
activitiesDto: Partial<CreateOrderDto>[]; activitiesDto: Partial<CreateOrderDto>[];
maxActivitiesToImport: number; maxActivitiesToImport: number;
user: UserWithSettings;
}) { }) {
if (activitiesDto?.length > maxActivitiesToImport) { if (activitiesDto?.length > maxActivitiesToImport) {
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`); throw new Error(`Too many activities (${maxActivitiesToImport} at most)`);
@ -575,7 +578,7 @@ export class ImportService {
for (const [ for (const [
index, index,
{ currency, dataSource, symbol } { currency, dataSource, symbol, type }
] of uniqueActivitiesDto.entries()) { ] of uniqueActivitiesDto.entries()) {
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) { if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
throw new Error( throw new Error(
@ -583,28 +586,48 @@ export class ImportService {
); );
} }
const assetProfile = ( if (
await this.dataProviderService.getAssetProfiles([ this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
{ dataSource, symbol } user.subscription.type === 'Basic'
]) ) {
)?.[symbol]; const dataProvider = this.dataProviderService.getDataProvider(
DataSource[dataSource]
if (!assetProfile?.name) {
throw new Error(
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
); );
if (dataProvider.getDataProviderInfo().isPremium) {
throw new Error(
`activities.${index}.dataSource ("${dataSource}") is not valid`
);
}
} }
if ( const assetProfile = {
assetProfile.currency !== currency && currency,
!this.exchangeRateDataService.hasCurrencyPair( ...(
currency, await this.dataProviderService.getAssetProfiles([
assetProfile.currency { dataSource, symbol }
) ])
) { )?.[symbol]
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 (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}"`
);
}
} }
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] = assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =

View File

@ -1,8 +0,0 @@
import { TimelinePeriod } from '@ghostfolio/api/app/portfolio/interfaces/timeline-period.interface';
import Big from 'big.js';
export interface TimelineInfoInterface {
maxNetPerformance: Big;
minNetPerformance: Big;
timelinePeriods: TimelinePeriod[];
}

View File

@ -1,9 +0,0 @@
import Big from 'big.js';
export interface TimelinePeriod {
date: string;
grossPerformance: Big;
investment: Big;
netPerformance: Big;
value: Big;
}

View File

@ -1,6 +0,0 @@
export type Accuracy = 'day' | 'month' | 'year';
export interface TimelineSpecification {
accuracy: Accuracy;
start: string;
}

View File

@ -68,14 +68,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime()); .mockImplementation(() => parseDate('2021-12-18').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2021-11-22')
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2021-11-22') parseDate('2021-11-22')
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
@ -135,7 +141,8 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: new Big('12.6') } { date: '2021-11-01', investment: 0 },
{ date: '2021-12-01', investment: 0 }
]); ]);
}); });
}); });

View File

@ -57,14 +57,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime()); .mockImplementation(() => parseDate('2021-12-18').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2021-11-30')
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2021-11-30') parseDate('2021-11-30')
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
@ -123,7 +129,8 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: new Big('273.2') } { date: '2021-11-01', investment: 273.2 },
{ date: '2021-12-01', investment: 0 }
]); ]);
}); });
}); });

View File

@ -81,14 +81,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2018-01-01').getTime()); .mockImplementation(() => parseDate('2018-01-01').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2015-01-01')
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2015-01-01') parseDate('2015-01-01')
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
@ -155,42 +161,43 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2015-01-01', investment: new Big('640.86') }, { date: '2015-01-01', investment: 637.0853345999999 },
{ date: '2015-02-01', investment: new Big('0') }, { date: '2015-02-01', investment: 0 },
{ date: '2015-03-01', investment: new Big('0') }, { date: '2015-03-01', investment: 0 },
{ date: '2015-04-01', investment: new Big('0') }, { date: '2015-04-01', investment: 0 },
{ date: '2015-05-01', investment: new Big('0') }, { date: '2015-05-01', investment: 0 },
{ date: '2015-06-01', investment: new Big('0') }, { date: '2015-06-01', investment: 0 },
{ date: '2015-07-01', investment: new Big('0') }, { date: '2015-07-01', investment: 0 },
{ date: '2015-08-01', investment: new Big('0') }, { date: '2015-08-01', investment: 0 },
{ date: '2015-09-01', investment: new Big('0') }, { date: '2015-09-01', investment: 0 },
{ date: '2015-10-01', investment: new Big('0') }, { date: '2015-10-01', investment: 0 },
{ date: '2015-11-01', investment: new Big('0') }, { date: '2015-11-01', investment: 0 },
{ date: '2015-12-01', investment: new Big('0') }, { date: '2015-12-01', investment: 0 },
{ date: '2016-01-01', investment: new Big('0') }, { date: '2016-01-01', investment: 0 },
{ date: '2016-02-01', investment: new Big('0') }, { date: '2016-02-01', investment: 0 },
{ date: '2016-03-01', investment: new Big('0') }, { date: '2016-03-01', investment: 0 },
{ date: '2016-04-01', investment: new Big('0') }, { date: '2016-04-01', investment: 0 },
{ date: '2016-05-01', investment: new Big('0') }, { date: '2016-05-01', investment: 0 },
{ date: '2016-06-01', investment: new Big('0') }, { date: '2016-06-01', investment: 0 },
{ date: '2016-07-01', investment: new Big('0') }, { date: '2016-07-01', investment: 0 },
{ date: '2016-08-01', investment: new Big('0') }, { date: '2016-08-01', investment: 0 },
{ date: '2016-09-01', investment: new Big('0') }, { date: '2016-09-01', investment: 0 },
{ date: '2016-10-01', investment: new Big('0') }, { date: '2016-10-01', investment: 0 },
{ date: '2016-11-01', investment: new Big('0') }, { date: '2016-11-01', investment: 0 },
{ date: '2016-12-01', investment: new Big('0') }, { date: '2016-12-01', investment: 0 },
{ date: '2017-01-01', investment: new Big('0') }, { date: '2017-01-01', investment: 0 },
{ date: '2017-02-01', investment: new Big('0') }, { date: '2017-02-01', investment: 0 },
{ date: '2017-03-01', investment: new Big('0') }, { date: '2017-03-01', investment: 0 },
{ date: '2017-04-01', investment: new Big('0') }, { date: '2017-04-01', investment: 0 },
{ date: '2017-05-01', investment: new Big('0') }, { date: '2017-05-01', investment: 0 },
{ date: '2017-06-01', investment: new Big('0') }, { date: '2017-06-01', investment: 0 },
{ date: '2017-07-01', investment: new Big('0') }, { date: '2017-07-01', investment: 0 },
{ date: '2017-08-01', investment: new Big('0') }, { date: '2017-08-01', investment: 0 },
{ date: '2017-09-01', investment: new Big('0') }, { date: '2017-09-01', investment: 0 },
{ date: '2017-10-01', investment: new Big('0') }, { date: '2017-10-01', investment: 0 },
{ date: '2017-11-01', investment: new Big('0') }, { date: '2017-11-01', investment: 0 },
{ date: '2017-12-01', investment: new Big('-14156.4') } { date: '2017-12-01', investment: -318.54266729999995 },
{ date: '2018-01-01', investment: 0 }
]); ]);
}); });
}); });

View File

@ -70,14 +70,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime()); .mockImplementation(() => parseDate('2023-07-10').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2023-01-03')
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2023-01-03') parseDate('2023-01-03')
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
@ -137,7 +143,31 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2023-01-01', investment: new Big('89.12') } { date: '2023-01-01', investment: 82.329056 },
{
date: '2023-02-01',
investment: 0
},
{
date: '2023-03-01',
investment: 0
},
{
date: '2023-04-01',
investment: 0
},
{
date: '2023-05-01',
investment: 0
},
{
date: '2023-06-01',
investment: 0
},
{
date: '2023-07-01',
investment: 0
}
]); ]);
}); });
}); });

View File

@ -45,14 +45,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime()); .mockImplementation(() => parseDate('2021-12-18').getTime());
const chartData = await portfolioCalculator.getChartData({
start: new Date()
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
new Date() new Date()
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();

View File

@ -68,14 +68,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime()); .mockImplementation(() => parseDate('2022-04-11').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2022-03-07')
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2022-03-07') parseDate('2022-03-07')
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
@ -137,8 +143,8 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2022-03-01', investment: new Big('151.6') }, { date: '2022-03-01', investment: 151.6 },
{ date: '2022-04-01', investment: new Big('-85.73') } { date: '2022-04-01', investment: -75.8 }
]); ]);
}); });
}); });

View File

@ -68,9 +68,9 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime()); .mockImplementation(() => parseDate('2022-04-11').getTime());
const chartData = await portfolioCalculator.getChartData( const chartData = await portfolioCalculator.getChartData({
parseDate('2022-03-07') start: parseDate('2022-03-07')
); });
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2022-03-07') parseDate('2022-03-07')
@ -78,13 +78,16 @@ describe('PortfolioCalculator', () => {
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
expect(chartData[0]).toEqual({ expect(chartData[0]).toEqual({
date: '2022-03-07', date: '2022-03-07',
investmentValueWithCurrencyEffect: 151.6,
netPerformance: 0, netPerformance: 0,
netPerformanceInPercentage: 0, netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceInPercentageWithCurrencyEffect: 0,
@ -97,6 +100,7 @@ describe('PortfolioCalculator', () => {
expect(chartData[chartData.length - 1]).toEqual({ expect(chartData[chartData.length - 1]).toEqual({
date: '2022-04-11', date: '2022-04-11',
investmentValueWithCurrencyEffect: 0,
netPerformance: 19.86, netPerformance: 19.86,
netPerformanceInPercentage: 13.100263852242744, netPerformanceInPercentage: 13.100263852242744,
netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744, netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744,
@ -163,8 +167,8 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2022-03-01', investment: new Big('151.6') }, { date: '2022-03-01', investment: 151.6 },
{ date: '2022-04-01', investment: new Big('-171.46') } { date: '2022-04-01', investment: -151.6 }
]); ]);
}); });
}); });

View File

@ -1,9 +1,10 @@
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { import {
DataProviderInfo, DataProviderInfo,
HistoricalDataItem,
InvestmentItem,
ResponseError, ResponseError,
SymbolMetrics, SymbolMetrics,
TimelinePosition TimelinePosition
@ -15,41 +16,19 @@ import Big from 'big.js';
import { import {
addDays, addDays,
addMilliseconds, addMilliseconds,
addMonths,
addYears,
differenceInDays, differenceInDays,
endOfDay, endOfDay,
format, format,
isAfter,
isBefore, isBefore,
isSameDay, isSameDay,
isSameMonth,
isSameYear,
max,
min,
set,
subDays subDays
} from 'date-fns'; } from 'date-fns';
import { import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash';
cloneDeep,
first,
flatten,
isNumber,
last,
sortBy,
uniq
} from 'lodash';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
import { CurrentPositions } from './interfaces/current-positions.interface'; import { CurrentPositions } from './interfaces/current-positions.interface';
import { GetValueObject } from './interfaces/get-value-object.interface';
import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface'; import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface';
import { PortfolioOrder } from './interfaces/portfolio-order.interface'; import { PortfolioOrder } from './interfaces/portfolio-order.interface';
import { TimelinePeriod } from './interfaces/timeline-period.interface';
import {
Accuracy,
TimelineSpecification
} from './interfaces/timeline-specification.interface';
import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface'; import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from './interfaces/transaction-point.interface'; import { TransactionPoint } from './interfaces/transaction-point.interface';
@ -193,7 +172,15 @@ export class PortfolioCalculator {
this.transactionPoints = transactionPoints; this.transactionPoints = transactionPoints;
} }
public async getChartData(start: Date, end = new Date(Date.now()), step = 1) { public async getChartData({
end = new Date(Date.now()),
start,
step = 1
}: {
end?: Date;
start: Date;
step?: number;
}): Promise<HistoricalDataItem[]> {
const symbols: { [symbol: string]: boolean } = {}; const symbols: { [symbol: string]: boolean } = {};
const transactionPointsBeforeEndDate = const transactionPointsBeforeEndDate =
@ -217,13 +204,15 @@ export class PortfolioCalculator {
dates.push(resetHours(end)); dates.push(resetHours(end));
} }
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) { if (transactionPointsBeforeEndDate.length > 0) {
dataGatheringItems.push({ for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
dataSource: item.dataSource, dataGatheringItems.push({
symbol: item.symbol dataSource: item.dataSource,
}); symbol: item.symbol
currencies[item.symbol] = item.currency; });
symbols[item.symbol] = true; currencies[item.symbol] = item.currency;
symbols[item.symbol] = true;
}
} }
const { dataProviderInfos, values: marketSymbols } = const { dataProviderInfos, values: marketSymbols } =
@ -262,6 +251,7 @@ export class PortfolioCalculator {
const accumulatedValuesByDate: { const accumulatedValuesByDate: {
[date: string]: { [date: string]: {
investmentValueWithCurrencyEffect: Big;
totalCurrentValue: Big; totalCurrentValue: Big;
totalCurrentValueWithCurrencyEffect: Big; totalCurrentValueWithCurrencyEffect: Big;
totalInvestmentValue: Big; totalInvestmentValue: Big;
@ -277,7 +267,8 @@ export class PortfolioCalculator {
[symbol: string]: { [symbol: string]: {
currentValues: { [date: string]: Big }; currentValues: { [date: string]: Big };
currentValuesWithCurrencyEffect: { [date: string]: Big }; currentValuesWithCurrencyEffect: { [date: string]: Big };
investmentValues: { [date: string]: Big }; investmentValuesAccumulated: { [date: string]: Big };
investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big };
investmentValuesWithCurrencyEffect: { [date: string]: Big }; investmentValuesWithCurrencyEffect: { [date: string]: Big };
netPerformanceValues: { [date: string]: Big }; netPerformanceValues: { [date: string]: Big };
netPerformanceValuesWithCurrencyEffect: { [date: string]: Big }; netPerformanceValuesWithCurrencyEffect: { [date: string]: Big };
@ -290,7 +281,8 @@ export class PortfolioCalculator {
const { const {
currentValues, currentValues,
currentValuesWithCurrencyEffect, currentValuesWithCurrencyEffect,
investmentValues, investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect, investmentValuesWithCurrencyEffect,
netPerformanceValues, netPerformanceValues,
netPerformanceValuesWithCurrencyEffect, netPerformanceValuesWithCurrencyEffect,
@ -310,7 +302,8 @@ export class PortfolioCalculator {
valuesBySymbol[symbol] = { valuesBySymbol[symbol] = {
currentValues, currentValues,
currentValuesWithCurrencyEffect, currentValuesWithCurrencyEffect,
investmentValues, investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect, investmentValuesWithCurrencyEffect,
netPerformanceValues, netPerformanceValues,
netPerformanceValuesWithCurrencyEffect, netPerformanceValuesWithCurrencyEffect,
@ -332,8 +325,13 @@ export class PortfolioCalculator {
symbolValues.currentValuesWithCurrencyEffect?.[dateString] ?? symbolValues.currentValuesWithCurrencyEffect?.[dateString] ??
new Big(0); new Big(0);
const investmentValue = const investmentValueAccumulated =
symbolValues.investmentValues?.[dateString] ?? new Big(0); symbolValues.investmentValuesAccumulated?.[dateString] ?? new Big(0);
const investmentValueAccumulatedWithCurrencyEffect =
symbolValues.investmentValuesAccumulatedWithCurrencyEffect?.[
dateString
] ?? new Big(0);
const investmentValueWithCurrencyEffect = const investmentValueWithCurrencyEffect =
symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ?? symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ??
@ -355,6 +353,10 @@ export class PortfolioCalculator {
] ?? new Big(0); ] ?? new Big(0);
accumulatedValuesByDate[dateString] = { accumulatedValuesByDate[dateString] = {
investmentValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString]
?.investmentValueWithCurrencyEffect ?? new Big(0)
).add(investmentValueWithCurrencyEffect),
totalCurrentValue: ( totalCurrentValue: (
accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0) accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
).add(currentValue), ).add(currentValue),
@ -365,11 +367,11 @@ export class PortfolioCalculator {
totalInvestmentValue: ( totalInvestmentValue: (
accumulatedValuesByDate[dateString]?.totalInvestmentValue ?? accumulatedValuesByDate[dateString]?.totalInvestmentValue ??
new Big(0) new Big(0)
).add(investmentValue), ).add(investmentValueAccumulated),
totalInvestmentValueWithCurrencyEffect: ( totalInvestmentValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString] accumulatedValuesByDate[dateString]
?.totalInvestmentValueWithCurrencyEffect ?? new Big(0) ?.totalInvestmentValueWithCurrencyEffect ?? new Big(0)
).add(investmentValueWithCurrencyEffect), ).add(investmentValueAccumulatedWithCurrencyEffect),
totalNetPerformanceValue: ( totalNetPerformanceValue: (
accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ?? accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ??
new Big(0) new Big(0)
@ -392,6 +394,7 @@ export class PortfolioCalculator {
return Object.entries(accumulatedValuesByDate).map(([date, values]) => { return Object.entries(accumulatedValuesByDate).map(([date, values]) => {
const { const {
investmentValueWithCurrencyEffect,
totalCurrentValue, totalCurrentValue,
totalCurrentValueWithCurrencyEffect, totalCurrentValueWithCurrencyEffect,
totalInvestmentValue, totalInvestmentValue,
@ -421,6 +424,8 @@ export class PortfolioCalculator {
date, date,
netPerformanceInPercentage, netPerformanceInPercentage,
netPerformanceInPercentageWithCurrencyEffect, netPerformanceInPercentageWithCurrencyEffect,
investmentValueWithCurrencyEffect:
investmentValueWithCurrencyEffect.toNumber(),
netPerformance: totalNetPerformanceValue.toNumber(), netPerformance: totalNetPerformanceValue.toNumber(),
netPerformanceWithCurrencyEffect: netPerformanceWithCurrencyEffect:
totalNetPerformanceValueWithCurrencyEffect.toNumber(), totalNetPerformanceValueWithCurrencyEffect.toNumber(),
@ -685,196 +690,27 @@ export class PortfolioCalculator {
}); });
} }
public getInvestmentsByGroup( public getInvestmentsByGroup({
groupBy: GroupBy data,
): { date: string; investment: Big }[] { groupBy
if (this.orders.length === 0) { }: {
return []; data: HistoricalDataItem[];
} groupBy: GroupBy;
}): InvestmentItem[] {
const groupedData: { [dateGroup: string]: Big } = {};
const investments: { date: string; investment: Big }[] = []; for (const { date, investmentValueWithCurrencyEffect } of data) {
let currentDate: Date; const dateGroup =
let investmentByGroup = new Big(0); groupBy === 'month' ? date.substring(0, 7) : date.substring(0, 4);
groupedData[dateGroup] = (groupedData[dateGroup] ?? new Big(0)).plus(
for (const [index, order] of this.orders.entries()) { investmentValueWithCurrencyEffect
if (
isSameYear(parseDate(order.date), currentDate) &&
(groupBy === 'year' || isSameMonth(parseDate(order.date), currentDate))
) {
// Same group: Add up investments
investmentByGroup = investmentByGroup.plus(
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
);
} else {
// New group: Store previous group and reset
if (currentDate) {
investments.push({
date: format(
set(currentDate, {
date: 1,
month: groupBy === 'year' ? 0 : currentDate.getMonth()
}),
DATE_FORMAT
),
investment: investmentByGroup
});
}
currentDate = parseDate(order.date);
investmentByGroup = order.quantity
.mul(order.unitPrice)
.mul(this.getFactor(order.type));
}
if (index === this.orders.length - 1) {
// Store current group (latest order)
investments.push({
date: format(
set(currentDate, {
date: 1,
month: groupBy === 'year' ? 0 : currentDate.getMonth()
}),
DATE_FORMAT
),
investment: investmentByGroup
});
}
}
// Fill in the missing dates with investment = 0
const startDate = parseDate(first(this.orders).date);
const endDate = parseDate(last(this.orders).date);
const allDates: string[] = [];
currentDate = startDate;
while (currentDate <= endDate) {
allDates.push(
format(
set(currentDate, {
date: 1,
month: groupBy === 'year' ? 0 : currentDate.getMonth()
}),
DATE_FORMAT
)
); );
currentDate.setMonth(currentDate.getMonth() + 1);
} }
for (const date of allDates) { return Object.keys(groupedData).map((dateGroup) => ({
const existingInvestment = investments.find((investment) => { date: groupBy === 'month' ? `${dateGroup}-01` : `${dateGroup}-01-01`,
return investment.date === date; investment: groupedData[dateGroup].toNumber()
}); }));
if (!existingInvestment) {
investments.push({ date, investment: new Big(0) });
}
}
return sortBy(investments, ({ date }) => {
return date;
});
}
public async calculateTimeline(
timelineSpecification: TimelineSpecification[],
endDate: string
): Promise<TimelineInfoInterface> {
if (timelineSpecification.length === 0) {
return {
maxNetPerformance: new Big(0),
minNetPerformance: new Big(0),
timelinePeriods: []
};
}
const startDate = timelineSpecification[0].start;
const start = parseDate(startDate);
const end = parseDate(endDate);
const timelinePeriodPromises: Promise<TimelineInfoInterface>[] = [];
let i = 0;
let j = -1;
for (
let currentDate = start;
!isAfter(currentDate, end);
currentDate = this.addToDate(
currentDate,
timelineSpecification[i].accuracy
)
) {
if (this.isNextItemActive(timelineSpecification, currentDate, i)) {
i++;
}
while (
j + 1 < this.transactionPoints.length &&
!isAfter(parseDate(this.transactionPoints[j + 1].date), currentDate)
) {
j++;
}
let periodEndDate = currentDate;
if (timelineSpecification[i].accuracy === 'day') {
let nextEndDate = end;
if (j + 1 < this.transactionPoints.length) {
nextEndDate = parseDate(this.transactionPoints[j + 1].date);
}
periodEndDate = min([
addMonths(currentDate, 3),
max([currentDate, nextEndDate])
]);
}
const timePeriodForDates = this.getTimePeriodForDate(
j,
currentDate,
endOfDay(periodEndDate)
);
currentDate = periodEndDate;
if (timePeriodForDates != null) {
timelinePeriodPromises.push(timePeriodForDates);
}
}
let minNetPerformance = new Big(0);
let maxNetPerformance = new Big(0);
const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all(
timelinePeriodPromises
);
try {
minNetPerformance = timelineInfoInterfaces
.map((timelineInfo) => timelineInfo.minNetPerformance)
.filter((performance) => performance !== null)
.reduce((minPerformance, current) => {
if (minPerformance.lt(current)) {
return minPerformance;
} else {
return current;
}
});
maxNetPerformance = timelineInfoInterfaces
.map((timelineInfo) => timelineInfo.maxNetPerformance)
.filter((performance) => performance !== null)
.reduce((maxPerformance, current) => {
if (maxPerformance.gt(current)) {
return maxPerformance;
} else {
return current;
}
});
} catch {}
const timelinePeriods = timelineInfoInterfaces.map(
(timelineInfo) => timelineInfo.timelinePeriods
);
return {
maxNetPerformance,
minNetPerformance,
timelinePeriods: flatten(timelinePeriods)
};
} }
private calculateOverallPerformance(positions: TimelinePosition[]) { private calculateOverallPerformance(positions: TimelinePosition[]) {
@ -983,123 +819,6 @@ export class PortfolioCalculator {
}; };
} }
private async getTimePeriodForDate(
j: number,
startDate: Date,
endDate: Date
): Promise<TimelineInfoInterface> {
let investment: Big = new Big(0);
let fees: Big = new Big(0);
const marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
} = {};
if (j >= 0) {
const currencies: { [name: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = [];
for (const item of this.transactionPoints[j].items) {
currencies[item.symbol] = item.currency;
dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
});
investment = investment.plus(item.investment);
fees = fees.plus(item.fee);
}
let marketSymbols: GetValueObject[] = [];
if (dataGatheringItems.length > 0) {
try {
const { values } = await this.currentRateService.getValues({
dataGatheringItems,
dateQuery: {
gte: startDate,
lt: endOfDay(endDate)
}
});
marketSymbols = values;
} catch (error) {
Logger.error(
`Failed to fetch info for date ${startDate} with exception`,
error,
'PortfolioCalculator'
);
return null;
}
}
for (const marketSymbol of marketSymbols) {
const date = format(marketSymbol.date, DATE_FORMAT);
if (!marketSymbolMap[date]) {
marketSymbolMap[date] = {};
}
if (marketSymbol.marketPrice) {
marketSymbolMap[date][marketSymbol.symbol] = new Big(
marketSymbol.marketPrice
);
}
}
}
const results: TimelinePeriod[] = [];
let maxNetPerformance: Big = null;
let minNetPerformance: Big = null;
for (
let currentDate = startDate;
isBefore(currentDate, endDate);
currentDate = addDays(currentDate, 1)
) {
let value = new Big(0);
const currentDateAsString = format(currentDate, DATE_FORMAT);
let invalid = false;
if (j >= 0) {
for (const item of this.transactionPoints[j].items) {
if (
!marketSymbolMap[currentDateAsString]?.hasOwnProperty(item.symbol)
) {
invalid = true;
break;
}
value = value.plus(
item.quantity.mul(marketSymbolMap[currentDateAsString][item.symbol])
);
}
}
if (!invalid) {
const grossPerformance = value.minus(investment);
const netPerformance = grossPerformance.minus(fees);
if (
minNetPerformance === null ||
minNetPerformance.gt(netPerformance)
) {
minNetPerformance = netPerformance;
}
if (
maxNetPerformance === null ||
maxNetPerformance.lt(netPerformance)
) {
maxNetPerformance = netPerformance;
}
const result = {
grossPerformance,
investment,
netPerformance,
value,
date: currentDateAsString
};
results.push(result);
}
}
return {
maxNetPerformance,
minNetPerformance,
timelinePeriods: results
};
}
private getFactor(type: TypeOfOrder) { private getFactor(type: TypeOfOrder) {
let factor: number; let factor: number;
@ -1118,17 +837,6 @@ export class PortfolioCalculator {
return factor; return factor;
} }
private addToDate(date: Date, accuracy: Accuracy): Date {
switch (accuracy) {
case 'day':
return addDays(date, 1);
case 'month':
return addMonths(date, 1);
case 'year':
return addYears(date, 1);
}
}
private getSymbolMetrics({ private getSymbolMetrics({
end, end,
exchangeRates, exchangeRates,
@ -1165,7 +873,10 @@ export class PortfolioCalculator {
let initialValueWithCurrencyEffect: Big; let initialValueWithCurrencyEffect: Big;
let investmentAtStartDate: Big; let investmentAtStartDate: Big;
let investmentAtStartDateWithCurrencyEffect: Big; let investmentAtStartDateWithCurrencyEffect: Big;
const investmentValues: { [date: string]: Big } = {}; const investmentValuesAccumulated: { [date: string]: Big } = {};
const investmentValuesAccumulatedWithCurrencyEffect: {
[date: string]: Big;
} = {};
const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {}; const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {};
let lastAveragePrice = new Big(0); let lastAveragePrice = new Big(0);
let lastAveragePriceWithCurrencyEffect = new Big(0); let lastAveragePriceWithCurrencyEffect = new Big(0);
@ -1207,7 +918,8 @@ export class PortfolioCalculator {
hasErrors: false, hasErrors: false,
initialValue: new Big(0), initialValue: new Big(0),
initialValueWithCurrencyEffect: new Big(0), initialValueWithCurrencyEffect: new Big(0),
investmentValues: {}, investmentValuesAccumulated: {},
investmentValuesAccumulatedWithCurrencyEffect: {},
investmentValuesWithCurrencyEffect: {}, investmentValuesWithCurrencyEffect: {},
netPerformance: new Big(0), netPerformance: new Big(0),
netPerformancePercentage: new Big(0), netPerformancePercentage: new Big(0),
@ -1246,7 +958,8 @@ export class PortfolioCalculator {
hasErrors: true, hasErrors: true,
initialValue: new Big(0), initialValue: new Big(0),
initialValueWithCurrencyEffect: new Big(0), initialValueWithCurrencyEffect: new Big(0),
investmentValues: {}, investmentValuesAccumulated: {},
investmentValuesAccumulatedWithCurrencyEffect: {},
investmentValuesWithCurrencyEffect: {}, investmentValuesWithCurrencyEffect: {},
netPerformance: new Big(0), netPerformance: new Big(0),
netPerformancePercentage: new Big(0), netPerformancePercentage: new Big(0),
@ -1639,11 +1352,15 @@ export class PortfolioCalculator {
feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect)
); );
investmentValues[order.date] = totalInvestment; investmentValuesAccumulated[order.date] = totalInvestment;
investmentValuesWithCurrencyEffect[order.date] = investmentValuesAccumulatedWithCurrencyEffect[order.date] =
totalInvestmentWithCurrencyEffect; totalInvestmentWithCurrencyEffect;
investmentValuesWithCurrencyEffect[order.date] = (
investmentValuesWithCurrencyEffect[order.date] ?? new Big(0)
).add(transactionInvestmentWithCurrencyEffect);
timeWeightedInvestmentValues[order.date] = timeWeightedInvestmentValues[order.date] =
totalInvestmentDays > 0 totalInvestmentDays > 0
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) ? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
@ -1801,7 +1518,8 @@ export class PortfolioCalculator {
grossPerformancePercentageWithCurrencyEffect, grossPerformancePercentageWithCurrencyEffect,
initialValue, initialValue,
initialValueWithCurrencyEffect, initialValueWithCurrencyEffect,
investmentValues, investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect, investmentValuesWithCurrencyEffect,
netPerformancePercentage, netPerformancePercentage,
netPerformancePercentageWithCurrencyEffect, netPerformancePercentageWithCurrencyEffect,
@ -1823,15 +1541,4 @@ export class PortfolioCalculator {
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect
}; };
} }
private isNextItemActive(
timelineSpecification: TimelineSpecification[],
currentDate: Date,
i: number
) {
return (
i + 1 < timelineSpecification.length &&
!isBefore(currentDate, parseDate(timelineSpecification[i + 1].start))
);
}
} }

View File

@ -74,6 +74,11 @@ export class PortfolioController {
): Promise<PortfolioDetails & { hasError: boolean }> { ): Promise<PortfolioDetails & { hasError: boolean }> {
let hasDetails = true; let hasDetails = true;
let hasError = false; let hasError = false;
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = this.request.user.subscription.type === 'Premium'; hasDetails = this.request.user.subscription.type === 'Premium';
@ -108,7 +113,7 @@ export class PortfolioController {
let portfolioSummary = summary; let portfolioSummary = summary;
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const totalInvestment = Object.values(holdings) const totalInvestment = Object.values(holdings)
@ -148,7 +153,7 @@ export class PortfolioController {
if ( if (
hasDetails === false || hasDetails === false ||
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
portfolioSummary = nullifyValuesInObject(summary, [ portfolioSummary = nullifyValuesInObject(summary, [
@ -164,6 +169,7 @@ export class PortfolioController {
'excludedAccountsAndActivities', 'excludedAccountsAndActivities',
'fees', 'fees',
'fireWealth', 'fireWealth',
'interest',
'items', 'items',
'liabilities', 'liabilities',
'netWorth', 'netWorth',
@ -216,6 +222,12 @@ export class PortfolioController {
@Query('range') dateRange: DateRange = 'max', @Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string
): Promise<PortfolioDividends> { ): Promise<PortfolioDividends> {
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
@ -230,7 +242,7 @@ export class PortfolioController {
}); });
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const maxDividend = dividends.reduce( const maxDividend = dividends.reduce(
@ -266,6 +278,12 @@ export class PortfolioController {
@Query('range') dateRange: DateRange = 'max', @Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string
): Promise<PortfolioInvestments> { ): Promise<PortfolioInvestments> {
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
@ -281,7 +299,7 @@ export class PortfolioController {
}); });
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const maxInvestment = investments.reduce( const maxInvestment = investments.reduce(
@ -329,6 +347,12 @@ export class PortfolioController {
@Query('tags') filterByTags?: string, @Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccounts = false @Query('withExcludedAccounts') withExcludedAccounts = false
): Promise<PortfolioPerformanceResponse> { ): Promise<PortfolioPerformanceResponse> {
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
@ -344,7 +368,7 @@ export class PortfolioController {
}); });
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.request.user.Settings.settings.viewMode === 'ZEN' || this.request.user.Settings.settings.viewMode === 'ZEN' ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {

View File

@ -79,7 +79,7 @@ import {
subDays, subDays,
subYears subYears
} from 'date-fns'; } from 'date-fns';
import { isEmpty, last, sortBy, uniq, uniqBy } from 'lodash'; import { isEmpty, last, uniq, uniqBy } from 'lodash';
import { import {
HistoricalDataContainer, HistoricalDataContainer,
@ -293,77 +293,32 @@ export class PortfolioService {
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
const { items } = await this.getChart({
dateRange,
impersonationId,
portfolioOrders,
transactionPoints,
userId,
userCurrency: this.request.user.Settings.settings.baseCurrency,
withDataDecimation: false
});
let investments: InvestmentItem[]; let investments: InvestmentItem[];
if (groupBy) { if (groupBy) {
investments = portfolioCalculator investments = portfolioCalculator.getInvestmentsByGroup({
.getInvestmentsByGroup(groupBy) groupBy,
.map((item) => { data: items
return {
date: item.date,
investment: item.investment.toNumber()
};
});
// Add investment of current group
const dateOfCurrentGroup = format(
set(new Date(), {
date: 1,
month: groupBy === 'year' ? 0 : new Date().getMonth()
}),
DATE_FORMAT
);
const investmentOfCurrentGroup = investments.filter(({ date }) => {
return date === dateOfCurrentGroup;
}); });
if (investmentOfCurrentGroup.length <= 0) {
investments.push({
date: dateOfCurrentGroup,
investment: 0
});
}
} else { } else {
investments = portfolioCalculator investments = items.map(({ date, investmentValueWithCurrencyEffect }) => {
.getInvestments() return {
.map(({ date, investment }) => { date,
return { investment: investmentValueWithCurrencyEffect
date, };
investment: investment.toNumber()
};
});
// Add investment of today
const investmentOfToday = investments.filter(({ date }) => {
return date === format(new Date(), DATE_FORMAT);
}); });
if (investmentOfToday.length <= 0) {
const pastInvestments = investments.filter(({ date }) => {
return isBefore(parseDate(date), new Date());
});
const lastInvestment = pastInvestments[pastInvestments.length - 1];
investments.push({
date: format(new Date(), DATE_FORMAT),
investment: lastInvestment?.investment ?? 0
});
}
} }
investments = sortBy(investments, ({ date }) => {
return date;
});
const startDate = this.getStartDate(
dateRange,
parseDate(investments[0]?.date)
);
investments = investments.filter(({ date }) => {
return !isBefore(parseDate(date), startDate);
});
let streaks: PortfolioInvestments['streaks']; let streaks: PortfolioInvestments['streaks'];
if (savingsRate) { if (savingsRate) {
@ -1448,7 +1403,8 @@ export class PortfolioService {
portfolioOrders, portfolioOrders,
transactionPoints, transactionPoints,
userCurrency, userCurrency,
userId userId,
withDataDecimation = true
}: { }: {
dateRange?: DateRange; dateRange?: DateRange;
impersonationId: string; impersonationId: string;
@ -1456,6 +1412,7 @@ export class PortfolioService {
transactionPoints: TransactionPoint[]; transactionPoints: TransactionPoint[];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
withDataDecimation?: boolean;
}): Promise<HistoricalDataContainer> { }): Promise<HistoricalDataContainer> {
if (transactionPoints.length === 0) { if (transactionPoints.length === 0) {
return { return {
@ -1481,16 +1438,18 @@ export class PortfolioService {
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(dateRange, portfolioStart); const startDate = this.getStartDate(dateRange, portfolioStart);
const daysInMarket = differenceInDays(new Date(), startDate); let step = 1;
const step = Math.round(
daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)
);
const items = await portfolioCalculator.getChartData( if (withDataDecimation) {
startDate, const daysInMarket = differenceInDays(new Date(), startDate);
endDate, step = Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS));
step }
);
const items = await portfolioCalculator.getChartData({
step,
end: endDate,
start: startDate
});
return { return {
items, items,

View File

@ -1,9 +1,11 @@
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
export interface LookupItem { export interface LookupItem {
assetClass: AssetClass; assetClass: AssetClass;
assetSubClass: AssetSubClass; assetSubClass: AssetSubClass;
currency: string; currency: string;
dataProviderInfo: DataProviderInfo;
dataSource: DataSource; dataSource: DataSource;
name: string; name: string;
symbol: string; symbol: string;

View File

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

View File

@ -105,6 +105,24 @@ export class UserService {
return usersWithAdminRole.length > 0; 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) { public isRestrictedView(aUser: UserWithSettings) {
return aUser.Settings.settings.isRestrictedView ?? false; return aUser.Settings.settings.isRestrictedView ?? false;
} }
@ -113,6 +131,7 @@ export class UserService {
userWhereUniqueInput: Prisma.UserWhereUniqueInput userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> { ): Promise<UserWithSettings | null> {
const { const {
Access,
accessToken, accessToken,
Account, Account,
Analytics, Analytics,
@ -127,6 +146,7 @@ export class UserService {
updatedAt updatedAt
} = await this.prismaService.user.findUnique({ } = await this.prismaService.user.findUnique({
include: { include: {
Access: true,
Account: { Account: {
include: { Platform: true } include: { Platform: true }
}, },
@ -138,6 +158,7 @@ export class UserService {
}); });
const user: UserWithSettings = { const user: UserWithSettings = {
Access,
accessToken, accessToken,
Account, Account,
authChallenge, authChallenge,
@ -198,18 +219,18 @@ export class UserService {
new Date(), new Date(),
user.createdAt user.createdAt
); );
let frequency = 15; let frequency = 10;
if (daysSinceRegistration > 365) { if (daysSinceRegistration > 365) {
frequency = 2; frequency = 2;
} else if (daysSinceRegistration > 180) { } else if (daysSinceRegistration > 180) {
frequency = 3; frequency = 3;
} else if (daysSinceRegistration > 60) { } else if (daysSinceRegistration > 60) {
frequency = 5; frequency = 4;
} else if (daysSinceRegistration > 30) { } else if (daysSinceRegistration > 30) {
frequency = 8; frequency = 6;
} else if (daysSinceRegistration > 15) { } else if (daysSinceRegistration > 15) {
frequency = 12; frequency = 8;
} }
if (Analytics?.activityCount % frequency === 1) { if (Analytics?.activityCount % frequency === 1) {

File diff suppressed because it is too large Load Diff

View File

@ -114,6 +114,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-exirio</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-exirio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-fina</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-finary</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -230,6 +234,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-vyzer</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </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> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthica</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -452,6 +460,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-exirio</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-exirio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-fina</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-finary</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -568,6 +580,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-vyzer</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </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> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthica</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -810,6 +826,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-exirio</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-exirio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-fina</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-finary</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -926,6 +946,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-vyzer</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </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> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthica</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -1014,6 +1038,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-exirio</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-exirio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-fina</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-finary</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -1130,6 +1158,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-vyzer</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </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> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthica</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>

View File

@ -1,6 +1,7 @@
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { UserWithSettings } from '@ghostfolio/common/types';
import { import {
CallHandler, CallHandler,
ExecutionContext, ExecutionContext,
@ -22,13 +23,20 @@ export class RedactValuesInResponseInterceptor<T>
): Observable<any> { ): Observable<any> {
return next.handle().pipe( return next.handle().pipe(
map((data: any) => { map((data: any) => {
const request = context.switchToHttp().getRequest(); const { headers, user }: { headers: Headers; user: UserWithSettings } =
const hasImpersonationId = context.switchToHttp().getRequest();
!!request.headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
const impersonationId =
headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
const hasReadRestrictedPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user
});
if ( if (
hasImpersonationId || hasReadRestrictedPermission ||
this.userService.isRestrictedView(request.user) this.userService.isRestrictedView(user)
) { ) {
data = redactAttributes({ data = redactAttributes({
object: data, object: data,

View File

@ -24,7 +24,7 @@ export class ApiService {
const searchQuery = filterBySearchQuery?.toLowerCase(); const searchQuery = filterBySearchQuery?.toLowerCase();
const tagIds = filterByTags?.split(',') ?? []; const tagIds = filterByTags?.split(',') ?? [];
return [ const filters = [
...accountIds.map((accountId) => { ...accountIds.map((accountId) => {
return <Filter>{ return <Filter>{
id: accountId, id: accountId,
@ -43,10 +43,6 @@ export class ApiService {
type: 'ASSET_SUB_CLASS' type: 'ASSET_SUB_CLASS'
}; };
}), }),
{
id: searchQuery,
type: 'SEARCH_QUERY'
},
...tagIds.map((tagId) => { ...tagIds.map((tagId) => {
return <Filter>{ return <Filter>{
id: tagId, id: tagId,
@ -54,5 +50,14 @@ export class ApiService {
}; };
}) })
]; ];
if (searchQuery) {
filters.push({
id: searchQuery,
type: 'SEARCH_QUERY'
});
}
return filters;
} }
} }

View File

@ -12,6 +12,7 @@ import {
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import * as Alphavantage from 'alphavantage'; import * as Alphavantage from 'alphavantage';
@ -44,6 +45,12 @@ export class AlphaVantageService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
};
}
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
return {}; return {};
} }
@ -118,6 +125,7 @@ export class AlphaVantageService implements DataProviderInterface {
assetClass: undefined, assetClass: undefined,
assetSubClass: undefined, assetSubClass: undefined,
currency: bestMatch['8. currency'], currency: bestMatch['8. currency'],
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName(), dataSource: this.getName(),
name: bestMatch['2. name'], name: bestMatch['2. name'],
symbol: bestMatch['1. symbol'] symbol: bestMatch['1. symbol']

View File

@ -80,7 +80,7 @@ export class CoinGeckoService implements DataProviderInterface {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${this.configurationService.get( message = `RequestError: The operation to get the asset profile for ${aSymbol} was aborted because the request to the data provider took more than ${this.configurationService.get(
'REQUEST_TIMEOUT' 'REQUEST_TIMEOUT'
)}ms`; )}ms`;
} }
@ -91,6 +91,14 @@ export class CoinGeckoService implements DataProviderInterface {
return response; return response;
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false,
name: 'CoinGecko',
url: 'https://coingecko.com'
};
}
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
return {}; return {};
} }
@ -195,7 +203,7 @@ export class CoinGeckoService implements DataProviderInterface {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${this.configurationService.get( message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${this.configurationService.get(
'REQUEST_TIMEOUT' 'REQUEST_TIMEOUT'
)}ms`; )}ms`;
} }
@ -235,6 +243,7 @@ export class CoinGeckoService implements DataProviderInterface {
assetClass: AssetClass.CASH, assetClass: AssetClass.CASH,
assetSubClass: AssetSubClass.CRYPTOCURRENCY, assetSubClass: AssetSubClass.CRYPTOCURRENCY,
currency: DEFAULT_CURRENCY, currency: DEFAULT_CURRENCY,
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName() dataSource: this.getName()
}; };
}); });
@ -242,7 +251,7 @@ export class CoinGeckoService implements DataProviderInterface {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${this.configurationService.get( message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${this.configurationService.get(
'REQUEST_TIMEOUT' 'REQUEST_TIMEOUT'
)}ms`; )}ms`;
} }
@ -252,11 +261,4 @@ export class CoinGeckoService implements DataProviderInterface {
return { items }; return { items };
} }
private getDataProviderInfo(): DataProviderInfo {
return {
name: 'CoinGecko',
url: 'https://coingecko.com'
};
}
} }

View File

@ -62,9 +62,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
}, this.configurationService.get('REQUEST_TIMEOUT')); }, this.configurationService.get('REQUEST_TIMEOUT'));
return got( return got(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split( `${TrackinsightDataEnhancerService.baseUrl}/funds/${
'.' symbol.split('.')?.[0]
)?.[0]}.json`, }.json`,
{ {
// @ts-ignore // @ts-ignore
signal: abortController.signal signal: abortController.signal
@ -104,9 +104,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
}, this.configurationService.get('REQUEST_TIMEOUT')); }, this.configurationService.get('REQUEST_TIMEOUT'));
return got( return got(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split( `${TrackinsightDataEnhancerService.baseUrl}/holdings/${
'.' symbol.split('.')?.[0]
)?.[0]}.json`, }.json`,
{ {
// @ts-ignore // @ts-ignore
signal: abortController.signal signal: abortController.signal

View File

@ -1,7 +1,11 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; 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 { isCurrency } from '@ghostfolio/common/helper';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import {
@ -137,18 +141,11 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
if (name) { if (name) {
name = name.replace('&amp;', '&'); name = name.replace('&amp;', '&');
name = name.replace('Amundi Index Solutions - ', ''); for (const part of REPLACE_NAME_PARTS) {
name = name.replace('iShares ETF (CH) - ', ''); name = name.replace(part, '');
name = name.replace('iShares III Public Limited Company - ', ''); }
name = name.replace('iShares V PLC - ', '');
name = name.replace('iShares VI Public Limited Company - ', ''); name = name.trim();
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 - ', '');
} }
if (quoteType === 'FUTURE') { if (quoteType === 'FUTURE') {

View File

@ -107,6 +107,31 @@ export class DataProviderService {
return response; return response;
} }
public getDataProvider(providerName: DataSource) {
for (const dataProviderInterface of this.dataProviderInterfaces) {
if (this.dataProviderMapping[dataProviderInterface.getName()]) {
const mappedDataProviderInterface = this.dataProviderInterfaces.find(
(currentDataProviderInterface) => {
return (
currentDataProviderInterface.getName() ===
this.dataProviderMapping[dataProviderInterface.getName()]
);
}
);
if (mappedDataProviderInterface) {
return mappedDataProviderInterface;
}
}
if (dataProviderInterface.getName() === providerName) {
return dataProviderInterface;
}
}
throw new Error('No data provider has been found.');
}
public getDataSourceForExchangeRates(): DataSource { public getDataSourceForExchangeRates(): DataSource {
return DataSource[ return DataSource[
this.configurationService.get('DATA_SOURCE_EXCHANGE_RATES') this.configurationService.get('DATA_SOURCE_EXCHANGE_RATES')
@ -520,20 +545,15 @@ export class DataProviderService {
return { items: lookupItems }; return { items: lookupItems };
} }
let dataSources = this.configurationService.get('DATA_SOURCES'); let dataProviderServices = this.configurationService
.get('DATA_SOURCES')
if ( .map((dataSource) => {
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && return this.getDataProvider(DataSource[dataSource]);
user.subscription.type === 'Basic'
) {
dataSources = dataSources.filter((dataSource) => {
return !this.isPremiumDataSource(DataSource[dataSource]);
}); });
}
for (const dataSource of dataSources) { for (const dataProviderService of dataProviderServices) {
promises.push( promises.push(
this.getDataProvider(DataSource[dataSource]).search({ dataProviderService.search({
includeIndices, includeIndices,
query query
}) })
@ -555,6 +575,16 @@ export class DataProviderService {
}) })
.sort(({ name: name1 }, { name: name2 }) => { .sort(({ name: name1 }, { name: name2 }) => {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase()); return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
})
.map((lookupItem) => {
if (
!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') ||
user.subscription.type === 'Premium'
) {
lookupItem.dataProviderInfo.isPremium = false;
}
return lookupItem;
}); });
return { return {
@ -562,31 +592,6 @@ export class DataProviderService {
}; };
} }
private getDataProvider(providerName: DataSource) {
for (const dataProviderInterface of this.dataProviderInterfaces) {
if (this.dataProviderMapping[dataProviderInterface.getName()]) {
const mappedDataProviderInterface = this.dataProviderInterfaces.find(
(currentDataProviderInterface) => {
return (
currentDataProviderInterface.getName() ===
this.dataProviderMapping[dataProviderInterface.getName()]
);
}
);
if (mappedDataProviderInterface) {
return mappedDataProviderInterface;
}
}
if (dataProviderInterface.getName() === providerName) {
return dataProviderInterface;
}
}
throw new Error('No data provider has been found.');
}
private hasCurrency({ private hasCurrency({
currency, currency,
dataGatheringItems dataGatheringItems
@ -602,14 +607,6 @@ export class DataProviderService {
}); });
} }
private isPremiumDataSource(aDataSource: DataSource) {
const premiumDataSources: DataSource[] = [
DataSource.EOD_HISTORICAL_DATA,
DataSource.FINANCIAL_MODELING_PREP
];
return premiumDataSources.includes(aDataSource);
}
private transformHistoricalData({ private transformHistoricalData({
allData, allData,
currency, currency,

View File

@ -11,8 +11,12 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } 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 { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import {
AssetClass, AssetClass,
@ -22,6 +26,7 @@ import {
} from '@prisma/client'; } from '@prisma/client';
import { addDays, format, isSameDay, isToday } from 'date-fns'; import { addDays, format, isSameDay, isToday } from 'date-fns';
import got from 'got'; import got from 'got';
import { isNumber } from 'lodash';
@Injectable() @Injectable()
export class EodHistoricalDataService implements DataProviderInterface { export class EodHistoricalDataService implements DataProviderInterface {
@ -54,6 +59,12 @@ export class EodHistoricalDataService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: true
};
}
public async getDividends({ public async getDividends({
from, from,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
@ -144,10 +155,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
).json<any>(); ).json<any>();
return response.reduce( return response.reduce(
(result, historicalItem, index, array) => { (result, { close, date }, index, array) => {
result[this.convertFromEodSymbol(symbol)][historicalItem.date] = { if (isNumber(close)) {
marketPrice: historicalItem.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; return result;
}, },
@ -232,14 +250,23 @@ export class EodHistoricalDataService implements DataProviderInterface {
return lookupItem.symbol === code; return lookupItem.symbol === code;
})?.currency; })?.currency;
result[this.convertFromEodSymbol(code)] = { if (isNumber(close)) {
currency: result[this.convertFromEodSymbol(code)] = {
currency ?? currency:
this.convertFromEodSymbol(code)?.replace(DEFAULT_CURRENCY, ''), currency ??
dataSource: DataSource.EOD_HISTORICAL_DATA, this.convertFromEodSymbol(code)?.replace(DEFAULT_CURRENCY, ''),
marketPrice: close, dataSource: this.getName(),
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed' 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; return result;
}, },
@ -251,7 +278,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${this.configurationService.get( message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${this.configurationService.get(
'REQUEST_TIMEOUT' 'REQUEST_TIMEOUT'
)}ms`; )}ms`;
} }
@ -292,7 +319,8 @@ export class EodHistoricalDataService implements DataProviderInterface {
dataSource, dataSource,
name, name,
symbol, symbol,
currency: this.convertCurrency(currency) currency: this.convertCurrency(currency),
dataProviderInfo: this.getDataProviderInfo()
}; };
} }
) )
@ -345,6 +373,18 @@ export class EodHistoricalDataService implements DataProviderInterface {
return aSymbol; 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< private async getSearchResult(aQuery: string): Promise<
(LookupItem & { (LookupItem & {
assetClass: AssetClass; assetClass: AssetClass;
@ -380,9 +420,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
assetClass, assetClass,
assetSubClass, assetSubClass,
isin, isin,
name,
currency: this.convertCurrency(Currency), currency: this.convertCurrency(Currency),
dataSource: this.getName(), dataSource: this.getName(),
name: this.formatName({ name }),
symbol: `${Code}.${Exchange}` symbol: `${Code}.${Exchange}`
}; };
} }
@ -391,7 +431,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${this.configurationService.get( message = `RequestError: The operation to search for ${aQuery} was aborted because the request to the data provider took more than ${this.configurationService.get(
'REQUEST_TIMEOUT' 'REQUEST_TIMEOUT'
)}ms`; )}ms`;
} }

View File

@ -45,6 +45,14 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: true,
name: 'Financial Modeling Prep',
url: 'https://financialmodelingprep.com/developer/docs'
};
}
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
return {}; return {};
} }
@ -143,7 +151,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${this.configurationService.get( message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${this.configurationService.get(
'REQUEST_TIMEOUT' 'REQUEST_TIMEOUT'
)}ms`; )}ms`;
} }
@ -192,7 +200,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
let message = error; let message = error;
if (error?.code === 'ABORT_ERR') { if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${this.configurationService.get( message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${this.configurationService.get(
'REQUEST_TIMEOUT' 'REQUEST_TIMEOUT'
)}ms`; )}ms`;
} }
@ -202,11 +210,4 @@ export class FinancialModelingPrepService implements DataProviderInterface {
return { items }; return { items };
} }
private getDataProviderInfo(): DataProviderInfo {
return {
name: 'Financial Modeling Prep',
url: 'https://financialmodelingprep.com/developer/docs'
};
}
} }

View File

@ -14,6 +14,7 @@ import {
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
@ -40,6 +41,12 @@ export class GoogleSheetsService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
};
}
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
return {}; return {};
} }
@ -177,7 +184,11 @@ export class GoogleSheetsService implements DataProviderInterface {
} }
}); });
return { items }; return {
items: items.map((item) => {
return { ...item, dataProviderInfo: this.getDataProviderInfo() };
})
};
} }
private async getSheet({ private async getSheet({

View File

@ -3,6 +3,7 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
@ -11,6 +12,8 @@ export interface DataProviderInterface {
getAssetProfile(aSymbol: string): Promise<Partial<SymbolProfile>>; getAssetProfile(aSymbol: string): Promise<Partial<SymbolProfile>>;
getDataProviderInfo(): DataProviderInfo;
getDividends({ from, granularity, symbol, to }: GetDividendsParams): Promise<{ getDividends({ from, granularity, symbol, to }: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse; [date: string]: IDataProviderHistoricalResponse;
}>; }>;

View File

@ -18,7 +18,10 @@ import {
extractNumberFromString, extractNumberFromString,
getYesterday getYesterday
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { ScraperConfiguration } from '@ghostfolio/common/interfaces'; import {
DataProviderInfo,
ScraperConfiguration
} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
@ -42,10 +45,27 @@ export class ManualService implements DataProviderInterface {
public async getAssetProfile( public async getAssetProfile(
aSymbol: string aSymbol: string
): Promise<Partial<SymbolProfile>> { ): Promise<Partial<SymbolProfile>> {
return { const assetProfile: Partial<SymbolProfile> = {
dataSource: this.getName(), dataSource: this.getName(),
symbol: aSymbol 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 getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
};
} }
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
@ -203,7 +223,11 @@ export class ManualService implements DataProviderInterface {
return !isUUID(symbol); return !isUUID(symbol);
}); });
return { items }; return {
items: items.map((item) => {
return { ...item, dataProviderInfo: this.getDataProviderInfo() };
})
};
} }
public async test(scraperConfiguration: ScraperConfiguration) { public async test(scraperConfiguration: ScraperConfiguration) {

View File

@ -13,6 +13,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config'; import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
@ -37,6 +38,12 @@ export class RapidApiService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
};
}
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
return {}; return {};
} }

View File

@ -14,6 +14,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { addDays, format, isSameDay } from 'date-fns'; import { addDays, format, isSameDay } from 'date-fns';
@ -47,6 +48,12 @@ export class YahooFinanceService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
};
}
public async getDividends({ public async getDividends({
from, from,
granularity = 'day', granularity = 'day',
@ -283,6 +290,7 @@ export class YahooFinanceService implements DataProviderInterface {
assetSubClass, assetSubClass,
symbol, symbol,
currency: marketDataItem.currency, currency: marketDataItem.currency,
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName(), dataSource: this.getName(),
name: this.yahooFinanceDataEnhancerService.formatName({ name: this.yahooFinanceDataEnhancerService.formatName({
longName: quote.longname, longName: quote.longname,

View File

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

View File

@ -17,8 +17,13 @@
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th> <th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell> <td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
<ion-icon class="mr-1" name="lock-closed-outline" /> @if (element.permissions.includes('READ')) {
<ng-container i18n>Restricted View</ng-container> <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> </div>
</td> </td>
</ng-container> </ng-container>

View File

@ -10,7 +10,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Sort, SortDirection } from '@angular/material/sort'; import { Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { downloadAsFile } from '@ghostfolio/common/helper'; import { downloadAsFile } from '@ghostfolio/common/helper';
import { import {
@ -43,7 +42,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
public currency: string; public currency: string;
public dataSource: MatTableDataSource<OrderWithAccount>; public dataSource: MatTableDataSource<OrderWithAccount>;
public equity: number; public equity: number;
public hasImpersonationId: boolean;
public hasPermissionToDeleteAccountBalance: boolean; public hasPermissionToDeleteAccountBalance: boolean;
public historicalDataItems: HistoricalDataItem[]; public historicalDataItems: HistoricalDataItem[];
public holdings: PortfolioPosition[]; public holdings: PortfolioPosition[];
@ -65,7 +63,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
@Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams, @Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams,
private dataService: DataService, private dataService: DataService,
public dialogRef: MatDialogRef<AccountDetailDialog>, public dialogRef: MatDialogRef<AccountDetailDialog>,
private impersonationStorageService: ImpersonationStorageService,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
@ -136,13 +133,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.fetchAccountBalances(); this.fetchAccountBalances();
this.fetchActivities(); this.fetchActivities();
this.fetchPortfolioPerformance(); this.fetchPortfolioPerformance();
@ -165,20 +155,12 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
} }
public onExport() { public onExport() {
let activityIds = []; let activityIds = this.dataSource.data.map(({ id }) => {
return id;
if (this.user?.settings?.isExperimentalFeatures === true) { });
activityIds = this.dataSource.data.map(({ id }) => {
return id;
});
} else {
activityIds = this.activities.map(({ id }) => {
return id;
});
}
this.dataService this.dataService
.fetchExport(activityIds) .fetchExport({ activityIds })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((data) => {
downloadAsFile({ downloadAsFile({
@ -215,36 +197,21 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
private fetchActivities() { private fetchActivities() {
this.isLoadingActivities = true; this.isLoadingActivities = true;
if (this.user?.settings?.isExperimentalFeatures === true) { this.dataService
this.dataService .fetchActivities({
.fetchActivities({ filters: [{ id: this.data.accountId, type: 'ACCOUNT' }],
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }], sortColumn: this.sortColumn,
sortColumn: this.sortColumn, sortDirection: this.sortDirection
sortDirection: this.sortDirection })
}) .pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ activities, count }) => {
.subscribe(({ activities, count }) => { this.dataSource = new MatTableDataSource(activities);
this.dataSource = new MatTableDataSource(activities); this.totalItems = count;
this.totalItems = count;
this.isLoadingActivities = false; this.isLoadingActivities = false;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} else {
this.dataService
.fetchActivities({
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }]
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.activities = activities;
this.isLoadingActivities = false;
this.changeDetectorRef.markForCheck();
});
}
} }
private fetchPortfolioPerformance() { private fetchPortfolioPerformance() {
@ -268,7 +235,8 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
return { return {
date, date,
value: value:
this.hasImpersonationId || this.user.settings.isRestrictedView this.data.hasImpersonationId ||
this.user.settings.isRestrictedView
? netWorthInPercentage ? netWorthInPercentage
: netWorth : netWorth
}; };

View File

@ -4,7 +4,7 @@
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[title]="name" [title]="name"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-header> />
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<div class="container p-0"> <div class="container p-0">
@ -16,7 +16,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="valueInBaseCurrency" [value]="valueInBaseCurrency"
></gf-value> />
</div> </div>
</div> </div>
@ -25,10 +25,10 @@
class="h-100" class="h-100"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[historicalDataItems]="historicalDataItems" [historicalDataItems]="historicalDataItems"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="data.hasImpersonationId || user.settings.isRestrictedView"
[isLoading]="isLoadingChart" [isLoading]="isLoadingChart"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
></gf-investment-chart> />
</div> </div>
<div class="mb-3 row"> <div class="mb-3 row">
@ -79,20 +79,19 @@
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[holdings]="holdings" [holdings]="holdings"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
></gf-holdings-table> />
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<ion-icon name="swap-vertical-outline" /> <ion-icon name="swap-vertical-outline" />
<div class="d-none d-sm-block ml-2" i18n>Activities</div> <div class="d-none d-sm-block ml-2" i18n>Activities</div>
</ng-template> </ng-template>
<gf-activities-table-lazy <gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures === true"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false" [hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!hasImpersonationId && !user.settings.isRestrictedView" [hasPermissionToExportActivities]="!data.hasImpersonationId && !user.settings.isRestrictedView"
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
@ -102,20 +101,7 @@
[totalItems]="totalItems" [totalItems]="totalItems"
(export)="onExport()" (export)="onExport()"
(sortChanged)="onSortChanged($event)" (sortChanged)="onSortChanged($event)"
></gf-activities-table-lazy> />
<gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="true"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale"
[showActions]="false"
(export)="onExport()"
></gf-activities-table>
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
@ -126,9 +112,9 @@
[accountBalances]="accountBalances" [accountBalances]="accountBalances"
[accountId]="data.accountId" [accountId]="data.accountId"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView" [showActions]="!data.hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView"
(accountBalanceDeleted)="onDeleteAccountBalance($event)" (accountBalanceDeleted)="onDeleteAccountBalance($event)"
></gf-account-balances> />
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>
@ -138,4 +124,4 @@
mat-dialog-actions mat-dialog-actions
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-footer> />

View File

@ -8,7 +8,6 @@ import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-heade
import { GfHoldingsTableModule } from '@ghostfolio/ui/holdings-table/holdings-table.module'; import { GfHoldingsTableModule } from '@ghostfolio/ui/holdings-table/holdings-table.module';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module'; import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { GfAccountBalancesModule } from '@ghostfolio/ui/account-balances/account-balances.module'; import { GfAccountBalancesModule } from '@ghostfolio/ui/account-balances/account-balances.module';
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -21,7 +20,6 @@ import { AccountDetailDialog } from './account-detail-dialog.component';
CommonModule, CommonModule,
GfAccountBalancesModule, GfAccountBalancesModule,
GfActivitiesTableModule, GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,
GfHoldingsTableModule, GfHoldingsTableModule,

View File

@ -39,7 +39,7 @@
class="d-inline d-sm-none mr-1" class="d-inline d-sm-none mr-1"
[tooltip]="element.Platform?.name" [tooltip]="element.Platform?.name"
[url]="element.Platform?.url" [url]="element.Platform?.url"
></gf-symbol-icon> />
<span>{{ element.name }} </span> <span>{{ element.name }} </span>
<span <span
*ngIf="element.isDefault" *ngIf="element.isDefault"
@ -83,7 +83,7 @@
class="mr-1" class="mr-1"
[tooltip]="element.Platform?.name" [tooltip]="element.Platform?.name"
[url]="element.Platform?.url" [url]="element.Platform?.url"
></gf-symbol-icon> />
<span>{{ element.Platform?.name }}</span> <span>{{ element.Platform?.name }}</span>
</div> </div>
</td> </td>
@ -131,7 +131,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="element.balance" [value]="element.balance"
></gf-value> />
</td> </td>
<td <td
*matFooterCellDef *matFooterCellDef
@ -143,7 +143,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="totalBalanceInBaseCurrency" [value]="totalBalanceInBaseCurrency"
></gf-value> />
</td> </td>
</ng-container> </ng-container>
@ -166,7 +166,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="element.value" [value]="element.value"
></gf-value> />
</td> </td>
<td <td
*matFooterCellDef *matFooterCellDef
@ -178,7 +178,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="totalValueInBaseCurrency" [value]="totalValueInBaseCurrency"
></gf-value> />
</td> </td>
</ng-container> </ng-container>
@ -201,7 +201,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="element.valueInBaseCurrency" [value]="element.valueInBaseCurrency"
></gf-value> />
</td> </td>
<td <td
*matFooterCellDef *matFooterCellDef
@ -213,7 +213,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="totalValueInBaseCurrency" [value]="totalValueInBaseCurrency"
></gf-value> />
</td> </td>
</ng-container> </ng-container>
@ -296,4 +296,4 @@
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />

View File

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

View File

@ -6,7 +6,7 @@
[isLoading]="isLoading" [isLoading]="isLoading"
[placeholder]="placeholder" [placeholder]="placeholder"
(valueChanged)="filters$.next($event)" (valueChanged)="filters$.next($event)"
></gf-activities-filter> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -213,7 +213,7 @@
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />
</div> </div>
</div> </div>

View File

@ -50,7 +50,7 @@
[marketData]="marketDataDetails" [marketData]="marketDataDetails"
[symbol]="data.symbol" [symbol]="data.symbol"
(marketDataChanged)="onMarketDataChanged($event)" (marketDataChanged)="onMarketDataChanged($event)"
></gf-admin-market-data-detail> />
<div class="mt-3" formGroupName="historicalData"> <div class="mt-3" formGroupName="historicalData">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field appearance="outline" class="w-100 without-hint">
@ -162,7 +162,7 @@
[keys]="['name']" [keys]="['name']"
[maxItems]="10" [maxItems]="10"
[positions]="sectors" [positions]="sectors"
></gf-portfolio-proportion-chart> />
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div> <div class="h5" i18n>Countries</div>
@ -172,7 +172,7 @@
[keys]="['name']" [keys]="['name']"
[maxItems]="10" [maxItems]="10"
[positions]="countries" [positions]="countries"
></gf-portfolio-proportion-chart> />
</div> </div>
</ng-template> </ng-template>
</ng-container> </ng-container>

View File

@ -16,7 +16,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[precision]="0" [precision]="0"
[value]="userCount" [value]="userCount"
></gf-value> />
</div> </div>
</div> </div>
<div class="d-flex my-3"> <div class="d-flex my-3">
@ -26,7 +26,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[precision]="0" [precision]="0"
[value]="transactionCount" [value]="transactionCount"
></gf-value> />
<div *ngIf="transactionCount && userCount"> <div *ngIf="transactionCount && userCount">
{{ transactionCount / userCount | number : '1.2-2' }} {{ transactionCount / userCount | number : '1.2-2' }}
<span i18n>per User</span> <span i18n>per User</span>
@ -39,10 +39,7 @@
<table> <table>
<tr *ngFor="let exchangeRate of exchangeRates"> <tr *ngFor="let exchangeRate of exchangeRates">
<td> <td>
<gf-value <gf-value [locale]="user?.settings?.locale" [value]="1" />
[locale]="user?.settings?.locale"
[value]="1"
></gf-value>
</td> </td>
<td class="pl-1">{{ exchangeRate.label1 }}</td> <td class="pl-1">{{ exchangeRate.label1 }}</td>
<td class="px-1">=</td> <td class="px-1">=</td>
@ -52,7 +49,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[precision]="4" [precision]="4"
[value]="exchangeRate.value" [value]="exchangeRate.value"
></gf-value> />
</td> </td>
<td class="pl-1">{{ exchangeRate.label2 }}</td> <td class="pl-1">{{ exchangeRate.label2 }}</td>
<td> <td>

View File

@ -35,7 +35,7 @@
class="d-inline mr-1" class="d-inline mr-1"
[tooltip]="element.name" [tooltip]="element.name"
[url]="element.url" [url]="element.url"
></gf-symbol-icon> />
<span>{{ element.name }}</span> <span>{{ element.name }}</span>
</td></ng-container </td></ng-container
> >

View File

@ -46,7 +46,7 @@
class="ml-1" class="ml-1"
[enableLink]="false" [enableLink]="false"
[title]="'Expires ' + formatDistanceToNow(element.subscription.expiresAt) + ' (' + (element.subscription.expiresAt | date: defaultDateFormat) + ')'" [title]="'Expires ' + formatDistanceToNow(element.subscription.expiresAt) + ' (' + (element.subscription.expiresAt | date: defaultDateFormat) + ')'"
></gf-premium-indicator> />
</div> </div>
</td> </td>
</ng-container> </ng-container>
@ -107,7 +107,7 @@
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="element.accountCount" [value]="element.accountCount"
></gf-value> />
</td> </td>
</ng-container> </ng-container>
@ -128,7 +128,7 @@
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="element.transactionCount" [value]="element.transactionCount"
></gf-value> />
</td> </td>
</ng-container> </ng-container>
@ -153,7 +153,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[precision]="0" [precision]="0"
[value]="element.engagement" [value]="element.engagement"
></gf-value> />
</td> </td>
</ng-container> </ng-container>

View File

@ -7,7 +7,7 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</div> </div>
</div> </div>
<div class="col-md-6 col-xs-12 d-flex justify-content-end"> <div class="col-md-6 col-xs-12 d-flex justify-content-end">
@ -50,7 +50,7 @@
height: '100%', height: '100%',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />
<canvas <canvas
#chartCanvas #chartCanvas
class="h-100" class="h-100"

View File

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

View File

@ -6,11 +6,12 @@
mat-button mat-button
[ngClass]="{ 'w-100': hasTabs }" [ngClass]="{ 'w-100': hasTabs }"
[routerLink]="['/']" [routerLink]="['/']"
(click)="onLogoClick()"
> >
<gf-logo class="px-2" [label]="pageTitle"></gf-logo> <gf-logo class="px-2" [label]="pageTitle" />
</a> </a>
</div> </div>
<span class="spacer"></span> <span class="gf-spacer"></span>
<ul class="alig-items-center d-flex list-inline m-0 px-2"> <ul class="alig-items-center d-flex list-inline m-0 px-2">
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
@ -119,11 +120,7 @@
[matMenuTriggerRestoreFocus]="false" [matMenuTriggerRestoreFocus]="false"
(menuOpened)="onOpenAssistant()" (menuOpened)="onOpenAssistant()"
> >
@if (user?.settings?.isExperimentalFeatures) { <ion-icon class="rotate-90" name="options-outline" />
<ion-icon class="rotate-90" name="options-outline" />
} @else {
<ion-icon name="search-outline" />
}
</button> </button>
<mat-menu <mat-menu
#assistantMenu="matMenu" #assistantMenu="matMenu"
@ -141,7 +138,7 @@
[user]="user" [user]="user"
(closed)="closeAssistant()" (closed)="closeAssistant()"
(dateRangeChanged)="onDateRangeChange($event)" (dateRangeChanged)="onDateRangeChange($event)"
(selectedTagChanged)="onSelectedTagChanged($event)" (filtersChanged)="onFiltersChanged($event)"
/> />
</mat-menu> </mat-menu>
</li> </li>
@ -165,6 +162,32 @@
/> />
</button> </button>
<mat-menu #accountMenu="matMenu" xPosition="before"> <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"> <ng-container *ngIf="user?.access?.length > 0">
<button mat-menu-item (click)="impersonateAccount(null)"> <button mat-menu-item (click)="impersonateAccount(null)">
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
@ -295,10 +318,10 @@
class="px-2" class="px-2"
[label]="pageTitle" [label]="pageTitle"
[showLabel]="currentRoute !== 'register'" [showLabel]="currentRoute !== 'register'"
></gf-logo> />
</a> </a>
</div> </div>
<span class="spacer"></span> <span class="gf-spacer"></span>
<ul class="alig-items-center d-flex list-inline m-0 px-2"> <ul class="alig-items-center d-flex list-inline m-0 px-2">
<li class="list-inline-item"> <li class="list-inline-item">
<a <a

View File

@ -38,10 +38,6 @@
} }
} }
} }
.spacer {
flex: 1 1 auto;
}
} }
} }

View File

@ -11,7 +11,9 @@ import {
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatMenuTrigger } from '@angular/material/menu'; import { MatMenuTrigger } from '@angular/material/menu';
import { Router } from '@angular/router'; 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 { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
import { LayoutService } from '@ghostfolio/client/core/layout.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { import {
@ -20,11 +22,10 @@ import {
} from '@ghostfolio/client/services/settings-storage.service'; } from '@ghostfolio/client/services/settings-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { AssistantComponent } from '@ghostfolio/ui/assistant/assistant.component'; import { AssistantComponent } from '@ghostfolio/ui/assistant/assistant.component';
import { Tag } from '@prisma/client';
import { EMPTY, Subject } from 'rxjs'; import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError, takeUntil } from 'rxjs/operators';
@ -89,6 +90,7 @@ export class HeaderComponent implements OnChanges {
private dataService: DataService, private dataService: DataService,
private dialog: MatDialog, private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private layoutService: LayoutService,
private router: Router, private router: Router,
private settingsStorageService: SettingsStorageService, private settingsStorageService: SettingsStorageService,
private tokenStorageService: TokenStorageService, private tokenStorageService: TokenStorageService,
@ -162,6 +164,42 @@ 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 === 'ASSET_CLASS') {
filtersType = 'assetClasses';
} 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 onLogoClick() {
if (this.currentRoute === 'home' || this.currentRoute === 'zen') {
this.layoutService.getShouldReloadSubject().next();
}
}
public onMenuClosed() { public onMenuClosed() {
this.isMenuOpen = false; this.isMenuOpen = false;
} }
@ -174,20 +212,6 @@ export class HeaderComponent implements OnChanges {
this.assistantElement.initialize(); 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() { public onSignOut() {
this.signOut.next(); 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 { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
import { GfAssistantModule } from '@ghostfolio/ui/assistant'; import { GfAssistantModule } from '@ghostfolio/ui/assistant';
import { GfLogoModule } from '@ghostfolio/ui/logo'; import { GfLogoModule } from '@ghostfolio/ui/logo';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { HeaderComponent } from './header.component'; import { HeaderComponent } from './header.component';
@ -17,6 +18,7 @@ import { HeaderComponent } from './header.component';
CommonModule, CommonModule,
GfAssistantModule, GfAssistantModule,
GfLogoModule, GfLogoModule,
GfPremiumIndicatorModule,
LoginWithAccessTokenDialogModule, LoginWithAccessTokenDialogModule,
MatButtonModule, MatButtonModule,
MatMenuModule, MatMenuModule,

View File

@ -1,12 +1,4 @@
<div class="container justify-content-center p-3"> <div class="container justify-content-center p-3">
<div *ngIf="!user?.settings?.isExperimentalFeatures" class="mb-3 text-center">
<gf-toggle
[defaultValue]="user?.settings?.dateRange"
[isLoading]="positions === undefined"
[options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)"
></gf-toggle>
</div>
<div class="row"> <div class="row">
<div class="align-items-center col-xs-12 col-md-8 offset-md-2"> <div class="align-items-center col-xs-12 col-md-8 offset-md-2">
<mat-card appearance="outlined"> <mat-card appearance="outlined">
@ -18,7 +10,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="positions" [positions]="positions"
[range]="user?.settings?.dateRange" [range]="user?.settings?.dateRange"
></gf-positions> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<div *ngIf="hasPermissionToCreateOrder" class="text-center"> <div *ngIf="hasPermissionToCreateOrder" class="text-center">

View File

@ -18,11 +18,11 @@
[yMaxLabel]="greedLabel" [yMaxLabel]="greedLabel"
[yMin]="0" [yMin]="0"
[yMinLabel]="fearLabel" [yMinLabel]="fearLabel"
></gf-line-chart> />
<gf-fear-and-greed-index <gf-fear-and-greed-index
class="d-flex justify-content-center" class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex" [fearAndGreedIndex]="fearAndGreedIndex"
></gf-fear-and-greed-index> />
</div> </div>
</div> </div>
@ -32,7 +32,7 @@
[benchmarks]="benchmarks" [benchmarks]="benchmarks"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[user]="user" [user]="user"
></gf-benchmark> />
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="isLoading" *ngIf="isLoading"
animation="pulse" animation="pulse"
@ -41,7 +41,7 @@
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component'; import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { LayoutService } from '@ghostfolio/client/core/layout.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
LineChartItem, LineChartItem,
@ -43,6 +44,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private layoutService: LayoutService,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
@ -73,8 +75,13 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
this.layoutService.shouldReloadContent$
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.update();
});
this.showDetails = this.showDetails =
!this.hasImpersonationId &&
!this.user.settings.isRestrictedView && !this.user.settings.isRestrictedView &&
this.user.settings.viewMode !== 'ZEN'; this.user.settings.viewMode !== 'ZEN';

View File

@ -78,7 +78,7 @@
[showLoader]="false" [showLoader]="false"
[showXAxis]="false" [showXAxis]="false"
[showYAxis]="false" [showYAxis]="false"
></gf-line-chart> />
</div> </div>
</div> </div>
</div> </div>
@ -95,18 +95,7 @@
[performance]="performance" [performance]="performance"
[showDetails]="showDetails" [showDetails]="showDetails"
[unit]="unit" [unit]="unit"
></gf-portfolio-performance> />
<div
*ngIf="showDetails && !user?.settings?.isExperimentalFeatures"
class="text-center"
>
<gf-toggle
[defaultValue]="user?.settings?.dateRange"
[isLoading]="isLoadingPerformance"
[options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)"
></gf-toggle>
</div>
</div> </div>
</div> </div>
</ng-template> </ng-template>

View File

@ -3,7 +3,6 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module'; import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
@ -16,7 +15,6 @@ import { HomeOverviewComponent } from './home-overview.component';
GfLineChartModule, GfLineChartModule,
GfNoTransactionsInfoModule, GfNoTransactionsInfoModule,
GfPortfolioPerformanceModule, GfPortfolioPerformanceModule,
GfToggleModule,
MatButtonModule, MatButtonModule,
RouterModule RouterModule
], ],

View File

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

View File

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

View File

@ -2,7 +2,7 @@
mat-dialog-title mat-dialog-title
[title]="data.title" [title]="data.title"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-header> />
<div class="py-3" mat-dialog-content> <div class="py-3" mat-dialog-content>
<div class="align-items-center d-flex flex-column"> <div class="align-items-center d-flex flex-column">

View File

@ -1,14 +1,12 @@
<div class="container p-0"> <div class="container p-0">
<div class="no-gutters row"> <div class="no-gutters row">
<div <div class="status-container text-muted text-right">
class="status-container text-muted text-right"
(click)="onShowErrors()"
>
@if (errors?.length > 0 && !isLoading) { @if (errors?.length > 0 && !isLoading) {
<ion-icon <ion-icon
i18n-title i18n-title
name="time-outline" 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> </div>
@ -20,7 +18,7 @@
height: '4rem', height: '4rem',
width: '15rem' width: '15rem'
}" }"
></ngx-skeleton-loader> />
</div> </div>
<div <div
class="display-4 font-weight-bold m-0 text-center value-container" class="display-4 font-weight-bold m-0 text-center value-container"
@ -43,7 +41,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : performance?.currentNetPerformance" [value]="isLoading ? undefined : performance?.currentNetPerformance"
></gf-value> />
</div> </div>
<div class="col"> <div class="col">
<gf-value <gf-value
@ -53,7 +51,7 @@
[value]=" [value]="
isLoading ? undefined : performance?.currentNetPerformancePercent isLoading ? undefined : performance?.currentNetPerformancePercent
" "
></gf-value> />
</div> </div>
</div> </div>
</div> </div>

View File

@ -58,7 +58,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
duration: 1, duration: 1,
separator: getNumberFormatGroup(this.locale) separator: getNumberFormatGroup(this.locale)
}).start(); }).start();
} else if (this.performance?.currentValue === null) { } else if (this.showDetails === false) {
new CountUp( new CountUp(
'value', 'value',
this.performance?.currentNetPerformancePercent * 100, this.performance?.currentNetPerformancePercent * 100,
@ -69,6 +69,8 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
separator: getNumberFormatGroup(this.locale) separator: getNumberFormatGroup(this.locale)
} }
).start(); ).start();
} else {
this.value.nativeElement.innerHTML = '*****';
} }
} }
} }

View File

@ -2,7 +2,7 @@
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 text-truncate" i18n>Time in Market</div> <div class="flex-grow-1 text-truncate" i18n>Time in Market</div>
<div class="justify-content-end"> <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> </div>
<div <div
@ -10,8 +10,8 @@
[hidden]="summary?.ordersCount === null" [hidden]="summary?.ordersCount === null"
> >
<div class="flex-grow-1 ml-3 text-truncate" i18n> <div class="flex-grow-1 ml-3 text-truncate" i18n>
{{ summary?.ordersCount }} {summary?.ordersCount, plural, =1 {transaction} {{ summary?.ordersCount }}
other {transactions}} {summary?.ordersCount, plural, =1 {transaction} other {transactions}}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -26,7 +26,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.totalBuy" [value]="isLoading ? undefined : summary?.totalBuy"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -38,7 +38,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.totalSell" [value]="isLoading ? undefined : summary?.totalSell"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -53,7 +53,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.committedFunds" [value]="isLoading ? undefined : summary?.committedFunds"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -65,13 +65,17 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.currentGrossPerformance" [value]="isLoading ? undefined : summary?.currentGrossPerformance"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
<div class="align-items-center d-flex flex-grow-1 ml-3 text-truncate"> <div class="align-items-center d-flex flex-grow-1 ml-3 text-truncate">
<ng-container i18n>Gross Performance</ng-container> <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>
<div class="flex-column flex-wrap justify-content-end"> <div class="flex-column flex-wrap justify-content-end">
<gf-value <gf-value
@ -83,7 +87,7 @@
[value]=" [value]="
isLoading ? undefined : summary?.currentGrossPerformancePercent isLoading ? undefined : summary?.currentGrossPerformancePercent
" "
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -96,7 +100,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.fees" [value]="isLoading ? undefined : summary?.fees"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -111,13 +115,17 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.currentNetPerformance" [value]="isLoading ? undefined : summary?.currentNetPerformance"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 text-truncate ml-3"> <div class="flex-grow-1 text-truncate ml-3">
<ng-container i18n>Net Performance</ng-container> <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>
<div class="flex-column flex-wrap justify-content-end"> <div class="flex-column flex-wrap justify-content-end">
<gf-value <gf-value
@ -127,7 +135,7 @@
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : summary?.currentNetPerformancePercent" [value]="isLoading ? undefined : summary?.currentNetPerformancePercent"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -143,7 +151,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.currentValue" [value]="isLoading ? undefined : summary?.currentValue"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -155,7 +163,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.items" [value]="isLoading ? undefined : summary?.items"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -176,7 +184,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.emergencyFund?.total" [value]="isLoading ? undefined : summary?.emergencyFund?.total"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -189,7 +197,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.emergencyFund?.cash" [value]="isLoading ? undefined : summary?.emergencyFund?.cash"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -202,7 +210,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.emergencyFund?.assets" [value]="isLoading ? undefined : summary?.emergencyFund?.assets"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -214,7 +222,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.cash" [value]="isLoading ? undefined : summary?.cash"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -226,7 +234,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.excludedAccountsAndActivities" [value]="isLoading ? undefined : summary?.excludedAccountsAndActivities"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -246,7 +254,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.liabilities" [value]="isLoading ? undefined : summary?.liabilities"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -261,7 +269,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.netWorth" [value]="isLoading ? undefined : summary?.netWorth"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -276,7 +284,7 @@
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : summary?.annualizedPerformancePercent" [value]="isLoading ? undefined : summary?.annualizedPerformancePercent"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -291,7 +299,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.interest" [value]="isLoading ? undefined : summary?.interest"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -303,7 +311,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.dividend" [value]="isLoading ? undefined : summary?.dividend"
></gf-value> />
</div> </div>
</div> </div>
</div> </div>

View File

@ -268,20 +268,12 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
} }
public onExport() { public onExport() {
let activityIds = []; let activityIds = this.dataSource.data.map(({ id }) => {
return id;
if (this.user?.settings?.isExperimentalFeatures === true) { });
activityIds = this.dataSource.data.map(({ id }) => {
return id;
});
} else {
activityIds = this.activities.map(({ id }) => {
return id;
});
}
this.dataService this.dataService
.fetchExport(activityIds) .fetchExport({ activityIds })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((data) => {
downloadAsFile({ downloadAsFile({

View File

@ -4,7 +4,7 @@
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[title]="SymbolProfile?.name ?? SymbolProfile?.symbol" [title]="SymbolProfile?.name ?? SymbolProfile?.symbol"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-header> />
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<div class="container p-0"> <div class="container p-0">
@ -16,7 +16,7 @@
[locale]="data.locale" [locale]="data.locale"
[unit]="data.baseCurrency" [unit]="data.baseCurrency"
[value]="value" [value]="value"
></gf-value> />
</div> </div>
</div> </div>
@ -33,7 +33,7 @@
[showXAxis]="true" [showXAxis]="true"
[showYAxis]="true" [showYAxis]="true"
[symbol]="data.symbol" [symbol]="data.symbol"
></gf-line-chart> />
<div class="row"> <div class="row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
@ -222,7 +222,7 @@
[locale]="data.locale" [locale]="data.locale"
[maxItems]="10" [maxItems]="10"
[positions]="sectors" [positions]="sectors"
></gf-portfolio-proportion-chart> />
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div> <div class="h5" i18n>Countries</div>
@ -234,7 +234,7 @@
[locale]="data.locale" [locale]="data.locale"
[maxItems]="10" [maxItems]="10"
[positions]="countries" [positions]="countries"
></gf-portfolio-proportion-chart> />
</div> </div>
</ng-template> </ng-template>
</ng-container> </ng-container>
@ -249,13 +249,12 @@
<div class="row" [ngClass]="{ 'd-none': !activities?.length }"> <div class="row" [ngClass]="{ 'd-none': !activities?.length }">
<div class="col mb-3"> <div class="col mb-3">
<div class="h5 mb-0" i18n>Activities</div> <div class="h5 mb-0" i18n>Activities</div>
<gf-activities-table-lazy <gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures === true"
[baseCurrency]="data.baseCurrency" [baseCurrency]="data.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false" [hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!hasImpersonationId && !user.settings.isRestrictedView" [hasPermissionToExportActivities]="!data.hasImpersonationId && !user.settings.isRestrictedView"
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[locale]="data.locale" [locale]="data.locale"
@ -266,21 +265,7 @@
[sortDisabled]="true" [sortDisabled]="true"
[totalItems]="totalItems" [totalItems]="totalItems"
(export)="onExport()" (export)="onExport()"
></gf-activities-table-lazy> />
<gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities"
[baseCurrency]="data.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="true"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="data.locale"
[showActions]="false"
[showNameColumn]="false"
(export)="onExport()"
></gf-activities-table>
</div> </div>
</div> </div>
@ -314,4 +299,4 @@
mat-dialog-actions mat-dialog-actions
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-footer> />

View File

@ -5,7 +5,6 @@ import { MatChipsModule } from '@angular/material/chips';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfDataProviderCreditsModule } from '@ghostfolio/ui/data-provider-credits/data-provider-credits.module'; import { GfDataProviderCreditsModule } from '@ghostfolio/ui/data-provider-credits/data-provider-credits.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
@ -20,7 +19,6 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
imports: [ imports: [
CommonModule, CommonModule,
GfActivitiesTableModule, GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfDataProviderCreditsModule, GfDataProviderCreditsModule,
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,

View File

@ -18,7 +18,7 @@
[marketState]="position?.marketState" [marketState]="position?.marketState"
[range]="range" [range]="range"
[value]="position?.netPerformancePercentage" [value]="position?.netPerformancePercentage"
></gf-trend-indicator> />
</div> </div>
<div *ngIf="isLoading" class="flex-grow-1"> <div *ngIf="isLoading" class="flex-grow-1">
<ngx-skeleton-loader <ngx-skeleton-loader
@ -28,14 +28,14 @@
height: '1.2rem', height: '1.2rem',
width: '12rem' width: '12rem'
}" }"
></ngx-skeleton-loader> />
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
height: '1rem', height: '1rem',
width: '8rem' width: '8rem'
}" }"
></ngx-skeleton-loader> />
</div> </div>
<div *ngIf="!isLoading" class="flex-grow-1 text-truncate"> <div *ngIf="!isLoading" class="flex-grow-1 text-truncate">
<div class="h6 m-0 text-truncate">{{ position?.name }}</div> <div class="h6 m-0 text-truncate">{{ position?.name }}</div>
@ -50,13 +50,13 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="position?.netPerformance" [value]="position?.netPerformance"
></gf-value> />
<gf-value <gf-value
[colorizeSign]="true" [colorizeSign]="true"
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="position?.netPerformancePercentage" [value]="position?.netPerformancePercentage"
></gf-value> />
</div> </div>
</div> </div>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">

View File

@ -2,7 +2,7 @@
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
<ng-container *ngIf="positions === undefined"> <ng-container *ngIf="positions === undefined">
<gf-position [isLoading]="true"></gf-position> <gf-position [isLoading]="true" />
</ng-container> </ng-container>
<ng-container *ngIf="positions !== undefined"> <ng-container *ngIf="positions !== undefined">
<ng-container *ngIf="hasPositions"> <ng-container *ngIf="hasPositions">
@ -13,7 +13,7 @@
[locale]="locale" [locale]="locale"
[position]="position" [position]="position"
[range]="range" [range]="range"
></gf-position> />
<gf-position <gf-position
*ngFor="let position of positionsRest" *ngFor="let position of positionsRest"
[baseCurrency]="baseCurrency" [baseCurrency]="baseCurrency"
@ -21,15 +21,13 @@
[locale]="locale" [locale]="locale"
[position]="position" [position]="position"
[range]="range" [range]="range"
></gf-position> />
</ng-container> </ng-container>
<div <div
*ngIf="hasPermissionToCreateOrder && !hasPositions" *ngIf="hasPermissionToCreateOrder && !hasPositions"
class="p-3 text-center" class="p-3 text-center"
> >
<gf-no-transactions-info-indicator <gf-no-transactions-info-indicator [hasBorder]="false" />
[hasBorder]="false"
></gf-no-transactions-info-indicator>
</div> </div>
</ng-container> </ng-container>
</div> </div>

View File

@ -8,7 +8,7 @@
height: '2rem', height: '2rem',
width: '2rem' width: '2rem'
}" }"
></ngx-skeleton-loader> />
</div> </div>
<div <div
*ngIf="!isLoading" *ngIf="!isLoading"
@ -26,14 +26,14 @@
height: '1rem', height: '1rem',
width: '10rem' width: '10rem'
}" }"
></ngx-skeleton-loader> />
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
height: '1rem', height: '1rem',
width: '15rem' width: '15rem'
}" }"
></ngx-skeleton-loader> />
</div> </div>
<div *ngIf="!isLoading" class="flex-grow-1"> <div *ngIf="!isLoading" class="flex-grow-1">
<div class="h6 my-1">{{ rule?.name }}</div> <div class="h6 my-1">{{ rule?.name }}</div>

View File

@ -7,15 +7,13 @@
class="my-2 text-center" class="my-2 text-center"
> >
<mat-card-content> <mat-card-content>
<gf-no-transactions-info-indicator <gf-no-transactions-info-indicator [hasBorder]="false" />
[hasBorder]="false" </mat-card-content>
></gf-no-transactions-info-indicator
></mat-card-content>
</mat-card> </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"> <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> </ng-container>
</div> </div>
</div> </div>

View File

@ -7,10 +7,7 @@
<div> <div>
<h5 class="align-items-center d-flex justify-content-center mb-3"> <h5 class="align-items-center d-flex justify-content-center mb-3">
<span>Ghostfolio Premium</span> <span>Ghostfolio Premium</span>
<gf-premium-indicator <gf-premium-indicator class="ml-1" [enableLink]="false" />
class="ml-1"
[enableLink]="false"
></gf-premium-indicator>
</h5> </h5>
<div class="font-weight-normal h5 mb-3 text-center" i18n> <div class="font-weight-normal h5 mb-3 text-center" i18n>
Are you an ambitious investor who needs the full picture? Are you an ambitious investor who needs the full picture?

View File

@ -37,19 +37,23 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
ngOnInit() { ngOnInit() {
this.accessForm = this.formBuilder.group({ this.accessForm = this.formBuilder.group({
alias: [this.data.access.alias], alias: [this.data.access.alias],
permissions: [this.data.access.permissions[0], Validators.required],
type: [this.data.access.type, Validators.required], type: [this.data.access.type, Validators.required],
userId: [this.data.access.grantee, 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'); const userIdControl = this.accessForm.get('userId');
if (value === 'PRIVATE') { if (accessType === 'PRIVATE') {
permissionsControl.setValidators(Validators.required);
userIdControl.setValidators(Validators.required); userIdControl.setValidators(Validators.required);
} else { } else {
userIdControl.clearValidators(); userIdControl.clearValidators();
} }
permissionsControl.updateValueAndValidity();
userIdControl.updateValueAndValidity(); userIdControl.updateValueAndValidity();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
@ -64,7 +68,7 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
const access: CreateAccessDto = { const access: CreateAccessDto = {
alias: this.accessForm.controls['alias'].value, alias: this.accessForm.controls['alias'].value,
granteeUserId: this.accessForm.controls['userId'].value, granteeUserId: this.accessForm.controls['userId'].value,
type: this.accessForm.controls['type'].value permissions: [this.accessForm.controls['permissions'].value]
}; };
this.dataService this.dataService

View File

@ -30,9 +30,20 @@
@if (accessForm.controls['type'].value === 'PRIVATE') { @if (accessForm.controls['type'].value === 'PRIVATE') {
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label <mat-label i18n>Permission</mat-label>
>Ghostfolio <ng-container i18n>User ID</ng-container></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 <input
formControlName="userId" formControlName="userId"
matInput matInput

View File

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

View File

@ -7,7 +7,6 @@ import {
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router'; 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 { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Access, User } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
@ -105,8 +104,10 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
data: { data: {
access: { access: {
alias: '', alias: '',
permissions: ['READ_RESTRICTED'],
type: 'PRIVATE' type: 'PRIVATE'
} },
user: this.user
}, },
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'

View File

@ -6,13 +6,13 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h1> </h1>
<gf-access-table <gf-access-table
[accesses]="accesses" [accesses]="accesses"
[showActions]="hasPermissionToDeleteAccess" [showActions]="hasPermissionToDeleteAccess"
(accessDeleted)="onDeleteAccess($event)" (accessDeleted)="onDeleteAccess($event)"
></gf-access-table> />
<div *ngIf="hasPermissionToCreateAccess" class="fab-container"> <div *ngIf="hasPermissionToCreateAccess" class="fab-container">
<a <a
class="align-items-center d-flex justify-content-center" class="align-items-center d-flex justify-content-center"

View File

@ -5,7 +5,7 @@
<gf-membership-card <gf-membership-card
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat" [expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
[name]="user?.subscription?.type" [name]="user?.subscription?.type"
></gf-membership-card> />
<div <div
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="d-flex flex-column mt-5" class="d-flex flex-column mt-5"
@ -15,10 +15,10 @@
> >
<button color="primary" mat-flat-button (click)="onCheckout()"> <button color="primary" mat-flat-button (click)="onCheckout()">
<ng-container *ngIf="user.subscription.offer === 'default'" i18n <ng-container *ngIf="user.subscription.offer === 'default'" i18n
>Upgrade</ng-container >Upgrade Plan</ng-container
> >
<ng-container *ngIf="user.subscription.offer === 'renewal'" i18n <ng-container *ngIf="user.subscription.offer === 'renewal'" i18n
>Renew</ng-container >Renew Plan</ng-container
> >
</button> </button>
<div *ngIf="price" class="mt-1 text-center"> <div *ngIf="price" class="mt-1 text-center">
@ -43,8 +43,8 @@
<gf-premium-indicator <gf-premium-indicator
class="d-inline-block ml-1" class="d-inline-block ml-1"
[enableLink]="false" [enableLink]="false"
></gf-premium-indicator />
></a> </a>
<a <a
*ngIf="hasPermissionToUpdateUserSettings" *ngIf="hasPermissionToUpdateUserSettings"
class="mx-1" class="mx-1"

View File

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

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class LayoutService {
public shouldReloadContent$: Observable<void>;
private shouldReloadSubject = new Subject<void>();
public constructor() {
this.shouldReloadContent$ = this.shouldReloadSubject.asObservable();
}
public getShouldReloadSubject() {
return this.shouldReloadSubject;
}
}

View File

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

View File

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

View File

@ -17,8 +17,7 @@
class="mr-1" class="mr-1"
[tooltip]="account.Platform?.name" [tooltip]="account.Platform?.name"
[url]="account.Platform?.url" [url]="account.Platform?.url"
></gf-symbol-icon /><span>{{ account.name }}</span>
><span>{{ account.name }}</span>
</div> </div>
</mat-option> </mat-option>
</mat-select> </mat-select>
@ -35,8 +34,7 @@
class="mr-1" class="mr-1"
[tooltip]="account.Platform?.name" [tooltip]="account.Platform?.name"
[url]="account.Platform?.url" [url]="account.Platform?.url"
></gf-symbol-icon /><span>{{ account.name }}</span>
><span>{{ account.name }}</span>
</div> </div>
</mat-option> </mat-option>
</mat-select> </mat-select>

View File

@ -20,8 +20,8 @@
<gf-premium-indicator <gf-premium-indicator
class="d-inline-block ml-1" class="d-inline-block ml-1"
[enableLink]="false" [enableLink]="false"
></gf-premium-indicator />
></span> </span>
annual plan for ambitious investors who need the full picture of annual plan for ambitious investors who need the full picture of
their financial assets. their financial assets.
</p> </p>

View File

@ -21,8 +21,8 @@
<gf-premium-indicator <gf-premium-indicator
class="d-inline-block ml-1" class="d-inline-block ml-1"
[enableLink]="false" [enableLink]="false"
></gf-premium-indicator />
></span> </span>
annual plan with our exclusive Black Week deal. Elevate your annual plan with our exclusive Black Week deal. Elevate your
financial strategy with the power of Ghostfolio designed to give you financial strategy with the power of Ghostfolio designed to give you
the full picture of your assets. the full picture of your assets.

View File

@ -50,11 +50,8 @@
You can sign up via the “<a [routerLink]="routerLinkRegister" You can sign up via the “<a [routerLink]="routerLinkRegister"
>Get Started</a >Get Started</a
>” button at the top of the page. You have multiple options to join >” button at the top of the page. You have multiple options to join
Ghostfolio: Create an account with a security token, using Ghostfolio: Create an account with a security token or
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity" <i>Google Sign</i>. We will guide you to set up your portfolio.
>Internet Identity</a
>
or <i>Google Sign</i>. We will guide you to set up your portfolio.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
@ -75,11 +72,9 @@
></mat-card-header ></mat-card-header
> >
<mat-card-content> <mat-card-content>
Yes, the authentication systems (via security token or Yes, the authentication system via security token enables you to sign
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity" in securely and anonymously to Ghostfolio. There is no need for an
>Internet Identity</a e-mail address, phone number, or a username.
>) enable you to sign in securely and anonymously to Ghostfolio. There
is no need for an e-mail address, phone number, or a username.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
@ -176,6 +171,19 @@
your university e-mail address.</mat-card-content your university e-mail address.</mat-card-content
> >
</mat-card> </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 appearance="outlined" class="mb-3">
<mat-card-header> <mat-card-header>
<mat-card-title>Which devices are supported?</mat-card-title> <mat-card-title>Which devices are supported?</mat-card-title>

View File

@ -142,7 +142,7 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<p class="m-0"> <p class="m-0">
Check the rate of return of your portfolio for Check the rate of return of your portfolio for
@ -162,7 +162,7 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<p class="m-0"> <p class="m-0">
Check the allocations of your portfolio by account, asset Check the allocations of your portfolio by account, asset
@ -207,7 +207,7 @@
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>Market Mood</span> <span i18n>Market Mood</span>
<gf-premium-indicator class="ml-1"></gf-premium-indicator> <gf-premium-indicator class="ml-1" />
</h4> </h4>
<p class="m-0"> <p class="m-0">
Check the current market mood (<a Check the current market mood (<a
@ -228,7 +228,7 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<p class="m-0"> <p class="m-0">
Identify potential risks in your portfolio with Ghostfolio Identify potential risks in your portfolio with Ghostfolio

View File

@ -328,11 +328,7 @@
<gf-carousel [aria-label]="'Testimonials'"> <gf-carousel [aria-label]="'Testimonials'">
<div *ngFor="let testimonial of testimonials" gf-carousel-item> <div *ngFor="let testimonial of testimonials" gf-carousel-item>
<div class="d-flex px-4"> <div class="d-flex px-4">
<gf-logo <gf-logo class="mr-3 mt-2 pt-1" size="medium" [showLabel]="false" />
class="mr-3 mt-2 pt-1"
size="medium"
[showLabel]="false"
></gf-logo>
<div> <div>
<div>{{ testimonial.quote }}</div> <div>{{ testimonial.quote }}</div>
<div class="mt-2 text-muted"> <div class="mt-2 text-muted">
@ -361,10 +357,7 @@
</h2> </h2>
</div> </div>
<div class="col-md-8 customer-map-container offset-md-2"> <div class="col-md-8 customer-map-container offset-md-2">
<gf-world-map-chart <gf-world-map-chart format="👻" [countries]="countriesOfSubscribersMap" />
format="👻"
[countries]="countriesOfSubscribersMap"
></gf-world-map-chart>
</div> </div>
</div> </div>
@ -450,7 +443,7 @@
<div <div
class="align-items-center d-flex flex-column justify-content-center w-100" 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>Wealth Management Software</div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<gf-home-market></gf-home-market> <gf-home-market />
</div> </div>
</div> </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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { downloadAsFile } from '@ghostfolio/common/helper'; import { downloadAsFile } from '@ghostfolio/common/helper';
import { Filter, User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DataSource, Order as OrderModel } from '@prisma/client'; import { DataSource, Order as OrderModel } from '@prisma/client';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
@ -121,43 +121,25 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
} }
public fetchActivities() { public fetchActivities() {
if (this.user?.settings?.isExperimentalFeatures === true) { this.dataService
this.dataService .fetchActivities({
.fetchActivities({ filters: this.userService.getFilters(),
filters: this.userService.getFilters(), skip: this.pageIndex * this.pageSize,
skip: this.pageIndex * this.pageSize, sortColumn: this.sortColumn,
sortColumn: this.sortColumn, sortDirection: this.sortDirection,
sortDirection: this.sortDirection, take: this.pageSize
take: this.pageSize })
}) .pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ activities, count }) => {
.subscribe(({ activities, count }) => { this.dataSource = new MatTableDataSource(activities);
this.dataSource = new MatTableDataSource(activities); this.totalItems = count;
this.totalItems = count;
if (this.hasPermissionToCreateActivity && this.totalItems <= 0) { if (this.hasPermissionToCreateActivity && this.totalItems <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } }); this.router.navigate([], { queryParams: { createDialog: true } });
} }
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} else {
this.dataService
.fetchActivities({})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.activities = activities;
if (
this.hasPermissionToCreateActivity &&
this.activities?.length <= 0
) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
this.changeDetectorRef.markForCheck();
});
}
} }
public onChangePage(page: PageEvent) { public onChangePage(page: PageEvent) {
@ -199,8 +181,14 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
} }
public onExport(activityIds?: string[]) { public onExport(activityIds?: string[]) {
let fetchExportParams: any = { activityIds };
if (!activityIds) {
fetchExportParams = { filters: this.userService.getFilters() };
}
this.dataService this.dataService
.fetchExport(activityIds) .fetchExport(fetchExportParams)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((data) => {
for (const activity of data.activities) { for (const activity of data.activities) {
@ -220,7 +208,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
public onExportDrafts(activityIds?: string[]) { public onExportDrafts(activityIds?: string[]) {
this.dataService this.dataService
.fetchExport(activityIds) .fetchExport({ activityIds })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((data) => {
downloadAsFile({ downloadAsFile({

View File

@ -2,8 +2,7 @@
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col"> <div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1>
<gf-activities-table-lazy <gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures === true"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="deviceType" [deviceType]="deviceType"
@ -20,31 +19,13 @@
(activityToClone)="onCloneActivity($event)" (activityToClone)="onCloneActivity($event)"
(activityToUpdate)="onUpdateActivity($event)" (activityToUpdate)="onUpdateActivity($event)"
(deleteAllActivities)="onDeleteAllActivities()" (deleteAllActivities)="onDeleteAllActivities()"
(export)="onExport($event)" (export)="onExport()"
(exportDrafts)="onExportDrafts($event)" (exportDrafts)="onExportDrafts($event)"
(import)="onImport()" (import)="onImport()"
(importDividends)="onImportDividends()" (importDividends)="onImportDividends()"
(pageChanged)="onChangePage($event)" (pageChanged)="onChangePage($event)"
(sortChanged)="onSortChanged($event)" (sortChanged)="onSortChanged($event)"
></gf-activities-table-lazy> />
<gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
[hasPermissionToExportActivities]="!hasImpersonationId"
[locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteActivity && !user.settings.isRestrictedView"
(activityDeleted)="onDeleteActivity($event)"
(activityToClone)="onCloneActivity($event)"
(activityToUpdate)="onUpdateActivity($event)"
(deleteAllActivities)="onDeleteAllActivities()"
(export)="onExport($event)"
(exportDrafts)="onExportDrafts($event)"
(import)="onImport()"
(importDividends)="onImportDividends()"
></gf-activities-table>
</div> </div>
</div> </div>

View File

@ -4,7 +4,6 @@ import { MatButtonModule } from '@angular/material/button';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service'; import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { ActivitiesPageRoutingModule } from './activities-page-routing.module'; import { ActivitiesPageRoutingModule } from './activities-page-routing.module';
@ -18,7 +17,6 @@ import { GfImportActivitiesDialogModule } from './import-activities-dialog/impor
ActivitiesPageRoutingModule, ActivitiesPageRoutingModule,
CommonModule, CommonModule,
GfActivitiesTableModule, GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfCreateOrUpdateActivityDialogModule, GfCreateOrUpdateActivityDialogModule,
GfImportActivitiesDialogModule, GfImportActivitiesDialogModule,
MatButtonModule, MatButtonModule,

View File

@ -82,8 +82,7 @@
class="mr-1" class="mr-1"
[tooltip]="account.Platform?.name" [tooltip]="account.Platform?.name"
[url]="account.Platform?.url" [url]="account.Platform?.url"
></gf-symbol-icon /><span>{{ account.name }}</span>
><span>{{ account.name }}</span>
</div> </div>
</mat-option> </mat-option>
</mat-select> </mat-select>
@ -357,7 +356,7 @@
[locale]="data.user?.settings?.locale" [locale]="data.user?.settings?.locale"
[unit]="activityForm.controls['currency']?.value ?? data.user?.settings?.baseCurrency" [unit]="activityForm.controls['currency']?.value ?? data.user?.settings?.baseCurrency"
[value]="total" [value]="total"
></gf-value> />
<div> <div>
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button> <button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
<button <button

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