Compare commits

..

54 Commits

Author SHA1 Message Date
dfb3365efb Release 2.84.0 (#3447) 2024-06-01 11:16:13 +02:00
0e08d8830e Handle reduce of empty array (#3446) 2024-06-01 11:14:34 +02:00
69d85eadfd Feature/setup cascading on delete for various relations in database schema (#3445)
* Setup cascading on delete

* Update changelog
2024-06-01 11:08:36 +02:00
c009f8c12f Feature/add data provider info to asset profile details dialog (#3434)
* Add data provider info to asset profile details dialog

* Update changelog
2024-06-01 10:55:42 +02:00
60ef46accf Bugfix/fix state handling of currency selector component (#2795) (#3429)
* Fix state handling of currency selector component

* Update changelog
2024-06-01 10:53:02 +02:00
b12ac1fe84 Feature/simplify module imports of api (#3443)
* Simplify module imports
2024-06-01 10:02:43 +02:00
4355c96ab6 Bugfix/fix initial annual interest rate in fire calculator (#3437)
* Fix initial annual interest rate

* Update changelog
2024-05-31 17:29:19 +02:00
fb326fe0cc Release 2.83.0 (#3442) 2024-05-30 20:47:43 +02:00
02cfebd98c Feature/upgrade yahoo finance2 to version 2.11.3 (#3441)
* Upgrade yahoo-finance2 to version 2.11.3

* Update changelog
2024-05-30 20:45:32 +02:00
dd2936d703 Feature/upgrade countup.js to version 2.8.0 (#3436)
* Upgrade countup.js to version 2.8.0

* Update changelog
2024-05-29 16:06:58 +02:00
918d0b85d4 Feature/update passport dependencies (#3433)
* Update passport dependencies

* Refactor Google strategy

* Update changelog
2024-05-28 13:47:45 +02:00
a061595101 Feature/upgrade class validator to version 0.14.1 (#3431)
* Upgrade class-validator to version 0.14.1

* Update changelog
2024-05-27 13:55:24 +02:00
dcd496ac50 Feature/upgrade angular to version 17.3.10 (#3430)
* Upgrade angular to version 17.3.10

* Update changelog
2024-05-26 14:13:43 +02:00
6b9ec549da Feature/upgrade prisma to version 5.14.0 (#3423)
* Upgrade prisma to version 5.14.0

* Update changelog
2024-05-24 15:36:36 +02:00
b5bd4df483 Feature/upgrade nx to version 19.0.5 (#3422)
* Upgrade Nx to version 19.0.5

* Update changelog
2024-05-23 20:48:26 +02:00
766a2d7c2f Release 2.82.0 (#3425) 2024-05-22 19:53:20 +02:00
6dabf7516a Feature/preselect account in create or update activity dialog (#3413)
* Preselect account if there is only one

* Update changelog
2024-05-22 18:15:08 +02:00
5d49ff7a4a Feature/improve usability of date range selector in assistant (#3409)
* Improve usability of date range selector

* Update changelog
2024-05-22 16:39:39 +02:00
8998c18836 Feature/upgrade internationalized number to version 3.5.2 (#3412)
* Upgrade @internationalized/number to version 3.5.2

* Update changelog
2024-05-22 16:36:50 +02:00
741a0e36d2 Add links to tagged issues (#3405)
* help wanted
* good first issue
2024-05-20 22:28:43 +03:00
e31b4c64cb Feature/refactor holding detail dialog to standalone (#3407)
* Refactor holding detail dialog to standalone

* Update changelog
2024-05-19 19:13:14 +03:00
812ff5cbdc Feature/refresh cryptocurrencies list 20240514 (#3411)
* Refresh cryptocurrencies list

* Update changelog
2024-05-18 17:40:25 +03:00
035b90a689 Feature/upgrade zone.js to version 0.14.5 (#3410)
* Upgrade zone.js to version 0.14.5

* Update changelog
2024-05-17 18:08:37 +03:00
5ea3a187f4 Feature/upgrade body parser to version 1.20.2 (#3406)
* Upgrade body-parser to version 1.20.2

* Update changelog
2024-05-16 17:26:27 +02:00
5d9c38663d Feature/migrate various pages to standalone components (#3404)
* Migrate to standalone components

* Update changelog
2024-05-15 12:04:07 +02:00
5616bc4956 Feature/validate account balance creation/update using DTO (#3400)
* Validate create account balance using DTO
2024-05-13 13:47:33 +02:00
9ad1c2177c Release 2.81.0 (#3403) 2024-05-12 10:42:04 +02:00
15bf9f2f9c Feature/add Türkçe to footer (#3401) 2024-05-12 10:34:33 +02:00
ebc5008569 Feature/improve language localization for de 20240512 (#3402)
* Update translations

* Update changelog
2024-05-12 10:25:59 +02:00
37759ba03f Feature/improve language localization for tr 20240501 (#3355)
* Improve translations

* Update changelog

---------

Co-authored-by: sadmimye <134071831+sadmimye@users.noreply.github.com>
2024-05-12 09:58:11 +02:00
8319b216bb Feature/support delete activities with filtering (#3394)
* Support delete activities with filtering

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-05-12 09:56:07 +02:00
782d131b0d Feature/add indicator for active filters (#3398)
* Add indicator for active filters

* Update changelog
2024-05-12 09:42:27 +02:00
72e75208df Bugfix/fix position detail dialog close functionality (#3396)
* Handle holding detail dialog open functionality in a single place (AppComponent)

* Update changelog
2024-05-11 20:27:18 +02:00
4b1c27c245 Feature/upgrade nx to version 19.0.2 (#3391)
* Upgrade Nx dependencies to version 19.0.2

* Update changelog
2024-05-11 08:33:33 +02:00
61f0da35bc Feature/disable delete all activities if filters are active (#3389)
* Disable delete all activities button if filters are active

* Update changelog
2024-05-10 08:51:34 +02:00
80464c7846 Release 2.80.0 (#3386) 2024-05-08 20:55:39 +02:00
74f4323903 Feature/increase spacing around floating action buttons (#3385)
* Increase spacing around floating action buttons

* Update changelog
2024-05-08 20:54:13 +02:00
127dbf9dcd Update translations (#3384) 2024-05-08 20:37:53 +02:00
66bdb374e8 Feature/set icon columns of tables to stick at beginning (#3377)
* Set icon columns to stick at the beginning

* Update changelog
2024-05-08 20:04:58 +02:00
4ad4fa2b30 Feature/clean up deprecated GET api/portfolio/positions endpoint (#3373) 2024-05-08 20:04:32 +02:00
1fd836194f Feature/add absolute change column to holdings table (#3378)
* Add absolute change column

* Update changelog
2024-05-08 20:02:50 +02:00
2090db1199 Feature/increase number of attempts of queue jobs (#3376)
* Increase number of attempts

* Update changelog
2024-05-07 20:48:02 +02:00
053c7e591e Feature/upgrade ionicons to version 7.4.0 (#3356)
* Upgrade ionicons to version 7.4.0

* Update changelog
2024-05-07 19:00:55 +02:00
9b5e350e3b Feature/harmonize log message (#3343) 2024-05-06 17:03:58 +02:00
378e57c3bc Feature/add links to Home Assistant add-on (#3367)
* Add links to Home Assistant add-on
2024-05-06 17:02:18 +02:00
6765191a8c Bugfix/fix position detail dialog open in holding search of assistant (#3374)
* Open position detail dialog (via holding search of assistant)

* Update changelog
2024-05-05 09:09:57 +02:00
8438a45bcf Update changelog (#3372) 2024-05-04 16:32:32 +02:00
30a64e7fc1 Release 2.79.0 (#3371) 2024-05-04 15:54:29 +02:00
f2cb671c7f Feature/optimize get porfolio details endpoint (#3366)
* Eliminate getPerformance() from getSummary() function

* Disable cache for getDetails()

* Add hint to portfolio summary

* Update changelog
2024-05-04 15:53:02 +02:00
3f41e5c5de Bugfix/fix locale in markets overview (#3369)
* Fix locale if no user is logged in

* Update changelog
2024-05-04 15:51:09 +02:00
c1ad483f33 Improve alignment (#3370) 2024-05-04 15:50:47 +02:00
f3d961bc16 Feature/move holdings table to holdings tab of home page (#3368)
* Move holdings table to holdings tab of home page

* Deprecate api/v1/portfolio/positions endpoint

* Update changelog
2024-05-04 14:11:37 +02:00
42b70ef568 Feature/improve performance labels in position detail dialog (#3363)
* Improve performance labels (with and without currency effects)

* Update changelog
2024-05-04 07:49:37 +02:00
77beaaba08 Refactoring portfolio service (#3365) 2024-05-03 21:48:46 +02:00
160 changed files with 7525 additions and 5571 deletions

View File

@ -5,6 +5,94 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.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
### Added
- Added the absolute change column to the holdings table on the home page
### Changed
- Increased the spacing around the floating action buttons (FAB)
- Set the icon column of the activities table to stick at the beginning
- Set the icon column of the holdings table to stick at the beginning
- Increased the number of attempts of queue jobs from `10` to `12` (fail later)
- Upgraded `ionicons` from version `7.3.0` to `7.4.0`
### Fixed
- Fixed the position detail dialog open functionality when searching for a holding in the assistant
## 2.79.0 - 2024-05-04
### Changed
- Moved the holdings table to the holdings tab of the home page
- Improved the performance labels (with and without currency effects) in the position detail dialog
- Optimized the calculations of the portfolio details endpoint
### Fixed
- Fixed an issue with the benchmarks in the markets overview
- Fixed an issue with the _Fear & Greed Index_ (market mood) in the markets overview
## 2.78.0 - 2024-05-02
### Added

View File

@ -142,7 +142,7 @@ docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d
### Home Server Systems (Community)
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), Home Assistant, [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), [Home Assistant](https://github.com/lildude/ha-addon-ghostfolio), [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
## Development
@ -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.
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).

View File

@ -2,7 +2,7 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/accou
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
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 { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import {

View File

@ -1,9 +1,7 @@
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.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 { 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 { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
@ -19,13 +17,11 @@ import { AccountService } from './account.service';
imports: [
AccountBalanceModule,
ConfigurationModule,
DataProviderModule,
ExchangeRateDataModule,
ImpersonationModule,
PortfolioModule,
PrismaModule,
RedisCacheModule,
UserModule
RedactValuesInResponseModule
],
providers: [AccountService]
})

View File

@ -1,6 +1,6 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
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 { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';

View File

@ -1,4 +1,5 @@
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 { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
@ -27,7 +28,8 @@ import { QueueModule } from './queue/queue.module';
PropertyModule,
QueueModule,
SubscriptionModule,
SymbolProfileModule
SymbolProfileModule,
TransformDataSourceInRequestModule
],
controllers: [AdminController],
providers: [AdminService],

View File

@ -313,6 +313,12 @@ export class AdminService {
})
]);
if (assetProfile) {
assetProfile.dataProviderInfo = this.dataProviderService
.getDataProvider(assetProfile.dataSource)
.getDataProviderInfo();
}
return {
marketData,
assetProfile: assetProfile ?? {

View File

@ -1,6 +1,5 @@
import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller';
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 { Module } from '@nestjs/common';
@ -9,7 +8,6 @@ import { JwtModule } from '@nestjs/jwt';
@Module({
controllers: [AuthDeviceController],
imports: [
ConfigurationModule,
JwtModule.register({
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '180 days' }

View File

@ -1,4 +1,3 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
@ -6,10 +5,7 @@ import { AuthDevice, Prisma } from '@prisma/client';
@Injectable()
export class AuthDeviceService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly prismaService: PrismaService
) {}
public constructor(private readonly prismaService: PrismaService) {}
public async authDevice(
where: Prisma.AuthDeviceWhereUniqueInput

View File

@ -3,7 +3,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Provider } from '@prisma/client';
import { Strategy } from 'passport-google-oauth20';
import { Profile, Strategy } from 'passport-google-oauth20';
import { AuthService } from './auth.service';
@ -11,7 +11,7 @@ import { AuthService } from './auth.service';
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
public constructor(
private readonly authService: AuthService,
readonly configurationService: ConfigurationService
private readonly configurationService: ConfigurationService
) {
super({
callbackURL: `${configurationService.get(
@ -20,7 +20,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
clientID: configurationService.get('GOOGLE_CLIENT_ID'),
clientSecret: configurationService.get('GOOGLE_SECRET'),
passReqToCallback: true,
scope: ['email', 'profile']
scope: ['profile']
});
}
@ -28,20 +28,17 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
request: any,
token: string,
refreshToken: string,
profile,
profile: Profile,
done: Function,
done2: Function
) {
try {
const jwt: string = await this.authService.validateOAuthLogin({
const jwt = await this.authService.validateOAuthLogin({
provider: Provider.GOOGLE,
thirdPartyId: profile.id
});
const user = {
jwt
};
done(null, user);
done(null, { jwt });
} catch (error) {
Logger.error(error, 'GoogleStrategy');
done(error, false);

View File

@ -1,8 +1,8 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.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/transform-data-source-in-response.interceptor';
import type {
BenchmarkMarketDataDetails,
BenchmarkResponse,

View File

@ -1,6 +1,7 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.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 { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
@ -17,7 +18,6 @@ import { BenchmarkService } from './benchmark.service';
controllers: [BenchmarkController],
exports: [BenchmarkService],
imports: [
ConfigurationModule,
DataProviderModule,
ExchangeRateDataModule,
MarketDataModule,
@ -25,7 +25,9 @@ import { BenchmarkService } from './benchmark.service';
PropertyModule,
RedisCacheModule,
SymbolModule,
SymbolProfileModule
SymbolProfileModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
],
providers: [BenchmarkService]
})

View File

@ -1,10 +1,4 @@
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';
@ -12,14 +6,6 @@ import { CacheController } from './cache.controller';
@Module({
controllers: [CacheController],
imports: [
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
PrismaModule,
RedisCacheModule,
SymbolProfileModule
]
imports: [RedisCacheModule]
})
export class CacheModule {}

View File

@ -1,10 +1,6 @@
import { AccountModule } from '@ghostfolio/api/app/account/account.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 { 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';
@ -12,15 +8,7 @@ import { ExportController } from './export.controller';
import { ExportService } from './export.service';
@Module({
imports: [
AccountModule,
ApiModule,
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
OrderModule,
RedisCacheModule
],
imports: [AccountModule, ApiModule, OrderModule],
controllers: [ExportController],
providers: [ExportService]
})

View File

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

View File

@ -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 { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -9,7 +9,11 @@ import { HealthService } from './health.service';
@Module({
controllers: [HealthController],
imports: [ConfigurationModule, DataEnhancerModule, DataProviderModule],
imports: [
DataEnhancerModule,
DataProviderModule,
TransformDataSourceInRequestModule
],
providers: [HealthService]
})
export class HealthModule {}

View File

@ -1,7 +1,7 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.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/transform-data-source-in-response.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ImportResponse } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';

View File

@ -4,6 +4,8 @@ import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.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 { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -30,7 +32,9 @@ import { ImportService } from './import.service';
PortfolioModule,
PrismaModule,
RedisCacheModule,
SymbolProfileModule
SymbolProfileModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
],
providers: [ImportService]
})

View File

@ -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 { Controller, Get, UseInterceptors } from '@nestjs/common';

View File

@ -2,6 +2,7 @@ import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module'
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.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 { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -34,6 +35,7 @@ import { InfoService } from './info.service';
RedisCacheModule,
SymbolProfileModule,
TagModule,
TransformDataSourceInResponseModule,
UserModule
],
providers: [InfoService]

View File

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

View File

@ -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 { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -8,7 +9,11 @@ import { LogoService } from './logo.service';
@Module({
controllers: [LogoController],
imports: [ConfigurationModule, SymbolProfileModule],
imports: [
ConfigurationModule,
SymbolProfileModule,
TransformDataSourceInRequestModule
],
providers: [LogoService]
})
export class LogoModule {}

View File

@ -1,9 +1,9 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-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/transform-data-source-in-request.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 { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
@ -11,7 +11,7 @@ import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
HEADER_KEY_IMPERSONATION
} 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 {
@ -53,8 +53,20 @@ export class OrderController {
@Delete()
@HasPermission(permissions.deleteOrder)
@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({
filters,
userCurrency: this.request.user.Settings.settings.baseCurrency,
userId: this.request.user.id
});
}

View File

@ -2,9 +2,10 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/accou
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CacheModule } from '@ghostfolio/api/app/cache/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 { 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';
@ -23,15 +24,16 @@ import { OrderService } from './order.service';
imports: [
ApiModule,
CacheModule,
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
ImpersonationModule,
PrismaModule,
RedactValuesInResponseModule,
RedisCacheModule,
SymbolProfileModule,
UserModule
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
],
providers: [AccountBalanceService, AccountService, OrderService]
})

View File

@ -194,16 +194,36 @@ export class OrderService {
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({
where
where: {
id: {
in: activities.map(({ id }) => {
return id;
})
}
}
});
this.eventEmitter.emit(
PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({
userId: <string>where.userId
})
new PortfolioChangedEvent({ userId })
);
return count;

View File

@ -7,7 +7,7 @@ import {
import { Account, Tag } from '@prisma/client';
export interface PortfolioPositionDetail {
export interface PortfolioHoldingDetail {
accounts: Account[];
averagePrice: number;
dataProviderInfo: DataProviderInfo;

View File

@ -1,5 +0,0 @@
import { Position } from '@ghostfolio/common/interfaces';
export interface PortfolioPositions {
positions: Position[];
}

View File

@ -7,9 +7,9 @@ import {
nullifyValuesInObject
} from '@ghostfolio/api/helper/object.helper';
import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-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/transform-data-source-in-request.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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@ -27,6 +27,10 @@ import {
PortfolioPublicDetails,
PortfolioReport
} from '@ghostfolio/common/interfaces';
import {
hasReadRestrictedAccessPermission,
isRestrictedView
} from '@ghostfolio/common/permissions';
import type {
DateRange,
GroupBy,
@ -51,8 +55,7 @@ import { AssetClass, AssetSubClass } from '@prisma/client';
import { Big } from 'big.js';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface';
import { PortfolioService } from './portfolio.service';
@Controller('portfolio')
@ -85,11 +88,6 @@ export class PortfolioController {
let hasDetails = true;
let hasError = false;
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = this.request.user.subscription.type === 'Premium';
@ -118,8 +116,11 @@ export class PortfolioController {
let portfolioSummary = summary;
if (
hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user)
hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
}) ||
isRestrictedView(this.request.user)
) {
const totalInvestment = Object.values(holdings)
.map(({ investment }) => {
@ -159,27 +160,30 @@ export class PortfolioController {
if (
hasDetails === false ||
hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user)
hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
}) ||
isRestrictedView(this.request.user)
) {
portfolioSummary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentGrossPerformanceWithCurrencyEffect',
'currentNetPerformance',
'currentNetPerformanceWithCurrencyEffect',
'currentNetWorth',
'currentValue',
'currentValueInBaseCurrency',
'dividendInBaseCurrency',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
'filteredValueInBaseCurrency',
'fireWealth',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'interest',
'items',
'liabilities',
'netPerformance',
'netPerformanceWithCurrencyEffect',
'totalBuy',
'totalInvestment',
'totalSell',
@ -227,12 +231,6 @@ export class PortfolioController {
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string
): Promise<PortfolioDividends> {
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
@ -260,8 +258,11 @@ export class PortfolioController {
});
if (
hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user)
hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
}) ||
isRestrictedView(this.request.user)
) {
const maxDividend = dividends.reduce(
(investment, item) => Math.max(investment, item.investment),
@ -327,12 +328,6 @@ export class PortfolioController {
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string
): Promise<PortfolioInvestments> {
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
@ -348,8 +343,11 @@ export class PortfolioController {
});
if (
hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user)
hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
}) ||
isRestrictedView(this.request.user)
) {
const maxInvestment = investments.reduce(
(investment, item) => Math.max(investment, item.investment),
@ -398,12 +396,6 @@ export class PortfolioController {
): Promise<PortfolioPerformanceResponse> {
const withExcludedAccounts = withExcludedAccountsParam === 'true';
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
@ -419,9 +411,12 @@ export class PortfolioController {
});
if (
hasReadRestrictedAccessPermission ||
this.request.user.Settings.settings.viewMode === 'ZEN' ||
this.userService.isRestrictedView(this.request.user)
hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
}) ||
isRestrictedView(this.request.user) ||
this.request.user.Settings.settings.viewMode === 'ZEN'
) {
performanceInformation.chart = performanceInformation.chart.map(
({
@ -449,10 +444,14 @@ export class PortfolioController {
.div(performanceInformation.performance.totalInvestment)
.toNumber(),
valueInPercentage:
performanceInformation.performance.currentValue === 0
performanceInformation.performance.currentValueInBaseCurrency ===
0
? 0
: new Big(value)
.div(performanceInformation.performance.currentValue)
.div(
performanceInformation.performance
.currentValueInBaseCurrency
)
.toNumber()
};
}
@ -461,12 +460,12 @@ export class PortfolioController {
performanceInformation.performance = nullifyValuesInObject(
performanceInformation.performance,
[
'currentGrossPerformance',
'currentGrossPerformanceWithCurrencyEffect',
'currentNetPerformance',
'currentNetPerformanceWithCurrencyEffect',
'currentNetWorth',
'currentValue',
'currentValueInBaseCurrency',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'netPerformance',
'netPerformanceWithCurrencyEffect',
'totalInvestment'
]
);
@ -483,39 +482,13 @@ export class PortfolioController {
);
performanceInformation.performance = nullifyValuesInObject(
performanceInformation.performance,
['currentNetPerformance', 'currentNetPerformancePercent']
['netPerformance']
);
}
return performanceInformation;
}
@Get('positions')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPositions(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('query') filterBySearchQuery?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string
): Promise<PortfolioPositions> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterBySearchQuery,
filterByTags
});
return this.portfolioService.getPositions({
dateRange,
filters,
impersonationId
});
}
@Get('public/:accessId')
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPublic(
@ -592,7 +565,7 @@ export class PortfolioController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('dataSource') dataSource,
@Param('symbol') symbol
): Promise<PortfolioPositionDetail> {
): Promise<PortfolioHoldingDetail> {
const position = await this.portfolioService.getPosition(
dataSource,
impersonationId,

View File

@ -4,6 +4,9 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderModule } from '@ghostfolio/api/app/order/order.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 { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
@ -36,8 +39,11 @@ import { RulesService } from './rules.service';
MarketDataModule,
OrderModule,
PrismaModule,
RedactValuesInResponseModule,
RedisCacheModule,
SymbolProfileModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule,
UserModule
],
providers: [

View File

@ -27,7 +27,7 @@ describe('PortfolioService', () => {
portfolioService
.getAnnualizedPerformancePercent({
daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day
netPerformancePercent: new Big(0)
netPerformancePercentage: new Big(0)
})
.toNumber()
).toEqual(0);
@ -36,7 +36,7 @@ describe('PortfolioService', () => {
portfolioService
.getAnnualizedPerformancePercent({
daysInMarket: 0,
netPerformancePercent: new Big(0)
netPerformancePercentage: new Big(0)
})
.toNumber()
).toEqual(0);
@ -48,7 +48,7 @@ describe('PortfolioService', () => {
portfolioService
.getAnnualizedPerformancePercent({
daysInMarket: 65, // < 1 year
netPerformancePercent: new Big(0.1025)
netPerformancePercentage: new Big(0.1025)
})
.toNumber()
).toBeCloseTo(0.729705);
@ -57,7 +57,7 @@ describe('PortfolioService', () => {
portfolioService
.getAnnualizedPerformancePercent({
daysInMarket: 365, // 1 year
netPerformancePercent: new Big(0.05)
netPerformancePercentage: new Big(0.05)
})
.toNumber()
).toBeCloseTo(0.05);
@ -69,7 +69,7 @@ describe('PortfolioService', () => {
portfolioService
.getAnnualizedPerformancePercent({
daysInMarket: 575, // > 1 year
netPerformancePercent: new Big(0.2374)
netPerformancePercentage: new Big(0.2374)
})
.toNumber()
).toBeCloseTo(0.145);

View File

@ -77,7 +77,7 @@ import {
PerformanceCalculationType,
PortfolioCalculatorFactory
} 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';
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
@ -208,16 +208,16 @@ export class PortfolioService {
public getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent
netPerformancePercentage
}: {
daysInMarket: number;
netPerformancePercent: Big;
netPerformancePercentage: Big;
}): Big {
if (isNumber(daysInMarket) && daysInMarket > 0) {
const exponent = new Big(365).div(daysInMarket).toNumber();
return new Big(
Math.pow(netPerformancePercent.plus(1).toNumber(), exponent)
Math.pow(netPerformancePercentage.plus(1).toNumber(), exponent)
).minus(1);
}
@ -360,7 +360,7 @@ export class PortfolioService {
userId,
calculationType: PerformanceCalculationType.TWR,
currency: userCurrency,
hasFilters: filters?.length > 0,
hasFilters: true, // disable cache
isExperimentalFeatures:
this.request.user?.Settings.settings.isExperimentalFeatures
});
@ -602,7 +602,7 @@ export class PortfolioService {
aDataSource: DataSource,
aImpersonationId: string,
aSymbol: string
): Promise<PortfolioPositionDetail> {
): Promise<PortfolioHoldingDetail> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user);
@ -693,7 +693,7 @@ export class PortfolioService {
transactionCount
} = position;
const accounts: PortfolioPositionDetail['accounts'] = uniqBy(
const accounts: PortfolioHoldingDetail['accounts'] = uniqBy(
orders.filter(({ Account }) => {
return Account;
}),
@ -704,7 +704,7 @@ export class PortfolioService {
const dividendYieldPercent = this.getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
netPerformancePercent: timeWeightedInvestment.eq(0)
netPerformancePercentage: timeWeightedInvestment.eq(0)
? new Big(0)
: dividendInBaseCurrency.div(timeWeightedInvestment)
});
@ -712,7 +712,9 @@ export class PortfolioService {
const dividendYieldPercentWithCurrencyEffect =
this.getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
netPerformancePercent: timeWeightedInvestmentWithCurrencyEffect.eq(0)
netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(
0
)
? new Big(0)
: dividendInBaseCurrency.div(
timeWeightedInvestmentWithCurrencyEffect
@ -1108,16 +1110,16 @@ export class PortfolioService {
firstOrderDate: undefined,
hasErrors: false,
performance: {
currentGrossPerformance: 0,
currentGrossPerformancePercent: 0,
currentGrossPerformancePercentWithCurrencyEffect: 0,
currentGrossPerformanceWithCurrencyEffect: 0,
currentNetPerformance: 0,
currentNetPerformancePercent: 0,
currentNetPerformancePercentWithCurrencyEffect: 0,
currentNetPerformanceWithCurrencyEffect: 0,
currentNetWorth: 0,
currentValue: 0,
currentValueInBaseCurrency: 0,
grossPerformance: 0,
grossPerformancePercentage: 0,
grossPerformancePercentageWithCurrencyEffect: 0,
grossPerformanceWithCurrencyEffect: 0,
netPerformance: 0,
netPerformancePercentage: 0,
netPerformancePercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0,
totalInvestment: 0
}
};
@ -1152,9 +1154,9 @@ export class PortfolioService {
let currentNetPerformance = netPerformance;
let currentNetPerformancePercent = netPerformancePercentage;
let currentNetPerformancePercentage = netPerformancePercentage;
let currentNetPerformancePercentWithCurrencyEffect =
let currentNetPerformancePercentageWithCurrencyEffect =
netPerformancePercentageWithCurrencyEffect;
let currentNetPerformanceWithCurrencyEffect =
@ -1173,11 +1175,11 @@ export class PortfolioService {
if (itemOfToday) {
currentNetPerformance = new Big(itemOfToday.netPerformance);
currentNetPerformancePercent = new Big(
currentNetPerformancePercentage = new Big(
itemOfToday.netPerformanceInPercentage
).div(100);
currentNetPerformancePercentWithCurrencyEffect = new Big(
currentNetPerformancePercentageWithCurrencyEffect = new Big(
itemOfToday.netPerformanceInPercentageWithCurrencyEffect
).div(100);
@ -1195,19 +1197,19 @@ export class PortfolioService {
firstOrderDate: parseDate(items[0]?.date),
performance: {
currentNetWorth,
currentGrossPerformance: grossPerformance.toNumber(),
currentGrossPerformancePercent: grossPerformancePercentage.toNumber(),
currentGrossPerformancePercentWithCurrencyEffect:
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
grossPerformance: grossPerformance.toNumber(),
grossPerformancePercentage: grossPerformancePercentage.toNumber(),
grossPerformancePercentageWithCurrencyEffect:
grossPerformancePercentageWithCurrencyEffect.toNumber(),
currentGrossPerformanceWithCurrencyEffect:
grossPerformanceWithCurrencyEffect:
grossPerformanceWithCurrencyEffect.toNumber(),
currentNetPerformance: currentNetPerformance.toNumber(),
currentNetPerformancePercent: currentNetPerformancePercent.toNumber(),
currentNetPerformancePercentWithCurrencyEffect:
currentNetPerformancePercentWithCurrencyEffect.toNumber(),
currentNetPerformanceWithCurrencyEffect:
netPerformance: currentNetPerformance.toNumber(),
netPerformancePercentage: currentNetPerformancePercentage.toNumber(),
netPerformancePercentageWithCurrencyEffect:
currentNetPerformancePercentageWithCurrencyEffect.toNumber(),
netPerformanceWithCurrencyEffect:
currentNetPerformanceWithCurrencyEffect.toNumber(),
currentValue: currentValueInBaseCurrency.toNumber(),
totalInvestment: totalInvestment.toNumber()
}
};
@ -1604,11 +1606,6 @@ export class PortfolioService {
userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId });
const performanceInformation = await this.getPerformance({
impersonationId,
userId
});
const { activities } = await this.orderService.getOrders({
userCurrency,
userId,
@ -1626,6 +1623,19 @@ export class PortfolioService {
}
}
const {
currentValueInBaseCurrency,
grossPerformance,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
grossPerformanceWithCurrencyEffect,
netPerformance,
netPerformancePercentage,
netPerformancePercentageWithCurrencyEffect,
netPerformanceWithCurrencyEffect,
totalInvestment
} = await portfolioCalculator.getSnapshot();
const dividendInBaseCurrency =
await portfolioCalculator.getDividendInBaseCurrency();
@ -1694,7 +1704,7 @@ export class PortfolioService {
.toNumber();
const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue)
.plus(currentValueInBaseCurrency)
.plus(valuables)
.plus(excludedAccountsAndActivities)
.minus(liabilities)
@ -1704,21 +1714,18 @@ export class PortfolioService {
const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercent
)
netPerformancePercentage: new Big(netPerformancePercentage)
})?.toNumber();
const annualizedPerformancePercentWithCurrencyEffect =
this.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercentWithCurrencyEffect
netPerformancePercentage: new Big(
netPerformancePercentageWithCurrencyEffect
)
})?.toNumber();
return {
...performanceInformation.performance,
annualizedPerformancePercent,
annualizedPerformancePercentWithCurrencyEffect,
cash,
@ -1727,6 +1734,7 @@ export class PortfolioService {
totalBuy,
totalSell,
committedFunds: committedFunds.toNumber(),
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
emergencyFund: {
assets: emergencyFundPositionsValueInBaseCurrency,
@ -1740,15 +1748,28 @@ export class PortfolioService {
filteredValueInPercentage: netWorth
? filteredValueInBaseCurrency.div(netWorth).toNumber()
: undefined,
fireWealth: new Big(performanceInformation.performance.currentValue)
fireWealth: new Big(currentValueInBaseCurrency)
.minus(emergencyFundPositionsValueInBaseCurrency)
.toNumber(),
grossPerformance: grossPerformance.toNumber(),
grossPerformancePercentage: grossPerformancePercentage.toNumber(),
grossPerformancePercentageWithCurrencyEffect:
grossPerformancePercentageWithCurrencyEffect.toNumber(),
grossPerformanceWithCurrencyEffect:
grossPerformanceWithCurrencyEffect.toNumber(),
interest: interest.toNumber(),
items: valuables.toNumber(),
liabilities: liabilities.toNumber(),
netPerformance: netPerformance.toNumber(),
netPerformancePercentage: netPerformancePercentage.toNumber(),
netPerformancePercentageWithCurrencyEffect:
netPerformancePercentageWithCurrencyEffect.toNumber(),
netPerformanceWithCurrencyEffect:
netPerformanceWithCurrencyEffect.toNumber(),
ordersCount: activities.filter(({ type }) => {
return type === 'BUY' || type === 'SELL';
return ['BUY', 'SELL'].includes(type);
}).length,
totalInvestment: totalInvestment.toNumber(),
totalValueInBaseCurrency: netWorth
};
}

View File

@ -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 { SitemapController } from './sitemap.controller';
@Module({
controllers: [SitemapController],
imports: [
ConfigurationModule,
DataGatheringModule,
DataProviderModule,
ExchangeRateDataModule,
PrismaModule,
RedisCacheModule,
SymbolProfileModule
]
controllers: [SitemapController]
})
export class SitemapModule {}

View File

@ -1,6 +1,6 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.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/transform-data-source-in-response.interceptor';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';

View File

@ -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 { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
@ -12,10 +13,11 @@ import { SymbolService } from './symbol.service';
controllers: [SymbolController],
exports: [SymbolService],
imports: [
ConfigurationModule,
DataProviderModule,
MarketDataModule,
PrismaModule
PrismaModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
],
providers: [SymbolService]
})

View File

@ -121,28 +121,6 @@ export class UserService {
return usersWithAdminRole.length > 0;
}
public hasReadRestrictedAccessPermission({
impersonationId,
user
}: {
impersonationId: string;
user: UserWithSettings;
}) {
if (!impersonationId) {
return false;
}
const access = user.Access?.find(({ id }) => {
return id === impersonationId;
});
return access?.permissions?.includes('READ_RESTRICTED') ?? true;
}
public isRestrictedView(aUser: UserWithSettings) {
return aUser.Settings.settings.isRestrictedView ?? false;
}
public async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> {

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ export class PortfolioChangedListener {
@OnEvent(PortfolioChangedEvent.getName())
handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
Logger.log(
`Portfolio of user with id ${event.getUserId()} has changed`,
`Portfolio of user '${event.getUserId()}' has changed`,
'PortfolioChangedListener'
);

View File

@ -1,6 +1,9 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import {
hasReadRestrictedAccessPermission,
isRestrictedView
} from '@ghostfolio/common/permissions';
import { UserWithSettings } from '@ghostfolio/common/types';
import {
@ -16,7 +19,7 @@ import { map } from 'rxjs/operators';
export class RedactValuesInResponseInterceptor<T>
implements NestInterceptor<T, any>
{
public constructor(private userService: UserService) {}
public constructor() {}
public intercept(
context: ExecutionContext,
@ -29,15 +32,13 @@ export class RedactValuesInResponseInterceptor<T>
const impersonationId =
headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
const hasReadRestrictedPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user
});
if (
hasReadRestrictedPermission ||
this.userService.isRestrictedView(user)
hasReadRestrictedAccessPermission({
impersonationId,
user
}) ||
isRestrictedView(user)
) {
data = redactAttributes({
object: data,

View File

@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class RedactValuesInResponseModule {}

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { Logger, ValidationPipe, VersioningType } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import * as bodyParser from 'body-parser';
import { json } from 'body-parser';
import helmet from 'helmet';
import { AppModule } from './app/app.module';
@ -34,7 +34,7 @@ async function bootstrap() {
);
// 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') {
app.use(

View File

@ -50,7 +50,9 @@ export class AlphaVantageService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
isPremium: false,
name: 'Alpha Vantage',
url: 'https://www.alphavantage.co'
};
}

View File

@ -598,10 +598,14 @@ export class DataProviderService {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
})
.map((lookupItem) => {
if (
!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') ||
user.subscription.type === 'Premium'
) {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
if (user.subscription.type === 'Premium') {
lookupItem.dataProviderInfo.isPremium = false;
}
lookupItem.dataProviderInfo.name = undefined;
lookupItem.dataProviderInfo.url = undefined;
} else {
lookupItem.dataProviderInfo.isPremium = false;
}

View File

@ -66,7 +66,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: true
isPremium: true,
name: 'EOD Historical Data',
url: 'https://eodhd.com'
};
}

View File

@ -46,7 +46,9 @@ export class GoogleSheetsService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
isPremium: false,
name: 'Google Sheets',
url: 'https://docs.google.com/spreadsheets'
};
}

View File

@ -43,7 +43,9 @@ export class RapidApiService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
isPremium: false,
name: 'Rapid API',
url: 'https://rapidapi.com'
};
}

View File

@ -43,7 +43,9 @@ export class YahooFinanceService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
isPremium: false,
name: 'Yahoo Finance',
url: 'https://finance.yahoo.com'
};
}

View File

@ -78,7 +78,7 @@ export class ExchangeRateDataService {
);
const lastDateString = dateStrings.reduce((a, b) => {
return a > b ? a : b;
});
}, undefined);
let previousExchangeRate =
exchangeRatesByCurrency[`${currency}${targetCurrency}`]?.[

View File

@ -61,7 +61,9 @@ export class SymbolProfileService {
})
}
})
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
.then((symbolProfiles) => {
return this.enhanceSymbolProfiles(symbolProfiles);
});
}
public async getSymbolProfilesByIds(
@ -83,7 +85,9 @@ export class SymbolProfileService {
}
}
})
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
.then((symbolProfiles) => {
return this.enhanceSymbolProfiles(symbolProfiles);
});
}
public updateSymbolProfile({
@ -119,7 +123,7 @@ export class SymbolProfileService {
});
}
private getSymbols(
private enhanceSymbolProfiles(
symbolProfiles: (SymbolProfile & {
_count: { Order: number };
Order?: {

View File

@ -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 { NgModule } from '@angular/core';
@ -5,18 +7,6 @@ import { RouterModule, Routes, TitleStrategy } from '@angular/router';
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 = [
{
path: paths.about,
@ -53,9 +43,12 @@ const routes: Routes = [
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
},
{
path: 'demo',
loadChildren: () =>
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
canActivate: [AuthGuard],
loadComponent: () =>
import('./pages/demo/demo-page.component').then(
(c) => c.GfDemoPageComponent
),
path: 'demo'
},
{
path: paths.faq,
@ -63,11 +56,13 @@ const routes: Routes = [
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,
loadChildren: () =>
import('./pages/features/features-page.module').then(
(m) => m.FeaturesPageModule
)
title: $localize`Features`
},
{
path: 'home',
@ -75,9 +70,13 @@ const routes: Routes = [
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',
loadChildren: () =>
import('./pages/i18n/i18n-page.module').then((m) => m.I18nPageModule)
title: $localize`Internationalization`
},
{
path: paths.markets,
@ -134,11 +133,12 @@ const routes: Routes = [
)
},
{
loadComponent: () =>
import('./pages/webauthn/webauthn-page.component').then(
(c) => c.GfWebauthnPageComponent
),
path: 'webauthn',
loadChildren: () =>
import('./pages/webauthn/webauthn-page.module').then(
(m) => m.WebauthnPageModule
)
title: $localize`Sign in`
},
{
path: 'zen',

View File

@ -158,11 +158,9 @@
<li>
<a href="../pt" title="Ghostfolio in Português">Português</a>
</li>
<!--
<li>
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
</li>
-->
<li>
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
</li>
<!--
<li>
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>

View File

@ -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 { InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -13,13 +15,21 @@ import {
OnDestroy,
OnInit
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
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 { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { DataService } from './services/data.service';
import { ImpersonationStorageService } from './services/impersonation-storage.service';
import { TokenStorageService } from './services/token-storage.service';
import { UserService } from './services/user/user.service';
@ -38,6 +48,7 @@ export class AppComponent implements OnDestroy, OnInit {
public currentRoute: string;
public currentYear = new Date().getFullYear();
public deviceType: string;
public hasImpersonationId: boolean;
public hasInfoMessage: boolean;
public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean;
@ -67,7 +78,10 @@ export class AppComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
@Inject(DOCUMENT) private document: Document,
private impersonationStorageService: ImpersonationStorageService,
private route: ActivatedRoute,
private router: Router,
private title: Title,
private tokenStorageService: TokenStorageService,
@ -75,6 +89,21 @@ export class AppComponent implements OnDestroy, OnInit {
) {
this.initializeTheme();
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() {
@ -96,6 +125,13 @@ export class AppComponent implements OnDestroy, OnInit {
permissions.enableFearAndGreedIndex
);
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.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) {
const themeColor = getCssVariable(
isDarkTheme ? '--dark-background' : '--light-background'

View File

@ -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 { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -95,19 +96,9 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
this.dialogRef.close();
}
public onAddAccountBalance({
balance,
date
}: {
balance: number;
date: Date;
}) {
public onAddAccountBalance(accountBalance: CreateAccountBalanceDto) {
this.dataService
.postAccountBalance({
balance,
date,
accountId: this.data.accountId
})
.postAccountBalance(accountBalance)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.fetchAccount();

View File

@ -94,6 +94,7 @@
[dataSource]="dataSource"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToDeleteActivity]="false"
[hasPermissionToExportActivities]="
!data.hasImpersonationId && !user.settings.isRestrictedView
"

View File

@ -38,6 +38,7 @@ import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/in
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'has-fab' },
selector: 'gf-admin-market-data',
styleUrls: ['./admin-market-data.scss'],
templateUrl: './admin-market-data.html'

View File

@ -8,7 +8,6 @@ import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
AdminMarketDataDetails,
Currency,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n';
@ -73,7 +72,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
public countries: {
[code: string]: { name: string; value: number };
};
public currencies: Currency[] = [];
public currencies: string[] = [];
public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix;
public isBenchmark = false;
public marketDataDetails: MarketData[] = [];
@ -102,10 +101,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
const { benchmarks, currencies } = this.dataService.fetchInfo();
this.benchmarks = benchmarks;
this.currencies = currencies.map((currency) => ({
label: currency,
value: currency
}));
this.currencies = currencies;
this.initialize();
}
@ -293,9 +289,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
assetClass: this.assetProfileForm.get('assetClass').value,
assetSubClass: this.assetProfileForm.get('assetSubClass').value,
comment: this.assetProfileForm.get('comment').value || null,
currency: (<Currency>(
(<unknown>this.assetProfileForm.get('currency').value)
))?.value,
currency: this.assetProfileForm.get('currency').value,
name: this.assetProfileForm.get('name').value,
url: this.assetProfileForm.get('url').value || null
};
@ -343,8 +337,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
' ' +
price +
' ' +
(<Currency>(<unknown>this.assetProfileForm.get('currency').value))
?.value
this.assetProfileForm.get('currency').value
);
});
}

View File

@ -115,11 +115,22 @@
>Symbol</gf-value
>
</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">
<gf-value i18n size="medium" [value]="assetProfile?.currency"
>Currency</gf-value
>
</div>
<div class="col-6 mb-3"></div>
<div class="col-6 mb-3">
<gf-value
i18n

View File

@ -116,7 +116,12 @@
#assistantTrigger="matMenuTrigger"
class="h-100 no-min-width px-2"
mat-button
matBadge="✓"
matBadgeSize="small"
[mat-menu-trigger-for]="assistantMenu"
[matBadgeHidden]="
!hasFilters || !user?.settings?.isExperimentalFeatures
"
[matMenuTriggerRestoreFocus]="false"
(menuOpened)="onOpenAssistant()"
>

View File

@ -28,6 +28,17 @@
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 {
font-size: 1.5rem;

View File

@ -65,6 +65,7 @@ export class HeaderComponent implements OnChanges {
@ViewChild('assistant') assistantElement: GfAssistantComponent;
@ViewChild('assistantTrigger') assistentMenuTriggerElement: MatMenuTrigger;
public hasFilters: boolean;
public hasPermissionForSocialLogin: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToAccessAdminControl: boolean;
@ -106,6 +107,8 @@ export class HeaderComponent implements OnChanges {
}
public ngOnChanges() {
this.hasFilters = this.userService.hasFilters();
this.hasPermissionForSocialLogin = hasPermission(
this.info?.globalPermissions,
permissions.enableSocialLogin

View File

@ -5,6 +5,7 @@ import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { MatToolbarModule } from '@angular/material/toolbar';
@ -21,6 +22,7 @@ import { HeaderComponent } from './header.component';
GfLogoComponent,
GfPremiumIndicatorComponent,
LoginWithAccessTokenDialogModule,
MatBadgeModule,
MatButtonModule,
MatMenuModule,
MatToolbarModule,

View File

@ -1,4 +1,7 @@
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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
@ -8,9 +11,16 @@ import {
LineChartItem,
User
} 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 { 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,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
@ -18,24 +28,50 @@ import {
OnDestroy,
OnInit
} 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 { MatTableDataSource } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { Account, Tag } from '@prisma/client';
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { PositionDetailDialogParams } from './interfaces/interfaces';
import { HoldingDetailDialogParams } from './interfaces/interfaces';
@Component({
host: { class: 'd-flex flex-column h-100' },
selector: 'gf-position-detail-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'position-detail-dialog.html',
styleUrls: ['./position-detail-dialog.component.scss']
host: { class: 'd-flex flex-column h-100' },
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 activities: Activity[];
public assetClass: string;
@ -56,6 +92,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
public marketPrice: number;
public maxPrice: number;
public minPrice: number;
public netPerformance: number;
public netPerformancePercent: number;
public netPerformancePercentWithCurrencyEffect: number;
public netPerformanceWithCurrencyEffect: number;
public quantity: number;
@ -78,14 +116,14 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
public dialogRef: MatDialogRef<PositionDetailDialog>,
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams,
public dialogRef: MatDialogRef<GfHoldingDetailDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: HoldingDetailDialogParams,
private userService: UserService
) {}
public ngOnInit() {
this.dataService
.fetchPositionDetail({
.fetchHoldingDetail({
dataSource: this.data.dataSource,
symbol: this.data.symbol
})
@ -104,6 +142,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
marketPrice,
maxPrice,
minPrice,
netPerformance,
netPerformancePercent,
netPerformancePercentWithCurrencyEffect,
netPerformanceWithCurrencyEffect,
orders,
@ -126,15 +166,15 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.feeInBaseCurrency = feeInBaseCurrency;
this.firstBuyDate = firstBuyDate;
this.historicalDataItems = historicalData.map(
(historicalDataItem) => {
({ averagePrice, date, marketPrice }) => {
this.benchmarkDataItems.push({
date: historicalDataItem.date,
value: historicalDataItem.averagePrice
date,
value: averagePrice
});
return {
date: historicalDataItem.date,
value: historicalDataItem.marketPrice
date,
value: marketPrice
};
}
);
@ -142,6 +182,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.marketPrice = marketPrice;
this.maxPrice = maxPrice;
this.minPrice = minPrice;
this.netPerformance = netPerformance;
this.netPerformancePercent = netPerformancePercent;
this.netPerformancePercentWithCurrencyEffect =
netPerformancePercentWithCurrencyEffect;
this.netPerformanceWithCurrencyEffect =

View File

@ -37,27 +37,58 @@
<div class="row">
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isCurrency]="true"
[locale]="data.locale"
[unit]="data.baseCurrency"
[value]="netPerformanceWithCurrencyEffect"
>Change</gf-value
>
@if (
SymbolProfile?.currency &&
data.baseCurrency !== SymbolProfile?.currency
) {
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isCurrency]="true"
[locale]="data.locale"
[unit]="data.baseCurrency"
[value]="netPerformanceWithCurrencyEffect"
>Change with currency effect</gf-value
>
} @else {
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isCurrency]="true"
[locale]="data.locale"
[unit]="data.baseCurrency"
[value]="netPerformance"
>Change</gf-value
>
}
</div>
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isPercent]="true"
[locale]="data.locale"
[value]="netPerformancePercentWithCurrencyEffect"
>Performance</gf-value
>
@if (
SymbolProfile?.currency &&
data.baseCurrency !== SymbolProfile?.currency
) {
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isPercent]="true"
[locale]="data.locale"
[value]="netPerformancePercentWithCurrencyEffect"
>Performance with currency effect</gf-value
>
} @else {
<gf-value
i18n
size="medium"
[colorizeSign]="true"
[isPercent]="true"
[locale]="data.locale"
[value]="netPerformancePercent"
>Performance</gf-value
>
}
</div>
<div class="col-6 mb-3">
<gf-value
@ -304,6 +335,7 @@
[dataSource]="dataSource"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToDeleteActivity]="false"
[hasPermissionToExportActivities]="
!data.hasImpersonationId && !user?.settings?.isRestrictedView
"

View File

@ -2,7 +2,7 @@ import { ColorScheme } from '@ghostfolio/common/types';
import { DataSource } from '@prisma/client';
export interface PositionDetailDialogParams {
export interface HoldingDetailDialogParams {
baseCurrency: string;
colorScheme: ColorScheme;
dataSource: DataSource;

View File

@ -1,33 +1,30 @@
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Position, User } from '@ghostfolio/common/interfaces';
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { HoldingType, ToggleOption } from '@ghostfolio/common/types';
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 { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { PositionDetailDialogParams } from '../position/position-detail-dialog/interfaces/interfaces';
@Component({
selector: 'gf-home-holdings',
styleUrls: ['./home-holdings.scss'],
templateUrl: './home-holdings.html'
})
export class HomeHoldingsComponent implements OnDestroy, OnInit {
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public deviceType: string;
public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean;
public positions: Position[];
public holdings: PortfolioPosition[];
public holdingType: HoldingType = 'ACTIVE';
public holdingTypeOptions: ToggleOption[] = [
{ label: $localize`Active`, value: 'ACTIVE' },
{ label: $localize`Closed`, value: 'CLOSED' }
];
public user: User;
private unsubscribeSubject = new Subject<void>();
@ -36,25 +33,18 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService,
private route: ActivatedRoute,
private router: Router,
private userService: UserService
) {
this.route.queryParams
) {}
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (
params['dataSource'] &&
params['positionDetailDialog'] &&
params['symbol']
) {
this.openPositionDialog({
dataSource: params['dataSource'],
symbol: params['symbol']
});
}
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.userService.stateChanged
@ -68,37 +58,32 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
permissions.createOrder
);
this.update();
this.holdings = undefined;
this.fetchHoldings()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ holdings }) => {
this.holdings = holdings;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
public onChangeHoldingType(aHoldingType: HoldingType) {
this.holdingType = aHoldingType;
this.impersonationStorageService
.onChangeHasImpersonation()
this.holdings = undefined;
this.fetchHoldings()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
}
.subscribe(({ holdings }) => {
this.holdings = holdings;
public onChangeDateRange(dateRange: DateRange) {
this.dataService
.putUserSetting({ dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
});
}
@ -107,59 +92,16 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete();
}
private openPositionDialog({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
private fetchHoldings() {
const filters = this.userService.getFilters();
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'
});
if (this.holdingType === 'CLOSED') {
filters.push({ id: 'CLOSED', type: 'HOLDING_TYPE' });
}
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
});
});
}
private update() {
this.positions = undefined;
this.dataService
.fetchPositions({ range: this.user?.settings?.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ positions }) => {
this.positions = positions;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
return this.dataService.fetchPortfolioHoldings({
filters,
range: this.user?.settings?.dateRange
});
}
}

View File

@ -1,27 +1,38 @@
<div class="container justify-content-center p-3">
<div class="container">
<div class="row">
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
<mat-card appearance="outlined">
<mat-card-content class="p-0">
<gf-positions
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[locale]="user?.settings?.locale"
[positions]="positions"
[range]="user?.settings?.dateRange"
/>
</mat-card-content>
</mat-card>
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
<a
class="mt-3"
i18n
mat-stroked-button
[routerLink]="['/portfolio', 'activities']"
>Manage Activities</a
>
<div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
</div>
</div>
<div class="row">
<div class="col-lg">
<div class="d-flex justify-content-end">
<gf-toggle
class="d-none d-lg-block"
[defaultValue]="holdingType"
[isLoading]="false"
[options]="holdingTypeOptions"
(change)="onChangeHoldingType($event.value)"
/>
</div>
<gf-holdings-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
[holdings]="holdings"
[locale]="user?.settings?.locale"
/>
@if (hasPermissionToCreateOrder && holdings?.length > 0) {
<div class="text-center">
<a
class="mt-3"
i18n
mat-stroked-button
[routerLink]="['/portfolio', 'activities']"
>Manage Activities</a
>
</div>
}
</div>
</div>
</div>

View File

@ -1,11 +1,9 @@
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
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 { RouterModule } from '@angular/router';
import { HomeHoldingsComponent } from './home-holdings.component';
@ -14,11 +12,9 @@ import { HomeHoldingsComponent } from './home-holdings.component';
declarations: [HomeHoldingsComponent],
imports: [
CommonModule,
GfPositionDetailDialogModule,
GfPositionsModule,
GfHoldingsTableComponent,
GfToggleModule,
MatButtonModule,
MatCardModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]

View File

@ -11,7 +11,7 @@
[colorScheme]="user?.settings?.colorScheme"
[historicalDataItems]="historicalDataItems"
[isAnimated]="true"
[locale]="user?.settings?.locale"
[locale]="user?.settings?.locale || undefined"
[showXAxis]="true"
[showYAxis]="true"
[yMax]="100"
@ -30,7 +30,7 @@
<div class="col-xs-12 col-md-8 offset-md-2">
<gf-benchmark
[benchmarks]="benchmarks"
[locale]="user?.settings?.locale"
[locale]="user?.settings?.locale || undefined"
[user]="user"
/>
<ngx-skeleton-loader

View File

@ -41,9 +41,7 @@
[isCurrency]="true"
[locale]="locale"
[value]="
isLoading
? undefined
: performance?.currentNetPerformanceWithCurrencyEffect
isLoading ? undefined : performance?.netPerformanceWithCurrencyEffect
"
/>
</div>
@ -55,7 +53,7 @@
[value]="
isLoading
? undefined
: performance?.currentNetPerformancePercentWithCurrencyEffect
: performance?.netPerformancePercentageWithCurrencyEffect
"
/>
</div>

View File

@ -49,12 +49,12 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
this.value.nativeElement.innerHTML = '';
}
} else {
if (isNumber(this.performance?.currentValue)) {
new CountUp('value', this.performance?.currentValue, {
if (isNumber(this.performance?.currentValueInBaseCurrency)) {
new CountUp('value', this.performance?.currentValueInBaseCurrency, {
decimal: getNumberFormatDecimal(this.locale),
decimalPlaces:
this.deviceType === 'mobile' &&
this.performance?.currentValue >= 100000
this.performance?.currentValueInBaseCurrency >= 100000
? 0
: 2,
duration: 1,
@ -63,8 +63,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
} else if (this.showDetails === false) {
new CountUp(
'value',
this.performance?.currentNetPerformancePercentWithCurrencyEffect *
100,
this.performance?.netPerformancePercentageWithCurrencyEffect * 100,
{
decimal: getNumberFormatDecimal(this.locale),
decimalPlaces: 2,

View File

@ -9,9 +9,19 @@
class="flex-nowrap px-3 py-1 row"
[hidden]="summary?.ordersCount === null"
>
<div class="flex-grow-1 ml-3 text-truncate" i18n>
<div class="d-flex flex-grow-1 ml-3 text-truncate">
{{ summary?.ordersCount }}
{summary?.ordersCount, plural, =1 {transaction} other {transactions}}
<ng-container i18n>{summary?.ordersCount, plural,
=1 {activity}
other {activities}
}</ng-container>
<span
class="align-items-center d-flex ml-1"
matTooltipPosition="above"
[matTooltip]="buyAndSellActivitiesTooltip"
>
<ion-icon name="information-circle-outline" />
</span>
</div>
</div>
<div class="row">
@ -65,9 +75,7 @@
[locale]="locale"
[unit]="baseCurrency"
[value]="
isLoading
? undefined
: summary?.currentGrossPerformanceWithCurrencyEffect
isLoading ? undefined : summary?.grossPerformanceWithCurrencyEffect
"
/>
</div>
@ -91,7 +99,7 @@
[value]="
isLoading
? undefined
: summary?.currentGrossPerformancePercentWithCurrencyEffect
: summary?.grossPerformancePercentageWithCurrencyEffect
"
/>
</div>
@ -121,9 +129,7 @@
[locale]="locale"
[unit]="baseCurrency"
[value]="
isLoading
? undefined
: summary?.currentNetPerformanceWithCurrencyEffect
isLoading ? undefined : summary?.netPerformanceWithCurrencyEffect
"
/>
</div>
@ -147,7 +153,7 @@
[value]="
isLoading
? undefined
: summary?.currentNetPerformancePercentWithCurrencyEffect
: summary?.netPerformancePercentageWithCurrencyEffect
"
/>
</div>
@ -164,7 +170,7 @@
[isCurrency]="true"
[locale]="locale"
[unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.currentValue"
[value]="isLoading ? undefined : summary?.currentValueInBaseCurrency"
/>
</div>
</div>

View File

@ -1,5 +1,6 @@
import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper';
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n';
import {
ChangeDetectionStrategy,
@ -28,6 +29,9 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
@Output() emergencyFundChanged = new EventEmitter<number>();
public buyAndSellActivitiesTooltip = translate(
'BUY_AND_SELL_ACTIVITIES_TOOLTIP'
);
public timeInMarket: string;
public constructor() {}

View File

@ -2,13 +2,14 @@ import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { PortfolioSummaryComponent } from './portfolio-summary.component';
@NgModule({
declarations: [PortfolioSummaryComponent],
exports: [PortfolioSummaryComponent],
imports: [CommonModule, GfValueComponent],
imports: [CommonModule, GfValueComponent, MatTooltipModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPortfolioSummaryModule {}

View File

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

View File

@ -1,72 +0,0 @@
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex p-3 w-100"
[ngClass]="{ 'cursor-default': isLoading }"
[queryParams]="{
dataSource: position?.dataSource,
positionDetailDialog: true,
symbol: position?.symbol
}"
[routerLink]="[]"
>
<div class="d-flex mr-2">
<gf-trend-indicator
class="d-flex"
size="large"
[isLoading]="isLoading"
[marketState]="position?.marketState"
[range]="range"
[value]="position?.netPerformancePercentageWithCurrencyEffect"
/>
</div>
<div *ngIf="isLoading" class="flex-grow-1">
<ngx-skeleton-loader
animation="pulse"
class="mb-1"
[theme]="{
height: '1.2rem',
width: '12rem'
}"
/>
<ngx-skeleton-loader
animation="pulse"
[theme]="{
height: '1rem',
width: '8rem'
}"
/>
</div>
<div *ngIf="!isLoading" class="flex-grow-1 text-truncate">
<div class="h6 m-0 text-truncate">{{ position?.name }}</div>
<div class="d-flex">
<small class="text-muted">{{ position?.symbol | gfSymbol }}</small>
</div>
<div class="d-flex mt-1">
<gf-value
class="mr-3"
[colorizeSign]="true"
[isCurrency]="true"
[locale]="locale"
[unit]="baseCurrency"
[value]="position?.netPerformanceWithCurrencyEffect"
/>
<gf-value
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="position?.netPerformancePercentageWithCurrencyEffect"
/>
</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
*ngIf="!isLoading"
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
/>
</div>
</a>
</div>
</div>

View File

@ -1,13 +0,0 @@
:host {
display: block;
.container {
gf-trend-indicator {
padding-top: 0.15rem;
}
.chevron {
opacity: 0.33;
}
}
}

View File

@ -1,40 +0,0 @@
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getLocale } from '@ghostfolio/common/helper';
import { Position } from '@ghostfolio/common/interfaces';
import {
ChangeDetectionStrategy,
Component,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { Subject } from 'rxjs';
@Component({
selector: 'gf-position',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './position.component.html',
styleUrls: ['./position.component.scss']
})
export class PositionComponent implements OnDestroy, OnInit {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() isLoading: boolean;
@Input() locale = getLocale();
@Input() position: Position;
@Input() range: string;
public unknownKey = UNKNOWN_KEY;
private unsubscribeSubject = new Subject<void>();
public constructor() {}
public ngOnInit() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -1,29 +0,0 @@
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfTrendIndicatorComponent } from '@ghostfolio/ui/trend-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatDialogModule } from '@angular/material/dialog';
import { RouterModule } from '@angular/router';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfPositionDetailDialogModule } from './position-detail-dialog/position-detail-dialog.module';
import { PositionComponent } from './position.component';
@NgModule({
declarations: [PositionComponent],
exports: [PositionComponent],
imports: [
CommonModule,
GfPositionDetailDialogModule,
GfSymbolModule,
GfTrendIndicatorComponent,
GfValueComponent,
MatDialogModule,
NgxSkeletonLoaderModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionModule {}

View File

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

View File

@ -1,17 +0,0 @@
:host {
display: block;
gf-position {
&:nth-child(even) {
background-color: rgba(0, 0, 0, var(--gf-theme-alpha-hover));
}
}
}
:host-context(.is-dark-theme) {
gf-position {
&:nth-child(even) {
background-color: rgba(255, 255, 255, var(--gf-theme-alpha-hover));
}
}
}

View File

@ -1,70 +0,0 @@
import { getLocale } from '@ghostfolio/common/helper';
import { Position } from '@ghostfolio/common/interfaces';
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
OnInit
} from '@angular/core';
@Component({
selector: 'gf-positions',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './positions.component.html',
styleUrls: ['./positions.component.scss']
})
export class PositionsComponent implements OnChanges, OnInit {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToCreateOrder: boolean;
@Input() locale = getLocale();
@Input() positions: Position[];
@Input() range: string;
public hasPositions: boolean;
public positionsRest: Position[] = [];
public positionsWithPriority: Position[] = [];
public constructor() {}
public ngOnInit() {}
public ngOnChanges() {
if (this.positions) {
this.hasPositions = this.positions.length > 0;
if (!this.hasPositions) {
return;
}
this.positionsRest = [];
this.positionsWithPriority = [];
for (const portfolioPosition of this.positions) {
if (portfolioPosition.marketState === 'open' || this.range !== '1d') {
// Only show positions where the market is open in today's view
this.positionsWithPriority.push(portfolioPosition);
} else {
this.positionsRest.push(portfolioPosition);
}
}
this.positionsRest.sort((a, b) =>
(a.name || a.symbol)?.toLowerCase() >
(b.name || b.symbol)?.toLowerCase()
? 1
: -1
);
this.positionsWithPriority.sort((a, b) =>
(a.name || a.symbol)?.toLowerCase() >
(b.name || b.symbol)?.toLowerCase()
? 1
: -1
);
} else {
this.hasPositions = false;
}
}
}

View File

@ -1,21 +0,0 @@
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { GfPositionModule } from '../position/position.module';
import { PositionsComponent } from './positions.component';
@NgModule({
declarations: [PositionsComponent],
exports: [PositionsComponent],
imports: [
CommonModule,
GfNoTransactionsInfoComponent,
GfPositionModule,
MatButtonModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionsModule {}

View File

@ -6,7 +6,6 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { GfPositionModule } from '../position/position.module';
import { RulesComponent } from './rules.component';
@NgModule({
@ -15,7 +14,6 @@ import { RulesComponent } from './rules.component';
imports: [
CommonModule,
GfNoTransactionsInfoComponent,
GfPositionModule,
GfRuleModule,
MatButtonModule,
MatCardModule

View File

@ -21,6 +21,7 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/cre
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'has-fab' },
selector: 'gf-user-account-access',
styleUrls: ['./user-account-access.scss'],
templateUrl: './user-account-access.html'

View File

@ -1,4 +1,3 @@
import { paths } from '@ghostfolio/client/app-routing.module';
import { DataService } from '@ghostfolio/client/services/data.service';
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -12,6 +11,8 @@ import {
import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { paths } from './paths';
@Injectable({ providedIn: 'root' })
export class AuthGuard {
private static PUBLIC_PAGE_ROUTES = [

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

View File

@ -1,5 +1,5 @@
import { paths } from '@ghostfolio/client/app-routing.module';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { paths } from '@ghostfolio/client/core/paths';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

View File

@ -77,7 +77,7 @@
mat-icon-button
title="Follow Ghostfolio on X (formerly Twitter)"
>
<span class="line-height-1 text-center w-100">𝕏</span>
<ion-icon name="logo-x" />
</a>
<a
*ngIf="user?.subscription?.type === 'Premium'"

View File

@ -21,7 +21,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/c
import { TransferBalanceDialog } from './transfer-balance/transfer-balance-dialog.component';
@Component({
host: { class: 'page' },
host: { class: 'has-fab page' },
selector: 'gf-accounts-page',
styleUrls: ['./accounts-page.scss'],
templateUrl: './accounts-page.html'

View File

@ -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 { DataService } from '@ghostfolio/client/services/data.service';
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
import { Currency } from '@ghostfolio/common/interfaces';
import {
ChangeDetectionStrategy,
@ -33,7 +32,7 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
})
export class CreateOrUpdateAccountDialog implements OnDestroy {
public accountForm: FormGroup;
public currencies: Currency[] = [];
public currencies: string[] = [];
public filteredPlatforms: Observable<Platform[]>;
public platforms: Platform[];
@ -49,10 +48,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
public ngOnInit() {
const { currencies, platforms } = this.dataService.fetchInfo();
this.currencies = currencies.map((currency) => ({
label: currency,
value: currency
}));
this.currencies = currencies;
this.platforms = platforms;
this.accountForm = this.formBuilder.group({
@ -107,7 +103,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
const account: CreateAccountDto | UpdateAccountDto = {
balance: this.accountForm.get('balance').value,
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,
isExcluded: this.accountForm.get('isExcluded').value,
name: this.accountForm.get('name').value,

View File

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

View File

@ -2,16 +2,19 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
import { Component, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [CommonModule],
selector: 'gf-demo-page',
standalone: true,
templateUrl: './demo-page.html'
})
export class DemoPageComponent implements OnDestroy {
export class GfDemoPageComponent implements OnDestroy {
public info: InfoItem;
private unsubscribeSubject = new Subject<void>();

View File

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

View File

@ -30,8 +30,10 @@
systems, including
<a href="https://github.com/bigbeartechworld/big-bear-casaos"
>CasaOS</a
>, Home Assistant,
<a href="https://www.runtipi.io/docs/apps-available">Runtipi</a>,
>,
<a href="https://github.com/lildude/ha-addon-ghostfolio"
>Home Assistant</a
>, <a href="https://www.runtipi.io/docs/apps-available">Runtipi</a>,
<a href="https://truecharts.org/charts/stable/ghostfolio"
>TrueCharts</a
>, <a href="https://apps.umbrel.com/app/ghostfolio">Umbrel</a>, and

View File

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

View File

@ -2,17 +2,30 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { InfoItem, User } from '@ghostfolio/common/interfaces';
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 { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { Subject, takeUntil } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [
CommonModule,
GfPremiumIndicatorComponent,
MatButtonModule,
MatCardModule,
RouterModule
],
selector: 'gf-features-page',
standalone: true,
styleUrls: ['./features-page.scss'],
templateUrl: './features-page.html'
})
export class FeaturesPageComponent implements OnDestroy {
export class GfFeaturesPageComponent implements OnDestroy {
public hasPermissionForSubscription: boolean;
public info: InfoItem;
public routerLinkRegister = ['/' + $localize`register`];

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