Compare commits
43 Commits
Author | SHA1 | Date | |
---|---|---|---|
137e8e090a | |||
6d941500cd | |||
9c4e22978d | |||
af65a99398 | |||
1a0cb561cd | |||
9f875adf0c | |||
6b0dadb895 | |||
98a9523eee | |||
dfb3365efb | |||
0e08d8830e | |||
69d85eadfd | |||
c009f8c12f | |||
60ef46accf | |||
b12ac1fe84 | |||
4355c96ab6 | |||
fb326fe0cc | |||
02cfebd98c | |||
dd2936d703 | |||
918d0b85d4 | |||
a061595101 | |||
dcd496ac50 | |||
6b9ec549da | |||
b5bd4df483 | |||
766a2d7c2f | |||
6dabf7516a | |||
5d49ff7a4a | |||
8998c18836 | |||
741a0e36d2 | |||
e31b4c64cb | |||
812ff5cbdc | |||
035b90a689 | |||
5ea3a187f4 | |||
5d9c38663d | |||
5616bc4956 | |||
9ad1c2177c | |||
15bf9f2f9c | |||
ebc5008569 | |||
37759ba03f | |||
8319b216bb | |||
782d131b0d | |||
72e75208df | |||
4b1c27c245 | |||
61f0da35bc |
72
CHANGELOG.md
72
CHANGELOG.md
@ -5,6 +5,78 @@ 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.85.0 - 2024-06-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the ability to close a user account
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Upgraded `ng-extract-i18n-merge` from version `2.10.0` to `2.12.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the default locale in the value component
|
||||||
|
|
||||||
|
## 2.84.0 - 2024-06-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the data provider information to the asset profile details dialog of the admin control
|
||||||
|
- Added the cascading on delete for various relations in the database schema
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the initial annual interest rate in the _FIRE_ calculator
|
||||||
|
- Fixed the state handling in the currency selector
|
||||||
|
- Fixed the deletion of an asset profile with symbol profile overrides in the asset profile details dialog of the admin control
|
||||||
|
|
||||||
|
## 2.83.0 - 2024-05-30
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `@nestjs/passport` from version `10.0.0` to `10.0.3`
|
||||||
|
- Upgraded `angular` from version `17.3.5` to `17.3.10`
|
||||||
|
- Upgraded `class-validator` from version `0.14.0` to `0.14.1`
|
||||||
|
- Upgraded `countup.js` from version `2.3.2` to `2.8.0`
|
||||||
|
- Upgraded `Nx` from version `19.0.2` to `19.0.5`
|
||||||
|
- Upgraded `passport` from version `0.6.0` to `0.7.0`
|
||||||
|
- Upgraded `passport-jwt` from version `4.0.0` to `4.0.1`
|
||||||
|
- Upgraded `prisma` from version `5.13.0` to `5.14.0`
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.11.2` to `2.11.3`
|
||||||
|
|
||||||
|
## 2.82.0 - 2024-05-22
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the usability of the create or update activity dialog by preselecting the (only) account
|
||||||
|
- Improved the usability of the date range selector in the assistant
|
||||||
|
- Refactored the holding detail dialog to a standalone component
|
||||||
|
- Refreshed the cryptocurrencies list
|
||||||
|
- Refactored various pages to standalone components
|
||||||
|
- Upgraded `@internationalized/number` from version `3.5.0` to `3.5.2`
|
||||||
|
- Upgraded `body-parser` from version `1.20.1` to `1.20.2`
|
||||||
|
- Upgraded `zone.js` from version `0.14.4` to `0.14.5`
|
||||||
|
|
||||||
|
## 2.81.0 - 2024-05-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an indicator for active filters (experimental)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the delete all activities functionality on the portfolio activities page to work with the filters of the assistant
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Improved the language localization for Türkçe (`tr`)
|
||||||
|
- Upgraded `Nx` from version `18.3.3` to `19.0.2`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the position detail dialog close functionality
|
||||||
|
|
||||||
## 2.80.0 - 2024-05-08
|
## 2.80.0 - 2024-05-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -275,7 +275,7 @@ Are you building your own project? Add the `ghostfolio` topic to your _GitHub_ r
|
|||||||
|
|
||||||
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
||||||
|
|
||||||
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or post to [@ghostfolio\_](https://twitter.com/ghostfolio_) on _X_. We would love to hear from you.
|
Not sure what to work on? We have [some ideas](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22), even for [newcomers](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or post to [@ghostfolio\_](https://twitter.com/ghostfolio_) on _X_. We would love to hear from you.
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/accou
|
|||||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
|
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
|
||||||
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
|
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
@ -19,13 +17,11 @@ import { AccountService } from './account.service';
|
|||||||
imports: [
|
imports: [
|
||||||
AccountBalanceModule,
|
AccountBalanceModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataProviderModule,
|
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
ImpersonationModule,
|
ImpersonationModule,
|
||||||
PortfolioModule,
|
PortfolioModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisCacheModule,
|
RedactValuesInResponseModule
|
||||||
UserModule
|
|
||||||
],
|
],
|
||||||
providers: [AccountService]
|
providers: [AccountService]
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
||||||
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
|
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
import { ApiModule } from '@ghostfolio/api/services/api/api.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';
|
||||||
@ -27,7 +28,8 @@ import { QueueModule } from './queue/queue.module';
|
|||||||
PropertyModule,
|
PropertyModule,
|
||||||
QueueModule,
|
QueueModule,
|
||||||
SubscriptionModule,
|
SubscriptionModule,
|
||||||
SymbolProfileModule
|
SymbolProfileModule,
|
||||||
|
TransformDataSourceInRequestModule
|
||||||
],
|
],
|
||||||
controllers: [AdminController],
|
controllers: [AdminController],
|
||||||
providers: [AdminService],
|
providers: [AdminService],
|
||||||
|
@ -313,6 +313,12 @@ export class AdminService {
|
|||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (assetProfile) {
|
||||||
|
assetProfile.dataProviderInfo = this.dataProviderService
|
||||||
|
.getDataProvider(assetProfile.dataSource)
|
||||||
|
.getDataProviderInfo();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
marketData,
|
marketData,
|
||||||
assetProfile: assetProfile ?? {
|
assetProfile: assetProfile ?? {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller';
|
import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller';
|
||||||
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
@ -9,7 +8,6 @@ import { JwtModule } from '@nestjs/jwt';
|
|||||||
@Module({
|
@Module({
|
||||||
controllers: [AuthDeviceController],
|
controllers: [AuthDeviceController],
|
||||||
imports: [
|
imports: [
|
||||||
ConfigurationModule,
|
|
||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
secret: process.env.JWT_SECRET_KEY,
|
secret: process.env.JWT_SECRET_KEY,
|
||||||
signOptions: { expiresIn: '180 days' }
|
signOptions: { expiresIn: '180 days' }
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
@ -6,10 +5,7 @@ import { AuthDevice, Prisma } from '@prisma/client';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthDeviceService {
|
export class AuthDeviceService {
|
||||||
public constructor(
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
private readonly configurationService: ConfigurationService,
|
|
||||||
private readonly prismaService: PrismaService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async authDevice(
|
public async authDevice(
|
||||||
where: Prisma.AuthDeviceWhereUniqueInput
|
where: Prisma.AuthDeviceWhereUniqueInput
|
||||||
|
@ -3,7 +3,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Provider } from '@prisma/client';
|
import { Provider } from '@prisma/client';
|
||||||
import { Strategy } from 'passport-google-oauth20';
|
import { Profile, Strategy } from 'passport-google-oauth20';
|
||||||
|
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ import { AuthService } from './auth.service';
|
|||||||
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
readonly configurationService: ConfigurationService
|
private readonly configurationService: ConfigurationService
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
callbackURL: `${configurationService.get(
|
callbackURL: `${configurationService.get(
|
||||||
@ -20,7 +20,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
|||||||
clientID: configurationService.get('GOOGLE_CLIENT_ID'),
|
clientID: configurationService.get('GOOGLE_CLIENT_ID'),
|
||||||
clientSecret: configurationService.get('GOOGLE_SECRET'),
|
clientSecret: configurationService.get('GOOGLE_SECRET'),
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
scope: ['email', 'profile']
|
scope: ['profile']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,20 +28,17 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
|||||||
request: any,
|
request: any,
|
||||||
token: string,
|
token: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
profile,
|
profile: Profile,
|
||||||
done: Function,
|
done: Function,
|
||||||
done2: Function
|
done2: Function
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const jwt: string = await this.authService.validateOAuthLogin({
|
const jwt = await this.authService.validateOAuthLogin({
|
||||||
provider: Provider.GOOGLE,
|
provider: Provider.GOOGLE,
|
||||||
thirdPartyId: profile.id
|
thirdPartyId: profile.id
|
||||||
});
|
});
|
||||||
const user = {
|
|
||||||
jwt
|
|
||||||
};
|
|
||||||
|
|
||||||
done(null, user);
|
done(null, { jwt });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'GoogleStrategy');
|
Logger.error(error, 'GoogleStrategy');
|
||||||
done(error, false);
|
done(error, false);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import type {
|
import type {
|
||||||
BenchmarkMarketDataDetails,
|
BenchmarkMarketDataDetails,
|
||||||
BenchmarkResponse,
|
BenchmarkResponse,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
||||||
@ -17,7 +18,6 @@ import { BenchmarkService } from './benchmark.service';
|
|||||||
controllers: [BenchmarkController],
|
controllers: [BenchmarkController],
|
||||||
exports: [BenchmarkService],
|
exports: [BenchmarkService],
|
||||||
imports: [
|
imports: [
|
||||||
ConfigurationModule,
|
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
@ -25,7 +25,9 @@ import { BenchmarkService } from './benchmark.service';
|
|||||||
PropertyModule,
|
PropertyModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolModule,
|
SymbolModule,
|
||||||
SymbolProfileModule
|
SymbolProfileModule,
|
||||||
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule
|
||||||
],
|
],
|
||||||
providers: [BenchmarkService]
|
providers: [BenchmarkService]
|
||||||
})
|
})
|
||||||
|
16
apps/api/src/app/cache/cache.module.ts
vendored
16
apps/api/src/app/cache/cache.module.ts
vendored
@ -1,10 +1,4 @@
|
|||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
@ -12,14 +6,6 @@ import { CacheController } from './cache.controller';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [CacheController],
|
controllers: [CacheController],
|
||||||
imports: [
|
imports: [RedisCacheModule]
|
||||||
ConfigurationModule,
|
|
||||||
DataGatheringModule,
|
|
||||||
DataProviderModule,
|
|
||||||
ExchangeRateDataModule,
|
|
||||||
PrismaModule,
|
|
||||||
RedisCacheModule,
|
|
||||||
SymbolProfileModule
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class CacheModule {}
|
export class CacheModule {}
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
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 { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
@ -12,15 +8,7 @@ import { ExportController } from './export.controller';
|
|||||||
import { ExportService } from './export.service';
|
import { ExportService } from './export.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [AccountModule, ApiModule, OrderModule],
|
||||||
AccountModule,
|
|
||||||
ApiModule,
|
|
||||||
ConfigurationModule,
|
|
||||||
DataGatheringModule,
|
|
||||||
DataProviderModule,
|
|
||||||
OrderModule,
|
|
||||||
RedisCacheModule
|
|
||||||
],
|
|
||||||
controllers: [ExportController],
|
controllers: [ExportController],
|
||||||
providers: [ExportService]
|
providers: [ExportService]
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
|
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
|
|
||||||
@ -9,7 +9,11 @@ import { HealthService } from './health.service';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
imports: [ConfigurationModule, DataEnhancerModule, DataProviderModule],
|
imports: [
|
||||||
|
DataEnhancerModule,
|
||||||
|
DataProviderModule,
|
||||||
|
TransformDataSourceInRequestModule
|
||||||
|
],
|
||||||
providers: [HealthService]
|
providers: [HealthService]
|
||||||
})
|
})
|
||||||
export class HealthModule {}
|
export class HealthModule {}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { ImportResponse } from '@ghostfolio/common/interfaces';
|
import { ImportResponse } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
@ -4,6 +4,8 @@ import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
|||||||
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
||||||
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.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';
|
||||||
@ -30,7 +32,9 @@ import { ImportService } from './import.service';
|
|||||||
PortfolioModule,
|
PortfolioModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolProfileModule
|
SymbolProfileModule,
|
||||||
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule
|
||||||
],
|
],
|
||||||
providers: [ImportService]
|
providers: [ImportService]
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||||
|
@ -2,6 +2,7 @@ import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module'
|
|||||||
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.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';
|
||||||
@ -34,6 +35,7 @@ import { InfoService } from './info.service';
|
|||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolProfileModule,
|
SymbolProfileModule,
|
||||||
TagModule,
|
TagModule,
|
||||||
|
TransformDataSourceInResponseModule,
|
||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
providers: [InfoService]
|
providers: [InfoService]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
@ -8,7 +9,11 @@ import { LogoService } from './logo.service';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [LogoController],
|
controllers: [LogoController],
|
||||||
imports: [ConfigurationModule, SymbolProfileModule],
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
SymbolProfileModule,
|
||||||
|
TransformDataSourceInRequestModule
|
||||||
|
],
|
||||||
providers: [LogoService]
|
providers: [LogoService]
|
||||||
})
|
})
|
||||||
export class LogoModule {}
|
export class LogoModule {}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||||
@ -11,7 +11,7 @@ import {
|
|||||||
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
||||||
HEADER_KEY_IMPERSONATION
|
HEADER_KEY_IMPERSONATION
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -53,8 +53,20 @@ export class OrderController {
|
|||||||
@Delete()
|
@Delete()
|
||||||
@HasPermission(permissions.deleteOrder)
|
@HasPermission(permissions.deleteOrder)
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
public async deleteOrders(): Promise<number> {
|
public async deleteOrders(
|
||||||
|
@Query('accounts') filterByAccounts?: string,
|
||||||
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
|
@Query('tags') filterByTags?: string
|
||||||
|
): Promise<number> {
|
||||||
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
|
filterByAccounts,
|
||||||
|
filterByAssetClasses,
|
||||||
|
filterByTags
|
||||||
|
});
|
||||||
|
|
||||||
return this.orderService.deleteOrders({
|
return this.orderService.deleteOrders({
|
||||||
|
filters,
|
||||||
|
userCurrency: this.request.user.Settings.settings.baseCurrency,
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,10 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/accou
|
|||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
|
||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
import { ApiModule } from '@ghostfolio/api/services/api/api.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';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
@ -23,15 +24,16 @@ import { OrderService } from './order.service';
|
|||||||
imports: [
|
imports: [
|
||||||
ApiModule,
|
ApiModule,
|
||||||
CacheModule,
|
CacheModule,
|
||||||
ConfigurationModule,
|
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
ImpersonationModule,
|
ImpersonationModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
RedactValuesInResponseModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolProfileModule,
|
SymbolProfileModule,
|
||||||
UserModule
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule
|
||||||
],
|
],
|
||||||
providers: [AccountBalanceService, AccountService, OrderService]
|
providers: [AccountBalanceService, AccountService, OrderService]
|
||||||
})
|
})
|
||||||
|
@ -194,16 +194,36 @@ export class OrderService {
|
|||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteOrders(where: Prisma.OrderWhereInput): Promise<number> {
|
public async deleteOrders({
|
||||||
|
filters,
|
||||||
|
userCurrency,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
filters?: Filter[];
|
||||||
|
userCurrency: string;
|
||||||
|
userId: string;
|
||||||
|
}): Promise<number> {
|
||||||
|
const { activities } = await this.getOrders({
|
||||||
|
filters,
|
||||||
|
userId,
|
||||||
|
userCurrency,
|
||||||
|
includeDrafts: true,
|
||||||
|
withExcludedAccounts: true
|
||||||
|
});
|
||||||
|
|
||||||
const { count } = await this.prismaService.order.deleteMany({
|
const { count } = await this.prismaService.order.deleteMany({
|
||||||
where
|
where: {
|
||||||
|
id: {
|
||||||
|
in: activities.map(({ id }) => {
|
||||||
|
return id;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventEmitter.emit(
|
this.eventEmitter.emit(
|
||||||
PortfolioChangedEvent.getName(),
|
PortfolioChangedEvent.getName(),
|
||||||
new PortfolioChangedEvent({
|
new PortfolioChangedEvent({ userId })
|
||||||
userId: <string>where.userId
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
|
|
||||||
import { Account, Tag } from '@prisma/client';
|
import { Account, Tag } from '@prisma/client';
|
||||||
|
|
||||||
export interface PortfolioPositionDetail {
|
export interface PortfolioHoldingDetail {
|
||||||
accounts: Account[];
|
accounts: Account[];
|
||||||
averagePrice: number;
|
averagePrice: number;
|
||||||
dataProviderInfo: DataProviderInfo;
|
dataProviderInfo: DataProviderInfo;
|
@ -7,9 +7,9 @@ import {
|
|||||||
nullifyValuesInObject
|
nullifyValuesInObject
|
||||||
} from '@ghostfolio/api/helper/object.helper';
|
} from '@ghostfolio/api/helper/object.helper';
|
||||||
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
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';
|
||||||
@ -27,6 +27,10 @@ import {
|
|||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport
|
PortfolioReport
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import {
|
||||||
|
hasReadRestrictedAccessPermission,
|
||||||
|
isRestrictedView
|
||||||
|
} from '@ghostfolio/common/permissions';
|
||||||
import type {
|
import type {
|
||||||
DateRange,
|
DateRange,
|
||||||
GroupBy,
|
GroupBy,
|
||||||
@ -51,7 +55,7 @@ import { AssetClass, AssetSubClass } from '@prisma/client';
|
|||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface';
|
||||||
import { PortfolioService } from './portfolio.service';
|
import { PortfolioService } from './portfolio.service';
|
||||||
|
|
||||||
@Controller('portfolio')
|
@Controller('portfolio')
|
||||||
@ -84,11 +88,6 @@ export class PortfolioController {
|
|||||||
|
|
||||||
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';
|
||||||
@ -117,8 +116,11 @@ export class PortfolioController {
|
|||||||
let portfolioSummary = summary;
|
let portfolioSummary = summary;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasReadRestrictedAccessPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.userService.isRestrictedView(this.request.user)
|
impersonationId,
|
||||||
|
user: this.request.user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
const totalInvestment = Object.values(holdings)
|
const totalInvestment = Object.values(holdings)
|
||||||
.map(({ investment }) => {
|
.map(({ investment }) => {
|
||||||
@ -158,8 +160,11 @@ export class PortfolioController {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
hasDetails === false ||
|
hasDetails === false ||
|
||||||
hasReadRestrictedAccessPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.userService.isRestrictedView(this.request.user)
|
impersonationId,
|
||||||
|
user: this.request.user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
portfolioSummary = nullifyValuesInObject(summary, [
|
portfolioSummary = nullifyValuesInObject(summary, [
|
||||||
'cash',
|
'cash',
|
||||||
@ -226,12 +231,6 @@ 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,
|
||||||
@ -259,8 +258,11 @@ export class PortfolioController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasReadRestrictedAccessPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.userService.isRestrictedView(this.request.user)
|
impersonationId,
|
||||||
|
user: this.request.user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
const maxDividend = dividends.reduce(
|
const maxDividend = dividends.reduce(
|
||||||
(investment, item) => Math.max(investment, item.investment),
|
(investment, item) => Math.max(investment, item.investment),
|
||||||
@ -326,12 +328,6 @@ 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,
|
||||||
@ -347,8 +343,11 @@ export class PortfolioController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasReadRestrictedAccessPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.userService.isRestrictedView(this.request.user)
|
impersonationId,
|
||||||
|
user: this.request.user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
const maxInvestment = investments.reduce(
|
const maxInvestment = investments.reduce(
|
||||||
(investment, item) => Math.max(investment, item.investment),
|
(investment, item) => Math.max(investment, item.investment),
|
||||||
@ -397,12 +396,6 @@ export class PortfolioController {
|
|||||||
): Promise<PortfolioPerformanceResponse> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
||||||
|
|
||||||
const hasReadRestrictedAccessPermission =
|
|
||||||
this.userService.hasReadRestrictedAccessPermission({
|
|
||||||
impersonationId,
|
|
||||||
user: this.request.user
|
|
||||||
});
|
|
||||||
|
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
filterByAccounts,
|
filterByAccounts,
|
||||||
filterByAssetClasses,
|
filterByAssetClasses,
|
||||||
@ -418,9 +411,12 @@ export class PortfolioController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasReadRestrictedAccessPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.request.user.Settings.settings.viewMode === 'ZEN' ||
|
impersonationId,
|
||||||
this.userService.isRestrictedView(this.request.user)
|
user: this.request.user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(this.request.user) ||
|
||||||
|
this.request.user.Settings.settings.viewMode === 'ZEN'
|
||||||
) {
|
) {
|
||||||
performanceInformation.chart = performanceInformation.chart.map(
|
performanceInformation.chart = performanceInformation.chart.map(
|
||||||
({
|
({
|
||||||
@ -569,7 +565,7 @@ export class PortfolioController {
|
|||||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
||||||
@Param('dataSource') dataSource,
|
@Param('dataSource') dataSource,
|
||||||
@Param('symbol') symbol
|
@Param('symbol') symbol
|
||||||
): Promise<PortfolioPositionDetail> {
|
): Promise<PortfolioHoldingDetail> {
|
||||||
const position = await this.portfolioService.getPosition(
|
const position = await this.portfolioService.getPosition(
|
||||||
dataSource,
|
dataSource,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
|
@ -4,6 +4,9 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
|||||||
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 { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
|
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
|
||||||
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
import { ApiModule } from '@ghostfolio/api/services/api/api.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';
|
||||||
@ -36,8 +39,11 @@ import { RulesService } from './rules.service';
|
|||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
OrderModule,
|
OrderModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
RedactValuesInResponseModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolProfileModule,
|
SymbolProfileModule,
|
||||||
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule,
|
||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@ -77,7 +77,7 @@ import {
|
|||||||
PerformanceCalculationType,
|
PerformanceCalculationType,
|
||||||
PortfolioCalculatorFactory
|
PortfolioCalculatorFactory
|
||||||
} from './calculator/portfolio-calculator.factory';
|
} from './calculator/portfolio-calculator.factory';
|
||||||
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface';
|
||||||
import { RulesService } from './rules.service';
|
import { RulesService } from './rules.service';
|
||||||
|
|
||||||
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
|
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
|
||||||
@ -602,7 +602,7 @@ export class PortfolioService {
|
|||||||
aDataSource: DataSource,
|
aDataSource: DataSource,
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aSymbol: string
|
aSymbol: string
|
||||||
): Promise<PortfolioPositionDetail> {
|
): Promise<PortfolioHoldingDetail> {
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
const userCurrency = this.getUserCurrency(user);
|
const userCurrency = this.getUserCurrency(user);
|
||||||
@ -693,7 +693,7 @@ export class PortfolioService {
|
|||||||
transactionCount
|
transactionCount
|
||||||
} = position;
|
} = position;
|
||||||
|
|
||||||
const accounts: PortfolioPositionDetail['accounts'] = uniqBy(
|
const accounts: PortfolioHoldingDetail['accounts'] = uniqBy(
|
||||||
orders.filter(({ Account }) => {
|
orders.filter(({ Account }) => {
|
||||||
return Account;
|
return Account;
|
||||||
}),
|
}),
|
||||||
|
@ -1,25 +1,8 @@
|
|||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { SitemapController } from './sitemap.controller';
|
import { SitemapController } from './sitemap.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [SitemapController],
|
controllers: [SitemapController]
|
||||||
imports: [
|
|
||||||
ConfigurationModule,
|
|
||||||
DataGatheringModule,
|
|
||||||
DataProviderModule,
|
|
||||||
ExchangeRateDataModule,
|
|
||||||
PrismaModule,
|
|
||||||
RedisCacheModule,
|
|
||||||
SymbolProfileModule
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class SitemapModule {}
|
export class SitemapModule {}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
@ -12,10 +13,11 @@ import { SymbolService } from './symbol.service';
|
|||||||
controllers: [SymbolController],
|
controllers: [SymbolController],
|
||||||
exports: [SymbolService],
|
exports: [SymbolService],
|
||||||
imports: [
|
imports: [
|
||||||
ConfigurationModule,
|
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
PrismaModule
|
PrismaModule,
|
||||||
|
TransformDataSourceInRequestModule,
|
||||||
|
TransformDataSourceInResponseModule
|
||||||
],
|
],
|
||||||
providers: [SymbolService]
|
providers: [SymbolService]
|
||||||
})
|
})
|
||||||
|
6
apps/api/src/app/user/delete-own-user.dto.ts
Normal file
6
apps/api/src/app/user/delete-own-user.dto.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class DeleteOwnUserDto {
|
||||||
|
@IsString()
|
||||||
|
accessToken: string;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { User, UserSettings } from '@ghostfolio/common/interfaces';
|
import { User, UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
@ -25,6 +26,7 @@ import { User as UserModel } from '@prisma/client';
|
|||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
import { size } from 'lodash';
|
import { size } from 'lodash';
|
||||||
|
|
||||||
|
import { DeleteOwnUserDto } from './delete-own-user.dto';
|
||||||
import { UserItem } from './interfaces/user-item.interface';
|
import { UserItem } from './interfaces/user-item.interface';
|
||||||
import { UpdateUserSettingDto } from './update-user-setting.dto';
|
import { UpdateUserSettingDto } from './update-user-setting.dto';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
@ -32,12 +34,41 @@ import { UserService } from './user.service';
|
|||||||
@Controller('user')
|
@Controller('user')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly propertyService: PropertyService,
|
private readonly propertyService: PropertyService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
private readonly userService: UserService
|
private readonly userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Delete()
|
||||||
|
@HasPermission(permissions.deleteOwnUser)
|
||||||
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
|
public async deleteOwnUser(
|
||||||
|
@Body() data: DeleteOwnUserDto
|
||||||
|
): Promise<UserModel> {
|
||||||
|
const hashedAccessToken = this.userService.createAccessToken(
|
||||||
|
data.accessToken,
|
||||||
|
this.configurationService.get('ACCESS_TOKEN_SALT')
|
||||||
|
);
|
||||||
|
|
||||||
|
const [user] = await this.userService.users({
|
||||||
|
where: { accessToken: hashedAccessToken, id: this.request.user.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userService.deleteUser({
|
||||||
|
accessToken: hashedAccessToken,
|
||||||
|
id: user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HasPermission(permissions.deleteUser)
|
@HasPermission(permissions.deleteUser)
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
|
@ -121,28 +121,6 @@ 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) {
|
|
||||||
return aUser.Settings.settings.isRestrictedView ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async user(
|
public async user(
|
||||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
||||||
): Promise<UserWithSettings | null> {
|
): Promise<UserWithSettings | null> {
|
||||||
@ -262,10 +240,13 @@ export class UserService {
|
|||||||
|
|
||||||
// Reset benchmark
|
// Reset benchmark
|
||||||
user.Settings.settings.benchmark = undefined;
|
user.Settings.settings.benchmark = undefined;
|
||||||
}
|
} else if (user.subscription?.type === 'Premium') {
|
||||||
|
|
||||||
if (user.subscription?.type === 'Premium') {
|
|
||||||
currentPermissions.push(permissions.reportDataGlitch);
|
currentPermissions.push(permissions.reportDataGlitch);
|
||||||
|
|
||||||
|
currentPermissions = without(
|
||||||
|
currentPermissions,
|
||||||
|
permissions.deleteOwnUser
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -146,6 +146,10 @@
|
|||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-justetf</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-justetf</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-koyfin</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-kubera</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-kubera</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -170,6 +174,10 @@
|
|||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monse</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monse</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-navexa</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-parqet</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-parqet</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -182,6 +190,10 @@
|
|||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portfolio-dividend-tracker</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portfolio-dividend-tracker</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-portfolio-visualizer</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-portseido</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portseido</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -210,6 +222,10 @@
|
|||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-snowball-analytics</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-snowball-analytics</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-stock-events</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-stockle</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockle</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -218,6 +234,10 @@
|
|||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockmarketeye</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockmarketeye</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-stonksfolio</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-sumio</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-sumio</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -234,6 +254,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-wallmine</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-wealthfolio</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthfolio</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -500,6 +524,10 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-justetf</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-justetf</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-koyfin</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-kubera</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-kubera</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -524,6 +552,10 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</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-navexa</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-parqet</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-parqet</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -536,6 +568,10 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portfolio-dividend-tracker</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portfolio-dividend-tracker</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-portfolio-visualizer</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-portseido</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portseido</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -564,6 +600,10 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</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-stock-events</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-stockle</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockle</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -572,6 +612,10 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockmarketeye</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockmarketeye</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-stonksfolio</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-sumio</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sumio</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -588,6 +632,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-wallmine</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-wealthfolio</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthfolio</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -866,6 +914,10 @@
|
|||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-justetf</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-justetf</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-koyfin</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-kubera</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-kubera</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -890,6 +942,10 @@
|
|||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</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-navexa</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-parqet</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-parqet</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -902,6 +958,10 @@
|
|||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portfolio-dividend-tracker</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portfolio-dividend-tracker</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-portfolio-visualizer</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-portseido</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portseido</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -930,6 +990,10 @@
|
|||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-snowball-analytics</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-snowball-analytics</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-stock-events</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-stockle</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockle</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -938,6 +1002,10 @@
|
|||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockmarketeye</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockmarketeye</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-stonksfolio</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-sumio</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sumio</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -954,6 +1022,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-wallmine</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-wealthfolio</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthfolio</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -1078,6 +1150,10 @@
|
|||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-justetf</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-justetf</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-koyfin</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-kubera</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-kubera</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -1102,6 +1178,10 @@
|
|||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</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-navexa</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-parqet</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-parqet</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -1114,6 +1194,10 @@
|
|||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portfolio-dividend-tracker</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portfolio-dividend-tracker</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-portfolio-visualizer</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-portseido</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portseido</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -1142,6 +1226,10 @@
|
|||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-snowball-analytics</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-snowball-analytics</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-stock-events</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-stockle</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockle</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -1150,6 +1238,10 @@
|
|||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockmarketeye</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockmarketeye</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-stonksfolio</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-sumio</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sumio</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -1166,6 +1258,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-wallmine</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-wealthfolio</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthfolio</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
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 {
|
||||||
|
hasReadRestrictedAccessPermission,
|
||||||
|
isRestrictedView
|
||||||
|
} from '@ghostfolio/common/permissions';
|
||||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -16,7 +19,7 @@ import { map } from 'rxjs/operators';
|
|||||||
export class RedactValuesInResponseInterceptor<T>
|
export class RedactValuesInResponseInterceptor<T>
|
||||||
implements NestInterceptor<T, any>
|
implements NestInterceptor<T, any>
|
||||||
{
|
{
|
||||||
public constructor(private userService: UserService) {}
|
public constructor() {}
|
||||||
|
|
||||||
public intercept(
|
public intercept(
|
||||||
context: ExecutionContext,
|
context: ExecutionContext,
|
||||||
@ -29,15 +32,13 @@ export class RedactValuesInResponseInterceptor<T>
|
|||||||
|
|
||||||
const impersonationId =
|
const impersonationId =
|
||||||
headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
|
headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
|
||||||
const hasReadRestrictedPermission =
|
|
||||||
this.userService.hasReadRestrictedAccessPermission({
|
|
||||||
impersonationId,
|
|
||||||
user
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasReadRestrictedPermission ||
|
hasReadRestrictedAccessPermission({
|
||||||
this.userService.isRestrictedView(user)
|
impersonationId,
|
||||||
|
user
|
||||||
|
}) ||
|
||||||
|
isRestrictedView(user)
|
||||||
) {
|
) {
|
||||||
data = redactAttributes({
|
data = redactAttributes({
|
||||||
object: data,
|
object: data,
|
@ -0,0 +1,4 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class RedactValuesInResponseModule {}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
|
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [ConfigurationService],
|
||||||
|
imports: [ConfigurationModule],
|
||||||
|
providers: [ConfigurationService]
|
||||||
|
})
|
||||||
|
export class TransformDataSourceInRequestModule {}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
|
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [ConfigurationService],
|
||||||
|
imports: [ConfigurationModule],
|
||||||
|
providers: [ConfigurationService]
|
||||||
|
})
|
||||||
|
export class TransformDataSourceInResponseModule {}
|
@ -2,7 +2,7 @@ import { Logger, ValidationPipe, VersioningType } from '@nestjs/common';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import type { NestExpressApplication } from '@nestjs/platform-express';
|
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import * as bodyParser from 'body-parser';
|
import { json } from 'body-parser';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
|
|
||||||
import { AppModule } from './app/app.module';
|
import { AppModule } from './app/app.module';
|
||||||
@ -34,7 +34,7 @@ async function bootstrap() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Support 10mb csv/json files for importing activities
|
// Support 10mb csv/json files for importing activities
|
||||||
app.use(bodyParser.json({ limit: '10mb' }));
|
app.use(json({ limit: '10mb' }));
|
||||||
|
|
||||||
if (configService.get<string>('ENABLE_FEATURE_SUBSCRIPTION') === 'true') {
|
if (configService.get<string>('ENABLE_FEATURE_SUBSCRIPTION') === 'true') {
|
||||||
app.use(
|
app.use(
|
||||||
|
@ -50,7 +50,9 @@ export class AlphaVantageService implements DataProviderInterface {
|
|||||||
|
|
||||||
public getDataProviderInfo(): DataProviderInfo {
|
public getDataProviderInfo(): DataProviderInfo {
|
||||||
return {
|
return {
|
||||||
isPremium: false
|
isPremium: false,
|
||||||
|
name: 'Alpha Vantage',
|
||||||
|
url: 'https://www.alphavantage.co'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -598,10 +598,14 @@ export class DataProviderService {
|
|||||||
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
|
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
|
||||||
})
|
})
|
||||||
.map((lookupItem) => {
|
.map((lookupItem) => {
|
||||||
if (
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') ||
|
if (user.subscription.type === 'Premium') {
|
||||||
user.subscription.type === 'Premium'
|
lookupItem.dataProviderInfo.isPremium = false;
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
lookupItem.dataProviderInfo.name = undefined;
|
||||||
|
lookupItem.dataProviderInfo.url = undefined;
|
||||||
|
} else {
|
||||||
lookupItem.dataProviderInfo.isPremium = false;
|
lookupItem.dataProviderInfo.isPremium = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
|
|
||||||
public getDataProviderInfo(): DataProviderInfo {
|
public getDataProviderInfo(): DataProviderInfo {
|
||||||
return {
|
return {
|
||||||
isPremium: true
|
isPremium: true,
|
||||||
|
name: 'EOD Historical Data',
|
||||||
|
url: 'https://eodhd.com'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,9 @@ export class GoogleSheetsService implements DataProviderInterface {
|
|||||||
|
|
||||||
public getDataProviderInfo(): DataProviderInfo {
|
public getDataProviderInfo(): DataProviderInfo {
|
||||||
return {
|
return {
|
||||||
isPremium: false
|
isPremium: false,
|
||||||
|
name: 'Google Sheets',
|
||||||
|
url: 'https://docs.google.com/spreadsheets'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,9 @@ export class RapidApiService implements DataProviderInterface {
|
|||||||
|
|
||||||
public getDataProviderInfo(): DataProviderInfo {
|
public getDataProviderInfo(): DataProviderInfo {
|
||||||
return {
|
return {
|
||||||
isPremium: false
|
isPremium: false,
|
||||||
|
name: 'Rapid API',
|
||||||
|
url: 'https://rapidapi.com'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,9 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
public getDataProviderInfo(): DataProviderInfo {
|
public getDataProviderInfo(): DataProviderInfo {
|
||||||
return {
|
return {
|
||||||
isPremium: false
|
isPremium: false,
|
||||||
|
name: 'Yahoo Finance',
|
||||||
|
url: 'https://finance.yahoo.com'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ export class ExchangeRateDataService {
|
|||||||
);
|
);
|
||||||
const lastDateString = dateStrings.reduce((a, b) => {
|
const lastDateString = dateStrings.reduce((a, b) => {
|
||||||
return a > b ? a : b;
|
return a > b ? a : b;
|
||||||
});
|
}, undefined);
|
||||||
|
|
||||||
let previousExchangeRate =
|
let previousExchangeRate =
|
||||||
exchangeRatesByCurrency[`${currency}${targetCurrency}`]?.[
|
exchangeRatesByCurrency[`${currency}${targetCurrency}`]?.[
|
||||||
|
@ -61,7 +61,9 @@ export class SymbolProfileService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
.then((symbolProfiles) => {
|
||||||
|
return this.enhanceSymbolProfiles(symbolProfiles);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSymbolProfilesByIds(
|
public async getSymbolProfilesByIds(
|
||||||
@ -83,7 +85,9 @@ export class SymbolProfileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
.then((symbolProfiles) => {
|
||||||
|
return this.enhanceSymbolProfiles(symbolProfiles);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateSymbolProfile({
|
public updateSymbolProfile({
|
||||||
@ -119,7 +123,7 @@ export class SymbolProfileService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSymbols(
|
private enhanceSymbolProfiles(
|
||||||
symbolProfiles: (SymbolProfile & {
|
symbolProfiles: (SymbolProfile & {
|
||||||
_count: { Order: number };
|
_count: { Order: number };
|
||||||
Order?: {
|
Order?: {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
import { paths } from '@ghostfolio/client/core/paths';
|
||||||
import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy';
|
import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy';
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
@ -5,18 +7,6 @@ import { RouterModule, Routes, TitleStrategy } from '@angular/router';
|
|||||||
|
|
||||||
import { ModulePreloadService } from './core/module-preload.service';
|
import { ModulePreloadService } from './core/module-preload.service';
|
||||||
|
|
||||||
export const paths = {
|
|
||||||
about: $localize`about`,
|
|
||||||
faq: $localize`faq`,
|
|
||||||
features: $localize`features`,
|
|
||||||
license: $localize`license`,
|
|
||||||
markets: $localize`markets`,
|
|
||||||
pricing: $localize`pricing`,
|
|
||||||
privacyPolicy: $localize`privacy-policy`,
|
|
||||||
register: $localize`register`,
|
|
||||||
resources: $localize`resources`
|
|
||||||
};
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: paths.about,
|
path: paths.about,
|
||||||
@ -53,9 +43,12 @@ const routes: Routes = [
|
|||||||
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'demo',
|
canActivate: [AuthGuard],
|
||||||
loadChildren: () =>
|
loadComponent: () =>
|
||||||
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
|
import('./pages/demo/demo-page.component').then(
|
||||||
|
(c) => c.GfDemoPageComponent
|
||||||
|
),
|
||||||
|
path: 'demo'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: paths.faq,
|
path: paths.faq,
|
||||||
@ -63,11 +56,13 @@ const routes: Routes = [
|
|||||||
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./pages/features/features-page.component').then(
|
||||||
|
(c) => c.GfFeaturesPageComponent
|
||||||
|
),
|
||||||
path: paths.features,
|
path: paths.features,
|
||||||
loadChildren: () =>
|
title: $localize`Features`
|
||||||
import('./pages/features/features-page.module').then(
|
|
||||||
(m) => m.FeaturesPageModule
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'home',
|
path: 'home',
|
||||||
@ -75,9 +70,13 @@ const routes: Routes = [
|
|||||||
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./pages/i18n/i18n-page.component').then(
|
||||||
|
(c) => c.GfI18nPageComponent
|
||||||
|
),
|
||||||
path: 'i18n',
|
path: 'i18n',
|
||||||
loadChildren: () =>
|
title: $localize`Internationalization`
|
||||||
import('./pages/i18n/i18n-page.module').then((m) => m.I18nPageModule)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: paths.markets,
|
path: paths.markets,
|
||||||
@ -134,11 +133,12 @@ const routes: Routes = [
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./pages/webauthn/webauthn-page.component').then(
|
||||||
|
(c) => c.GfWebauthnPageComponent
|
||||||
|
),
|
||||||
path: 'webauthn',
|
path: 'webauthn',
|
||||||
loadChildren: () =>
|
title: $localize`Sign in`
|
||||||
import('./pages/webauthn/webauthn-page.module').then(
|
|
||||||
(m) => m.WebauthnPageModule
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'zen',
|
path: 'zen',
|
||||||
|
@ -158,11 +158,9 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
||||||
</li>
|
</li>
|
||||||
<!--
|
<li>
|
||||||
<li>
|
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
|
||||||
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
|
</li>
|
||||||
</li>
|
|
||||||
-->
|
|
||||||
<!--
|
<!--
|
||||||
<li>
|
<li>
|
||||||
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>
|
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { GfHoldingDetailDialogComponent } from '@ghostfolio/client/components/holding-detail-dialog/holding-detail-dialog.component';
|
||||||
|
import { HoldingDetailDialogParams } from '@ghostfolio/client/components/holding-detail-dialog/interfaces/interfaces';
|
||||||
import { getCssVariable } from '@ghostfolio/common/helper';
|
import { getCssVariable } from '@ghostfolio/common/helper';
|
||||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
@ -13,13 +15,21 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { NavigationEnd, PRIMARY_OUTLET, Router } from '@angular/router';
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
NavigationEnd,
|
||||||
|
PRIMARY_OUTLET,
|
||||||
|
Router
|
||||||
|
} from '@angular/router';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { filter, takeUntil } from 'rxjs/operators';
|
import { filter, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { DataService } from './services/data.service';
|
import { DataService } from './services/data.service';
|
||||||
|
import { ImpersonationStorageService } from './services/impersonation-storage.service';
|
||||||
import { TokenStorageService } from './services/token-storage.service';
|
import { TokenStorageService } from './services/token-storage.service';
|
||||||
import { UserService } from './services/user/user.service';
|
import { UserService } from './services/user/user.service';
|
||||||
|
|
||||||
@ -38,6 +48,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
public currentRoute: string;
|
public currentRoute: string;
|
||||||
public currentYear = new Date().getFullYear();
|
public currentYear = new Date().getFullYear();
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
public hasInfoMessage: boolean;
|
public hasInfoMessage: boolean;
|
||||||
public hasPermissionForStatistics: boolean;
|
public hasPermissionForStatistics: boolean;
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
@ -67,7 +78,10 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
|
private dialog: MatDialog,
|
||||||
@Inject(DOCUMENT) private document: Document,
|
@Inject(DOCUMENT) private document: Document,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private title: Title,
|
private title: Title,
|
||||||
private tokenStorageService: TokenStorageService,
|
private tokenStorageService: TokenStorageService,
|
||||||
@ -75,6 +89,21 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
) {
|
) {
|
||||||
this.initializeTheme();
|
this.initializeTheme();
|
||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
|
|
||||||
|
this.route.queryParams
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((params) => {
|
||||||
|
if (
|
||||||
|
params['dataSource'] &&
|
||||||
|
params['holdingDetailDialog'] &&
|
||||||
|
params['symbol']
|
||||||
|
) {
|
||||||
|
this.openHoldingDetailDialog({
|
||||||
|
dataSource: params['dataSource'],
|
||||||
|
symbol: params['symbol']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
@ -96,6 +125,13 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
permissions.enableFearAndGreedIndex
|
permissions.enableFearAndGreedIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((impersonationId) => {
|
||||||
|
this.hasImpersonationId = !!impersonationId;
|
||||||
|
});
|
||||||
|
|
||||||
this.router.events
|
this.router.events
|
||||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
@ -197,6 +233,55 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openHoldingDetailDialog({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(GfHoldingDetailDialogComponent, {
|
||||||
|
autoFocus: false,
|
||||||
|
data: <HoldingDetailDialogParams>{
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
|
colorScheme: this.user?.settings?.colorScheme,
|
||||||
|
deviceType: this.deviceType,
|
||||||
|
hasImpersonationId: this.hasImpersonationId,
|
||||||
|
hasPermissionToReportDataGlitch: hasPermission(
|
||||||
|
this.user?.permissions,
|
||||||
|
permissions.reportDataGlitch
|
||||||
|
),
|
||||||
|
locale: this.user?.settings?.locale
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: {
|
||||||
|
dataSource: null,
|
||||||
|
holdingDetailDialog: null,
|
||||||
|
symbol: null
|
||||||
|
},
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
relativeTo: this.route
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private toggleTheme(isDarkTheme: boolean) {
|
private toggleTheme(isDarkTheme: boolean) {
|
||||||
const themeColor = getCssVariable(
|
const themeColor = getCssVariable(
|
||||||
isDarkTheme ? '--dark-background' : '--light-background'
|
isDarkTheme ? '--dark-background' : '--light-background'
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { CreateAccountBalanceDto } from '@ghostfolio/api/app/account-balance/create-account-balance.dto';
|
||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
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';
|
||||||
@ -95,19 +96,9 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onAddAccountBalance({
|
public onAddAccountBalance(accountBalance: CreateAccountBalanceDto) {
|
||||||
balance,
|
|
||||||
date
|
|
||||||
}: {
|
|
||||||
balance: number;
|
|
||||||
date: Date;
|
|
||||||
}) {
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.postAccountBalance({
|
.postAccountBalance(accountBalance)
|
||||||
balance,
|
|
||||||
date,
|
|
||||||
accountId: this.data.accountId
|
|
||||||
})
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.fetchAccount();
|
this.fetchAccount();
|
||||||
|
@ -94,6 +94,7 @@
|
|||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
[deviceType]="data.deviceType"
|
[deviceType]="data.deviceType"
|
||||||
[hasPermissionToCreateActivity]="false"
|
[hasPermissionToCreateActivity]="false"
|
||||||
|
[hasPermissionToDeleteActivity]="false"
|
||||||
[hasPermissionToExportActivities]="
|
[hasPermissionToExportActivities]="
|
||||||
!data.hasImpersonationId && !user.settings.isRestrictedView
|
!data.hasImpersonationId && !user.settings.isRestrictedView
|
||||||
"
|
"
|
||||||
|
@ -8,7 +8,6 @@ import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
|
|||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
AdminMarketDataDetails,
|
AdminMarketDataDetails,
|
||||||
Currency,
|
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
@ -73,7 +72,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
public countries: {
|
public countries: {
|
||||||
[code: string]: { name: string; value: number };
|
[code: string]: { name: string; value: number };
|
||||||
};
|
};
|
||||||
public currencies: Currency[] = [];
|
public currencies: string[] = [];
|
||||||
public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix;
|
public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix;
|
||||||
public isBenchmark = false;
|
public isBenchmark = false;
|
||||||
public marketDataDetails: MarketData[] = [];
|
public marketDataDetails: MarketData[] = [];
|
||||||
@ -102,10 +101,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
const { benchmarks, currencies } = this.dataService.fetchInfo();
|
const { benchmarks, currencies } = this.dataService.fetchInfo();
|
||||||
|
|
||||||
this.benchmarks = benchmarks;
|
this.benchmarks = benchmarks;
|
||||||
this.currencies = currencies.map((currency) => ({
|
this.currencies = currencies;
|
||||||
label: currency,
|
|
||||||
value: currency
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
@ -293,9 +289,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
assetClass: this.assetProfileForm.get('assetClass').value,
|
assetClass: this.assetProfileForm.get('assetClass').value,
|
||||||
assetSubClass: this.assetProfileForm.get('assetSubClass').value,
|
assetSubClass: this.assetProfileForm.get('assetSubClass').value,
|
||||||
comment: this.assetProfileForm.get('comment').value || null,
|
comment: this.assetProfileForm.get('comment').value || null,
|
||||||
currency: (<Currency>(
|
currency: this.assetProfileForm.get('currency').value,
|
||||||
(<unknown>this.assetProfileForm.get('currency').value)
|
|
||||||
))?.value,
|
|
||||||
name: this.assetProfileForm.get('name').value,
|
name: this.assetProfileForm.get('name').value,
|
||||||
url: this.assetProfileForm.get('url').value || null
|
url: this.assetProfileForm.get('url').value || null
|
||||||
};
|
};
|
||||||
@ -343,8 +337,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
' ' +
|
' ' +
|
||||||
price +
|
price +
|
||||||
' ' +
|
' ' +
|
||||||
(<Currency>(<unknown>this.assetProfileForm.get('currency').value))
|
this.assetProfileForm.get('currency').value
|
||||||
?.value
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -115,11 +115,22 @@
|
|||||||
>Symbol</gf-value
|
>Symbol</gf-value
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<gf-value
|
||||||
|
i18n
|
||||||
|
size="medium"
|
||||||
|
[value]="
|
||||||
|
assetProfile?.dataProviderInfo?.name ?? assetProfile?.dataSource
|
||||||
|
"
|
||||||
|
>Data Source</gf-value
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value i18n size="medium" [value]="assetProfile?.currency"
|
<gf-value i18n size="medium" [value]="assetProfile?.currency"
|
||||||
>Currency</gf-value
|
>Currency</gf-value
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-6 mb-3"></div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
i18n
|
i18n
|
||||||
|
@ -116,7 +116,12 @@
|
|||||||
#assistantTrigger="matMenuTrigger"
|
#assistantTrigger="matMenuTrigger"
|
||||||
class="h-100 no-min-width px-2"
|
class="h-100 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
|
matBadge="✓"
|
||||||
|
matBadgeSize="small"
|
||||||
[mat-menu-trigger-for]="assistantMenu"
|
[mat-menu-trigger-for]="assistantMenu"
|
||||||
|
[matBadgeHidden]="
|
||||||
|
!hasFilters || !user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
[matMenuTriggerRestoreFocus]="false"
|
[matMenuTriggerRestoreFocus]="false"
|
||||||
(menuOpened)="onOpenAssistant()"
|
(menuOpened)="onOpenAssistant()"
|
||||||
>
|
>
|
||||||
|
@ -28,6 +28,17 @@
|
|||||||
text-underline-offset: 0.25rem;
|
text-underline-offset: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mat-badge {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-badge-content {
|
||||||
|
--mat-badge-small-size-container-overlap-offset: -0.9rem;
|
||||||
|
--mat-badge-small-size-text-size: 0;
|
||||||
|
|
||||||
|
transform: scale(0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@ export class HeaderComponent implements OnChanges {
|
|||||||
@ViewChild('assistant') assistantElement: GfAssistantComponent;
|
@ViewChild('assistant') assistantElement: GfAssistantComponent;
|
||||||
@ViewChild('assistantTrigger') assistentMenuTriggerElement: MatMenuTrigger;
|
@ViewChild('assistantTrigger') assistentMenuTriggerElement: MatMenuTrigger;
|
||||||
|
|
||||||
|
public hasFilters: boolean;
|
||||||
public hasPermissionForSocialLogin: boolean;
|
public hasPermissionForSocialLogin: boolean;
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
public hasPermissionToAccessAdminControl: boolean;
|
public hasPermissionToAccessAdminControl: boolean;
|
||||||
@ -106,6 +107,8 @@ export class HeaderComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
|
this.hasFilters = this.userService.hasFilters();
|
||||||
|
|
||||||
this.hasPermissionForSocialLogin = hasPermission(
|
this.hasPermissionForSocialLogin = hasPermission(
|
||||||
this.info?.globalPermissions,
|
this.info?.globalPermissions,
|
||||||
permissions.enableSocialLogin
|
permissions.enableSocialLogin
|
||||||
|
@ -5,6 +5,7 @@ import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
|||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatBadgeModule } from '@angular/material/badge';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
@ -21,6 +22,7 @@ import { HeaderComponent } from './header.component';
|
|||||||
GfLogoComponent,
|
GfLogoComponent,
|
||||||
GfPremiumIndicatorComponent,
|
GfPremiumIndicatorComponent,
|
||||||
LoginWithAccessTokenDialogModule,
|
LoginWithAccessTokenDialogModule,
|
||||||
|
MatBadgeModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatToolbarModule,
|
MatToolbarModule,
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||||
|
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||||
|
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||||
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 { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
@ -8,9 +11,16 @@ import {
|
|||||||
LineChartItem,
|
LineChartItem,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
|
||||||
|
import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
|
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
||||||
|
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
|
||||||
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
@ -18,24 +28,50 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import {
|
||||||
|
MAT_DIALOG_DATA,
|
||||||
|
MatDialogModule,
|
||||||
|
MatDialogRef
|
||||||
|
} from '@angular/material/dialog';
|
||||||
import { SortDirection } from '@angular/material/sort';
|
import { SortDirection } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { Account, Tag } from '@prisma/client';
|
import { Account, Tag } from '@prisma/client';
|
||||||
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { PositionDetailDialogParams } from './interfaces/interfaces';
|
import { HoldingDetailDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'd-flex flex-column h-100' },
|
|
||||||
selector: 'gf-position-detail-dialog',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
templateUrl: 'position-detail-dialog.html',
|
host: { class: 'd-flex flex-column h-100' },
|
||||||
styleUrls: ['./position-detail-dialog.component.scss']
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfAccountsTableModule,
|
||||||
|
GfActivitiesTableComponent,
|
||||||
|
GfDataProviderCreditsComponent,
|
||||||
|
GfDialogFooterModule,
|
||||||
|
GfDialogHeaderModule,
|
||||||
|
GfLineChartComponent,
|
||||||
|
GfPortfolioProportionChartComponent,
|
||||||
|
GfValueComponent,
|
||||||
|
MatButtonModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatTabsModule,
|
||||||
|
NgxSkeletonLoaderModule
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
|
selector: 'gf-holding-detail-dialog',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['./holding-detail-dialog.component.scss'],
|
||||||
|
templateUrl: 'holding-detail-dialog.html'
|
||||||
})
|
})
|
||||||
export class PositionDetailDialog implements OnDestroy, OnInit {
|
export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||||
public accounts: Account[];
|
public accounts: Account[];
|
||||||
public activities: Activity[];
|
public activities: Activity[];
|
||||||
public assetClass: string;
|
public assetClass: string;
|
||||||
@ -80,14 +116,14 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
public dialogRef: MatDialogRef<PositionDetailDialog>,
|
public dialogRef: MatDialogRef<GfHoldingDetailDialogComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams,
|
@Inject(MAT_DIALOG_DATA) public data: HoldingDetailDialogParams,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPositionDetail({
|
.fetchHoldingDetail({
|
||||||
dataSource: this.data.dataSource,
|
dataSource: this.data.dataSource,
|
||||||
symbol: this.data.symbol
|
symbol: this.data.symbol
|
||||||
})
|
})
|
@ -335,6 +335,7 @@
|
|||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
[deviceType]="data.deviceType"
|
[deviceType]="data.deviceType"
|
||||||
[hasPermissionToCreateActivity]="false"
|
[hasPermissionToCreateActivity]="false"
|
||||||
|
[hasPermissionToDeleteActivity]="false"
|
||||||
[hasPermissionToExportActivities]="
|
[hasPermissionToExportActivities]="
|
||||||
!data.hasImpersonationId && !user?.settings?.isRestrictedView
|
!data.hasImpersonationId && !user?.settings?.isRestrictedView
|
||||||
"
|
"
|
@ -2,7 +2,7 @@ import { ColorScheme } from '@ghostfolio/common/types';
|
|||||||
|
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
export interface PositionDetailDialogParams {
|
export interface HoldingDetailDialogParams {
|
||||||
baseCurrency: string;
|
baseCurrency: string;
|
||||||
colorScheme: ColorScheme;
|
colorScheme: ColorScheme;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
@ -1,5 +1,3 @@
|
|||||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
|
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.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 { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
@ -8,9 +6,6 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
|||||||
import { HoldingType, ToggleOption } from '@ghostfolio/common/types';
|
import { HoldingType, ToggleOption } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -38,27 +33,9 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
private dialog: MatDialog,
|
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router,
|
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {}
|
||||||
this.route.queryParams
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((params) => {
|
|
||||||
if (
|
|
||||||
params['dataSource'] &&
|
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol']
|
|
||||||
) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
dataSource: params['dataSource'],
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
@ -127,45 +104,4 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
range: this.user?.settings?.dateRange
|
range: this.user?.settings?.dateRange
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private openPositionDialog({
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((user) => {
|
|
||||||
this.user = user;
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: <PositionDetailDialogParams>{
|
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
|
||||||
colorScheme: this.user?.settings?.colorScheme,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
|
||||||
this.user?.permissions,
|
|
||||||
permissions.reportDataGlitch
|
|
||||||
),
|
|
||||||
locale: this.user?.settings?.locale
|
|
||||||
},
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef
|
|
||||||
.afterClosed()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
|
||||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
|
||||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
|
||||||
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
|
|
||||||
import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits';
|
|
||||||
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
|
||||||
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
|
|
||||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatChipsModule } from '@angular/material/chips';
|
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
|
||||||
|
|
||||||
import { PositionDetailDialog } from './position-detail-dialog.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [PositionDetailDialog],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
GfAccountsTableModule,
|
|
||||||
GfActivitiesTableComponent,
|
|
||||||
GfDataProviderCreditsComponent,
|
|
||||||
GfDialogFooterModule,
|
|
||||||
GfDialogHeaderModule,
|
|
||||||
GfLineChartComponent,
|
|
||||||
GfPortfolioProportionChartComponent,
|
|
||||||
GfValueComponent,
|
|
||||||
MatButtonModule,
|
|
||||||
MatChipsModule,
|
|
||||||
MatDialogModule,
|
|
||||||
MatTabsModule,
|
|
||||||
NgxSkeletonLoaderModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class GfPositionDetailDialogModule {}
|
|
@ -4,6 +4,7 @@ import {
|
|||||||
KEY_TOKEN,
|
KEY_TOKEN,
|
||||||
SettingsStorageService
|
SettingsStorageService
|
||||||
} from '@ghostfolio/client/services/settings-storage.service';
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
||||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
@ -17,6 +18,7 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { FormBuilder, Validators } from '@angular/forms';
|
||||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
@ -33,8 +35,13 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
|||||||
public appearancePlaceholder = $localize`Auto`;
|
public appearancePlaceholder = $localize`Auto`;
|
||||||
public baseCurrency: string;
|
public baseCurrency: string;
|
||||||
public currencies: string[] = [];
|
public currencies: string[] = [];
|
||||||
|
public deleteOwnUserForm = this.formBuilder.group({
|
||||||
|
accessToken: ['', Validators.required]
|
||||||
|
});
|
||||||
|
public hasPermissionToDeleteOwnUser: boolean;
|
||||||
public hasPermissionToUpdateViewMode: boolean;
|
public hasPermissionToUpdateViewMode: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
|
public isAccessTokenHidden = true;
|
||||||
public isWebAuthnEnabled: boolean;
|
public isWebAuthnEnabled: boolean;
|
||||||
public language = document.documentElement.lang;
|
public language = document.documentElement.lang;
|
||||||
public locales = [
|
public locales = [
|
||||||
@ -58,7 +65,9 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
private settingsStorageService: SettingsStorageService,
|
private settingsStorageService: SettingsStorageService,
|
||||||
|
private tokenStorageService: TokenStorageService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
public webAuthnService: WebAuthnService
|
public webAuthnService: WebAuthnService
|
||||||
) {
|
) {
|
||||||
@ -73,6 +82,11 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
|||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.hasPermissionToDeleteOwnUser = hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.deleteOwnUser
|
||||||
|
);
|
||||||
|
|
||||||
this.hasPermissionToUpdateUserSettings = hasPermission(
|
this.hasPermissionToUpdateUserSettings = hasPermission(
|
||||||
this.user.permissions,
|
this.user.permissions,
|
||||||
permissions.updateUserSettings
|
permissions.updateUserSettings
|
||||||
@ -125,6 +139,33 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onCloseAccount() {
|
||||||
|
const confirmation = confirm(
|
||||||
|
$localize`Do you really want to close your Ghostfolio account?`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmation) {
|
||||||
|
this.dataService
|
||||||
|
.deleteOwnUser({
|
||||||
|
accessToken: this.deleteOwnUserForm.get('accessToken').value
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
catchError(() => {
|
||||||
|
alert($localize`Oops! Incorrect Security Token.`);
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
takeUntil(this.unsubscribeSubject)
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.tokenStorageService.signOut();
|
||||||
|
this.userService.remove();
|
||||||
|
|
||||||
|
document.location.href = `/${document.documentElement.lang}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onExperimentalFeaturesChange(aEvent: MatSlideToggleChange) {
|
public onExperimentalFeaturesChange(aEvent: MatSlideToggleChange) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.putUserSetting({ isExperimentalFeatures: aEvent.checked })
|
.putUserSetting({ isExperimentalFeatures: aEvent.checked })
|
||||||
|
@ -232,6 +232,55 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (hasPermissionToDeleteOwnUser) {
|
||||||
|
<hr class="mt-5" />
|
||||||
|
<form
|
||||||
|
class="w-100"
|
||||||
|
[formGroup]="deleteOwnUserForm"
|
||||||
|
(ngSubmit)="onCloseAccount()"
|
||||||
|
>
|
||||||
|
<div class="d-flex py-1">
|
||||||
|
<div class="pr-1 text-danger w-50" i18n>Danger Zone</div>
|
||||||
|
<div class="pl-1 w-50">
|
||||||
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="without-hint w-100"
|
||||||
|
[hideRequiredMarker]="true"
|
||||||
|
>
|
||||||
|
<mat-label i18n>Security Token</mat-label>
|
||||||
|
<input
|
||||||
|
formControlName="accessToken"
|
||||||
|
matInput
|
||||||
|
[type]="isAccessTokenHidden ? 'password' : 'text'"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
matSuffix
|
||||||
|
type="button"
|
||||||
|
(click)="isAccessTokenHidden = !isAccessTokenHidden"
|
||||||
|
>
|
||||||
|
<ion-icon
|
||||||
|
[name]="
|
||||||
|
isAccessTokenHidden ? 'eye-outline' : 'eye-off-outline'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
<button
|
||||||
|
class="mt-2"
|
||||||
|
color="warn"
|
||||||
|
mat-flat-button
|
||||||
|
type="submit"
|
||||||
|
[disabled]="
|
||||||
|
!(deleteOwnUserForm.dirty && deleteOwnUserForm.valid)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span i18n>Close Account</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
@ -22,10 +23,12 @@ import { UserAccountSettingsComponent } from './user-account-settings.component'
|
|||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatSlideToggleModule,
|
MatSlideToggleModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
]
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfUserAccountSettingsModule {}
|
export class GfUserAccountSettingsModule {}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { paths } from '@ghostfolio/client/app-routing.module';
|
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
@ -12,6 +11,8 @@ import {
|
|||||||
import { EMPTY } from 'rxjs';
|
import { EMPTY } from 'rxjs';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { paths } from './paths';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AuthGuard {
|
export class AuthGuard {
|
||||||
private static PUBLIC_PAGE_ROUTES = [
|
private static PUBLIC_PAGE_ROUTES = [
|
||||||
|
11
apps/client/src/app/core/paths.ts
Normal file
11
apps/client/src/app/core/paths.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const paths = {
|
||||||
|
about: $localize`about`,
|
||||||
|
faq: $localize`faq`,
|
||||||
|
features: $localize`features`,
|
||||||
|
license: $localize`license`,
|
||||||
|
markets: $localize`markets`,
|
||||||
|
pricing: $localize`pricing`,
|
||||||
|
privacyPolicy: $localize`privacy-policy`,
|
||||||
|
register: $localize`register`,
|
||||||
|
resources: $localize`resources`
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { paths } from '@ghostfolio/client/app-routing.module';
|
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
import { paths } from '@ghostfolio/client/core/paths';
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
@ -2,7 +2,6 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
|
|||||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||||
import { Currency } from '@ghostfolio/common/interfaces';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@ -33,7 +32,7 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
|
|||||||
})
|
})
|
||||||
export class CreateOrUpdateAccountDialog implements OnDestroy {
|
export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||||
public accountForm: FormGroup;
|
public accountForm: FormGroup;
|
||||||
public currencies: Currency[] = [];
|
public currencies: string[] = [];
|
||||||
public filteredPlatforms: Observable<Platform[]>;
|
public filteredPlatforms: Observable<Platform[]>;
|
||||||
public platforms: Platform[];
|
public platforms: Platform[];
|
||||||
|
|
||||||
@ -49,10 +48,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
|||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
const { currencies, platforms } = this.dataService.fetchInfo();
|
const { currencies, platforms } = this.dataService.fetchInfo();
|
||||||
|
|
||||||
this.currencies = currencies.map((currency) => ({
|
this.currencies = currencies;
|
||||||
label: currency,
|
|
||||||
value: currency
|
|
||||||
}));
|
|
||||||
this.platforms = platforms;
|
this.platforms = platforms;
|
||||||
|
|
||||||
this.accountForm = this.formBuilder.group({
|
this.accountForm = this.formBuilder.group({
|
||||||
@ -107,7 +103,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
|||||||
const account: CreateAccountDto | UpdateAccountDto = {
|
const account: CreateAccountDto | UpdateAccountDto = {
|
||||||
balance: this.accountForm.get('balance').value,
|
balance: this.accountForm.get('balance').value,
|
||||||
comment: this.accountForm.get('comment').value || null,
|
comment: this.accountForm.get('comment').value || null,
|
||||||
currency: this.accountForm.get('currency').value?.value,
|
currency: this.accountForm.get('currency').value,
|
||||||
id: this.accountForm.get('accountId').value,
|
id: this.accountForm.get('accountId').value,
|
||||||
isExcluded: this.accountForm.get('isExcluded').value,
|
isExcluded: this.accountForm.get('isExcluded').value,
|
||||||
name: this.accountForm.get('name').value,
|
name: this.accountForm.get('name').value,
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
|
|
||||||
import { DemoPageComponent } from './demo-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{ path: '', component: DemoPageComponent, canActivate: [AuthGuard] }
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class DemoPageRoutingModule {}
|
|
@ -2,16 +2,19 @@ import { DataService } from '@ghostfolio/client/services/data.service';
|
|||||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, OnDestroy } from '@angular/core';
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule],
|
||||||
selector: 'gf-demo-page',
|
selector: 'gf-demo-page',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './demo-page.html'
|
templateUrl: './demo-page.html'
|
||||||
})
|
})
|
||||||
export class DemoPageComponent implements OnDestroy {
|
export class GfDemoPageComponent implements OnDestroy {
|
||||||
public info: InfoItem;
|
public info: InfoItem;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
|
|
||||||
import { DemoPageRoutingModule } from './demo-page-routing.module';
|
|
||||||
import { DemoPageComponent } from './demo-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [DemoPageComponent],
|
|
||||||
imports: [CommonModule, DemoPageRoutingModule],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class DemoPageModule {}
|
|
@ -1,21 +0,0 @@
|
|||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
|
|
||||||
import { FeaturesPageComponent } from './features-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: FeaturesPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: $localize`Features`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class FeaturesPageRoutingModule {}
|
|
@ -2,17 +2,30 @@ 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 { InfoItem, User } from '@ghostfolio/common/interfaces';
|
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfPremiumIndicatorComponent,
|
||||||
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
selector: 'gf-features-page',
|
selector: 'gf-features-page',
|
||||||
|
standalone: true,
|
||||||
styleUrls: ['./features-page.scss'],
|
styleUrls: ['./features-page.scss'],
|
||||||
templateUrl: './features-page.html'
|
templateUrl: './features-page.html'
|
||||||
})
|
})
|
||||||
export class FeaturesPageComponent implements OnDestroy {
|
export class GfFeaturesPageComponent implements OnDestroy {
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
public info: InfoItem;
|
public info: InfoItem;
|
||||||
public routerLinkRegister = ['/' + $localize`register`];
|
public routerLinkRegister = ['/' + $localize`register`];
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
|
|
||||||
import { FeaturesPageRoutingModule } from './features-page-routing.module';
|
|
||||||
import { FeaturesPageComponent } from './features-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [FeaturesPageComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
FeaturesPageRoutingModule,
|
|
||||||
GfPremiumIndicatorComponent,
|
|
||||||
MatButtonModule,
|
|
||||||
MatCardModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class FeaturesPageModule {}
|
|
@ -1,14 +1,8 @@
|
|||||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
|
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
|
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.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 { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
|
||||||
|
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -30,27 +24,9 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
private dialog: MatDialog,
|
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router,
|
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
this.route.queryParams
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((params) => {
|
|
||||||
if (
|
|
||||||
params['dataSource'] &&
|
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol']
|
|
||||||
) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
dataSource: params['dataSource'],
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
@ -99,45 +75,4 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private openPositionDialog({
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((user) => {
|
|
||||||
this.user = user;
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: <PositionDetailDialogParams>{
|
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
|
||||||
colorScheme: this.user?.settings?.colorScheme,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
|
||||||
this.user?.permissions,
|
|
||||||
permissions.reportDataGlitch
|
|
||||||
),
|
|
||||||
locale: this.user?.settings?.locale
|
|
||||||
},
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef
|
|
||||||
.afterClosed()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
|
|
||||||
import { I18nPageComponent } from './i18n-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: I18nPageComponent,
|
|
||||||
path: ''
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class I18nPageRoutingModule {}
|
|
@ -1,13 +1,16 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule],
|
||||||
selector: 'gf-i18n-page',
|
selector: 'gf-i18n-page',
|
||||||
|
standalone: true,
|
||||||
styleUrls: ['./i18n-page.scss'],
|
styleUrls: ['./i18n-page.scss'],
|
||||||
templateUrl: './i18n-page.html'
|
templateUrl: './i18n-page.html'
|
||||||
})
|
})
|
||||||
export class I18nPageComponent implements OnInit {
|
export class GfI18nPageComponent implements OnInit {
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
|
|
||||||
import { I18nPageRoutingModule } from './i18n-page-routing.module';
|
|
||||||
import { I18nPageComponent } from './i18n-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [I18nPageComponent],
|
|
||||||
imports: [CommonModule, I18nPageRoutingModule],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class I18nPageModule {}
|
|
@ -1,8 +1,6 @@
|
|||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
|
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
|
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
@ -18,7 +16,7 @@ import { PageEvent } from '@angular/material/paginator';
|
|||||||
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 { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { Order as OrderModel } from '@prisma/client';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
@ -83,15 +81,6 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
} else {
|
} else {
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
}
|
}
|
||||||
} else if (
|
|
||||||
params['dataSource'] &&
|
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol']
|
|
||||||
) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
dataSource: params['dataSource'],
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -153,32 +142,24 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
this.openCreateActivityDialog(aActivity);
|
this.openCreateActivityDialog(aActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteActivity(aId: string) {
|
public onDeleteActivities() {
|
||||||
this.dataService
|
this.dataService
|
||||||
.deleteOrder(aId)
|
.deleteActivities({
|
||||||
|
filters: this.userService.getFilters()
|
||||||
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe({
|
.subscribe(() => {
|
||||||
next: () => {
|
this.fetchActivities();
|
||||||
this.fetchActivities();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteAllActivities() {
|
public onDeleteActivity(aId: string) {
|
||||||
const confirmation = confirm(
|
this.dataService
|
||||||
$localize`Do you really want to delete all your activities?`
|
.deleteActivity(aId)
|
||||||
);
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
if (confirmation) {
|
this.fetchActivities();
|
||||||
this.dataService
|
});
|
||||||
.deleteAllOrders()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.fetchActivities();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onExport(activityIds?: string[]) {
|
public onExport(activityIds?: string[]) {
|
||||||
@ -351,47 +332,6 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private openPositionDialog({
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((user) => {
|
|
||||||
this.updateUser(user);
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: <PositionDetailDialogParams>{
|
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
|
||||||
colorScheme: this.user?.settings?.colorScheme,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
|
||||||
this.user?.permissions,
|
|
||||||
permissions.reportDataGlitch
|
|
||||||
),
|
|
||||||
locale: this.user?.settings?.locale
|
|
||||||
},
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef
|
|
||||||
.afterClosed()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateUser(aUser: User) {
|
private updateUser(aUser: User) {
|
||||||
this.user = aUser;
|
this.user = aUser;
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
|
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
|
||||||
|
[hasPermissionToDeleteActivity]="hasPermissionToDeleteActivity"
|
||||||
[hasPermissionToExportActivities]="!hasImpersonationId"
|
[hasPermissionToExportActivities]="!hasImpersonationId"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[pageIndex]="pageIndex"
|
[pageIndex]="pageIndex"
|
||||||
@ -19,10 +20,10 @@
|
|||||||
[sortColumn]="sortColumn"
|
[sortColumn]="sortColumn"
|
||||||
[sortDirection]="sortDirection"
|
[sortDirection]="sortDirection"
|
||||||
[totalItems]="totalItems"
|
[totalItems]="totalItems"
|
||||||
|
(activitiesDeleted)="onDeleteActivities()"
|
||||||
(activityDeleted)="onDeleteActivity($event)"
|
(activityDeleted)="onDeleteActivity($event)"
|
||||||
(activityToClone)="onCloneActivity($event)"
|
(activityToClone)="onCloneActivity($event)"
|
||||||
(activityToUpdate)="onUpdateActivity($event)"
|
(activityToUpdate)="onUpdateActivity($event)"
|
||||||
(deleteAllActivities)="onDeleteAllActivities()"
|
|
||||||
(export)="onExport()"
|
(export)="onExport()"
|
||||||
(exportDrafts)="onExportDrafts($event)"
|
(exportDrafts)="onExportDrafts($event)"
|
||||||
(import)="onImport()"
|
(import)="onImport()"
|
||||||
|
@ -91,7 +91,12 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.activityForm = this.formBuilder.group({
|
this.activityForm = this.formBuilder.group({
|
||||||
accountId: [this.data.activity?.accountId, Validators.required],
|
accountId: [
|
||||||
|
this.data.accounts.length === 1 && !this.data.activity?.accountId
|
||||||
|
? this.data.accounts[0].id
|
||||||
|
: this.data.activity?.accountId,
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
assetClass: [this.data.activity?.SymbolProfile?.assetClass],
|
assetClass: [this.data.activity?.SymbolProfile?.assetClass],
|
||||||
assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass],
|
assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass],
|
||||||
comment: [this.data.activity?.comment],
|
comment: [this.data.activity?.comment],
|
||||||
|
@ -126,6 +126,7 @@
|
|||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
[deviceType]="data?.deviceType"
|
[deviceType]="data?.deviceType"
|
||||||
[hasPermissionToCreateActivity]="false"
|
[hasPermissionToCreateActivity]="false"
|
||||||
|
[hasPermissionToDeleteActivity]="false"
|
||||||
[hasPermissionToExportActivities]="false"
|
[hasPermissionToExportActivities]="false"
|
||||||
[hasPermissionToFilter]="false"
|
[hasPermissionToFilter]="false"
|
||||||
[hasPermissionToOpenDetails]="false"
|
[hasPermissionToOpenDetails]="false"
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
|
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
|
||||||
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
|
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
|
||||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
|
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.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 { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
@ -13,7 +11,6 @@ import {
|
|||||||
UniqueAsset,
|
UniqueAsset,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
|
||||||
import { Market, MarketAdvanced } from '@ghostfolio/common/types';
|
import { Market, MarketAdvanced } from '@ghostfolio/common/types';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
|
|
||||||
@ -108,15 +105,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
if (params['accountId'] && params['accountDetailDialog']) {
|
if (params['accountId'] && params['accountDetailDialog']) {
|
||||||
this.openAccountDetailDialog(params['accountId']);
|
this.openAccountDetailDialog(params['accountId']);
|
||||||
} else if (
|
|
||||||
params['dataSource'] &&
|
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol']
|
|
||||||
) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
dataSource: params['dataSource'],
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -178,7 +166,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
|
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
|
||||||
if (dataSource && symbol) {
|
if (dataSource && symbol) {
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
queryParams: { dataSource, symbol, holdingDetailDialog: true }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -551,45 +539,4 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private openPositionDialog({
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((user) => {
|
|
||||||
this.user = user;
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: <PositionDetailDialogParams>{
|
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
|
||||||
colorScheme: this.user?.settings?.colorScheme,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
|
||||||
this.user?.permissions,
|
|
||||||
permissions.reportDataGlitch
|
|
||||||
),
|
|
||||||
locale: this.user?.settings?.locale
|
|
||||||
},
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef
|
|
||||||
.afterClosed()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
|
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
|
|
||||||
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';
|
||||||
@ -12,14 +10,11 @@ import {
|
|||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
|
||||||
import { GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
import { GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
|
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
import { isNumber, sortBy } from 'lodash';
|
import { isNumber, sortBy } from 'lodash';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
@ -70,30 +65,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private dialog: MatDialog,
|
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router,
|
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
const { benchmarks } = this.dataService.fetchInfo();
|
const { benchmarks } = this.dataService.fetchInfo();
|
||||||
this.benchmarks = benchmarks;
|
this.benchmarks = benchmarks;
|
||||||
|
|
||||||
this.route.queryParams
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((params) => {
|
|
||||||
if (
|
|
||||||
params['dataSource'] &&
|
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol']
|
|
||||||
) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
dataSource: params['dataSource'],
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get savingsRate() {
|
get savingsRate() {
|
||||||
@ -212,47 +189,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private openPositionDialog({
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((user) => {
|
|
||||||
this.user = user;
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: <PositionDetailDialogParams>{
|
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
|
||||||
colorScheme: this.user?.settings?.colorScheme,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
|
||||||
this.user?.permissions,
|
|
||||||
permissions.reportDataGlitch
|
|
||||||
),
|
|
||||||
locale: this.user?.settings?.locale
|
|
||||||
},
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef
|
|
||||||
.afterClosed()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.isLoadingInvestmentChart = true;
|
this.isLoadingInvestmentChart = true;
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@
|
|||||||
class="d-flex"
|
class="d-flex"
|
||||||
[queryParams]="{
|
[queryParams]="{
|
||||||
dataSource: holding.dataSource,
|
dataSource: holding.dataSource,
|
||||||
positionDetailDialog: true,
|
holdingDetailDialog: true,
|
||||||
symbol: holding.symbol
|
symbol: holding.symbol
|
||||||
}"
|
}"
|
||||||
[routerLink]="[]"
|
[routerLink]="[]"
|
||||||
@ -221,7 +221,7 @@
|
|||||||
class="d-flex"
|
class="d-flex"
|
||||||
[queryParams]="{
|
[queryParams]="{
|
||||||
dataSource: holding.dataSource,
|
dataSource: holding.dataSource,
|
||||||
positionDetailDialog: true,
|
holdingDetailDialog: true,
|
||||||
symbol: holding.symbol
|
symbol: holding.symbol
|
||||||
}"
|
}"
|
||||||
[routerLink]="[]"
|
[routerLink]="[]"
|
||||||
|
@ -23,15 +23,18 @@ import { GetquinPageComponent } from './products/getquin-page.component';
|
|||||||
import { GoSpatzPageComponent } from './products/gospatz-page.component';
|
import { GoSpatzPageComponent } from './products/gospatz-page.component';
|
||||||
import { IntuitMintPageComponent } from './products/intuit-mint-page.component';
|
import { IntuitMintPageComponent } from './products/intuit-mint-page.component';
|
||||||
import { JustEtfPageComponent } from './products/justetf-page.component';
|
import { JustEtfPageComponent } from './products/justetf-page.component';
|
||||||
|
import { KoyfinPageComponent } from './products/koyfin-page.component';
|
||||||
import { KuberaPageComponent } from './products/kubera-page.component';
|
import { KuberaPageComponent } from './products/kubera-page.component';
|
||||||
import { MagnifiPageComponent } from './products/magnifi-page.component';
|
import { MagnifiPageComponent } from './products/magnifi-page.component';
|
||||||
import { MarketsShPageComponent } from './products/markets.sh-page.component';
|
import { MarketsShPageComponent } from './products/markets.sh-page.component';
|
||||||
import { MaybeFinancePageComponent } from './products/maybe-finance-page.component';
|
import { MaybeFinancePageComponent } from './products/maybe-finance-page.component';
|
||||||
import { MonarchMoneyPageComponent } from './products/monarch-money-page.component';
|
import { MonarchMoneyPageComponent } from './products/monarch-money-page.component';
|
||||||
import { MonsePageComponent } from './products/monse-page.component';
|
import { MonsePageComponent } from './products/monse-page.component';
|
||||||
|
import { NavexaPageComponent } from './products/navexa-page.component';
|
||||||
import { ParqetPageComponent } from './products/parqet-page.component';
|
import { ParqetPageComponent } from './products/parqet-page.component';
|
||||||
import { PlannixPageComponent } from './products/plannix-page.component';
|
import { PlannixPageComponent } from './products/plannix-page.component';
|
||||||
import { PortfolioDividendTrackerPageComponent } from './products/portfolio-dividend-tracker-page.component';
|
import { PortfolioDividendTrackerPageComponent } from './products/portfolio-dividend-tracker-page.component';
|
||||||
|
import { PortfolioVisualizerPageComponent } from './products/portfolio-visualizer-page.component';
|
||||||
import { PortseidoPageComponent } from './products/portseido-page.component';
|
import { PortseidoPageComponent } from './products/portseido-page.component';
|
||||||
import { ProjectionLabPageComponent } from './products/projectionlab-page.component';
|
import { ProjectionLabPageComponent } from './products/projectionlab-page.component';
|
||||||
import { RocketMoneyPageComponent } from './products/rocket-money-page.component';
|
import { RocketMoneyPageComponent } from './products/rocket-money-page.component';
|
||||||
@ -39,12 +42,15 @@ import { SeekingAlphaPageComponent } from './products/seeking-alpha-page.compone
|
|||||||
import { SharesightPageComponent } from './products/sharesight-page.component';
|
import { SharesightPageComponent } from './products/sharesight-page.component';
|
||||||
import { SimplePortfolioPageComponent } from './products/simple-portfolio-page.component';
|
import { SimplePortfolioPageComponent } from './products/simple-portfolio-page.component';
|
||||||
import { SnowballAnalyticsPageComponent } from './products/snowball-analytics-page.component';
|
import { SnowballAnalyticsPageComponent } from './products/snowball-analytics-page.component';
|
||||||
|
import { StockEventsPageComponent } from './products/stock-events-page.component';
|
||||||
import { StocklePageComponent } from './products/stockle-page.component';
|
import { StocklePageComponent } from './products/stockle-page.component';
|
||||||
import { StockMarketEyePageComponent } from './products/stockmarketeye-page.component';
|
import { StockMarketEyePageComponent } from './products/stockmarketeye-page.component';
|
||||||
|
import { StonksfolioPageComponent } from './products/stonksfolio-page.component';
|
||||||
import { SumioPageComponent } from './products/sumio-page.component';
|
import { SumioPageComponent } from './products/sumio-page.component';
|
||||||
import { TillerPageComponent } from './products/tiller-page.component';
|
import { TillerPageComponent } from './products/tiller-page.component';
|
||||||
import { UtlunaPageComponent } from './products/utluna-page.component';
|
import { UtlunaPageComponent } from './products/utluna-page.component';
|
||||||
import { VyzerPageComponent } from './products/vyzer-page.component';
|
import { VyzerPageComponent } from './products/vyzer-page.component';
|
||||||
|
import { WallminePageComponent } from './products/wallmine-page.component';
|
||||||
import { WealthfolioPageComponent } from './products/wealthfolio-page.component';
|
import { WealthfolioPageComponent } from './products/wealthfolio-page.component';
|
||||||
import { WealthicaPageComponent } from './products/wealthica-page.component';
|
import { WealthicaPageComponent } from './products/wealthica-page.component';
|
||||||
import { WhalPageComponent } from './products/whal-page.component';
|
import { WhalPageComponent } from './products/whal-page.component';
|
||||||
@ -66,7 +72,9 @@ export const products: Product[] = [
|
|||||||
'Français',
|
'Français',
|
||||||
'Italiano',
|
'Italiano',
|
||||||
'Nederlands',
|
'Nederlands',
|
||||||
'Português'
|
'Português',
|
||||||
|
'Türkçe',
|
||||||
|
'简体中文'
|
||||||
],
|
],
|
||||||
name: 'Ghostfolio',
|
name: 'Ghostfolio',
|
||||||
origin: $localize`Switzerland`,
|
origin: $localize`Switzerland`,
|
||||||
@ -306,6 +314,17 @@ export const products: Product[] = [
|
|||||||
pricingPerYear: '€119',
|
pricingPerYear: '€119',
|
||||||
slogan: 'ETF portfolios made simple'
|
slogan: 'ETF portfolios made simple'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: KoyfinPageComponent,
|
||||||
|
founded: 2016,
|
||||||
|
hasFreePlan: true,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
key: 'koyfin',
|
||||||
|
name: 'Koyfin',
|
||||||
|
origin: $localize`United States`,
|
||||||
|
pricingPerYear: '$468',
|
||||||
|
slogan: 'Comprehensive financial data analysis'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: KuberaPageComponent,
|
component: KuberaPageComponent,
|
||||||
founded: 2019,
|
founded: 2019,
|
||||||
@ -374,6 +393,17 @@ export const products: Product[] = [
|
|||||||
pricingPerYear: '$60',
|
pricingPerYear: '$60',
|
||||||
slogan: 'Gain financial control and keep your data private.'
|
slogan: 'Gain financial control and keep your data private.'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: NavexaPageComponent,
|
||||||
|
founded: 2017,
|
||||||
|
hasFreePlan: false,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
key: 'navexa',
|
||||||
|
name: 'Navexa',
|
||||||
|
origin: $localize`Australia`,
|
||||||
|
pricingPerYear: '$90',
|
||||||
|
slogan: 'The Intelligent Portfolio Tracker'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: ParqetPageComponent,
|
component: ParqetPageComponent,
|
||||||
founded: 2020,
|
founded: 2020,
|
||||||
@ -407,6 +437,16 @@ export const products: Product[] = [
|
|||||||
pricingPerYear: '€60',
|
pricingPerYear: '€60',
|
||||||
slogan: 'Manage all your portfolios'
|
slogan: 'Manage all your portfolios'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: PortfolioVisualizerPageComponent,
|
||||||
|
hasFreePlan: true,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
key: 'portfolio-visualizer',
|
||||||
|
languages: ['English'],
|
||||||
|
name: 'Portfolio Visualizer',
|
||||||
|
pricingPerYear: '$360',
|
||||||
|
slogan: 'Tools for Better Investors'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: PortseidoPageComponent,
|
component: PortseidoPageComponent,
|
||||||
founded: 2021,
|
founded: 2021,
|
||||||
@ -483,6 +523,15 @@ export const products: Product[] = [
|
|||||||
pricingPerYear: '$80',
|
pricingPerYear: '$80',
|
||||||
slogan: 'Simple and powerful portfolio tracker'
|
slogan: 'Simple and powerful portfolio tracker'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: StockEventsPageComponent,
|
||||||
|
founded: 2019,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
key: 'stock-events',
|
||||||
|
name: 'Stock Events',
|
||||||
|
origin: $localize`Germany`,
|
||||||
|
slogan: 'Track all your Investments'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: StocklePageComponent,
|
component: StocklePageComponent,
|
||||||
key: 'stockle',
|
key: 'stockle',
|
||||||
@ -499,6 +548,17 @@ export const products: Product[] = [
|
|||||||
note: 'StockMarketEye has discontinued in 2023',
|
note: 'StockMarketEye has discontinued in 2023',
|
||||||
slogan: 'A Powerful Portfolio & Investment Tracking App'
|
slogan: 'A Powerful Portfolio & Investment Tracking App'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: StonksfolioPageComponent,
|
||||||
|
hasFreePlan: true,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
key: 'stonksfolio',
|
||||||
|
languages: ['English'],
|
||||||
|
name: 'Stonksfolio',
|
||||||
|
origin: $localize`Bulgaria`,
|
||||||
|
pricingPerYear: '€49.90',
|
||||||
|
slogan: 'Visualize all of your portfolios'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: SumioPageComponent,
|
component: SumioPageComponent,
|
||||||
hasFreePlan: true,
|
hasFreePlan: true,
|
||||||
@ -542,6 +602,16 @@ export const products: Product[] = [
|
|||||||
pricingPerYear: '$348',
|
pricingPerYear: '$348',
|
||||||
slogan: 'Virtual Family Office for Smart Wealth Management'
|
slogan: 'Virtual Family Office for Smart Wealth Management'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: WallminePageComponent,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
key: 'wallmine',
|
||||||
|
languages: ['English'],
|
||||||
|
name: 'wallmine',
|
||||||
|
origin: $localize`Czech Republic`,
|
||||||
|
pricingPerYear: '$600',
|
||||||
|
slogan: 'Make Smarter Investments'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: WealthfolioPageComponent,
|
component: WealthfolioPageComponent,
|
||||||
hasSelfHostingAbility: true,
|
hasSelfHostingAbility: true,
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
import { BaseProductPageComponent } from './base-page.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-koyfin-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class KoyfinPageComponent extends BaseProductPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'koyfin';
|
||||||
|
});
|
||||||
|
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResourcesPersonalFinanceTools = [
|
||||||
|
'/' + $localize`resources`,
|
||||||
|
'personal-finance-tools'
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
import { BaseProductPageComponent } from './base-page.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-navexa-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class NavexaPageComponent extends BaseProductPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'navexa';
|
||||||
|
});
|
||||||
|
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResourcesPersonalFinanceTools = [
|
||||||
|
'/' + $localize`resources`,
|
||||||
|
'personal-finance-tools'
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
import { BaseProductPageComponent } from './base-page.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-portfolio-visualizer-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class PortfolioVisualizerPageComponent extends BaseProductPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'portfolio-visualizer';
|
||||||
|
});
|
||||||
|
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResourcesPersonalFinanceTools = [
|
||||||
|
'/' + $localize`resources`,
|
||||||
|
'personal-finance-tools'
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
import { BaseProductPageComponent } from './base-page.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-stock-events-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class StockEventsPageComponent extends BaseProductPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'stock-events';
|
||||||
|
});
|
||||||
|
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResourcesPersonalFinanceTools = [
|
||||||
|
'/' + $localize`resources`,
|
||||||
|
'personal-finance-tools'
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
import { BaseProductPageComponent } from './base-page.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-stonksfolio-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class StonksfolioPageComponent extends BaseProductPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'stonksfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResourcesPersonalFinanceTools = [
|
||||||
|
'/' + $localize`resources`,
|
||||||
|
'personal-finance-tools'
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
import { BaseProductPageComponent } from './base-page.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-wallmine-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class WallminePageComponent extends BaseProductPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'wallmine';
|
||||||
|
});
|
||||||
|
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResourcesPersonalFinanceTools = [
|
||||||
|
'/' + $localize`resources`,
|
||||||
|
'personal-finance-tools'
|
||||||
|
];
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user