Compare commits

..

52 Commits

Author SHA1 Message Date
cc16ba5dc8 Release 1.169.0 (#1077) 2022-07-14 16:31:03 +02:00
d10227bc39 Feature/add support for luna2 and songbird cryptocurrencies (#1075)
* Add LUNA2 and SGB1

* Update changelog
2022-07-14 16:28:50 +02:00
4e214c32e8 Feature/update cryptocurrencies.json 20220714 (#1074)
* Update cryptocurrencies.json

* Update changelog
2022-07-14 16:27:32 +02:00
49e2862e03 Feature/add blog post about personal finances (#1073)
* Add blog post

* Update changelog
2022-07-14 16:16:07 +02:00
34e33a2400 Feature/upgrade date fns to version 2.28.0 (#1070)
* Upgrade date-fns

* Update changelog
2022-07-11 19:39:03 +02:00
ec9bc984af Release 1.168.0 (#1071) 2022-07-10 22:25:58 +02:00
2388c494df Feature/handle currency pair inconsistency in yahoo finance service (#1069)
* Handle occasional currency pair inconsistency: GBP=X instead of USDGBP=X

* Update changelog
2022-07-10 22:24:27 +02:00
d71ab10eed Bugfix/fix content height of account detail dialog (#1068)
* Fix height

* Update changelog
2022-07-10 21:44:23 +02:00
0e0592180f Add current month (#1067) 2022-07-10 09:41:48 +02:00
60e2aff488 Extend investment timeline by month (#1066)
* Extend investment timeline grouped by month

* Update changelog
2022-07-09 21:18:05 +02:00
7b5454e7de Release 1.167.0 (#1065) 2022-07-07 21:08:32 +02:00
30835ced88 Bugfix/fix holdings for basic users (#1064)
* Fix holdings for basic users

* Update changelog
2022-07-07 21:06:12 +02:00
8897f32bc5 Clean up modules (#1063) 2022-07-07 07:04:23 +02:00
abaa6b5f27 Feature/improve create account link in live demo (#1061)
* Improve router link

* Update changelog
2022-07-06 17:49:19 +02:00
2060fcaf0b Feature/add markets to public pages (#1062)
* Add Markets to public pages

* Update changelog
2022-07-05 21:45:27 +02:00
fd2408dd62 fix: add git when building docker image (#1052) 2022-07-03 19:57:04 +02:00
31cca024f1 Feature/upgrade ngx markdown to version 14.0.1 (#1055)
* Upgrade ngx-markdown

* Update changelog
2022-07-02 10:58:46 +02:00
b535122945 Make use of demo route (#1060) 2022-07-01 20:07:49 +02:00
5113e4e3ad Release 1.166.0 (#1059) 2022-06-30 21:09:58 +02:00
35e039748f Feature/refactor demo account as route (#1058)
* Refactor demo account as route

* Update changelog
2022-06-30 21:07:35 +02:00
c6b9e0aa5b Feature/upgrade zone.js to version 0.11.6 (#1054)
* Upgrade zone.js

* Update changelog
2022-06-30 19:38:33 +02:00
b250491ca5 Feature/upgrade yahoo finance2 to version 2.3.3 (#1053)
* Upgrade yahoo-finance2 to version 2.3.3

* Update changelog
2022-06-30 08:04:39 +02:00
61e501c659 Feature/fix version of @angular/cli (#1056)
* Fix version

* Update yarn.lock
2022-06-29 21:07:55 +02:00
c0f19d56ec Feature/add account detail dialog (#1047)
* Add account detail dialog

* Update changelog
2022-06-28 21:08:34 +02:00
8e2b235b1f Feature/improve search label (#1048)
* Improve search label

* Update changelog
2022-06-28 13:33:59 +02:00
c3407e9b34 Feature/upgrade prisma to version 3.15.2 (#1046)
* Upgrade prisma to version 3.15.2

* Update changelog
2022-06-25 19:04:01 +02:00
74193e4ee2 Feature/upgrade nestjs dependencies to version 8.4.7 (#1045)
* Upgrade nestjs dependencies to version 8.4.7

* Update changelog
2022-06-25 19:03:25 +02:00
3fe8f9c882 Release 1.165.0 (#1044) 2022-06-25 17:34:06 +02:00
d130efad47 Clean up comments (#1043)
* Clean up comments
2022-06-25 17:30:43 +02:00
109f0ebd70 Feature/move positions table to holdings section (#1042)
* Move positions table to holdings section

* Update changelog
2022-06-25 14:47:20 +02:00
069ddcc6b2 Feature/add reusable premium indicator component (#1041)
* Add premium indicator component

* Update changelog
2022-06-25 12:38:15 +02:00
f7bf6e652b Feature/add icon and name to positions table (#1040)
* Add icon and name

* Update changelog
2022-06-25 11:33:59 +02:00
eb059a024a Feature/delete data in data gathering by symbol (#1039)
* Delete market data

* Update changelog
2022-06-25 11:15:01 +02:00
ad88acff1c Release 1.164.0 (#1038) 2022-06-23 19:25:46 +02:00
1ff736537c Feature/add positions table to public page (#1037)
* Add positions table

* Update changelog
2022-06-23 19:24:24 +02:00
1fa65e1efd Release 1.163.0 (#1036) 2022-06-22 20:32:06 +02:00
df6bb489c2 Feature/improve onboarding for ios (#1035)
* Improve onboarding for iOS

* Update changelog
2022-06-22 20:30:03 +02:00
928a13310d Improve title (#1031) 2022-06-21 17:58:13 +02:00
2384861953 Remove experimental (#1032) 2022-06-20 20:32:51 +02:00
fe90bda6fb Release 1.162.0 (#1029) 2022-06-18 17:48:54 +02:00
d4b29ff11c Feature/add privacy policy page (#1028)
* Add privacy policy page

* Update changelog
2022-06-18 17:46:51 +02:00
a0a26cfa58 Feature/simplify header (#1027)
* Hide pricing page link for Premium users

* Harmonize content

* Update changelog
2022-06-18 11:57:27 +02:00
1610150427 Bugfix/fix currency conversion of ila to ils (#1026)
* Fix currency conversion: ILA to ILS

* Update changelog
2022-06-17 20:34:41 +02:00
cff8acd7b1 Clean up (#1022) 2022-06-17 20:04:52 +02:00
0f36d6cbdb Release 1.161.1 (#1025) 2022-06-16 17:20:44 +02:00
046e28b521 Release 1.161.0 (#1024) 2022-06-16 16:51:57 +02:00
aba562cb35 Bugfix/fix error handling for missing market prices (#1023)
* Add fallback for missing market price

* Update changelog
2022-06-16 16:49:29 +02:00
03f2f33344 Feature/restructure landing page (#1021)
* Restructure landing page

* Update changelog
2022-06-16 16:29:35 +02:00
a996dd7ed5 Feature/add vertical hover line to performance chart (#1020)
* Add vertical hover line

* Update changelog
2022-06-16 14:16:53 +02:00
002b883668 Feature/upgrade to angular 14 (#1019)
* Upgrade to angular 14

* Migrate UntypedFormControl to FormControl

* Update changelog
2022-06-16 10:28:23 +02:00
0b06823893 Release 1.160.0 (#1018) 2022-06-15 20:18:13 +02:00
2dfd779444 Bugfix/fix no data provider has been found in search (#1017)
* Fix default value of DATA_SOURCES

* Update changelog
2022-06-15 20:16:38 +02:00
202 changed files with 7642 additions and 7787 deletions

View File

@ -5,6 +5,124 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.169.0 - 14.07.2022
### Added
- Added support for the cryptocurrency _Songbird_ (`SGB1-USD`)
- Added support for the cryptocurrency _Terra 2.0_ (`LUNA2-USD`)
- Added a blog post
### Changed
- Refreshed the cryptocurrencies list to support more coins by default
- Upgraded `date-fns` from version `2.22.1` to `2.28.0`
## 1.168.0 - 10.07.2022
### Added
- Extended the investment timeline grouped by month
### Changed
- Handled an occasional currency pair inconsistency in the _Yahoo Finance_ service (`GBP=X` instead of `USDGBP=X`)
### Fixed
- Fixed the content height of the account detail dialog
## 1.167.0 - 07.07.2022
### Added
- Added _Markets_ to the public pages
### Changed
- Improved the _Create Account_ link in the _Live Demo_
- Upgraded `ngx-markdown` from version `13.0.0` to `14.0.1`
### Fixed
- Fixed an issue in the _Holdings_ section for users without a subscription
## 1.166.0 - 30.06.2022
### Added
- Added an account detail dialog
### Changed
- Improved the label of the (symbol) search
- Refactored the demo account as a route (`/demo`)
- Upgraded `nestjs` from version `8.2.3` to `8.4.7`
- Upgraded `prisma` from version `3.14.0` to `3.15.2`
- Upgraded `yahoo-finance2` from version `2.3.2` to `2.3.3`
- Upgraded `zone.js` from version `0.11.4` to `0.11.6`
## 1.165.0 - 25.06.2022
### Added
- Added an icon and name column to the positions table
- Added a reusable premium indicator component
### Changed
- Moved the positions table to a dedicated section (_Holdings_)
- Changed the data gathering by symbol endpoint to delete data first
## 1.164.0 - 23.06.2022
### Added
- Added the positions table including performance to the public page
## 1.163.0 - 22.06.2022
### Changed
- Improved the onboarding for iOS
## 1.162.0 - 18.06.2022
### Added
- Added a _Privacy Policy_ page
### Changed
- Simplified the header
### Fixed
- Fixed an issue with the currency inconsistency in the _Yahoo Finance_ service (convert from `ILA` to `ILS`)
## 1.161.1 - 16.06.2022
### Added
- Added the vertical hover line to inspect data points in the performance chart on the home page
### Changed
- Improved the landing page
- Upgraded `angular` from version `13.3.6` to `14.0.2`
- Upgraded `Nx` from version `14.1.4` to `14.3.5`
- Upgraded `storybook` from version `6.4.22` to `6.5.9`
### Fixed
- Improved the error handling of missing market prices
## 1.160.0 - 15.06.2022
### Fixed
- Fixed the `No data provider has been found` error in the search (regression after `envalid` upgrade to `7.3.1` in Ghostfolio `1.157.0`)
## 1.159.0 - 15.06.2022 ## 1.159.0 - 15.06.2022
### Changed ### Changed

View File

@ -12,7 +12,7 @@ COPY ./package.json package.json
COPY ./yarn.lock yarn.lock COPY ./yarn.lock yarn.lock
COPY ./prisma/schema.prisma prisma/schema.prisma COPY ./prisma/schema.prisma prisma/schema.prisma
RUN apk add --no-cache python3 g++ make openssl RUN apk add --no-cache python3 g++ make openssl git
RUN yarn install RUN yarn install
# See https://github.com/nrwl/nx/issues/6586 for further details # See https://github.com/nrwl/nx/issues/6586 for further details
@ -22,7 +22,7 @@ RUN node decorate-angular-cli.js
COPY ./angular.json angular.json COPY ./angular.json angular.json
COPY ./nx.json nx.json COPY ./nx.json nx.json
COPY ./replace.build.js replace.build.js COPY ./replace.build.js replace.build.js
COPY ./jest.preset.ts jest.preset.ts COPY ./jest.preset.js jest.preset.js
COPY ./jest.config.ts jest.config.ts COPY ./jest.config.ts jest.config.ts
COPY ./tsconfig.base.json tsconfig.base.json COPY ./tsconfig.base.json tsconfig.base.json
COPY ./libs libs COPY ./libs libs

View File

@ -12,7 +12,7 @@
<strong>Open Source Wealth Management Software</strong> <strong>Open Source Wealth Management Software</strong>
</p> </p>
<p> <p>
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a> <a href="https://ghostfol.io"><strong>Ghostfol.io</strong></a> | <a href="https://ghostfol.io/demo"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
</p> </p>
<p> <p>
<a href="#contributing"> <a href="#contributing">
@ -136,7 +136,7 @@ Open http://localhost:3333 in your browser and accomplish these steps:
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d` 1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
1. Then, run the following command to keep your database schema in sync: `docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:migrate` 1. Then, run the following command to keep your database schema in sync: `docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:migrate`
### Run with _Unraid_ (unofficial) ### Run with _Unraid_ (Community)
Please follow the instructions of the Ghostfolio [Unraid Community App](https://unraid.net/community/apps?q=ghostfolio). Please follow the instructions of the Ghostfolio [Unraid Community App](https://unraid.net/community/apps?q=ghostfolio).
@ -186,7 +186,7 @@ yarn database:push
Run `yarn test` Run `yarn test`
## Public API (experimental) ## Public API
### Import Activities ### Import Activities

View File

@ -2,6 +2,7 @@
"version": 1, "version": 1,
"projects": { "projects": {
"api": { "api": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"root": "apps/api", "root": "apps/api",
"sourceRoot": "apps/api/src", "sourceRoot": "apps/api/src",
"projectType": "application", "projectType": "application",
@ -56,6 +57,7 @@
"tags": [] "tags": []
}, },
"client": { "client": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
@ -113,7 +115,7 @@
} }
], ],
"styles": ["apps/client/src/styles.scss"], "styles": ["apps/client/src/styles.scss"],
"scripts": ["node_modules/marked/lib/marked.js"], "scripts": ["node_modules/marked/marked.min.js"],
"vendorChunk": true, "vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
"buildOptimizer": false, "buildOptimizer": false,
@ -189,6 +191,7 @@
"tags": [] "tags": []
}, },
"client-e2e": { "client-e2e": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"root": "apps/client-e2e", "root": "apps/client-e2e",
"sourceRoot": "apps/client-e2e/src", "sourceRoot": "apps/client-e2e/src",
"projectType": "application", "projectType": "application",
@ -211,6 +214,7 @@
"implicitDependencies": ["client"] "implicitDependencies": ["client"]
}, },
"common": { "common": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"root": "libs/common", "root": "libs/common",
"sourceRoot": "libs/common/src", "sourceRoot": "libs/common/src",
"projectType": "library", "projectType": "library",
@ -233,6 +237,7 @@
"tags": [] "tags": []
}, },
"ui": { "ui": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library", "projectType": "library",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
@ -258,14 +263,12 @@
} }
}, },
"storybook": { "storybook": {
"builder": "@nrwl/storybook:storybook", "builder": "@storybook/angular:start-storybook",
"options": { "options": {
"uiFramework": "@storybook/angular",
"port": 4400, "port": 4400,
"config": { "configDir": "libs/ui/.storybook",
"configFolder": "libs/ui/.storybook" "browserTarget": "ui:build-storybook",
}, "compodoc": false
"projectBuildConfig": "ui:build-storybook"
}, },
"configurations": { "configurations": {
"ci": { "ci": {
@ -274,15 +277,13 @@
} }
}, },
"build-storybook": { "build-storybook": {
"builder": "@nrwl/storybook:build", "builder": "@storybook/angular:build-storybook",
"outputs": ["{options.outputPath}"], "outputs": ["{options.outputPath}"],
"options": { "options": {
"uiFramework": "@storybook/angular", "outputDir": "dist/storybook/ui",
"outputPath": "dist/storybook/ui", "configDir": "libs/ui/.storybook",
"config": { "browserTarget": "ui:build-storybook",
"configFolder": "libs/ui/.storybook" "compodoc": false
},
"projectBuildConfig": "ui:build-storybook"
}, },
"configurations": { "configurations": {
"ci": { "ci": {
@ -294,6 +295,7 @@
"tags": [] "tags": []
}, },
"ui-e2e": { "ui-e2e": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"root": "apps/ui-e2e", "root": "apps/ui-e2e",
"sourceRoot": "apps/ui-e2e/src", "sourceRoot": "apps/ui-e2e/src",
"projectType": "application", "projectType": "application",

View File

@ -1,4 +1,4 @@
module.exports = { export default {
displayName: 'api', displayName: 'api',
globals: { globals: {
@ -13,5 +13,5 @@ module.exports = {
coverageDirectory: '../../coverage/apps/api', coverageDirectory: '../../coverage/apps/api',
testTimeout: 10000, testTimeout: 10000,
testEnvironment: 'node', testEnvironment: 'node',
preset: '../../jest.preset.ts' preset: '../../jest.preset.js'
}; };

View File

@ -7,7 +7,10 @@ import {
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { Accounts } from '@ghostfolio/common/interfaces'; import { Accounts } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type {
AccountWithValue,
RequestWithUser
} from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
@ -123,13 +126,45 @@ export class AccountController {
@Get(':id') @Get(':id')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getAccountById(@Param('id') id: string): Promise<AccountModel> { public async getAccountById(
return this.accountService.account({ @Headers('impersonation-id') impersonationId,
id_userId: { @Param('id') id: string
id, ): Promise<AccountWithValue> {
userId: this.request.user.id const impersonationUserId =
} await this.impersonationService.validateImpersonationId(
}); impersonationId,
this.request.user.id
);
let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id,
[{ id, type: 'ACCOUNT' }]
);
if (
impersonationUserId ||
this.userService.isRestrictedView(this.request.user)
) {
accountsWithAggregations = {
...nullifyValuesInObject(accountsWithAggregations, [
'totalBalanceInBaseCurrency',
'totalValueInBaseCurrency'
]),
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
'balance',
'balanceInBaseCurrency',
'convertedBalance',
'fee',
'quantity',
'unitPrice',
'value',
'valueInBaseCurrency'
])
};
}
return accountsWithAggregations.accounts[0];
} }
@Post() @Post()

View File

@ -3,8 +3,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config'; import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces'; import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
import { Controller, Get, UseGuards, UseInterceptors } from '@nestjs/common'; import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { BenchmarkService } from './benchmark.service'; import { BenchmarkService } from './benchmark.service';
@ -16,7 +15,6 @@ export class BenchmarkController {
) {} ) {}
@Get() @Get()
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> { public async getBenchmark(): Promise<BenchmarkResponse> {

View File

@ -63,6 +63,8 @@ export class InfoService {
} else { } else {
info.fearAndGreedDataSource = ghostfolioFearAndGreedIndexDataSource; info.fearAndGreedDataSource = ghostfolioFearAndGreedIndexDataSource;
} }
globalPermissions.push(permissions.enableFearAndGreedIndex);
} }
if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) { if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) {

View File

@ -4,6 +4,7 @@ import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.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 { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { Filter } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
@ -17,6 +18,7 @@ import {
Param, Param,
Post, Post,
Put, Put,
Query,
UseGuards, UseGuards,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
@ -66,8 +68,36 @@ export class OrderController {
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getAllOrders( public async getAllOrders(
@Headers('impersonation-id') impersonationId @Headers('impersonation-id') impersonationId,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string
): Promise<Activities> { ): Promise<Activities> {
const accountIds = filterByAccounts?.split(',') ?? [];
const assetClasses = filterByAssetClasses?.split(',') ?? [];
const tagIds = filterByTags?.split(',') ?? [];
const filters: Filter[] = [
...accountIds.map((accountId) => {
return <Filter>{
id: accountId,
type: 'ACCOUNT'
};
}),
...assetClasses.map((assetClass) => {
return <Filter>{
id: assetClass,
type: 'ASSET_CLASS'
};
}),
...tagIds.map((tagId) => {
return <Filter>{
id: tagId,
type: 'TAG'
};
})
];
const impersonationUserId = const impersonationUserId =
await this.impersonationService.validateImpersonationId( await this.impersonationService.validateImpersonationId(
impersonationId, impersonationId,
@ -76,6 +106,7 @@ export class OrderController {
const userCurrency = this.request.user.Settings.currency; const userCurrency = this.request.user.Settings.currency;
let activities = await this.orderService.getOrders({ let activities = await this.orderService.getOrders({
filters,
userCurrency, userCurrency,
includeDrafts: true, includeDrafts: true,
userId: impersonationUserId || this.request.user.id userId: impersonationUserId || this.request.user.id

View File

@ -14,8 +14,11 @@ import {
format, format,
isAfter, isAfter,
isBefore, isBefore,
isSameMonth,
isSameYear,
max, max,
min min,
set
} from 'date-fns'; } from 'date-fns';
import { first, flatten, isNumber, sortBy } from 'lodash'; import { first, flatten, isNumber, sortBy } from 'lodash';
@ -323,6 +326,46 @@ export class PortfolioCalculator {
}); });
} }
public getInvestmentsByMonth(): { date: string; investment: Big }[] {
if (this.orders.length === 0) {
return [];
}
const investments = [];
let currentDate = parseDate(this.orders[0].date);
let investmentByMonth = new Big(0);
for (const [index, order] of this.orders.entries()) {
if (
isSameMonth(parseDate(order.date), currentDate) &&
isSameYear(parseDate(order.date), currentDate)
) {
investmentByMonth = investmentByMonth.plus(
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
);
if (index === this.orders.length - 1) {
investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
investment: investmentByMonth
});
}
} else {
investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
investment: investmentByMonth
});
currentDate = parseDate(order.date);
investmentByMonth = order.quantity
.mul(order.unitPrice)
.mul(this.getFactor(order.type));
}
}
return investments;
}
public async calculateTimeline( public async calculateTimeline(
timelineSpecification: TimelineSpecification[], timelineSpecification: TimelineSpecification[],
endDate: string endDate: string

View File

@ -20,7 +20,12 @@ import {
PortfolioReport, PortfolioReport,
PortfolioSummary PortfolioSummary
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import type {
DateRange,
GroupBy,
RequestWithUser
} from '@ghostfolio/common/types';
import { import {
Controller, Controller,
Get, Get,
@ -190,21 +195,35 @@ export class PortfolioController {
} }
} }
const isBasicUser = let hasDetails = true;
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
this.request.user.subscription.type === 'Basic'; hasDetails = this.request.user.subscription.type === 'Premium';
}
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
holdings[symbol] = {
...portfolioPosition,
assetClass: hasDetails ? portfolioPosition.assetClass : undefined,
assetSubClass: hasDetails ? portfolioPosition.assetSubClass : undefined,
countries: hasDetails ? portfolioPosition.countries : [],
currency: hasDetails ? portfolioPosition.currency : undefined,
markets: hasDetails ? portfolioPosition.markets : undefined,
sectors: hasDetails ? portfolioPosition.sectors : []
};
}
return { return {
accounts, accounts,
hasError, hasError,
holdings: isBasicUser ? {} : holdings holdings
}; };
} }
@Get('investments') @Get('investments')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getInvestments( public async getInvestments(
@Headers('impersonation-id') impersonationId: string @Headers('impersonation-id') impersonationId: string,
@Query('groupBy') groupBy?: GroupBy
): Promise<PortfolioInvestments> { ): Promise<PortfolioInvestments> {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
@ -216,9 +235,16 @@ export class PortfolioController {
); );
} }
let investments = await this.portfolioService.getInvestments( let investments: InvestmentItem[];
impersonationId
); if (groupBy === 'month') {
investments = await this.portfolioService.getInvestments(
impersonationId,
'month'
);
} else {
investments = await this.portfolioService.getInvestments(impersonationId);
}
if ( if (
impersonationId || impersonationId ||
@ -317,7 +343,7 @@ export class PortfolioController {
const { holdings } = await this.portfolioService.getDetails( const { holdings } = await this.portfolioService.getDetails(
access.userId, access.userId,
access.userId, access.userId,
'1d', 'max',
[{ id: 'EQUITY', type: 'ASSET_CLASS' }] [{ id: 'EQUITY', type: 'ASSET_CLASS' }]
); );
@ -338,12 +364,15 @@ export class PortfolioController {
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPublicDetails.holdings[symbol] = { portfolioPublicDetails.holdings[symbol] = {
allocationCurrent: portfolioPosition.allocationCurrent, allocationCurrent: portfolioPosition.value / totalValue,
countries: hasDetails ? portfolioPosition.countries : [], countries: hasDetails ? portfolioPosition.countries : [],
currency: portfolioPosition.currency, currency: hasDetails ? portfolioPosition.currency : undefined,
markets: portfolioPosition.markets, markets: hasDetails ? portfolioPosition.markets : undefined,
name: portfolioPosition.name, name: portfolioPosition.name,
netPerformancePercent: portfolioPosition.netPerformancePercent,
sectors: hasDetails ? portfolioPosition.sectors : [], sectors: hasDetails ? portfolioPosition.sectors : [],
symbol: portfolioPosition.symbol,
url: portfolioPosition.url,
value: portfolioPosition.value / totalValue value: portfolioPosition.value / totalValue
}; };
} }

View File

@ -41,6 +41,7 @@ import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.in
import type { import type {
AccountWithValue, AccountWithValue,
DateRange, DateRange,
GroupBy,
Market, Market,
OrderWithAccount, OrderWithAccount,
RequestWithUser RequestWithUser
@ -50,6 +51,7 @@ import { REQUEST } from '@nestjs/core';
import { import {
AssetClass, AssetClass,
DataSource, DataSource,
Prisma,
Tag, Tag,
Type as TypeOfOrder Type as TypeOfOrder
} from '@prisma/client'; } from '@prisma/client';
@ -63,6 +65,7 @@ import {
max, max,
parse, parse,
parseISO, parseISO,
set,
setDayOfYear, setDayOfYear,
startOfDay, startOfDay,
subDays, subDays,
@ -100,14 +103,23 @@ export class PortfolioService {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY'); this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
} }
public async getAccounts(aUserId: string): Promise<AccountWithValue[]> { public async getAccounts(
aUserId: string,
aFilters?: Filter[]
): Promise<AccountWithValue[]> {
const where: Prisma.AccountWhereInput = { userId: aUserId };
if (aFilters?.[0].id && aFilters?.[0].type === 'ACCOUNT') {
where.id = aFilters[0].id;
}
const [accounts, details] = await Promise.all([ const [accounts, details] = await Promise.all([
this.accountService.accounts({ this.accountService.accounts({
where,
include: { Order: true, Platform: true }, include: { Order: true, Platform: true },
orderBy: { name: 'asc' }, orderBy: { name: 'asc' }
where: { userId: aUserId }
}), }),
this.getDetails(aUserId, aUserId) this.getDetails(aUserId, aUserId, undefined, aFilters)
]); ]);
const userCurrency = this.request.user.Settings.currency; const userCurrency = this.request.user.Settings.currency;
@ -145,8 +157,11 @@ export class PortfolioService {
}); });
} }
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> { public async getAccountsWithAggregations(
const accounts = await this.getAccounts(aUserId); aUserId: string,
aFilters?: Filter[]
): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId, aFilters);
let totalBalanceInBaseCurrency = new Big(0); let totalBalanceInBaseCurrency = new Big(0);
let totalValueInBaseCurrency = new Big(0); let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0; let transactionCount = 0;
@ -170,7 +185,8 @@ export class PortfolioService {
} }
public async getInvestments( public async getInvestments(
aImpersonationId: string aImpersonationId: string,
groupBy?: GroupBy
): Promise<InvestmentItem[]> { ): Promise<InvestmentItem[]> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
@ -191,28 +207,57 @@ export class PortfolioService {
return []; return [];
} }
const investments = portfolioCalculator.getInvestments().map((item) => { let investments: InvestmentItem[];
return {
date: item.date,
investment: item.investment.toNumber()
};
});
// Add investment of today if (groupBy === 'month') {
const investmentOfToday = investments.filter((investment) => { investments = portfolioCalculator.getInvestmentsByMonth().map((item) => {
return investment.date === format(new Date(), DATE_FORMAT); return {
}); date: item.date,
investment: item.investment.toNumber()
if (investmentOfToday.length <= 0) { };
const pastInvestments = investments.filter((investment) => {
return isBefore(parseDate(investment.date), new Date());
}); });
const lastInvestment = pastInvestments[pastInvestments.length - 1];
investments.push({ // Add investment of current month
date: format(new Date(), DATE_FORMAT), const dateOfCurrentMonth = format(
investment: lastInvestment?.investment ?? 0 set(new Date(), { date: 1 }),
DATE_FORMAT
);
const investmentOfCurrentMonth = investments.filter(({ date }) => {
return date === dateOfCurrentMonth;
}); });
if (investmentOfCurrentMonth.length <= 0) {
investments.push({
date: dateOfCurrentMonth,
investment: 0
});
}
} else {
investments = portfolioCalculator
.getInvestments()
.map(({ date, investment }) => {
return {
date,
investment: investment.toNumber()
};
});
// Add investment of today
const investmentOfToday = investments.filter(({ date }) => {
return date === format(new Date(), DATE_FORMAT);
});
if (investmentOfToday.length <= 0) {
const pastInvestments = investments.filter(({ date }) => {
return isBefore(parseDate(date), new Date());
});
const lastInvestment = pastInvestments[pastInvestments.length - 1];
investments.push({
date: format(new Date(), DATE_FORMAT),
investment: lastInvestment?.investment ?? 0
});
}
} }
return sortBy(investments, (investment) => { return sortBy(investments, (investment) => {
@ -273,7 +318,6 @@ export class PortfolioService {
.filter((timelineItem) => timelineItem !== null) .filter((timelineItem) => timelineItem !== null)
.map((timelineItem) => ({ .map((timelineItem) => ({
date: timelineItem.date, date: timelineItem.date,
marketPrice: timelineItem.value,
value: timelineItem.netPerformance.toNumber() value: timelineItem.netPerformance.toNumber()
})); }));
@ -394,7 +438,7 @@ export class PortfolioService {
continue; continue;
} }
const value = item.quantity.mul(item.marketPrice); const value = item.quantity.mul(item.marketPrice ?? 0);
const symbolProfile = symbolProfileMap[item.symbol]; const symbolProfile = symbolProfileMap[item.symbol];
const dataProviderResponse = dataProviderResponses[item.symbol]; const dataProviderResponse = dataProviderResponses[item.symbol];
@ -442,6 +486,7 @@ export class PortfolioService {
sectors: symbolProfile.sectors, sectors: symbolProfile.sectors,
symbol: item.symbol, symbol: item.symbol,
transactionCount: item.transactionCount, transactionCount: item.transactionCount,
url: symbolProfile.url,
value: value.toNumber() value: value.toNumber()
}; };
} }
@ -658,7 +703,7 @@ export class PortfolioService {
netPerformancePercent: position.netPerformancePercentage?.toNumber(), netPerformancePercent: position.netPerformancePercentage?.toNumber(),
quantity: quantity.toNumber(), quantity: quantity.toNumber(),
value: this.exchangeRateDataService.toCurrency( value: this.exchangeRateDataService.toCurrency(
quantity.mul(marketPrice).toNumber(), quantity.mul(marketPrice ?? 0).toNumber(),
currency, currency,
userCurrency userCurrency
) )
@ -1290,6 +1335,10 @@ export class PortfolioService {
if (filters.length === 0) { if (filters.length === 0) {
currentAccounts = await this.accountService.getAccounts(userId); currentAccounts = await this.accountService.getAccounts(userId);
} else if (filters.length === 1 && filters[0].type === 'ACCOUNT') {
currentAccounts = await this.accountService.accounts({
where: { id: filters[0].id }
});
} else { } else {
const accountIds = uniq( const accountIds = uniq(
orders.map(({ accountId }) => { orders.map(({ accountId }) => {

View File

@ -46,7 +46,6 @@ export class SymbolController {
* Must be after /lookup * Must be after /lookup
*/ */
@Get(':dataSource/:symbol') @Get(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getSymbolData( public async getSymbolData(

View File

@ -158,10 +158,6 @@ export class UserService {
let currentPermissions = getPermissions(user.role); let currentPermissions = getPermissions(user.role);
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
currentPermissions.push(permissions.accessFearAndGreedIndex);
}
if (user.subscription?.type === 'Premium') { if (user.subscription?.type === 'Premium') {
currentPermissions.push(permissions.reportDataGlitch); currentPermissions.push(permissions.reportDataGlitch);
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
{ {
"LUNA1": "Terra", "LUNA1": "Terra",
"LUNA2": "Terra",
"SGB1": "Songbird",
"UNI1": "Uniswap" "UNI1": "Uniswap"
} }

View File

@ -15,7 +15,7 @@ export class ConfigurationService {
BASE_CURRENCY: str({ default: 'USD' }), BASE_CURRENCY: str({ default: 'USD' }),
CACHE_TTL: num({ default: 1 }), CACHE_TTL: num({ default: 1 }),
DATA_SOURCE_PRIMARY: str({ default: DataSource.YAHOO }), DATA_SOURCE_PRIMARY: str({ default: DataSource.YAHOO }),
DATA_SOURCES: json({ default: JSON.stringify([DataSource.YAHOO]) }), DATA_SOURCES: json({ default: [DataSource.YAHOO] }),
ENABLE_FEATURE_BLOG: bool({ default: false }), ENABLE_FEATURE_BLOG: bool({ default: false }),
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }), ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),

View File

@ -10,6 +10,7 @@ import ms from 'ms';
import { DataGatheringProcessor } from './data-gathering.processor'; import { DataGatheringProcessor } from './data-gathering.processor';
import { ExchangeRateDataModule } from './exchange-rate-data.module'; import { ExchangeRateDataModule } from './exchange-rate-data.module';
import { MarketDataModule } from './market-data.module';
import { SymbolProfileModule } from './symbol-profile.module'; import { SymbolProfileModule } from './symbol-profile.module';
@Module({ @Module({
@ -25,6 +26,7 @@ import { SymbolProfileModule } from './symbol-profile.module';
DataEnhancerModule, DataEnhancerModule,
DataProviderModule, DataProviderModule,
ExchangeRateDataModule, ExchangeRateDataModule,
MarketDataModule,
PrismaModule, PrismaModule,
SymbolProfileModule SymbolProfileModule
], ],

View File

@ -17,6 +17,7 @@ import { DataProviderService } from './data-provider/data-provider.service';
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
import { ExchangeRateDataService } from './exchange-rate-data.service'; import { ExchangeRateDataService } from './exchange-rate-data.service';
import { IDataGatheringItem } from './interfaces/interfaces'; import { IDataGatheringItem } from './interfaces/interfaces';
import { MarketDataService } from './market-data.service';
import { PrismaService } from './prisma.service'; import { PrismaService } from './prisma.service';
@Injectable() @Injectable()
@ -28,6 +29,7 @@ export class DataGatheringService {
private readonly dataGatheringQueue: Queue, private readonly dataGatheringQueue: Queue,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
@ -56,6 +58,8 @@ export class DataGatheringService {
} }
public async gatherSymbol({ dataSource, symbol }: UniqueAsset) { public async gatherSymbol({ dataSource, symbol }: UniqueAsset) {
await this.marketDataService.deleteMany({ dataSource, symbol });
const symbols = (await this.getSymbolsMax()).filter((dataGatheringItem) => { const symbols = (await this.getSymbolsMax()).filter((dataGatheringItem) => {
return ( return (
dataGatheringItem.dataSource === dataSource && dataGatheringItem.dataSource === dataSource &&

View File

@ -37,10 +37,15 @@ export class YahooFinanceService implements DataProviderInterface {
} }
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) { public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
const symbol = aYahooFinanceSymbol.replace( let symbol = aYahooFinanceSymbol.replace(
new RegExp(`-${this.baseCurrency}$`), new RegExp(`-${this.baseCurrency}$`),
this.baseCurrency this.baseCurrency
); );
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
symbol = `${this.baseCurrency}${symbol}`;
}
return symbol.replace('=X', ''); return symbol.replace('=X', '');
} }
@ -181,6 +186,9 @@ export class YahooFinanceService implements DataProviderInterface {
if (symbol === 'USDGBp') { if (symbol === 'USDGBp') {
// Convert GPB to GBp (pence) // Convert GPB to GBp (pence)
marketPrice = new Big(marketPrice).mul(100).toNumber(); marketPrice = new Big(marketPrice).mul(100).toNumber();
} else if (symbol === 'USDILA') {
// Convert ILS to ILA
marketPrice = new Big(marketPrice).mul(100).toNumber();
} }
response[symbol][format(historicalItem.date, DATE_FORMAT)] = { response[symbol][format(historicalItem.date, DATE_FORMAT)] = {
@ -243,6 +251,18 @@ export class YahooFinanceService implements DataProviderInterface {
.mul(100) .mul(100)
.toNumber() .toNumber()
}; };
} else if (
symbol === 'USDILS' &&
yahooFinanceSymbols.includes('USDILA=X')
) {
// Convert ILS to ILA
response['USDILA'] = {
...response[symbol],
currency: 'ILA',
marketPrice: new Big(response[symbol].marketPrice)
.mul(100)
.toNumber()
};
} }
} }

View File

@ -6,7 +6,7 @@ export interface Environment extends CleanedEnvAccessors {
BASE_CURRENCY: string; BASE_CURRENCY: string;
CACHE_TTL: number; CACHE_TTL: number;
DATA_SOURCE_PRIMARY: string; DATA_SOURCE_PRIMARY: string;
DATA_SOURCES: string | string[]; // string is not correct, error in envalid? DATA_SOURCES: string[];
ENABLE_FEATURE_BLOG: boolean; ENABLE_FEATURE_BLOG: boolean;
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean; ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;

View File

@ -1,4 +1,4 @@
module.exports = { export default {
displayName: 'client', displayName: 'client',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
@ -18,5 +18,5 @@ module.exports = {
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular' '^.+.(ts|mjs|js|html)$': 'jest-preset-angular'
}, },
transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'],
preset: '../../jest.preset.ts' preset: '../../jest.preset.js'
}; };

View File

@ -5,9 +5,6 @@ import { getDateFormatString } from '@ghostfolio/common/helper';
import { format, parse } from 'date-fns'; import { format, parse } from 'date-fns';
export class CustomDateAdapter extends NativeDateAdapter { export class CustomDateAdapter extends NativeDateAdapter {
/**
* @constructor
*/
public constructor( public constructor(
@Inject(MAT_DATE_LOCALE) public locale: string, @Inject(MAT_DATE_LOCALE) public locale: string,
@Inject(forwardRef(() => MAT_DATE_LOCALE)) matDateLocale: string, @Inject(forwardRef(() => MAT_DATE_LOCALE)) matDateLocale: string,

View File

@ -16,6 +16,13 @@ const routes: Routes = [
(m) => m.ChangelogPageModule (m) => m.ChangelogPageModule
) )
}, },
{
path: 'about/privacy-policy',
loadChildren: () =>
import('./pages/about/privacy-policy/privacy-policy-page.module').then(
(m) => m.PrivacyPolicyPageModule
)
},
{ {
path: 'account', path: 'account',
loadChildren: () => loadChildren: () =>
@ -52,6 +59,11 @@ const routes: Routes = [
'./pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module' './pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module'
).then((m) => m.HalloGhostfolioPageModule) ).then((m) => m.HalloGhostfolioPageModule)
}, },
{
path: 'demo',
loadChildren: () =>
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
},
{ {
path: 'en/blog/2021/07/hello-ghostfolio', path: 'en/blog/2021/07/hello-ghostfolio',
loadChildren: () => loadChildren: () =>
@ -66,6 +78,13 @@ const routes: Routes = [
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module' './pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
).then((m) => m.FirstMonthsInOpenSourcePageModule) ).then((m) => m.FirstMonthsInOpenSourcePageModule)
}, },
{
path: 'en/blog/2022/07/how-do-i-get-my-finances-in-order',
loadChildren: () =>
import(
'./pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.module'
).then((m) => m.HowDoIGetMyFinancesInOrderPageModule)
},
{ {
path: 'features', path: 'features',
loadChildren: () => loadChildren: () =>
@ -78,6 +97,13 @@ const routes: Routes = [
loadChildren: () => loadChildren: () =>
import('./pages/home/home-page.module').then((m) => m.HomePageModule) import('./pages/home/home-page.module').then((m) => m.HomePageModule)
}, },
{
path: 'markets',
loadChildren: () =>
import('./pages/markets/markets-page.module').then(
(m) => m.MarketsPageModule
)
},
{ {
path: 'p', path: 'p',
loadChildren: () => loadChildren: () =>
@ -120,6 +146,13 @@ const routes: Routes = [
(m) => m.FirePageModule (m) => m.FirePageModule
) )
}, },
{
path: 'portfolio/holdings',
loadChildren: () =>
import('./pages/portfolio/holdings/holdings-page.module').then(
(m) => m.HoldingsPageModule
)
},
{ {
path: 'portfolio/report', path: 'portfolio/report',
loadChildren: () => loadChildren: () =>

View File

@ -15,13 +15,17 @@
> >
<div class="row"> <div class="row">
<div class="col-md-8 offset-md-2 text-center"> <div class="col-md-8 offset-md-2 text-center">
<a *ngIf="canCreateAccount" class="text-center" [routerLink]="['/']"> <a
*ngIf="canCreateAccount"
class="text-center"
[routerLink]="['/register']"
>
<div <div
class="cursor-pointer d-inline-block info-message px-3 py-2" class="cursor-pointer d-inline-block info-message px-3 py-2"
(click)="onCreateAccount()" (click)="onCreateAccount()"
> >
<span i18n>You are using the Live Demo.</span> <span i18n>You are using the Live Demo.</span>
<a class="ml-2" href="#" i18n>Create Account</a> <span class="a ml-2" i18n>Create Account</span>
</div></a </div></a
> >
<div <div

View File

@ -17,7 +17,7 @@
border-radius: 2rem; border-radius: 2rem;
font-size: 80%; font-size: 80%;
a { .a {
color: rgba(var(--palette-primary-500), 1); color: rgba(var(--palette-primary-500), 1);
font-weight: 500; font-weight: 500;
} }

View File

@ -1,6 +1,8 @@
import { Platform } from '@angular/cdk/platform'; import { Platform } from '@angular/cdk/platform';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material/chips';
import { import {
DateAdapter, DateAdapter,
MAT_DATE_FORMATS, MAT_DATE_FORMATS,
@ -38,6 +40,8 @@ export function NgxStripeFactory(): string {
GfHeaderModule, GfHeaderModule,
HttpClientModule, HttpClientModule,
MarkdownModule.forRoot(), MarkdownModule.forRoot(),
MatAutocompleteModule,
MatChipsModule,
MaterialCssVarsModule.forRoot({ MaterialCssVarsModule.forRoot({
darkThemeClass: 'is-dark-theme', darkThemeClass: 'is-dark-theme',
isAutoContrast: true, isAutoContrast: true,

View File

@ -10,7 +10,6 @@ import { AccessTableComponent } from './access-table.component';
declarations: [AccessTableComponent], declarations: [AccessTableComponent],
exports: [AccessTableComponent], exports: [AccessTableComponent],
imports: [CommonModule, MatButtonModule, MatMenuModule, MatTableModule], imports: [CommonModule, MatButtonModule, MatMenuModule, MatTableModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfPortfolioAccessTableModule {} export class GfPortfolioAccessTableModule {}

View File

@ -0,0 +1,7 @@
:host {
display: block;
.mat-dialog-content {
max-height: unset;
}
}

View File

@ -0,0 +1,112 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
OnDestroy,
OnInit
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { downloadAsFile } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { AccountType } from '@prisma/client';
import { format, parseISO } from 'date-fns';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AccountDetailDialogParams } from './interfaces/interfaces';
@Component({
host: { class: 'd-flex flex-column h-100' },
selector: 'gf-account-detail-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'account-detail-dialog.html',
styleUrls: ['./account-detail-dialog.component.scss']
})
export class AccountDetailDialog implements OnDestroy, OnInit {
public accountType: AccountType;
public name: string;
public orders: OrderWithAccount[];
public platformName: string;
public user: User;
public valueInBaseCurrency: number;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams,
private dataService: DataService,
public dialogRef: MatDialogRef<AccountDetailDialog>,
private userService: UserService
) {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit(): void {
this.dataService
.fetchAccount(this.data.accountId)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
this.accountType = accountType;
this.name = name;
this.platformName = Platform?.name;
this.valueInBaseCurrency = valueInBaseCurrency;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchActivities({
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }]
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.orders = activities;
this.changeDetectorRef.markForCheck();
});
}
public onClose(): void {
this.dialogRef.close();
}
public onExport() {
this.dataService
.fetchExport(
this.orders.map((order) => {
return order.id;
})
)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => {
downloadAsFile({
content: data,
fileName: `ghostfolio-export-${this.name
.replace(/\s+/g, '-')
.toLowerCase()}-${format(
parseISO(data.meta.date),
'yyyyMMddHHmm'
)}.json`,
format: 'json'
});
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -0,0 +1,65 @@
<gf-dialog-header
mat-dialog-title
position="center"
[deviceType]="data.deviceType"
[title]="name"
(closeButtonClicked)="onClose()"
></gf-dialog-header>
<div class="flex-grow-1" mat-dialog-content>
<div class="container p-0">
<div class="row">
<div class="col-12 d-flex justify-content-center mb-3">
<gf-value
size="large"
[currency]="user?.settings?.baseCurrency"
[locale]="user?.settings?.locale"
[value]="valueInBaseCurrency"
></gf-value>
</div>
</div>
<div class="row">
<div class="col-6 mb-3">
<gf-value
label="Account Type"
size="medium"
[value]="accountType"
></gf-value>
</div>
<div class="col-6 mb-3">
<gf-value
label="Platform"
size="medium"
[value]="platformName"
></gf-value>
</div>
</div>
<div *ngIf="orders?.length > 0" class="row">
<div class="col mb-3">
<div class="h5 mb-0" i18n>Activities</div>
<gf-activities-table
[activities]="orders"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!hasImpersonationId"
[hasPermissionToFilter]="false"
[hasPermissionToImportActivities]="false"
[hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale"
[showActions]="false"
[showSymbolColumn]="false"
(export)="onExport()"
></gf-activities-table>
</div>
</div>
</div>
</div>
<gf-dialog-footer
mat-dialog-actions
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
></gf-dialog-footer>

View File

@ -0,0 +1,27 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { AccountDetailDialog } from './account-detail-dialog.component';
@NgModule({
declarations: [AccountDetailDialog],
imports: [
CommonModule,
GfActivitiesTableModule,
GfDialogFooterModule,
GfDialogHeaderModule,
GfValueModule,
MatButtonModule,
MatDialogModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAccountDetailDialogModule {}

View File

@ -0,0 +1,5 @@
export interface AccountDetailDialogParams {
accountId: string;
deviceType: string;
hasImpersonationId: boolean;
}

View File

@ -65,7 +65,7 @@
<ng-container matColumnDef="transactions"> <ng-container matColumnDef="transactions">
<th *matHeaderCellDef class="px-1 text-right" mat-header-cell> <th *matHeaderCellDef class="px-1 text-right" mat-header-cell>
<span class="d-block d-sm-none">#</span> <span class="d-block d-sm-none">#</span>
<span class="d-none d-sm-block" i18n>Transactions</span> <span class="d-none d-sm-block" i18n>Activities</span>
</th> </th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell> <td *matCellDef="let element" class="px-1 text-right" mat-cell>
<ng-container *ngIf="element.accountType === 'SECURITIES'">{{ <ng-container *ngIf="element.accountType === 'SECURITIES'">{{
@ -212,7 +212,12 @@
</ng-container> </ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> <tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> <tr
*matRowDef="let row; columns: displayedColumns"
class="cursor-pointer"
mat-row
(click)="onOpenAccountDetailDialog(row.id)"
></tr>
<tr <tr
*matFooterRowDef="displayedColumns" *matFooterRowDef="displayedColumns"
mat-footer-row mat-footer-row

View File

@ -9,6 +9,7 @@ import {
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { Account as AccountModel } from '@prisma/client'; import { Account as AccountModel } from '@prisma/client';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
@ -39,7 +40,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor() {} public constructor(private router: Router) {}
public ngOnInit() {} public ngOnInit() {}
@ -75,6 +76,12 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
} }
} }
public onOpenAccountDetailDialog(accountId: string) {
this.router.navigate([], {
queryParams: { accountId, accountDetailDialog: true }
});
}
public onUpdateAccount(aAccount: AccountModel) { public onUpdateAccount(aAccount: AccountModel) {
this.accountToUpdate.emit(aAccount); this.accountToUpdate.emit(aAccount);
} }

View File

@ -25,7 +25,6 @@ import { AccountsTableComponent } from './accounts-table.component';
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule,
RouterModule RouterModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfAccountsTableModule {} export class GfAccountsTableModule {}

View File

@ -30,9 +30,6 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@ -52,9 +49,6 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
}); });
} }
/**
* Initializes the controller
*/
public ngOnInit() { public ngOnInit() {
this.filterForm = this.formBuilder.group({ this.filterForm = this.formBuilder.group({
status: [] status: []

View File

@ -9,7 +9,6 @@ import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/mark
declarations: [AdminMarketDataDetailComponent], declarations: [AdminMarketDataDetailComponent],
exports: [AdminMarketDataDetailComponent], exports: [AdminMarketDataDetailComponent],
imports: [CommonModule, GfLineChartModule, GfMarketDataDetailDialogModule], imports: [CommonModule, GfLineChartModule, GfMarketDataDetailDialogModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfAdminMarketDataDetailModule {} export class GfAdminMarketDataDetailModule {}

View File

@ -11,7 +11,6 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
@NgModule({ @NgModule({
declarations: [MarketDataDetailDialog], declarations: [MarketDataDetailDialog],
exports: [],
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
@ -22,7 +21,6 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
MatInputModule, MatInputModule,
ReactiveFormsModule ReactiveFormsModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfMarketDataDetailDialogModule {} export class GfMarketDataDetailDialogModule {}

View File

@ -31,9 +31,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@ -53,9 +50,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
}); });
} }
/**
* Initializes the controller
*/
public ngOnInit() { public ngOnInit() {
this.fetchAdminMarketData(); this.fetchAdminMarketData();
} }

View File

@ -42,9 +42,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private cacheService: CacheService, private cacheService: CacheService,
@ -78,9 +75,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
}); });
} }
/**
* Initializes the controller
*/
public ngOnInit() { public ngOnInit() {
this.fetchAdminData(); this.fetchAdminData();
} }

View File

@ -21,9 +21,6 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
@ -38,9 +35,6 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
}); });
} }
/**
* Initializes the controller
*/
public ngOnInit() { public ngOnInit() {
this.fetchAdminData(); this.fetchAdminData();
} }

View File

@ -35,11 +35,10 @@
>{{ userItem.alias || (userItem.id | slice:0:5) + >{{ userItem.alias || (userItem.id | slice:0:5) +
'...' }}</span '...' }}</span
> >
<ion-icon <gf-premium-indicator
*ngIf="userItem?.subscription?.type === 'Premium'" *ngIf="userItem?.subscription?.type === 'Premium'"
class="ml-1 text-muted" class="ml-1"
name="diamond-outline" ></gf-premium-indicator>
></ion-icon>
</div> </div>
</td> </td>
<td class="mat-cell px-1 py-2 text-right"> <td class="mat-cell px-1 py-2 text-right">

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { AdminUsersComponent } from './admin-users.component'; import { AdminUsersComponent } from './admin-users.component';
@ -9,7 +10,13 @@ import { AdminUsersComponent } from './admin-users.component';
@NgModule({ @NgModule({
declarations: [AdminUsersComponent], declarations: [AdminUsersComponent],
exports: [], exports: [],
imports: [CommonModule, GfValueModule, MatButtonModule, MatMenuModule], imports: [
CommonModule,
GfPremiumIndicatorModule,
GfValueModule,
MatButtonModule,
MatMenuModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfAdminUsersModule {} export class GfAdminUsersModule {}

View File

@ -8,7 +8,6 @@ import { DialogFooterComponent } from './dialog-footer.component';
declarations: [DialogFooterComponent], declarations: [DialogFooterComponent],
exports: [DialogFooterComponent], exports: [DialogFooterComponent],
imports: [CommonModule, MatButtonModule], imports: [CommonModule, MatButtonModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfDialogFooterModule {} export class GfDialogFooterModule {}

View File

@ -8,7 +8,6 @@ import { DialogHeaderComponent } from './dialog-header.component';
declarations: [DialogHeaderComponent], declarations: [DialogHeaderComponent],
exports: [DialogHeaderComponent], exports: [DialogHeaderComponent],
imports: [CommonModule, MatButtonModule], imports: [CommonModule, MatButtonModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfDialogHeaderModule {} export class GfDialogHeaderModule {}

View File

@ -66,7 +66,9 @@
>Resources</a >Resources</a
> >
<a <a
*ngIf="hasPermissionForSubscription" *ngIf="
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
"
class="d-none d-sm-block mx-1" class="d-none d-sm-block mx-1"
i18n i18n
mat-flat-button mat-flat-button
@ -203,7 +205,9 @@
>Resources</a >Resources</a
> >
<a <a
*ngIf="hasPermissionForSubscription" *ngIf="
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
"
class="d-block d-sm-none" class="d-block d-sm-none"
i18n i18n
mat-menu-item mat-menu-item
@ -229,13 +233,7 @@
mat-button mat-button
[routerLink]="['/']" [routerLink]="['/']"
> >
<gf-logo <gf-logo [hideName]="currentRoute === 'register'"></gf-logo>
[hideName]="
!currentRoute ||
currentRoute === 'register' ||
currentRoute === 'start'
"
></gf-logo>
</a> </a>
<span class="spacer"></span> <span class="spacer"></span>
<a <a
@ -271,6 +269,18 @@
[routerLink]="['/pricing']" [routerLink]="['/pricing']"
>Pricing</a >Pricing</a
> >
<a
*ngIf="hasPermissionToAccessFearAndGreedIndex"
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[ngClass]="{
'font-weight-bold': currentRoute === 'markets',
'text-decoration-underline': currentRoute === 'markets'
}"
[routerLink]="['/markets']"
>Markets</a
>
<a <a
class="d-none d-sm-block mx-1 no-min-width px-1" class="d-none d-sm-block mx-1 no-min-width px-1"
href="https://github.com/ghostfolio/ghostfolio" href="https://github.com/ghostfolio/ghostfolio"

View File

@ -37,6 +37,7 @@ export class HeaderComponent implements OnChanges {
public hasPermissionForSocialLogin: boolean; public hasPermissionForSocialLogin: boolean;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public hasPermissionToAccessAdminControl: boolean; public hasPermissionToAccessAdminControl: boolean;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public impersonationId: string; public impersonationId: string;
public isMenuOpen: boolean; public isMenuOpen: boolean;
@ -73,6 +74,11 @@ export class HeaderComponent implements OnChanges {
this.user?.permissions, this.user?.permissions,
permissions.accessAdminControl permissions.accessAdminControl
); );
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
} }
public impersonateAccount(aId: string) { public impersonateAccount(aId: string) {

View File

@ -21,7 +21,6 @@ import { HeaderComponent } from './header.component';
MatToolbarModule, MatToolbarModule,
RouterModule RouterModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfHeaderModule {} export class GfHeaderModule {}

View File

@ -36,9 +36,6 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
@ -81,9 +78,6 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
}); });
} }
/**
* Initializes the controller
*/
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;

View File

@ -11,7 +11,6 @@ import { HomeHoldingsComponent } from './home-holdings.component';
@NgModule({ @NgModule({
declarations: [HomeHoldingsComponent], declarations: [HomeHoldingsComponent],
exports: [],
imports: [ imports: [
CommonModule, CommonModule,
GfPositionDetailDialogModule, GfPositionDetailDialogModule,
@ -21,7 +20,6 @@ import { HomeHoldingsComponent } from './home-holdings.component';
MatCardModule, MatCardModule,
RouterModule RouterModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfHomeHoldingsModule {} export class GfHomeHoldingsModule {}

View File

@ -30,9 +30,6 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
@ -47,52 +44,49 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.user.permissions,
permissions.accessFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem({
dataSource: this.info.fearAndGreedDataSource,
includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.historicalData = [
...historicalData,
{
date: resetHours(new Date()).toISOString(),
value: marketPrice
}
];
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
this.dataService
.fetchBenchmarks()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ benchmarks }) => {
this.benchmarks = benchmarks;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
}); });
} }
/** public ngOnInit() {
* Initializes the controller this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
*/ this.info?.globalPermissions,
public ngOnInit() {} permissions.enableFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem({
dataSource: this.info.fearAndGreedDataSource,
includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.historicalData = [
...historicalData,
{
date: resetHours(new Date()).toISOString(),
value: marketPrice
}
];
this.changeDetectorRef.markForCheck();
});
}
this.dataService
.fetchBenchmarks()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ benchmarks }) => {
this.benchmarks = benchmarks;
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();

View File

@ -28,16 +28,17 @@
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col-xs-12 col-md-8 offset-md-2"> <div class="col-xs-12 col-md-8 offset-md-2">
<gf-benchmark <gf-benchmark
*ngFor="let benchmark of benchmarks" [benchmarks]="benchmarks"
class="py-2"
[benchmark]="benchmark"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
></gf-benchmark> ></gf-benchmark>
<gf-benchmark <ngx-skeleton-loader
*ngIf="!benchmarks" *ngIf="isLoading"
class="py-2" animation="pulse"
[benchmark]="undefined" [theme]="{
></gf-benchmark> height: '1.5rem',
width: '100%'
}"
></ngx-skeleton-loader>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,19 +3,20 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module'; import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
import { GfBenchmarkModule } from '@ghostfolio/ui/benchmark/benchmark.module'; import { GfBenchmarkModule } from '@ghostfolio/ui/benchmark/benchmark.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { HomeMarketComponent } from './home-market.component'; import { HomeMarketComponent } from './home-market.component';
@NgModule({ @NgModule({
declarations: [HomeMarketComponent], declarations: [HomeMarketComponent],
exports: [], exports: [HomeMarketComponent],
imports: [ imports: [
CommonModule, CommonModule,
GfBenchmarkModule, GfBenchmarkModule,
GfFearAndGreedIndexModule, GfFearAndGreedIndexModule,
GfLineChartModule GfLineChartModule,
NgxSkeletonLoaderModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfHomeMarketModule {} export class GfHomeMarketModule {}

View File

@ -42,9 +42,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
@ -69,9 +66,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
}); });
} }
/**
* Initializes the controller
*/
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;

View File

@ -4,7 +4,9 @@
<div class="row w-100"> <div class="row w-100">
<div class="chart-container col"> <div class="chart-container col">
<gf-line-chart <gf-line-chart
class="position-absolute"
symbol="Performance" symbol="Performance"
[currency]="user?.settings?.baseCurrency"
[historicalDataItems]="historicalDataItems" [historicalDataItems]="historicalDataItems"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[ngClass]="{ 'pr-3': deviceType === 'mobile' }" [ngClass]="{ 'pr-3': deviceType === 'mobile' }"

View File

@ -10,7 +10,6 @@ import { HomeOverviewComponent } from './home-overview.component';
@NgModule({ @NgModule({
declarations: [HomeOverviewComponent], declarations: [HomeOverviewComponent],
exports: [],
imports: [ imports: [
CommonModule, CommonModule,
GfLineChartModule, GfLineChartModule,
@ -19,7 +18,6 @@ import { HomeOverviewComponent } from './home-overview.component';
GfToggleModule, GfToggleModule,
RouterModule RouterModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfHomeOverviewModule {} export class GfHomeOverviewModule {}

View File

@ -25,10 +25,8 @@
gf-line-chart { gf-line-chart {
bottom: 0; bottom: 0;
left: 0; left: 0;
position: absolute;
right: 0; right: 0;
top: 0; top: 0;
z-index: -1;
} }
} }
} }

View File

@ -21,9 +21,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
@ -46,9 +43,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
}); });
} }
/**
* Initializes the controller
*/
public ngOnInit() { public ngOnInit() {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()

View File

@ -8,14 +8,12 @@ import { HomeSummaryComponent } from './home-summary.component';
@NgModule({ @NgModule({
declarations: [HomeSummaryComponent], declarations: [HomeSummaryComponent],
exports: [],
imports: [ imports: [
CommonModule, CommonModule,
GfPortfolioSummaryModule, GfPortfolioSummaryModule,
MatCardModule, MatCardModule,
RouterModule RouterModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfHomeSummaryModule {} export class GfHomeSummaryModule {}

View File

@ -22,7 +22,10 @@ import {
transformTickToAbbreviation transformTickToAbbreviation
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { GroupBy } from '@ghostfolio/common/types';
import { import {
BarController,
BarElement,
Chart, Chart,
LineController, LineController,
LineElement, LineElement,
@ -42,6 +45,7 @@ import { addDays, isAfter, parseISO, subDays } from 'date-fns';
export class InvestmentChartComponent implements OnChanges, OnDestroy { export class InvestmentChartComponent implements OnChanges, OnDestroy {
@Input() currency: string; @Input() currency: string;
@Input() daysInMarket: number; @Input() daysInMarket: number;
@Input() groupBy: GroupBy;
@Input() investments: InvestmentItem[]; @Input() investments: InvestmentItem[];
@Input() isInPercent = false; @Input() isInPercent = false;
@Input() locale: string; @Input() locale: string;
@ -53,6 +57,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
public constructor() { public constructor() {
Chart.register( Chart.register(
BarController,
BarElement,
LinearScale, LinearScale,
LineController, LineController,
LineElement, LineElement,
@ -78,7 +84,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
private initialize() { private initialize() {
this.isLoading = true; this.isLoading = true;
if (this.investments?.length > 0) { if (!this.groupBy && this.investments?.length > 0) {
// Extend chart by 5% of days in market (before) // Extend chart by 5% of days in market (before)
const firstItem = this.investments[0]; const firstItem = this.investments[0];
this.investments.unshift({ this.investments.unshift({
@ -102,13 +108,14 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
} }
const data = { const data = {
labels: this.investments.map((position) => { labels: this.investments.map((investmentItem) => {
return position.date; return investmentItem.date;
}), }),
datasets: [ datasets: [
{ {
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderWidth: 2, borderWidth: this.groupBy ? 0 : 2,
data: this.investments.map((position) => { data: this.investments.map((position) => {
return position.investment; return position.investment;
}), }),
@ -137,6 +144,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart(this.chartCanvas.nativeElement, {
data, data,
options: { options: {
animation: false,
elements: { elements: {
line: { line: {
tension: 0 tension: 0
@ -178,8 +186,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
grid: { grid: {
borderColor: `rgba(${getTextColor()}, 0.1)`, borderColor: `rgba(${getTextColor()}, 0.1)`,
color: `rgba(${getTextColor()}, 0.8)`, color: `rgba(${getTextColor()}, 0.8)`,
display: false display: false,
drawBorder: false
}, },
position: 'right',
ticks: { ticks: {
callback: (value: number) => { callback: (value: number) => {
return transformTickToAbbreviation(value); return transformTickToAbbreviation(value);
@ -192,12 +202,12 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
} }
}, },
plugins: [getVerticalHoverLinePlugin(this.chartCanvas)], plugins: [getVerticalHoverLinePlugin(this.chartCanvas)],
type: 'line' type: this.groupBy ? 'bar' : 'line'
}); });
this.isLoading = false;
} }
} }
this.isLoading = false;
} }
private getTooltipPluginConfiguration() { private getTooltipPluginConfiguration() {

View File

@ -13,7 +13,6 @@ import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.com
@NgModule({ @NgModule({
declarations: [LoginWithAccessTokenDialog], declarations: [LoginWithAccessTokenDialog],
exports: [],
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
@ -26,7 +25,6 @@ import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.com
ReactiveFormsModule, ReactiveFormsModule,
TextFieldModule TextFieldModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class LoginWithAccessTokenDialogModule {} export class LoginWithAccessTokenDialogModule {}

View File

@ -8,7 +8,6 @@ import { PortfolioPerformanceComponent } from './portfolio-performance.component
@NgModule({ @NgModule({
declarations: [PortfolioPerformanceComponent], declarations: [PortfolioPerformanceComponent],
exports: [PortfolioPerformanceComponent], exports: [PortfolioPerformanceComponent],
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule], imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule]
providers: []
}) })
export class GfPortfolioPerformanceModule {} export class GfPortfolioPerformanceModule {}

View File

@ -15,7 +15,6 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
@NgModule({ @NgModule({
declarations: [PositionDetailDialog], declarations: [PositionDetailDialog],
exports: [],
imports: [ imports: [
CommonModule, CommonModule,
GfActivitiesTableModule, GfActivitiesTableModule,
@ -29,7 +28,6 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
MatDialogModule, MatDialogModule,
NgxSkeletonLoaderModule NgxSkeletonLoaderModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfPositionDetailDialogModule {} export class GfPositionDetailDialogModule {}

View File

@ -23,7 +23,6 @@ import { PositionComponent } from './position.component';
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule,
RouterModule RouterModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfPositionModule {} export class GfPositionModule {}

View File

@ -6,12 +6,40 @@
mat-table mat-table
[dataSource]="dataSource" [dataSource]="dataSource"
> >
<ng-container matColumnDef="icon">
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-symbol-icon
*ngIf="element.url"
[tooltip]="element.name"
[url]="element.url"
></gf-symbol-icon>
</td>
</ng-container>
<ng-container matColumnDef="symbol"> <ng-container matColumnDef="symbol">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header> <th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
Symbol Symbol
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
{{ element.symbol | gfSymbol }} <span [title]="element.name">{{ element.symbol | gfSymbol }}</span>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1"
i18n
mat-header-cell
mat-sort-header
>
Name
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<ng-container *ngIf="element.name !== element.symbol">{{
element.name
}}</ng-container>
</td> </td>
</ng-container> </ng-container>
@ -36,48 +64,6 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="performance">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell
>
Performance
</th>
<td class="d-none d-lg-table-cell px-1" mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end">
<gf-value
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.netPerformancePercent"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="allocationInvestment">
<th
*matHeaderCellDef
class="justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
Initial Allocation
</th>
<td mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end px-1">
<gf-value
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.allocationInvestment"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="allocationCurrent"> <ng-container matColumnDef="allocationCurrent">
<th <th
*matHeaderCellDef *matHeaderCellDef
@ -86,7 +72,7 @@
mat-header-cell mat-header-cell
mat-sort-header mat-sort-header
> >
Current Allocation Allocation
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
@ -99,15 +85,39 @@
</td> </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <ng-container matColumnDef="performance">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell
>
Performance
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.netPerformancePercent"
></gf-value>
</div>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr <tr
*matRowDef="let row; columns: displayedColumns" *matRowDef="let row; columns: displayedColumns"
mat-row mat-row
[ngClass]="{ [ngClass]="{
'cursor-pointer': !ignoreAssetSubClasses.includes(row.assetSubClass) 'cursor-pointer':
hasPermissionToShowValues &&
!ignoreAssetSubClasses.includes(row.assetSubClass)
}" }"
(click)=" (click)="
!ignoreAssetSubClasses.includes(row.assetSubClass) && hasPermissionToShowValues &&
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
onOpenPositionDialog({ dataSource: row.dataSource, symbol: row.symbol }) onOpenPositionDialog({ dataSource: row.dataSource, symbol: row.symbol })
" "
></tr> ></tr>

View File

@ -27,7 +27,9 @@ import { Subject, Subscription } from 'rxjs';
export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToShowValues = true;
@Input() locale: string; @Input() locale: string;
@Input() pageSize = Number.MAX_SAFE_INTEGER;
@Input() positions: PortfolioPosition[]; @Input() positions: PortfolioPosition[];
@Output() transactionDeleted = new EventEmitter<string>(); @Output() transactionDeleted = new EventEmitter<string>();
@ -44,7 +46,6 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
ASSET_SUB_CLASS_EMERGENCY_FUND ASSET_SUB_CLASS_EMERGENCY_FUND
]; ];
public isLoading = true; public isLoading = true;
public pageSize = 7;
public routeQueryParams: Subscription; public routeQueryParams: Subscription;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -54,13 +55,14 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
public ngOnInit() {} public ngOnInit() {}
public ngOnChanges() { public ngOnChanges() {
this.displayedColumns = [ this.displayedColumns = ['icon', 'symbol', 'name'];
'symbol',
'value', if (this.hasPermissionToShowValues) {
'performance', this.displayedColumns.push('value');
'allocationInvestment', }
'allocationCurrent'
]; this.displayedColumns.push('allocationCurrent');
this.displayedColumns.push('performance');
this.isLoading = true; this.isLoading = true;

View File

@ -35,7 +35,6 @@ import { PositionsTableComponent } from './positions-table.component';
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule,
RouterModule RouterModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfPositionsTableModule {} export class GfPositionsTableModule {}

View File

@ -15,7 +15,6 @@ import { PositionsComponent } from './positions.component';
GfPositionModule, GfPositionModule,
MatButtonModule MatButtonModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfPositionsModule {} export class GfPositionsModule {}

View File

@ -8,7 +8,6 @@ import { RuleComponent } from './rule.component';
declarations: [RuleComponent], declarations: [RuleComponent],
exports: [RuleComponent], exports: [RuleComponent],
imports: [CommonModule, NgxSkeletonLoaderModule], imports: [CommonModule, NgxSkeletonLoaderModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfRuleModule {} export class GfRuleModule {}

View File

@ -19,7 +19,6 @@ import { RulesComponent } from './rules.component';
MatButtonModule, MatButtonModule,
MatCardModule MatCardModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class RulesModule {} export class RulesModule {}

View File

@ -7,7 +7,6 @@ import { SymbolIconComponent } from './symbol-icon.component';
declarations: [SymbolIconComponent], declarations: [SymbolIconComponent],
exports: [SymbolIconComponent], exports: [SymbolIconComponent],
imports: [CommonModule], imports: [CommonModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfSymbolIconModule {} export class GfSymbolIconModule {}

View File

@ -23,7 +23,7 @@ export class ToggleComponent implements OnChanges, OnInit {
@Output() change = new EventEmitter<Pick<ToggleOption, 'value'>>(); @Output() change = new EventEmitter<Pick<ToggleOption, 'value'>>();
public option = new FormControl(); public option = new FormControl<string>(undefined);
public constructor() {} public constructor() {}

View File

@ -8,7 +8,6 @@ import { ToggleComponent } from './toggle.component';
@NgModule({ @NgModule({
declarations: [ToggleComponent], declarations: [ToggleComponent],
exports: [ToggleComponent], exports: [ToggleComponent],
imports: [CommonModule, MatRadioModule, ReactiveFormsModule], imports: [CommonModule, MatRadioModule, ReactiveFormsModule]
providers: []
}) })
export class GfToggleModule {} export class GfToggleModule {}

View File

@ -7,7 +7,6 @@ import { WorldMapChartComponent } from './world-map-chart.component';
@NgModule({ @NgModule({
declarations: [WorldMapChartComponent], declarations: [WorldMapChartComponent],
exports: [WorldMapChartComponent], exports: [WorldMapChartComponent],
imports: [CommonModule, NgxSkeletonLoaderModule], imports: [CommonModule, NgxSkeletonLoaderModule]
providers: []
}) })
export class GfWorldMapChartModule {} export class GfWorldMapChartModule {}

View File

@ -5,22 +5,24 @@ import {
Router, Router,
RouterStateSnapshot RouterStateSnapshot
} from '@angular/router'; } from '@angular/router';
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { ViewMode } from '@prisma/client'; import { ViewMode } from '@prisma/client';
import { EMPTY } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { SettingsStorageService } from '../services/settings-storage.service';
import { UserService } from '../services/user/user.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
private static PUBLIC_PAGE_ROUTES = [ private static PUBLIC_PAGE_ROUTES = [
'/about', '/about',
'/about/changelog', '/about/changelog',
'/about/privacy-policy',
'/blog', '/blog',
'/de/blog', '/de/blog',
'/demo',
'/en/blog', '/en/blog',
'/features', '/features',
'/markets',
'/p', '/p',
'/pricing', '/pricing',
'/register', '/register',
@ -34,11 +36,10 @@ export class AuthGuard implements CanActivate {
) {} ) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (route.queryParams?.utm_source) { const utmSource = route.queryParams?.utm_source;
this.settingsStorageService.setSetting(
'utm_source', if (utmSource) {
route.queryParams?.utm_source this.settingsStorageService.setSetting('utm_source', utmSource);
);
} }
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
@ -46,7 +47,10 @@ export class AuthGuard implements CanActivate {
.get() .get()
.pipe( .pipe(
catchError(() => { catchError(() => {
if (route.queryParams?.utm_source) { if (utmSource === 'ios') {
this.router.navigate(['/demo']);
resolve(false);
} else if (utmSource === 'trusted-web-activity') {
this.router.navigate(['/register']); this.router.navigate(['/register']);
resolve(false); resolve(false);
} else if ( } else if (

View File

@ -5,7 +5,6 @@ import {
HttpRequest HttpRequest
} from '@angular/common/http'; } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ImpersonationStorageService } from '../services/impersonation-storage.service'; import { ImpersonationStorageService } from '../services/impersonation-storage.service';
@ -18,7 +17,6 @@ const TOKEN_HEADER_KEY = 'Authorization';
export class AuthInterceptor implements HttpInterceptor { export class AuthInterceptor implements HttpInterceptor {
public constructor( public constructor(
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private router: Router,
private tokenStorageService: TokenStorageService private tokenStorageService: TokenStorageService
) {} ) {}

View File

@ -26,9 +26,6 @@ export class AboutPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
@ -54,9 +51,6 @@ export class AboutPageComponent implements OnDestroy, OnInit {
this.statistics = statistics; this.statistics = statistics;
} }
/**
* Initializes the controller
*/
public ngOnInit() { public ngOnInit() {
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

View File

@ -4,11 +4,11 @@
<h3 class="d-flex justify-content-center mb-3" i18n>About Ghostfolio</h3> <h3 class="d-flex justify-content-center mb-3" i18n>About Ghostfolio</h3>
<div class="about-container"> <div class="about-container">
<p> <p>
<strong>Ghostfolio</strong> is a lightweight wealth management Ghostfolio is a lightweight wealth management application for
application for individuals to keep track of stocks, ETFs or individuals to keep track of stocks, ETFs or cryptocurrencies and make
cryptocurrencies and make solid, data-driven investment decisions. The solid, data-driven investment decisions. The source code is fully
source code is fully available as open source software (OSS). The available as open source software (OSS). The project has been
project has been initiated by initiated by
<a href="https://dotsilver.ch" title="Website of Thomas Kaul" <a href="https://dotsilver.ch" title="Website of Thomas Kaul"
>Thomas Kaul</a >Thomas Kaul</a
> >
@ -174,8 +174,8 @@
<div class="row"> <div class="row">
<div <div
class="col-md-6 col-xs-12 my-2" class="col-md-4 col-xs-12 my-2"
[ngClass]="{ 'offset-md-3': !hasPermissionForBlog }" [ngClass]="{ 'offset-md-4': !hasPermissionForBlog }"
> >
<a <a
class="py-2 w-100" class="py-2 w-100"
@ -186,7 +186,17 @@
>Changelog & License</a >Changelog & License</a
> >
</div> </div>
<div *ngIf="hasPermissionForBlog" class="col-md-6 col-xs-12 my-2"> <div *ngIf="hasPermissionForSubscription" class="col-md-4 col-xs-12 my-2">
<a
class="py-2 w-100"
color="primary"
i18n
mat-stroked-button
[routerLink]="['/about', 'privacy-policy']"
>Privacy Policy</a
>
</div>
<div *ngIf="hasPermissionForBlog" class="col-md-4 col-xs-12 my-2">
<a <a
class="py-2 w-100" class="py-2 w-100"
color="primary" color="primary"

View File

@ -9,7 +9,6 @@ import { AboutPageComponent } from './about-page.component';
@NgModule({ @NgModule({
declarations: [AboutPageComponent], declarations: [AboutPageComponent],
exports: [],
imports: [ imports: [
AboutPageRoutingModule, AboutPageRoutingModule,
CommonModule, CommonModule,
@ -17,7 +16,6 @@ import { AboutPageComponent } from './about-page.component';
MatButtonModule, MatButtonModule,
MatCardModule MatCardModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class AboutPageModule {} export class AboutPageModule {}

View File

@ -10,9 +10,6 @@ import { Subject } from 'rxjs';
export class ChangelogPageComponent implements OnDestroy { export class ChangelogPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor() {} public constructor() {}
public ngOnDestroy() { public ngOnDestroy() {

View File

@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { PrivacyPolicyPageComponent } from './privacy-policy-page.component';
const routes: Routes = [
{ path: '', component: PrivacyPolicyPageComponent, canActivate: [AuthGuard] }
];
@NgModule({
exports: [RouterModule],
imports: [RouterModule.forChild(routes)]
})
export class PrivacyPolicyPageRoutingModule {}

View File

@ -0,0 +1,19 @@
import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
selector: 'gf-privacy-policy-page',
styleUrls: ['./privacy-policy-page.scss'],
templateUrl: './privacy-policy-page.html'
})
export class PrivacyPolicyPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
public constructor() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -0,0 +1,8 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h3 class="mb-3 text-center" i18n>Privacy Policy</h3>
<markdown [src]="'assets/privacy-policy.md'"></markdown>
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MarkdownModule } from 'ngx-markdown';
import { PrivacyPolicyPageRoutingModule } from './privacy-policy-page-routing.module';
import { PrivacyPolicyPageComponent } from './privacy-policy-page.component';
@NgModule({
declarations: [PrivacyPolicyPageComponent],
imports: [
CommonModule,
MarkdownModule.forChild(),
PrivacyPolicyPageRoutingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class PrivacyPolicyPageModule {}

View File

@ -0,0 +1,21 @@
:host {
color: rgb(var(--dark-primary-text));
display: block;
::ng-deep {
markdown {
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
}
}
}
:host-context(.is-dark-theme) {
color: rgb(var(--light-primary-text));
}

View File

@ -63,9 +63,6 @@ export class AccountPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
@ -145,9 +142,6 @@ export class AccountPageComponent implements OnDestroy, OnInit {
}); });
} }
/**
* Initializes the controller
*/
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;

View File

@ -12,16 +12,20 @@
<div class="pr-1 w-50" i18n>Alias</div> <div class="pr-1 w-50" i18n>Alias</div>
<div class="pl-1 w-50">{{ user.alias }}</div> <div class="pl-1 w-50">{{ user.alias }}</div>
</div> </div>
<div *ngIf="user?.subscription" class="d-flex py-1"> <div
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
class="d-flex py-1"
>
<div class="pr-1 w-50" i18n>Membership</div> <div class="pr-1 w-50" i18n>Membership</div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<div class="align-items-center d-flex mb-1"> <div class="align-items-center d-flex mb-1">
{{ user?.subscription?.type }} <a [routerLink]="['/pricing']"
<ion-icon >{{ user?.subscription?.type }}</a
>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Premium'" *ngIf="user?.subscription?.type === 'Premium'"
class="ml-1 text-muted" class="ml-1"
name="diamond-outline" ></gf-premium-indicator>
></ion-icon>
</div> </div>
<div *ngIf="user?.subscription?.type === 'Premium'"> <div *ngIf="user?.subscription?.type === 'Premium'">
Valid until {{ user?.subscription?.expiresAt | date: Valid until {{ user?.subscription?.expiresAt | date:
@ -54,11 +58,11 @@
class="mr-2 my-2" class="mr-2 my-2"
mat-stroked-button mat-stroked-button
[href]="trySubscriptionMail" [href]="trySubscriptionMail"
><span i18n>Try Premium</span ><span i18n>Try Premium</span>
><ion-icon <gf-premium-indicator
class="ml-1 text-muted" class="d-inline-block ml-1"
name="diamond-outline" [enableLink]="false"
></ion-icon ></gf-premium-indicator
></a> ></a>
<a <a
class="mr-2 my-2" class="mr-2 my-2"
@ -170,7 +174,7 @@
</div> </div>
</div> </div>
<div class="align-items-center d-flex mt-4 py-1"> <div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50" i18n>ID</div> <div class="pr-1 w-50" i18n>User ID</div>
<div class="pl-1 w-50">{{ user?.id }}</div> <div class="pl-1 w-50">{{ user?.id }}</div>
</div> </div>
</mat-card-content> </mat-card-content>

View File

@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module'; import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { AccountPageRoutingModule } from './account-page-routing.module'; import { AccountPageRoutingModule } from './account-page-routing.module';
@ -18,13 +19,13 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
@NgModule({ @NgModule({
declarations: [AccountPageComponent], declarations: [AccountPageComponent],
exports: [],
imports: [ imports: [
AccountPageRoutingModule, AccountPageRoutingModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
GfCreateOrUpdateAccessDialogModule, GfCreateOrUpdateAccessDialogModule,
GfPortfolioAccessTableModule, GfPortfolioAccessTableModule,
GfPremiumIndicatorModule,
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatCardModule, MatCardModule,
@ -35,7 +36,6 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
MatSlideToggleModule, MatSlideToggleModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule RouterModule
], ]
providers: []
}) })
export class AccountPageModule {} export class AccountPageModule {}

View File

@ -2,15 +2,6 @@
color: rgb(var(--dark-primary-text)); color: rgb(var(--dark-primary-text));
display: block; display: block;
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
gf-access-table { gf-access-table {
overflow-x: auto; overflow-x: auto;

View File

@ -10,7 +10,6 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.com
@NgModule({ @NgModule({
declarations: [CreateOrUpdateAccessDialog], declarations: [CreateOrUpdateAccessDialog],
exports: [],
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
@ -19,7 +18,6 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.com
MatFormFieldModule, MatFormFieldModule,
MatSelectModule, MatSelectModule,
ReactiveFormsModule ReactiveFormsModule
], ]
providers: []
}) })
export class GfCreateOrUpdateAccessDialogModule {} export class GfCreateOrUpdateAccessDialogModule {}

View File

@ -3,6 +3,8 @@ import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -35,9 +37,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
@ -51,12 +50,17 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.route.queryParams this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => { .subscribe((params) => {
if (params['createDialog'] && this.hasPermissionToCreateAccount) { if (params['accountId'] && params['accountDetailDialog']) {
this.openAccountDetailDialog(params['accountId']);
} else if (
params['createDialog'] &&
this.hasPermissionToCreateAccount
) {
this.openCreateAccountDialog(); this.openCreateAccountDialog();
} else if (params['editDialog']) { } else if (params['editDialog']) {
if (this.accounts) { if (this.accounts) {
const account = this.accounts.find((account) => { const account = this.accounts.find((account) => {
return account.id === params['transactionId']; return account.id === params['accountId'];
}); });
this.openUpdateAccountDialog(account); this.openUpdateAccountDialog(account);
@ -67,9 +71,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
}); });
} }
/**
* Initializes the controller
*/
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
@ -145,7 +146,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
public onUpdateAccount(aAccount: AccountModel) { public onUpdateAccount(aAccount: AccountModel) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { editDialog: true, transactionId: aAccount.id } queryParams: { accountId: aAccount.id, editDialog: true }
}); });
} }
@ -203,6 +204,26 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open(AccountDetailDialog, {
autoFocus: false,
data: <AccountDetailDialogParams>{
accountId: aAccountId,
deviceType: this.deviceType,
hasImpersonationId: this.hasImpersonationId
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
});
}
private openCreateAccountDialog(): void { private openCreateAccountDialog(): void {
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, { const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
data: { data: {

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module'; import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
import { AccountsPageRoutingModule } from './accounts-page-routing.module'; import { AccountsPageRoutingModule } from './accounts-page-routing.module';
@ -10,16 +11,15 @@ import { GfCreateOrUpdateAccountDialogModule } from './create-or-update-account-
@NgModule({ @NgModule({
declarations: [AccountsPageComponent], declarations: [AccountsPageComponent],
exports: [],
imports: [ imports: [
AccountsPageRoutingModule, AccountsPageRoutingModule,
CommonModule, CommonModule,
GfAccountDetailDialogModule,
GfAccountsTableModule, GfAccountsTableModule,
GfCreateOrUpdateAccountDialogModule, GfCreateOrUpdateAccountDialogModule,
MatButtonModule, MatButtonModule,
RouterModule RouterModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class AccountsPageModule {} export class AccountsPageModule {}

View File

@ -50,6 +50,17 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="data.account.id">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account ID</mat-label>
<input
disabled
matInput
name="accountId"
[(ngModel)]="data.account.id"
/>
</mat-form-field>
</div>
</div> </div>
<div class="justify-content-end" mat-dialog-actions> <div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Cancel</button> <button i18n mat-button (click)="onCancel()">Cancel</button>

View File

@ -11,7 +11,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
@NgModule({ @NgModule({
declarations: [CreateOrUpdateAccountDialog], declarations: [CreateOrUpdateAccountDialog],
exports: [],
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
@ -21,7 +20,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
MatInputModule, MatInputModule,
MatSelectModule, MatSelectModule,
ReactiveFormsModule ReactiveFormsModule
], ]
providers: []
}) })
export class GfCreateOrUpdateAccountDialogModule {} export class GfCreateOrUpdateAccountDialogModule {}

View File

@ -16,18 +16,12 @@ export class AdminPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(private dataService: DataService) { public constructor(private dataService: DataService) {
const { systemMessage } = this.dataService.fetchInfo(); const { systemMessage } = this.dataService.fetchInfo();
this.hasMessage = !!systemMessage; this.hasMessage = !!systemMessage;
} }
/**
* Initializes the controller
*/
public ngOnInit() {} public ngOnInit() {}
public ngOnDestroy() { public ngOnDestroy() {

View File

@ -16,9 +16,6 @@ import { takeUntil } from 'rxjs/operators';
export class AuthPageComponent implements OnDestroy, OnInit { export class AuthPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor( public constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
@ -26,9 +23,6 @@ export class AuthPageComponent implements OnDestroy, OnInit {
private tokenStorageService: TokenStorageService private tokenStorageService: TokenStorageService
) {} ) {}
/**
* Initializes the controller
*/
public ngOnInit() { public ngOnInit() {
this.route.params this.route.params
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

View File

@ -6,8 +6,6 @@ import { AuthPageComponent } from './auth-page.component';
@NgModule({ @NgModule({
declarations: [AuthPageComponent], declarations: [AuthPageComponent],
exports: [], imports: [AuthPageRoutingModule, CommonModule]
imports: [AuthPageRoutingModule, CommonModule],
providers: []
}) })
export class AuthPageModule {} export class AuthPageModule {}

View File

@ -3,7 +3,7 @@
<div class="col-md-8 offset-md-2"> <div class="col-md-8 offset-md-2">
<article> <article>
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<h1 class="mb-1" i18n>Hallo Ghostfolio 👋</h1> <h1 class="mb-1">Hallo Ghostfolio 👋</h1>
<div class="text-muted"><small>31.07.2021</small></div> <div class="text-muted"><small>31.07.2021</small></div>
</div> </div>
<section class="mb-4"> <section class="mb-4">

View File

@ -3,7 +3,7 @@
<div class="col-md-8 offset-md-2"> <div class="col-md-8 offset-md-2">
<article> <article>
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<h1 class="mb-1" i18n>Hello Ghostfolio 👋</h1> <h1 class="mb-1">Hello Ghostfolio 👋</h1>
<div class="text-muted"><small>31.07.2021</small></div> <div class="text-muted"><small>31.07.2021</small></div>
</div> </div>
<section class="mb-4"> <section class="mb-4">

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