Compare commits
50 Commits
Author | SHA1 | Date | |
---|---|---|---|
00a2b60eb5 | |||
fcbf2f1645 | |||
460266a501 | |||
9fe90273c7 | |||
4078229fe6 | |||
609c03f174 | |||
e7d4641d13 | |||
cc1d9811e0 | |||
35450ac004 | |||
9c18f48a32 | |||
87529490c3 | |||
893e76f83f | |||
06ba7a4b1b | |||
c68d113d27 | |||
69e3bee52c | |||
cea569c987 | |||
2a38a16f6b | |||
0f9455cf02 | |||
d4afa03505 | |||
c9237146e2 | |||
faad65b6f3 | |||
e459c72100 | |||
a8add30125 | |||
b535aee91d | |||
4434d0315f | |||
8b10695353 | |||
e82dcc8ace | |||
6dcb0d8583 | |||
40b6777814 | |||
25deba16df | |||
be93ca8968 | |||
0436cc6487 | |||
857708dc4d | |||
1ca4f885b0 | |||
c9368c5cf2 | |||
29423efea3 | |||
f3ee99fb2b | |||
3df8810412 | |||
b8ca88c6df | |||
2c068c412d | |||
9fdbd22cb5 | |||
8f5f4c5875 | |||
50fb82a6e6 | |||
2c10cd7edf | |||
bbde86c66e | |||
73c0843d51 | |||
04fc2cd3e1 | |||
b39c97ab9f | |||
1dd5e9c787 | |||
a9985b65b8 |
85
CHANGELOG.md
85
CHANGELOG.md
@ -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
|
||||||
|
|
||||||
|
@ -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/*
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
© 2021 - 2024 [Ghostfolio](https://ghostfol.io)
|
© 2021 - 2024 [Ghostfolio](https://ghostfol.io)
|
||||||
|
@ -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 {
|
||||||
|
@ -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';
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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 };
|
||||||
|
@ -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 })] =
|
||||||
|
@ -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[];
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import Big from 'big.js';
|
|
||||||
|
|
||||||
export interface TimelinePeriod {
|
|
||||||
date: string;
|
|
||||||
grossPerformance: Big;
|
|
||||||
investment: Big;
|
|
||||||
netPerformance: Big;
|
|
||||||
value: Big;
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
export type Accuracy = 'day' | 'month' | 'year';
|
|
||||||
|
|
||||||
export interface TimelineSpecification {
|
|
||||||
accuracy: Accuracy;
|
|
||||||
start: string;
|
|
||||||
}
|
|
@ -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 }
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 }
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 }
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 }
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
) {
|
) {
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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[];
|
||||||
|
@ -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
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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']
|
||||||
|
@ -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'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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('&', '&');
|
name = name.replace('&', '&');
|
||||||
|
|
||||||
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') {
|
||||||
|
@ -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,
|
||||||
|
@ -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`;
|
||||||
}
|
}
|
||||||
|
@ -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'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
|
@ -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;
|
||||||
}>;
|
}>;
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {};
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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>
|
/>
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
/>
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -19,5 +19,5 @@
|
|||||||
[theme]="{
|
[theme]="{
|
||||||
height: '100%'
|
height: '100%'
|
||||||
}"
|
}"
|
||||||
></ngx-skeleton-loader>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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
|
||||||
|
@ -38,10 +38,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacer {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
],
|
],
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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 = '*****';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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({
|
||||||
|
@ -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>
|
/>
|
||||||
|
@ -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,
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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?
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
19
apps/client/src/app/core/layout.service.ts
Normal file
19
apps/client/src/app/core/layout.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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.
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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({
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
Reference in New Issue
Block a user