Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
cc16ba5dc8 | |||
d10227bc39 | |||
4e214c32e8 | |||
49e2862e03 | |||
34e33a2400 | |||
ec9bc984af | |||
2388c494df | |||
d71ab10eed | |||
0e0592180f | |||
60e2aff488 | |||
7b5454e7de | |||
30835ced88 | |||
8897f32bc5 | |||
abaa6b5f27 | |||
2060fcaf0b | |||
fd2408dd62 | |||
31cca024f1 | |||
b535122945 | |||
5113e4e3ad | |||
35e039748f | |||
c6b9e0aa5b | |||
b250491ca5 | |||
61e501c659 | |||
c0f19d56ec | |||
8e2b235b1f | |||
c3407e9b34 | |||
74193e4ee2 | |||
3fe8f9c882 | |||
d130efad47 | |||
109f0ebd70 | |||
069ddcc6b2 | |||
f7bf6e652b | |||
eb059a024a | |||
ad88acff1c | |||
1ff736537c | |||
1fa65e1efd | |||
df6bb489c2 | |||
928a13310d | |||
2384861953 | |||
fe90bda6fb | |||
d4b29ff11c | |||
a0a26cfa58 | |||
1610150427 | |||
cff8acd7b1 | |||
0f36d6cbdb | |||
046e28b521 | |||
aba562cb35 | |||
03f2f33344 | |||
a996dd7ed5 | |||
002b883668 | |||
0b06823893 | |||
2dfd779444 |
118
CHANGELOG.md
118
CHANGELOG.md
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
30
angular.json
30
angular.json
@ -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",
|
||||||
|
@ -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'
|
||||||
};
|
};
|
||||||
|
@ -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()
|
||||||
|
@ -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> {
|
||||||
|
@ -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')) {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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,21 +207,49 @@ export class PortfolioService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const investments = portfolioCalculator.getInvestments().map((item) => {
|
let investments: InvestmentItem[];
|
||||||
|
|
||||||
|
if (groupBy === 'month') {
|
||||||
|
investments = portfolioCalculator.getInvestmentsByMonth().map((item) => {
|
||||||
return {
|
return {
|
||||||
date: item.date,
|
date: item.date,
|
||||||
investment: item.investment.toNumber()
|
investment: item.investment.toNumber()
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add investment of current month
|
||||||
|
const dateOfCurrentMonth = format(
|
||||||
|
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
|
// Add investment of today
|
||||||
const investmentOfToday = investments.filter((investment) => {
|
const investmentOfToday = investments.filter(({ date }) => {
|
||||||
return investment.date === format(new Date(), DATE_FORMAT);
|
return date === format(new Date(), DATE_FORMAT);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (investmentOfToday.length <= 0) {
|
if (investmentOfToday.length <= 0) {
|
||||||
const pastInvestments = investments.filter((investment) => {
|
const pastInvestments = investments.filter(({ date }) => {
|
||||||
return isBefore(parseDate(investment.date), new Date());
|
return isBefore(parseDate(date), new Date());
|
||||||
});
|
});
|
||||||
const lastInvestment = pastInvestments[pastInvestments.length - 1];
|
const lastInvestment = pastInvestments[pastInvestments.length - 1];
|
||||||
|
|
||||||
@ -214,6 +258,7 @@ export class PortfolioService {
|
|||||||
investment: lastInvestment?.investment ?? 0
|
investment: lastInvestment?.investment ?? 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sortBy(investments, (investment) => {
|
return sortBy(investments, (investment) => {
|
||||||
return investment.date;
|
return investment.date;
|
||||||
@ -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 }) => {
|
||||||
|
@ -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(
|
||||||
|
@ -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
@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
"LUNA1": "Terra",
|
"LUNA1": "Terra",
|
||||||
|
"LUNA2": "Terra",
|
||||||
|
"SGB1": "Songbird",
|
||||||
"UNI1": "Uniswap"
|
"UNI1": "Uniswap"
|
||||||
}
|
}
|
||||||
|
@ -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 }),
|
||||||
|
@ -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
|
||||||
],
|
],
|
||||||
|
@ -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 &&
|
||||||
|
@ -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()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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'
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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: () =>
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 {}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.mat-dialog-content {
|
||||||
|
max-height: unset;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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 {}
|
@ -0,0 +1,5 @@
|
|||||||
|
export interface AccountDetailDialogParams {
|
||||||
|
accountId: string;
|
||||||
|
deviceType: string;
|
||||||
|
hasImpersonationId: boolean;
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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: []
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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"
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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 {}
|
||||||
|
@ -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,9 +44,15 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
|||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
||||||
this.user.permissions,
|
this.info?.globalPermissions,
|
||||||
permissions.accessFearAndGreedIndex
|
permissions.enableFearAndGreedIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
||||||
@ -69,7 +72,6 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
|||||||
value: marketPrice
|
value: marketPrice
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
this.isLoading = false;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
@ -80,19 +82,11 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ benchmarks }) => {
|
.subscribe(({ benchmarks }) => {
|
||||||
this.benchmarks = benchmarks;
|
this.benchmarks = benchmarks;
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the controller
|
|
||||||
*/
|
|
||||||
public ngOnInit() {}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
|
@ -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>
|
||||||
|
@ -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 {}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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' }"
|
||||||
|
@ -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 {}
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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 {}
|
||||||
|
@ -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,13 +202,13 @@ 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() {
|
||||||
return {
|
return {
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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,14 +85,38 @@
|
|||||||
</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)="
|
||||||
|
hasPermissionToShowValues &&
|
||||||
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
|
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
|
||||||
onOpenPositionDialog({ dataSource: row.dataSource, symbol: row.symbol })
|
onOpenPositionDialog({ dataSource: row.dataSource, symbol: row.symbol })
|
||||||
"
|
"
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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() {}
|
||||||
|
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 (
|
||||||
|
@ -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
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
@ -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"
|
||||||
|
@ -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 {}
|
||||||
|
@ -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() {
|
||||||
|
@ -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 {}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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 {}
|
@ -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));
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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 {}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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 {}
|
||||||
|
@ -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: {
|
||||||
|
@ -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 {}
|
||||||
|
@ -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>
|
||||||
|
@ -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 {}
|
||||||
|
@ -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() {
|
||||||
|
@ -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))
|
||||||
|
@ -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 {}
|
||||||
|
@ -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">
|
||||||
|
@ -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
Reference in New Issue
Block a user