Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
8d8e55fd0b | |||
ca18621ce8 | |||
b8574d24b2 | |||
6d12c27f9c | |||
c2c5326049 | |||
2a1339b61e | |||
c8a2579624 | |||
832ae063df | |||
b5e026934f | |||
901c997908 | |||
3b6e0b20e2 | |||
e449d51c3c | |||
f72d31bab3 | |||
4c893c4dcc | |||
ffb11cd10e | |||
d424b7731e | |||
6043c87481 | |||
fca0a688b6 | |||
5c6cc4fed5 | |||
64a7d38ff9 | |||
68d0d39161 | |||
233a8a8a18 | |||
190779ee35 | |||
6ef8121561 | |||
58bf57d1e6 | |||
71c5412dd5 | |||
ae85398c3d | |||
048900d01b | |||
074b09b543 | |||
f9e04022f4 | |||
8fd1fbd44a | |||
0fb33ae71c | |||
3a35d72ec2 | |||
32fe3e195f |
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@ -5,11 +5,11 @@
|
|||||||
"name": "Debug Jest File",
|
"name": "Debug Jest File",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}/node_modules/@angular/cli/bin/ng",
|
"program": "${workspaceFolder}/node_modules/@nrwl/cli/bin/nx",
|
||||||
"args": [
|
"args": [
|
||||||
"test",
|
"test",
|
||||||
"--codeCoverage=false",
|
"--codeCoverage=false",
|
||||||
"--testFile=${workspaceFolder}/apps/api/src/models/portfolio.spec.ts"
|
"--testFile=${workspaceFolder}/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts"
|
||||||
],
|
],
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"console": "internalConsole"
|
"console": "internalConsole"
|
||||||
|
58
CHANGELOG.md
58
CHANGELOG.md
@ -5,7 +5,63 @@ 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.205.1 - 16.10.2022
|
## 1.209.0 - 05.11.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the _Buy me a coffee_ button to the about page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the usability of the activities import
|
||||||
|
- Improved the usage of the premium indicator component
|
||||||
|
- Removed the intro image in dark mode
|
||||||
|
- Refactored the `TransactionsPageComponent` to `ActivitiesPageComponent`
|
||||||
|
|
||||||
|
## 1.208.0 - 03.11.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added pagination to the activities table
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Restructured the actions in the admin control panel
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the calculation in the portfolio evolution chart
|
||||||
|
|
||||||
|
## 1.207.0 - 31.10.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for translated labels of asset and asset sub class
|
||||||
|
- Added support for dates in _ISO 8601_ date format (`YYYY-MM-DD`) in the activities import
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Darkened the background color of the dark mode
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the public page
|
||||||
|
- Improved the loading indicator of the portfolio evolution chart
|
||||||
|
|
||||||
|
## 1.206.2 - 20.10.2022
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Fixed the `rxjs` version to `7.5.6` (resolutions)
|
||||||
|
- Migrated the `angular.json` to `project.json` files in the `Nx` workspace
|
||||||
|
- Upgraded `nestjs` from version `9.0.7` to `9.1.4`
|
||||||
|
- Upgraded `Nx` from version `14.6.4` to `15.0.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the performance calculation including `SELL` activities with a significant performance gain
|
||||||
|
|
||||||
|
## 1.205.2 - 16.10.2022
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ RUN yarn install
|
|||||||
COPY ./decorate-angular-cli.js decorate-angular-cli.js
|
COPY ./decorate-angular-cli.js decorate-angular-cli.js
|
||||||
RUN node decorate-angular-cli.js
|
RUN node decorate-angular-cli.js
|
||||||
|
|
||||||
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.js jest.preset.js
|
COPY ./jest.preset.js jest.preset.js
|
||||||
|
22
README.md
22
README.md
@ -128,7 +128,7 @@ docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
|||||||
Open http://localhost:3333 in your browser and accomplish these steps:
|
Open http://localhost:3333 in your browser and accomplish these steps:
|
||||||
|
|
||||||
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
|
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
|
||||||
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
1. Go to the _Market Data_ tab in the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
||||||
1. Click _Sign out_ and check out the _Live Demo_
|
1. Click _Sign out_ and check out the _Live Demo_
|
||||||
|
|
||||||
#### Upgrade Version
|
#### Upgrade Version
|
||||||
@ -158,7 +158,7 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https://
|
|||||||
1. Run `yarn database:setup` to initialize the database schema and populate your database with (example) data
|
1. Run `yarn database:setup` to initialize the database schema and populate your database with (example) data
|
||||||
1. Start the server and the client (see [_Development_](#Development))
|
1. Start the server and the client (see [_Development_](#Development))
|
||||||
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
|
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
|
||||||
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
1. Go to the _Market Data_ tab in the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
||||||
1. Click _Sign out_ and check out the _Live Demo_
|
1. Click _Sign out_ and check out the _Live Demo_
|
||||||
|
|
||||||
### Start Server
|
### Start Server
|
||||||
@ -190,20 +190,22 @@ Run `yarn test`
|
|||||||
|
|
||||||
## Public API
|
## Public API
|
||||||
|
|
||||||
|
### Authorization: Bearer Token
|
||||||
|
|
||||||
|
Set the header for each request as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
"Authorization": "Bearer eyJh..."
|
||||||
|
```
|
||||||
|
|
||||||
|
You can get the _Bearer Token_ via `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`.
|
||||||
|
|
||||||
### Import Activities
|
### Import Activities
|
||||||
|
|
||||||
#### Request
|
#### Request
|
||||||
|
|
||||||
`POST http://localhost:3333/api/v1/import`
|
`POST http://localhost:3333/api/v1/import`
|
||||||
|
|
||||||
#### Authorization: Bearer Token
|
|
||||||
|
|
||||||
Set the header as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
"Authorization": "Bearer eyJh..."
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Body
|
#### Body
|
||||||
|
|
||||||
```
|
```
|
||||||
|
395
angular.json
395
angular.json
@ -1,395 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"projects": {
|
|
||||||
"api": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"root": "apps/api",
|
|
||||||
"sourceRoot": "apps/api/src",
|
|
||||||
"projectType": "application",
|
|
||||||
"prefix": "api",
|
|
||||||
"schematics": {},
|
|
||||||
"architect": {
|
|
||||||
"build": {
|
|
||||||
"builder": "@nrwl/node:webpack",
|
|
||||||
"options": {
|
|
||||||
"outputPath": "dist/apps/api",
|
|
||||||
"main": "apps/api/src/main.ts",
|
|
||||||
"tsConfig": "apps/api/tsconfig.app.json",
|
|
||||||
"assets": ["apps/api/src/assets"]
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"generatePackageJson": true,
|
|
||||||
"optimization": true,
|
|
||||||
"extractLicenses": true,
|
|
||||||
"inspect": false,
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "apps/api/src/environments/environment.ts",
|
|
||||||
"with": "apps/api/src/environments/environment.prod.ts"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"outputs": ["{options.outputPath}"]
|
|
||||||
},
|
|
||||||
"serve": {
|
|
||||||
"builder": "@nrwl/node:node",
|
|
||||||
"options": {
|
|
||||||
"buildTarget": "api:build"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@nrwl/linter:eslint",
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["apps/api/**/*.ts"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"builder": "@nrwl/jest:jest",
|
|
||||||
"options": {
|
|
||||||
"jestConfig": "apps/api/jest.config.ts",
|
|
||||||
"passWithNoTests": true
|
|
||||||
},
|
|
||||||
"outputs": ["coverage/apps/api"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
},
|
|
||||||
"client": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"projectType": "application",
|
|
||||||
"schematics": {
|
|
||||||
"@schematics/angular:component": {
|
|
||||||
"style": "scss"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "apps/client",
|
|
||||||
"sourceRoot": "apps/client/src",
|
|
||||||
"prefix": "gf",
|
|
||||||
"architect": {
|
|
||||||
"build": {
|
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
|
||||||
"options": {
|
|
||||||
"outputPath": "dist/apps/client",
|
|
||||||
"index": "apps/client/src/index.html",
|
|
||||||
"main": "apps/client/src/main.ts",
|
|
||||||
"polyfills": "apps/client/src/polyfills.ts",
|
|
||||||
"tsConfig": "apps/client/tsconfig.app.json",
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"glob": "assetlinks.json",
|
|
||||||
"input": "apps/client/src/assets",
|
|
||||||
"output": "./../.well-known"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "CHANGELOG.md",
|
|
||||||
"input": "",
|
|
||||||
"output": "./../assets"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "LICENSE",
|
|
||||||
"input": "",
|
|
||||||
"output": "./../assets"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "robots.txt",
|
|
||||||
"input": "apps/client/src/assets",
|
|
||||||
"output": "./../"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "sitemap.xml",
|
|
||||||
"input": "apps/client/src/assets",
|
|
||||||
"output": "./../"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "**/*",
|
|
||||||
"input": "node_modules/ionicons/dist/ionicons",
|
|
||||||
"output": "./../ionicons"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "**/*.js",
|
|
||||||
"input": "node_modules/ionicons/dist/",
|
|
||||||
"output": "./../"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"glob": "**/*",
|
|
||||||
"input": "apps/client/src/assets",
|
|
||||||
"output": "./../assets/"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"styles": ["apps/client/src/styles.scss"],
|
|
||||||
"scripts": ["node_modules/marked/marked.min.js"],
|
|
||||||
"vendorChunk": true,
|
|
||||||
"extractLicenses": false,
|
|
||||||
"buildOptimizer": false,
|
|
||||||
"sourceMap": true,
|
|
||||||
"optimization": false,
|
|
||||||
"namedChunks": true
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"development-de": {
|
|
||||||
"baseHref": "/de/",
|
|
||||||
"localize": ["de"]
|
|
||||||
},
|
|
||||||
"development-en": {
|
|
||||||
"baseHref": "/en/",
|
|
||||||
"localize": ["en"]
|
|
||||||
},
|
|
||||||
"development-es": {
|
|
||||||
"baseHref": "/es/",
|
|
||||||
"localize": ["es"]
|
|
||||||
},
|
|
||||||
"development-it": {
|
|
||||||
"baseHref": "/it/",
|
|
||||||
"localize": ["it"]
|
|
||||||
},
|
|
||||||
"development-nl": {
|
|
||||||
"baseHref": "/nl/",
|
|
||||||
"localize": ["nl"]
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "apps/client/src/environments/environment.ts",
|
|
||||||
"with": "apps/client/src/environments/environment.prod.ts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"optimization": true,
|
|
||||||
"outputHashing": "all",
|
|
||||||
"sourceMap": false,
|
|
||||||
"namedChunks": false,
|
|
||||||
"extractLicenses": true,
|
|
||||||
"vendorChunk": false,
|
|
||||||
"buildOptimizer": true,
|
|
||||||
"budgets": [
|
|
||||||
{
|
|
||||||
"type": "initial",
|
|
||||||
"maximumWarning": "2mb",
|
|
||||||
"maximumError": "5mb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "anyComponentStyle",
|
|
||||||
"maximumWarning": "6kb",
|
|
||||||
"maximumError": "10kb"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"outputs": ["{options.outputPath}"],
|
|
||||||
"defaultConfiguration": ""
|
|
||||||
},
|
|
||||||
"serve": {
|
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
|
||||||
"options": {
|
|
||||||
"browserTarget": "client:build",
|
|
||||||
"proxyConfig": "apps/client/proxy.conf.json"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"development-de": {
|
|
||||||
"browserTarget": "client:build:development-de"
|
|
||||||
},
|
|
||||||
"development-en": {
|
|
||||||
"browserTarget": "client:build:development-en"
|
|
||||||
},
|
|
||||||
"development-es": {
|
|
||||||
"browserTarget": "client:build:development-es"
|
|
||||||
},
|
|
||||||
"development-it": {
|
|
||||||
"browserTarget": "client:build:development-it"
|
|
||||||
},
|
|
||||||
"development-nl": {
|
|
||||||
"browserTarget": "client:build:development-nl"
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
"browserTarget": "client:build:production"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"extract-i18n": {
|
|
||||||
"builder": "ng-extract-i18n-merge:ng-extract-i18n-merge",
|
|
||||||
"options": {
|
|
||||||
"browserTarget": "client:build",
|
|
||||||
"includeContext": true,
|
|
||||||
"outputPath": "src/locales",
|
|
||||||
"targetFiles": [
|
|
||||||
"messages.de.xlf",
|
|
||||||
"messages.es.xlf",
|
|
||||||
"messages.it.xlf",
|
|
||||||
"messages.nl.xlf"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@nrwl/linter:eslint",
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["apps/client/**/*.ts"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"builder": "@nrwl/jest:jest",
|
|
||||||
"options": {
|
|
||||||
"jestConfig": "apps/client/jest.config.ts",
|
|
||||||
"passWithNoTests": true
|
|
||||||
},
|
|
||||||
"outputs": ["coverage/apps/client"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"i18n": {
|
|
||||||
"locales": {
|
|
||||||
"de": {
|
|
||||||
"baseHref": "/de/",
|
|
||||||
"translation": "apps/client/src/locales/messages.de.xlf"
|
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"baseHref": "/es/",
|
|
||||||
"translation": "apps/client/src/locales/messages.es.xlf"
|
|
||||||
},
|
|
||||||
"it": {
|
|
||||||
"baseHref": "/it/",
|
|
||||||
"translation": "apps/client/src/locales/messages.it.xlf"
|
|
||||||
},
|
|
||||||
"nl": {
|
|
||||||
"baseHref": "/nl/",
|
|
||||||
"translation": "apps/client/src/locales/messages.nl.xlf"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sourceLocale": "en"
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
},
|
|
||||||
"client-e2e": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"root": "apps/client-e2e",
|
|
||||||
"sourceRoot": "apps/client-e2e/src",
|
|
||||||
"projectType": "application",
|
|
||||||
"architect": {
|
|
||||||
"e2e": {
|
|
||||||
"builder": "@nrwl/cypress:cypress",
|
|
||||||
"options": {
|
|
||||||
"cypressConfig": "apps/client-e2e/cypress.json",
|
|
||||||
"tsConfig": "apps/client-e2e/tsconfig.e2e.json",
|
|
||||||
"devServerTarget": "client:serve"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"devServerTarget": "client:serve:production"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [],
|
|
||||||
"implicitDependencies": ["client"]
|
|
||||||
},
|
|
||||||
"common": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"root": "libs/common",
|
|
||||||
"sourceRoot": "libs/common/src",
|
|
||||||
"projectType": "library",
|
|
||||||
"architect": {
|
|
||||||
"lint": {
|
|
||||||
"builder": "@nrwl/linter:eslint",
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["libs/common/**/*.ts"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"builder": "@nrwl/jest:jest",
|
|
||||||
"outputs": ["coverage/libs/common"],
|
|
||||||
"options": {
|
|
||||||
"jestConfig": "libs/common/jest.config.ts",
|
|
||||||
"passWithNoTests": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
},
|
|
||||||
"ui": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"projectType": "library",
|
|
||||||
"schematics": {
|
|
||||||
"@schematics/angular:component": {
|
|
||||||
"style": "scss"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "libs/ui",
|
|
||||||
"sourceRoot": "libs/ui/src",
|
|
||||||
"prefix": "gf",
|
|
||||||
"architect": {
|
|
||||||
"test": {
|
|
||||||
"builder": "@nrwl/jest:jest",
|
|
||||||
"outputs": ["coverage/libs/ui"],
|
|
||||||
"options": {
|
|
||||||
"jestConfig": "libs/ui/jest.config.ts",
|
|
||||||
"passWithNoTests": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@nrwl/linter:eslint",
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["libs/ui/src/**/*.ts", "libs/ui/src/**/*.html"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"storybook": {
|
|
||||||
"builder": "@storybook/angular:start-storybook",
|
|
||||||
"options": {
|
|
||||||
"port": 4400,
|
|
||||||
"configDir": "libs/ui/.storybook",
|
|
||||||
"browserTarget": "ui:build-storybook",
|
|
||||||
"compodoc": false
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"quiet": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"build-storybook": {
|
|
||||||
"builder": "@storybook/angular:build-storybook",
|
|
||||||
"outputs": ["{options.outputPath}"],
|
|
||||||
"options": {
|
|
||||||
"outputDir": "dist/storybook/ui",
|
|
||||||
"configDir": "libs/ui/.storybook",
|
|
||||||
"browserTarget": "ui:build-storybook",
|
|
||||||
"compodoc": false
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"quiet": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
},
|
|
||||||
"ui-e2e": {
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"root": "apps/ui-e2e",
|
|
||||||
"sourceRoot": "apps/ui-e2e/src",
|
|
||||||
"projectType": "application",
|
|
||||||
"architect": {
|
|
||||||
"e2e": {
|
|
||||||
"builder": "@nrwl/cypress:cypress",
|
|
||||||
"options": {
|
|
||||||
"cypressConfig": "apps/ui-e2e/cypress.json",
|
|
||||||
"devServerTarget": "ui:storybook",
|
|
||||||
"tsConfig": "apps/ui-e2e/tsconfig.json"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"devServerTarget": "ui:storybook:ci"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@nrwl/linter:eslint",
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["apps/ui-e2e/**/*.{js,ts}"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [],
|
|
||||||
"implicitDependencies": ["ui"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
56
apps/api/project.json
Normal file
56
apps/api/project.json
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "apps/api/src",
|
||||||
|
"projectType": "application",
|
||||||
|
"prefix": "api",
|
||||||
|
"generators": {},
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@nrwl/webpack:webpack",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/apps/api",
|
||||||
|
"main": "apps/api/src/main.ts",
|
||||||
|
"tsConfig": "apps/api/tsconfig.app.json",
|
||||||
|
"assets": ["apps/api/src/assets"],
|
||||||
|
"target": "node",
|
||||||
|
"compiler": "tsc"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"generatePackageJson": true,
|
||||||
|
"optimization": true,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"inspect": false,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "apps/api/src/environments/environment.ts",
|
||||||
|
"with": "apps/api/src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": ["{options.outputPath}"]
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"executor": "@nrwl/node:node",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "api:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nrwl/linter:eslint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["apps/api/**/*.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nrwl/jest:jest",
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "apps/api/jest.config.ts",
|
||||||
|
"passWithNoTests": true
|
||||||
|
},
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/apps/api"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
@ -21,6 +21,17 @@ function mockGetValue(symbol: string, date: Date) {
|
|||||||
|
|
||||||
return { marketPrice: 0 };
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
|
case 'BTCUSD':
|
||||||
|
if (isSameDay(parseDate('2015-01-01'), date)) {
|
||||||
|
return { marketPrice: 314.25 };
|
||||||
|
} else if (isSameDay(parseDate('2017-12-31'), date)) {
|
||||||
|
return { marketPrice: 14156.4 };
|
||||||
|
} else if (isSameDay(parseDate('2018-01-01'), date)) {
|
||||||
|
return { marketPrice: 13657.2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
case 'NOVN.SW':
|
case 'NOVN.SW':
|
||||||
if (isSameDay(parseDate('2022-04-11'), date)) {
|
if (isSameDay(parseDate('2022-04-11'), date)) {
|
||||||
return { marketPrice: 87.8 };
|
return { marketPrice: 87.8 };
|
||||||
|
@ -0,0 +1,110 @@
|
|||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
|
import { PortfolioCalculator } from './portfolio-calculator';
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||||
|
return CurrentRateServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PortfolioCalculator', () => {
|
||||||
|
let currentRateService: CurrentRateService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentRateService = new CurrentRateService(null, null, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get current positions', () => {
|
||||||
|
it.only('with BTCUSD buy and sell partially', async () => {
|
||||||
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
|
currentRateService,
|
||||||
|
currency: 'CHF',
|
||||||
|
orders: [
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2015-01-01',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(0),
|
||||||
|
name: 'Bitcoin USD',
|
||||||
|
quantity: new Big(2),
|
||||||
|
symbol: 'BTCUSD',
|
||||||
|
type: 'BUY',
|
||||||
|
unitPrice: new Big(320.43)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2017-12-31',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(0),
|
||||||
|
name: 'Bitcoin USD',
|
||||||
|
quantity: new Big(1),
|
||||||
|
symbol: 'BTCUSD',
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: new Big(14156.4)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
portfolioCalculator.computeTransactionPoints();
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2018-01-01').getTime());
|
||||||
|
|
||||||
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
|
parseDate('2015-01-01')
|
||||||
|
);
|
||||||
|
|
||||||
|
const investments = portfolioCalculator.getInvestments();
|
||||||
|
|
||||||
|
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(currentPositions).toEqual({
|
||||||
|
currentValue: new Big('13657.2'),
|
||||||
|
errors: [],
|
||||||
|
grossPerformance: new Big('27172.74'),
|
||||||
|
grossPerformancePercentage: new Big('42.40043067128546016291'),
|
||||||
|
hasErrors: false,
|
||||||
|
netPerformance: new Big('27172.74'),
|
||||||
|
netPerformancePercentage: new Big('42.40043067128546016291'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('320.43'),
|
||||||
|
currency: 'CHF',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
firstBuyDate: '2015-01-01',
|
||||||
|
grossPerformance: new Big('27172.74'),
|
||||||
|
grossPerformancePercentage: new Big('42.40043067128546016291'),
|
||||||
|
investment: new Big('320.43'),
|
||||||
|
netPerformance: new Big('27172.74'),
|
||||||
|
netPerformancePercentage: new Big('42.40043067128546016291'),
|
||||||
|
marketPrice: 13657.2,
|
||||||
|
quantity: new Big('1'),
|
||||||
|
symbol: 'BTCUSD',
|
||||||
|
transactionCount: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalInvestment: new Big('320.43')
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(investments).toEqual([
|
||||||
|
{ date: '2015-01-01', investment: new Big('640.86') },
|
||||||
|
{ date: '2017-12-31', investment: new Big('320.43') }
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(investmentsByMonth).toEqual([
|
||||||
|
{ date: '2015-01-01', investment: new Big('640.86') },
|
||||||
|
{ date: '2017-12-01', investment: new Big('-14156.4') }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -22,7 +22,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('get current positions', () => {
|
describe('get current positions', () => {
|
||||||
it.only('with BALN.SW buy and sell', async () => {
|
it.only('with NOVN.SW buy and sell partially', async () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currentRateService,
|
currentRateService,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
@ -0,0 +1,130 @@
|
|||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
|
import { PortfolioCalculator } from './portfolio-calculator';
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||||
|
return CurrentRateServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PortfolioCalculator', () => {
|
||||||
|
let currentRateService: CurrentRateService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentRateService = new CurrentRateService(null, null, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get current positions', () => {
|
||||||
|
it.only('with NOVN.SW buy and sell', async () => {
|
||||||
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
|
currentRateService,
|
||||||
|
currency: 'CHF',
|
||||||
|
orders: [
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2022-03-07',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(0),
|
||||||
|
name: 'Novartis AG',
|
||||||
|
quantity: new Big(2),
|
||||||
|
symbol: 'NOVN.SW',
|
||||||
|
type: 'BUY',
|
||||||
|
unitPrice: new Big(75.8)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2022-04-08',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(0),
|
||||||
|
name: 'Novartis AG',
|
||||||
|
quantity: new Big(2),
|
||||||
|
symbol: 'NOVN.SW',
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: new Big(85.73)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
portfolioCalculator.computeTransactionPoints();
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||||
|
|
||||||
|
const chartData = await portfolioCalculator.getChartData(
|
||||||
|
parseDate('2022-03-07')
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
|
parseDate('2022-03-07')
|
||||||
|
);
|
||||||
|
|
||||||
|
const investments = portfolioCalculator.getInvestments();
|
||||||
|
|
||||||
|
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(chartData[0]).toEqual({
|
||||||
|
date: '2022-03-07',
|
||||||
|
netPerformanceInPercentage: 0,
|
||||||
|
netPerformance: 0,
|
||||||
|
totalInvestment: 151.6,
|
||||||
|
value: 151.6
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chartData[chartData.length - 1]).toEqual({
|
||||||
|
date: '2022-04-11',
|
||||||
|
netPerformanceInPercentage: 13.100263852242744,
|
||||||
|
netPerformance: 19.86,
|
||||||
|
totalInvestment: 0,
|
||||||
|
value: 19.86
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(currentPositions).toEqual({
|
||||||
|
currentValue: new Big('0'),
|
||||||
|
errors: [],
|
||||||
|
grossPerformance: new Big('19.86'),
|
||||||
|
grossPerformancePercentage: new Big('0.13100263852242744063'),
|
||||||
|
hasErrors: false,
|
||||||
|
netPerformance: new Big('19.86'),
|
||||||
|
netPerformancePercentage: new Big('0.13100263852242744063'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('0'),
|
||||||
|
currency: 'CHF',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
firstBuyDate: '2022-03-07',
|
||||||
|
grossPerformance: new Big('19.86'),
|
||||||
|
grossPerformancePercentage: new Big('0.13100263852242744063'),
|
||||||
|
investment: new Big('0'),
|
||||||
|
netPerformance: new Big('19.86'),
|
||||||
|
netPerformancePercentage: new Big('0.13100263852242744063'),
|
||||||
|
marketPrice: 87.8,
|
||||||
|
quantity: new Big('0'),
|
||||||
|
symbol: 'NOVN.SW',
|
||||||
|
transactionCount: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalInvestment: new Big('0')
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(investments).toEqual([
|
||||||
|
{ date: '2022-03-07', investment: new Big('151.6') },
|
||||||
|
{ date: '2022-04-08', investment: new Big('0') }
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(investmentsByMonth).toEqual([
|
||||||
|
{ date: '2022-03-01', investment: new Big('151.6') },
|
||||||
|
{ date: '2022-04-01', investment: new Big('-171.46') }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -234,11 +234,17 @@ export class PortfolioCalculator {
|
|||||||
[symbol: string]: { [date: string]: Big };
|
[symbol: string]: { [date: string]: Big };
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
const maxInvestmentValuesBySymbol: {
|
||||||
|
[symbol: string]: { [date: string]: Big };
|
||||||
|
} = {};
|
||||||
|
|
||||||
const totalNetPerformanceValues: { [date: string]: Big } = {};
|
const totalNetPerformanceValues: { [date: string]: Big } = {};
|
||||||
const totalInvestmentValues: { [date: string]: Big } = {};
|
const totalInvestmentValues: { [date: string]: Big } = {};
|
||||||
|
const maxTotalInvestmentValues: { [date: string]: Big } = {};
|
||||||
|
|
||||||
for (const symbol of Object.keys(symbols)) {
|
for (const symbol of Object.keys(symbols)) {
|
||||||
const { netPerformanceValues, investmentValues } = this.getSymbolMetrics({
|
const { investmentValues, maxInvestmentValues, netPerformanceValues } =
|
||||||
|
this.getSymbolMetrics({
|
||||||
end,
|
end,
|
||||||
marketSymbolMap,
|
marketSymbolMap,
|
||||||
start,
|
start,
|
||||||
@ -249,6 +255,7 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
netPerformanceValuesBySymbol[symbol] = netPerformanceValues;
|
netPerformanceValuesBySymbol[symbol] = netPerformanceValues;
|
||||||
investmentValuesBySymbol[symbol] = investmentValues;
|
investmentValuesBySymbol[symbol] = investmentValues;
|
||||||
|
maxInvestmentValuesBySymbol[symbol] = maxInvestmentValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const currentDate of dates) {
|
for (const currentDate of dates) {
|
||||||
@ -267,19 +274,28 @@ export class PortfolioCalculator {
|
|||||||
totalInvestmentValues[dateString] =
|
totalInvestmentValues[dateString] =
|
||||||
totalInvestmentValues[dateString] ?? new Big(0);
|
totalInvestmentValues[dateString] ?? new Big(0);
|
||||||
|
|
||||||
|
maxTotalInvestmentValues[dateString] =
|
||||||
|
maxTotalInvestmentValues[dateString] ?? new Big(0);
|
||||||
|
|
||||||
if (investmentValuesBySymbol[symbol]?.[dateString]) {
|
if (investmentValuesBySymbol[symbol]?.[dateString]) {
|
||||||
totalInvestmentValues[dateString] = totalInvestmentValues[
|
totalInvestmentValues[dateString] = totalInvestmentValues[
|
||||||
dateString
|
dateString
|
||||||
].add(investmentValuesBySymbol[symbol][dateString]);
|
].add(investmentValuesBySymbol[symbol][dateString]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (maxInvestmentValuesBySymbol[symbol]?.[dateString]) {
|
||||||
|
maxTotalInvestmentValues[dateString] = maxTotalInvestmentValues[
|
||||||
|
dateString
|
||||||
|
].add(maxInvestmentValuesBySymbol[symbol][dateString]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.keys(totalNetPerformanceValues).map((date) => {
|
return Object.keys(totalNetPerformanceValues).map((date) => {
|
||||||
const netPerformanceInPercentage = totalInvestmentValues[date].eq(0)
|
const netPerformanceInPercentage = maxTotalInvestmentValues[date].eq(0)
|
||||||
? 0
|
? 0
|
||||||
: totalNetPerformanceValues[date]
|
: totalNetPerformanceValues[date]
|
||||||
.div(totalInvestmentValues[date])
|
.div(maxTotalInvestmentValues[date])
|
||||||
.mul(100)
|
.mul(100)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
|
|
||||||
@ -899,13 +915,14 @@ export class PortfolioCalculator {
|
|||||||
let initialValue: Big;
|
let initialValue: Big;
|
||||||
let investmentAtStartDate: Big;
|
let investmentAtStartDate: Big;
|
||||||
const investmentValues: { [date: string]: Big } = {};
|
const investmentValues: { [date: string]: Big } = {};
|
||||||
|
const maxInvestmentValues: { [date: string]: Big } = {};
|
||||||
let lastAveragePrice = new Big(0);
|
let lastAveragePrice = new Big(0);
|
||||||
let lastTransactionInvestment = new Big(0);
|
// let lastTransactionInvestment = new Big(0);
|
||||||
let lastValueOfInvestmentBeforeTransaction = new Big(0);
|
// let lastValueOfInvestmentBeforeTransaction = new Big(0);
|
||||||
let maxTotalInvestment = new Big(0);
|
let maxTotalInvestment = new Big(0);
|
||||||
const netPerformanceValues: { [date: string]: Big } = {};
|
const netPerformanceValues: { [date: string]: Big } = {};
|
||||||
let timeWeightedGrossPerformancePercentage = new Big(1);
|
// let timeWeightedGrossPerformancePercentage = new Big(1);
|
||||||
let timeWeightedNetPerformancePercentage = new Big(1);
|
// let timeWeightedNetPerformancePercentage = new Big(1);
|
||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
let totalInvestmentWithGrossPerformanceFromSell = new Big(0);
|
let totalInvestmentWithGrossPerformanceFromSell = new Big(0);
|
||||||
let totalUnits = new Big(0);
|
let totalUnits = new Big(0);
|
||||||
@ -1000,6 +1017,12 @@ export class PortfolioCalculator {
|
|||||||
for (let i = 0; i < orders.length; i += 1) {
|
for (let i = 0; i < orders.length; i += 1) {
|
||||||
const order = orders[i];
|
const order = orders[i];
|
||||||
|
|
||||||
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
|
console.log();
|
||||||
|
console.log();
|
||||||
|
console.log(i + 1, order.type, order.itemType);
|
||||||
|
}
|
||||||
|
|
||||||
if (order.itemType === 'start') {
|
if (order.itemType === 'start') {
|
||||||
// Take the unit price of the order as the market price if there are no
|
// Take the unit price of the order as the market price if there are no
|
||||||
// orders of this symbol before the start date
|
// orders of this symbol before the start date
|
||||||
@ -1027,9 +1050,21 @@ export class PortfolioCalculator {
|
|||||||
valueAtStartDate = valueOfInvestmentBeforeTransaction;
|
valueAtStartDate = valueOfInvestmentBeforeTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transactionInvestment = order.quantity
|
const transactionInvestment =
|
||||||
.mul(order.unitPrice)
|
order.type === 'BUY'
|
||||||
.mul(this.getFactor(order.type));
|
? order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
|
||||||
|
: totalUnits.gt(0)
|
||||||
|
? totalInvestment
|
||||||
|
.div(totalUnits)
|
||||||
|
.mul(order.quantity)
|
||||||
|
.mul(this.getFactor(order.type))
|
||||||
|
: new Big(0);
|
||||||
|
|
||||||
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
|
console.log('totalInvestment', totalInvestment.toNumber());
|
||||||
|
console.log('order.quantity', order.quantity.toNumber());
|
||||||
|
console.log('transactionInvestment', transactionInvestment.toNumber());
|
||||||
|
}
|
||||||
|
|
||||||
totalInvestment = totalInvestment.plus(transactionInvestment);
|
totalInvestment = totalInvestment.plus(transactionInvestment);
|
||||||
|
|
||||||
@ -1078,58 +1113,69 @@ export class PortfolioCalculator {
|
|||||||
? new Big(0)
|
? new Big(0)
|
||||||
: totalInvestmentWithGrossPerformanceFromSell.div(totalUnits);
|
: totalInvestmentWithGrossPerformanceFromSell.div(totalUnits);
|
||||||
|
|
||||||
const newGrossPerformance = valueOfInvestment
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
.minus(totalInvestmentWithGrossPerformanceFromSell)
|
console.log(
|
||||||
.plus(grossPerformanceFromSells);
|
'totalInvestmentWithGrossPerformanceFromSell',
|
||||||
|
totalInvestmentWithGrossPerformanceFromSell.toNumber()
|
||||||
if (
|
|
||||||
i > indexOfStartOrder &&
|
|
||||||
!lastValueOfInvestmentBeforeTransaction
|
|
||||||
.plus(lastTransactionInvestment)
|
|
||||||
.eq(0)
|
|
||||||
) {
|
|
||||||
const grossHoldingPeriodReturn = valueOfInvestmentBeforeTransaction
|
|
||||||
.minus(
|
|
||||||
lastValueOfInvestmentBeforeTransaction.plus(
|
|
||||||
lastTransactionInvestment
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.div(
|
|
||||||
lastValueOfInvestmentBeforeTransaction.plus(
|
|
||||||
lastTransactionInvestment
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
console.log(
|
||||||
timeWeightedGrossPerformancePercentage =
|
'grossPerformanceFromSells',
|
||||||
timeWeightedGrossPerformancePercentage.mul(
|
grossPerformanceFromSells.toNumber()
|
||||||
new Big(1).plus(grossHoldingPeriodReturn)
|
|
||||||
);
|
|
||||||
|
|
||||||
const netHoldingPeriodReturn = valueOfInvestmentBeforeTransaction
|
|
||||||
.minus(fees.minus(feesAtStartDate))
|
|
||||||
.minus(
|
|
||||||
lastValueOfInvestmentBeforeTransaction.plus(
|
|
||||||
lastTransactionInvestment
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.div(
|
|
||||||
lastValueOfInvestmentBeforeTransaction.plus(
|
|
||||||
lastTransactionInvestment
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
timeWeightedNetPerformancePercentage =
|
|
||||||
timeWeightedNetPerformancePercentage.mul(
|
|
||||||
new Big(1).plus(netHoldingPeriodReturn)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newGrossPerformance = valueOfInvestment
|
||||||
|
.minus(totalInvestment)
|
||||||
|
.plus(grossPerformanceFromSells);
|
||||||
|
|
||||||
|
// if (
|
||||||
|
// i > indexOfStartOrder &&
|
||||||
|
// !lastValueOfInvestmentBeforeTransaction
|
||||||
|
// .plus(lastTransactionInvestment)
|
||||||
|
// .eq(0)
|
||||||
|
// ) {
|
||||||
|
// const grossHoldingPeriodReturn = valueOfInvestmentBeforeTransaction
|
||||||
|
// .minus(
|
||||||
|
// lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
|
// lastTransactionInvestment
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// .div(
|
||||||
|
// lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
|
// lastTransactionInvestment
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
|
||||||
|
// timeWeightedGrossPerformancePercentage =
|
||||||
|
// timeWeightedGrossPerformancePercentage.mul(
|
||||||
|
// new Big(1).plus(grossHoldingPeriodReturn)
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const netHoldingPeriodReturn = valueOfInvestmentBeforeTransaction
|
||||||
|
// .minus(fees.minus(feesAtStartDate))
|
||||||
|
// .minus(
|
||||||
|
// lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
|
// lastTransactionInvestment
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// .div(
|
||||||
|
// lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
|
// lastTransactionInvestment
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
|
||||||
|
// timeWeightedNetPerformancePercentage =
|
||||||
|
// timeWeightedNetPerformancePercentage.mul(
|
||||||
|
// new Big(1).plus(netHoldingPeriodReturn)
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
grossPerformance = newGrossPerformance;
|
grossPerformance = newGrossPerformance;
|
||||||
|
|
||||||
lastTransactionInvestment = transactionInvestment;
|
// lastTransactionInvestment = transactionInvestment;
|
||||||
|
|
||||||
lastValueOfInvestmentBeforeTransaction =
|
// lastValueOfInvestmentBeforeTransaction =
|
||||||
valueOfInvestmentBeforeTransaction;
|
// valueOfInvestmentBeforeTransaction;
|
||||||
|
|
||||||
if (order.itemType === 'start') {
|
if (order.itemType === 'start') {
|
||||||
feesAtStartDate = fees;
|
feesAtStartDate = fees;
|
||||||
@ -1142,6 +1188,15 @@ export class PortfolioCalculator {
|
|||||||
.minus(fees.minus(feesAtStartDate));
|
.minus(fees.minus(feesAtStartDate));
|
||||||
|
|
||||||
investmentValues[order.date] = totalInvestment;
|
investmentValues[order.date] = totalInvestment;
|
||||||
|
maxInvestmentValues[order.date] = maxTotalInvestment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
|
console.log('totalInvestment', totalInvestment.toNumber());
|
||||||
|
console.log(
|
||||||
|
'totalGrossPerformance',
|
||||||
|
grossPerformance.minus(grossPerformanceAtStartDate).toNumber()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i === indexOfEndOrder) {
|
if (i === indexOfEndOrder) {
|
||||||
@ -1149,11 +1204,11 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timeWeightedGrossPerformancePercentage =
|
// timeWeightedGrossPerformancePercentage =
|
||||||
timeWeightedGrossPerformancePercentage.minus(1);
|
// timeWeightedGrossPerformancePercentage.minus(1);
|
||||||
|
|
||||||
timeWeightedNetPerformancePercentage =
|
// timeWeightedNetPerformancePercentage =
|
||||||
timeWeightedNetPerformancePercentage.minus(1);
|
// timeWeightedNetPerformancePercentage.minus(1);
|
||||||
|
|
||||||
const totalGrossPerformance = grossPerformance.minus(
|
const totalGrossPerformance = grossPerformance.minus(
|
||||||
grossPerformanceAtStartDate
|
grossPerformanceAtStartDate
|
||||||
@ -1218,6 +1273,7 @@ export class PortfolioCalculator {
|
|||||||
Average price: ${averagePriceAtStartDate.toFixed(
|
Average price: ${averagePriceAtStartDate.toFixed(
|
||||||
2
|
2
|
||||||
)} -> ${averagePriceAtEndDate.toFixed(2)}
|
)} -> ${averagePriceAtEndDate.toFixed(2)}
|
||||||
|
Total investment: ${totalInvestment.toFixed(2)}
|
||||||
Max. total investment: ${maxTotalInvestment.toFixed(2)}
|
Max. total investment: ${maxTotalInvestment.toFixed(2)}
|
||||||
Gross performance: ${totalGrossPerformance.toFixed(
|
Gross performance: ${totalGrossPerformance.toFixed(
|
||||||
2
|
2
|
||||||
@ -1233,6 +1289,7 @@ export class PortfolioCalculator {
|
|||||||
initialValue,
|
initialValue,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
investmentValues,
|
investmentValues,
|
||||||
|
maxInvestmentValues,
|
||||||
netPerformancePercentage,
|
netPerformancePercentage,
|
||||||
netPerformanceValues,
|
netPerformanceValues,
|
||||||
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
|
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
|
||||||
|
@ -10,7 +10,6 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
|
|||||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
|
||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
@ -72,8 +71,13 @@ export class PortfolioController {
|
|||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('tags') filterByTags?: string
|
@Query('tags') filterByTags?: string
|
||||||
): Promise<PortfolioDetails & { hasError: boolean }> {
|
): Promise<PortfolioDetails & { hasError: boolean }> {
|
||||||
|
let hasDetails = true;
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
|
|
||||||
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
|
hasDetails = this.request.user.subscription.type === 'Premium';
|
||||||
|
}
|
||||||
|
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
filterByAccounts,
|
filterByAccounts,
|
||||||
filterByAssetClasses,
|
filterByAssetClasses,
|
||||||
@ -134,7 +138,13 @@ export class PortfolioController {
|
|||||||
accounts[name].current = current / totalValue;
|
accounts[name].current = current / totalValue;
|
||||||
accounts[name].original = original / totalInvestment;
|
accounts[name].original = original / totalInvestment;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasDetails === false ||
|
||||||
|
impersonationId ||
|
||||||
|
this.userService.isRestrictedView(this.request.user)
|
||||||
|
) {
|
||||||
portfolioSummary = nullifyValuesInObject(summary, [
|
portfolioSummary = nullifyValuesInObject(summary, [
|
||||||
'cash',
|
'cash',
|
||||||
'committedFunds',
|
'committedFunds',
|
||||||
@ -152,11 +162,6 @@ export class PortfolioController {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasDetails = true;
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
|
||||||
hasDetails = this.request.user.subscription.type === 'Premium';
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||||
holdings[symbol] = {
|
holdings[symbol] = {
|
||||||
...portfolioPosition,
|
...portfolioPosition,
|
||||||
@ -176,7 +181,7 @@ export class PortfolioController {
|
|||||||
hasError,
|
hasError,
|
||||||
holdings,
|
holdings,
|
||||||
totalValueInBaseCurrency,
|
totalValueInBaseCurrency,
|
||||||
summary: hasDetails ? portfolioSummary : undefined
|
summary: portfolioSummary
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,16 +192,6 @@ export class PortfolioController {
|
|||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
@Query('groupBy') groupBy?: GroupBy
|
@Query('groupBy') groupBy?: GroupBy
|
||||||
): Promise<PortfolioInvestments> {
|
): Promise<PortfolioInvestments> {
|
||||||
if (
|
|
||||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
|
||||||
this.request.user.subscription.type === 'Basic'
|
|
||||||
) {
|
|
||||||
throw new HttpException(
|
|
||||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
|
||||||
StatusCodes.FORBIDDEN
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let investments: InvestmentItem[];
|
let investments: InvestmentItem[];
|
||||||
|
|
||||||
if (groupBy === 'month') {
|
if (groupBy === 'month') {
|
||||||
@ -227,6 +222,15 @@ export class PortfolioController {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||||
|
this.request.user.subscription.type === 'Basic'
|
||||||
|
) {
|
||||||
|
investments = investments.map((item) => {
|
||||||
|
return nullifyValuesInObject(item, ['investment']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { investments };
|
return { investments };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,7 +244,8 @@ export class PortfolioController {
|
|||||||
): Promise<PortfolioPerformanceResponse> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const performanceInformation = await this.portfolioService.getPerformance({
|
const performanceInformation = await this.portfolioService.getPerformance({
|
||||||
dateRange,
|
dateRange,
|
||||||
impersonationId
|
impersonationId,
|
||||||
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -274,6 +279,17 @@ export class PortfolioController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||||
|
this.request.user.subscription.type === 'Basic'
|
||||||
|
) {
|
||||||
|
performanceInformation.chart = performanceInformation.chart.map(
|
||||||
|
(item) => {
|
||||||
|
return nullifyValuesInObject(item, ['totalInvestment', 'value']);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return performanceInformation;
|
return performanceInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -331,7 +347,7 @@ export class PortfolioController {
|
|||||||
dateRange: 'max',
|
dateRange: 'max',
|
||||||
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
|
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
|
||||||
impersonationId: access.userId,
|
impersonationId: access.userId,
|
||||||
userId: access.userId
|
userId: user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioPublicDetails: PortfolioPublicDetails = {
|
const portfolioPublicDetails: PortfolioPublicDetails = {
|
||||||
|
@ -3,7 +3,6 @@ import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details
|
|||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
||||||
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
|
|
||||||
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
||||||
@ -36,7 +35,8 @@ import {
|
|||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
TimelinePosition,
|
TimelinePosition,
|
||||||
UserSettings
|
UserSettings,
|
||||||
|
UserWithSettings
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import type {
|
import type {
|
||||||
@ -67,11 +67,9 @@ import {
|
|||||||
isAfter,
|
isAfter,
|
||||||
isBefore,
|
isBefore,
|
||||||
max,
|
max,
|
||||||
parse,
|
|
||||||
parseISO,
|
parseISO,
|
||||||
set,
|
set,
|
||||||
setDayOfYear,
|
setDayOfYear,
|
||||||
startOfDay,
|
|
||||||
subDays,
|
subDays,
|
||||||
subYears
|
subYears
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
@ -130,9 +128,9 @@ export class PortfolioService {
|
|||||||
}),
|
}),
|
||||||
this.getDetails({
|
this.getDetails({
|
||||||
filters,
|
filters,
|
||||||
userId,
|
|
||||||
withExcludedAccounts,
|
withExcludedAccounts,
|
||||||
impersonationId: userId
|
impersonationId: userId,
|
||||||
|
userId: this.request.user.id
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -304,12 +302,16 @@ export class PortfolioService {
|
|||||||
|
|
||||||
public async getChart({
|
public async getChart({
|
||||||
dateRange = 'max',
|
dateRange = 'max',
|
||||||
impersonationId
|
impersonationId,
|
||||||
|
userCurrency,
|
||||||
|
userId
|
||||||
}: {
|
}: {
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
|
userCurrency: string;
|
||||||
|
userId: string;
|
||||||
}): Promise<HistoricalDataContainer> {
|
}): Promise<HistoricalDataContainer> {
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
|
|
||||||
const { portfolioOrders, transactionPoints } =
|
const { portfolioOrders, transactionPoints } =
|
||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
@ -317,7 +319,7 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
currency: userCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
orders: portfolioOrders
|
orders: portfolioOrders
|
||||||
});
|
});
|
||||||
@ -355,28 +357,24 @@ export class PortfolioService {
|
|||||||
|
|
||||||
public async getDetails({
|
public async getDetails({
|
||||||
impersonationId,
|
impersonationId,
|
||||||
userId,
|
|
||||||
dateRange = 'max',
|
dateRange = 'max',
|
||||||
filters,
|
filters,
|
||||||
|
userId,
|
||||||
withExcludedAccounts = false
|
withExcludedAccounts = false
|
||||||
}: {
|
}: {
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
userId: string;
|
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
||||||
// TODO
|
|
||||||
userId = await this.getUserId(impersonationId, userId);
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
|
const userCurrency = this.getUserCurrency(user);
|
||||||
|
|
||||||
const emergencyFund = new Big(
|
const emergencyFund = new Big(
|
||||||
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
);
|
);
|
||||||
const userCurrency =
|
|
||||||
user.Settings?.settings.baseCurrency ??
|
|
||||||
this.request.user?.Settings?.settings.baseCurrency ??
|
|
||||||
this.baseCurrency;
|
|
||||||
|
|
||||||
const { orders, portfolioOrders, transactionPoints } =
|
const { orders, portfolioOrders, transactionPoints } =
|
||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
@ -540,7 +538,11 @@ export class PortfolioService {
|
|||||||
withExcludedAccounts
|
withExcludedAccounts
|
||||||
});
|
});
|
||||||
|
|
||||||
const summary = await this.getSummary({ impersonationId });
|
const summary = await this.getSummary({
|
||||||
|
impersonationId,
|
||||||
|
userCurrency,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
@ -560,8 +562,9 @@ export class PortfolioService {
|
|||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aSymbol: string
|
aSymbol: string
|
||||||
): Promise<PortfolioPositionDetail> {
|
): Promise<PortfolioPositionDetail> {
|
||||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
const user = await this.userService.user({ id: userId });
|
||||||
|
const userCurrency = this.getUserCurrency(user);
|
||||||
|
|
||||||
const orders = (
|
const orders = (
|
||||||
await this.orderService.getOrders({
|
await this.orderService.getOrders({
|
||||||
@ -883,12 +886,16 @@ export class PortfolioService {
|
|||||||
|
|
||||||
public async getPerformance({
|
public async getPerformance({
|
||||||
dateRange = 'max',
|
dateRange = 'max',
|
||||||
impersonationId
|
impersonationId,
|
||||||
|
userId
|
||||||
}: {
|
}: {
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
|
userId: string;
|
||||||
}): Promise<PortfolioPerformanceResponse> {
|
}): Promise<PortfolioPerformanceResponse> {
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
|
const user = await this.userService.user({ id: userId });
|
||||||
|
const userCurrency = this.getUserCurrency(user);
|
||||||
|
|
||||||
const { portfolioOrders, transactionPoints } =
|
const { portfolioOrders, transactionPoints } =
|
||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
@ -896,7 +903,7 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
currency: userCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
orders: portfolioOrders
|
orders: portfolioOrders
|
||||||
});
|
});
|
||||||
@ -947,7 +954,9 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const historicalDataContainer = await this.getChart({
|
const historicalDataContainer = await this.getChart({
|
||||||
dateRange,
|
dateRange,
|
||||||
impersonationId
|
impersonationId,
|
||||||
|
userCurrency,
|
||||||
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemOfToday = historicalDataContainer.items.find((item) => {
|
const itemOfToday = historicalDataContainer.items.find((item) => {
|
||||||
@ -995,8 +1004,9 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
||||||
const currency = this.request.user.Settings.settings.baseCurrency;
|
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||||
|
const user = await this.userService.user({ id: userId });
|
||||||
|
const userCurrency = this.getUserCurrency(user);
|
||||||
|
|
||||||
const { orders, portfolioOrders, transactionPoints } =
|
const { orders, portfolioOrders, transactionPoints } =
|
||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
@ -1010,7 +1020,7 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currency,
|
currency: userCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
orders: portfolioOrders
|
orders: portfolioOrders
|
||||||
});
|
});
|
||||||
@ -1030,7 +1040,7 @@ export class PortfolioService {
|
|||||||
orders,
|
orders,
|
||||||
portfolioItemsNow,
|
portfolioItemsNow,
|
||||||
userId,
|
userId,
|
||||||
userCurrency: currency
|
userCurrency
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
@ -1077,7 +1087,7 @@ export class PortfolioService {
|
|||||||
new FeeRatioInitialInvestment(
|
new FeeRatioInitialInvestment(
|
||||||
this.exchangeRateDataService,
|
this.exchangeRateDataService,
|
||||||
currentPositions.totalInvestment.toNumber(),
|
currentPositions.totalInvestment.toNumber(),
|
||||||
this.getFees(orders).toNumber()
|
this.getFees({ orders, userCurrency }).toNumber()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
<UserSettings>this.request.user.Settings.settings
|
<UserSettings>this.request.user.Settings.settings
|
||||||
@ -1180,7 +1190,15 @@ export class PortfolioService {
|
|||||||
return cashPositions;
|
return cashPositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDividend(orders: OrderWithAccount[], date = new Date(0)) {
|
private getDividend({
|
||||||
|
date = new Date(0),
|
||||||
|
orders,
|
||||||
|
userCurrency
|
||||||
|
}: {
|
||||||
|
date?: Date;
|
||||||
|
orders: OrderWithAccount[];
|
||||||
|
userCurrency: string;
|
||||||
|
}) {
|
||||||
return orders
|
return orders
|
||||||
.filter((order) => {
|
.filter((order) => {
|
||||||
// Filter out all orders before given date and type dividend
|
// Filter out all orders before given date and type dividend
|
||||||
@ -1193,7 +1211,7 @@ export class PortfolioService {
|
|||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
||||||
order.SymbolProfile.currency,
|
order.SymbolProfile.currency,
|
||||||
this.request.user.Settings.settings.baseCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.reduce(
|
.reduce(
|
||||||
@ -1202,7 +1220,15 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFees(orders: OrderWithAccount[], date = new Date(0)) {
|
private getFees({
|
||||||
|
date = new Date(0),
|
||||||
|
orders,
|
||||||
|
userCurrency
|
||||||
|
}: {
|
||||||
|
date?: Date;
|
||||||
|
orders: OrderWithAccount[];
|
||||||
|
userCurrency: string;
|
||||||
|
}) {
|
||||||
return orders
|
return orders
|
||||||
.filter((order) => {
|
.filter((order) => {
|
||||||
// Filter out all orders before given date
|
// Filter out all orders before given date
|
||||||
@ -1212,7 +1238,7 @@ export class PortfolioService {
|
|||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
order.fee,
|
order.fee,
|
||||||
order.SymbolProfile.currency,
|
order.SymbolProfile.currency,
|
||||||
this.request.user.Settings.settings.baseCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.reduce(
|
.reduce(
|
||||||
@ -1262,16 +1288,20 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getSummary({
|
private async getSummary({
|
||||||
impersonationId
|
impersonationId,
|
||||||
|
userCurrency,
|
||||||
|
userId
|
||||||
}: {
|
}: {
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
|
userCurrency: string;
|
||||||
|
userId: string;
|
||||||
}): Promise<PortfolioSummary> {
|
}): Promise<PortfolioSummary> {
|
||||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
|
|
||||||
const performanceInformation = await this.getPerformance({
|
const performanceInformation = await this.getPerformance({
|
||||||
impersonationId
|
impersonationId,
|
||||||
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
|
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
|
||||||
@ -1293,11 +1323,11 @@ export class PortfolioService {
|
|||||||
return account?.isExcluded ?? false;
|
return account?.isExcluded ?? false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const dividend = this.getDividend(orders).toNumber();
|
const dividend = this.getDividend({ orders, userCurrency }).toNumber();
|
||||||
const emergencyFund = new Big(
|
const emergencyFund = new Big(
|
||||||
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
);
|
);
|
||||||
const fees = this.getFees(orders).toNumber();
|
const fees = this.getFees({ orders, userCurrency }).toNumber();
|
||||||
const firstOrderDate = orders[0]?.date;
|
const firstOrderDate = orders[0]?.date;
|
||||||
const items = this.getItems(orders).toNumber();
|
const items = this.getItems(orders).toNumber();
|
||||||
|
|
||||||
@ -1565,4 +1595,12 @@ export class PortfolioService {
|
|||||||
})
|
})
|
||||||
.reduce((previous, current) => previous + current, 0);
|
.reduce((previous, current) => previous + current, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getUserCurrency(aUser: UserWithSettings) {
|
||||||
|
return (
|
||||||
|
aUser.Settings?.settings.baseCurrency ??
|
||||||
|
this.request.user?.Settings?.settings.baseCurrency ??
|
||||||
|
this.baseCurrency
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
22
apps/client-e2e/project.json
Normal file
22
apps/client-e2e/project.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "apps/client-e2e/src",
|
||||||
|
"projectType": "application",
|
||||||
|
"targets": {
|
||||||
|
"e2e": {
|
||||||
|
"executor": "@nrwl/cypress:cypress",
|
||||||
|
"options": {
|
||||||
|
"cypressConfig": "apps/client-e2e/cypress.json",
|
||||||
|
"tsConfig": "apps/client-e2e/tsconfig.e2e.json",
|
||||||
|
"devServerTarget": "client:serve"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "client:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [],
|
||||||
|
"implicitDependencies": ["client"]
|
||||||
|
}
|
201
apps/client/project.json
Normal file
201
apps/client/project.json
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "application",
|
||||||
|
"generators": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sourceRoot": "apps/client/src",
|
||||||
|
"prefix": "gf",
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/apps/client",
|
||||||
|
"index": "apps/client/src/index.html",
|
||||||
|
"main": "apps/client/src/main.ts",
|
||||||
|
"polyfills": "apps/client/src/polyfills.ts",
|
||||||
|
"tsConfig": "apps/client/tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "assetlinks.json",
|
||||||
|
"input": "apps/client/src/assets",
|
||||||
|
"output": "./../.well-known"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "CHANGELOG.md",
|
||||||
|
"input": "",
|
||||||
|
"output": "./../assets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "LICENSE",
|
||||||
|
"input": "",
|
||||||
|
"output": "./../assets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "robots.txt",
|
||||||
|
"input": "apps/client/src/assets",
|
||||||
|
"output": "./../"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "sitemap.xml",
|
||||||
|
"input": "apps/client/src/assets",
|
||||||
|
"output": "./../"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "node_modules/ionicons/dist/ionicons",
|
||||||
|
"output": "./../ionicons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*.js",
|
||||||
|
"input": "node_modules/ionicons/dist/",
|
||||||
|
"output": "./../"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "apps/client/src/assets",
|
||||||
|
"output": "./../assets/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": ["apps/client/src/styles.scss"],
|
||||||
|
"scripts": ["node_modules/marked/marked.min.js"],
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimization": false,
|
||||||
|
"namedChunks": true
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"development-de": {
|
||||||
|
"baseHref": "/de/",
|
||||||
|
"localize": ["de"]
|
||||||
|
},
|
||||||
|
"development-en": {
|
||||||
|
"baseHref": "/en/",
|
||||||
|
"localize": ["en"]
|
||||||
|
},
|
||||||
|
"development-es": {
|
||||||
|
"baseHref": "/es/",
|
||||||
|
"localize": ["es"]
|
||||||
|
},
|
||||||
|
"development-it": {
|
||||||
|
"baseHref": "/it/",
|
||||||
|
"localize": ["it"]
|
||||||
|
},
|
||||||
|
"development-nl": {
|
||||||
|
"baseHref": "/nl/",
|
||||||
|
"localize": ["nl"]
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "apps/client/src/environments/environment.ts",
|
||||||
|
"with": "apps/client/src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"namedChunks": false,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "2mb",
|
||||||
|
"maximumError": "5mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"defaultConfiguration": ""
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"executor": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "client:build",
|
||||||
|
"proxyConfig": "apps/client/proxy.conf.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"development-de": {
|
||||||
|
"browserTarget": "client:build:development-de"
|
||||||
|
},
|
||||||
|
"development-en": {
|
||||||
|
"browserTarget": "client:build:development-en"
|
||||||
|
},
|
||||||
|
"development-es": {
|
||||||
|
"browserTarget": "client:build:development-es"
|
||||||
|
},
|
||||||
|
"development-it": {
|
||||||
|
"browserTarget": "client:build:development-it"
|
||||||
|
},
|
||||||
|
"development-nl": {
|
||||||
|
"browserTarget": "client:build:development-nl"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "client:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"executor": "ng-extract-i18n-merge:ng-extract-i18n-merge",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "client:build",
|
||||||
|
"includeContext": true,
|
||||||
|
"outputPath": "src/locales",
|
||||||
|
"targetFiles": [
|
||||||
|
"messages.de.xlf",
|
||||||
|
"messages.es.xlf",
|
||||||
|
"messages.it.xlf",
|
||||||
|
"messages.nl.xlf"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nrwl/linter:eslint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["apps/client/**/*.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nrwl/jest:jest",
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "apps/client/jest.config.ts",
|
||||||
|
"passWithNoTests": true
|
||||||
|
},
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/apps/client"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"i18n": {
|
||||||
|
"locales": {
|
||||||
|
"de": {
|
||||||
|
"baseHref": "/de/",
|
||||||
|
"translation": "apps/client/src/locales/messages.de.xlf"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"baseHref": "/es/",
|
||||||
|
"translation": "apps/client/src/locales/messages.es.xlf"
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"baseHref": "/it/",
|
||||||
|
"translation": "apps/client/src/locales/messages.it.xlf"
|
||||||
|
},
|
||||||
|
"nl": {
|
||||||
|
"baseHref": "/nl/",
|
||||||
|
"translation": "apps/client/src/locales/messages.nl.xlf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sourceLocale": "en"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
@ -148,8 +148,8 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'portfolio/activities',
|
path: 'portfolio/activities',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/portfolio/transactions/transactions-page.module').then(
|
import('./pages/portfolio/activities/activities-page.module').then(
|
||||||
(m) => m.TransactionsPageModule
|
(m) => m.ActivitiesPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<form class="align-items-center d-flex" [formGroup]="filterForm">
|
<form class="align-items-center d-flex" [formGroup]="filterForm">
|
||||||
<mat-form-field
|
<mat-form-field
|
||||||
appearance="outline"
|
appearance="outline"
|
||||||
class="compact-with-outline flex-grow-1 mr-2 without-hint"
|
class="compact-with-outline without-hint w-100"
|
||||||
>
|
>
|
||||||
<mat-select formControlName="status">
|
<mat-select formControlName="status">
|
||||||
<mat-option></mat-option>
|
<mat-option></mat-option>
|
||||||
@ -15,14 +15,6 @@
|
|||||||
>
|
>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button
|
|
||||||
class="mt-1"
|
|
||||||
color="warn"
|
|
||||||
mat-flat-button
|
|
||||||
(click)="onDeleteJobs()"
|
|
||||||
>
|
|
||||||
<span i18n>Delete Jobs</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
<table class="gf-table w-100">
|
<table class="gf-table w-100">
|
||||||
<thead>
|
<thead>
|
||||||
@ -35,7 +27,21 @@
|
|||||||
<th class="mat-header-cell px-1 py-2" i18n>Created</th>
|
<th class="mat-header-cell px-1 py-2" i18n>Created</th>
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>Finished</th>
|
<th class="mat-header-cell px-1 py-2" i18n>Finished</th>
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>Status</th>
|
<th class="mat-header-cell px-1 py-2" i18n>Status</th>
|
||||||
<th class="mat-header-cell px-1 py-2"></th>
|
<th class="mat-header-cell px-1 py-2">
|
||||||
|
<button
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[matMenuTriggerFor]="jobsActionsMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #jobsActionsMenu="matMenu" xPosition="before">
|
||||||
|
<button mat-menu-item (click)="onDeleteJobs()">
|
||||||
|
<ng-container i18n>Delete Jobs</ng-container>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -102,12 +108,12 @@
|
|||||||
<button
|
<button
|
||||||
class="mx-1 no-min-width px-2"
|
class="mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
[matMenuTriggerFor]="accountMenu"
|
[matMenuTriggerFor]="jobActionsMenu"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
<mat-menu #jobActionsMenu="matMenu" xPosition="before">
|
||||||
<button mat-menu-item (click)="onViewData(job.data)">
|
<button mat-menu-item (click)="onViewData(job.data)">
|
||||||
<ng-container i18n>View Data</ng-container>
|
<ng-container i18n>View Data</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
@ -150,6 +150,35 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
.subscribe(() => {});
|
.subscribe(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onGather7Days() {
|
||||||
|
this.adminService
|
||||||
|
.gather7Days()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onGatherMax() {
|
||||||
|
this.adminService
|
||||||
|
.gatherMax()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onGatherProfileData() {
|
||||||
|
this.adminService
|
||||||
|
.gatherProfileData()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
public onGatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
|
public onGatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
|
||||||
this.adminService
|
this.adminService
|
||||||
.gatherProfileDataBySymbol({ dataSource, symbol })
|
.gatherProfileDataBySymbol({ dataSource, symbol })
|
||||||
|
@ -13,10 +13,10 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<table
|
<table
|
||||||
class="gf-table w-100"
|
class="gf-table w-100"
|
||||||
|
mat-table
|
||||||
matSort
|
matSort
|
||||||
matSortActive="symbol"
|
matSortActive="symbol"
|
||||||
matSortDirection="asc"
|
matSortDirection="asc"
|
||||||
mat-table
|
|
||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
>
|
>
|
||||||
<ng-container matColumnDef="symbol">
|
<ng-container matColumnDef="symbol">
|
||||||
@ -101,17 +101,37 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
|
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
||||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
|
||||||
<button
|
<button
|
||||||
class="mx-1 no-min-width px-2"
|
class="mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
[matMenuTriggerFor]="accountMenu"
|
[matMenuTriggerFor]="assetProfilesActionsMenu"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
<mat-menu #assetProfilesActionsMenu="matMenu" xPosition="before">
|
||||||
|
<button mat-menu-item (click)="onGather7Days()">
|
||||||
|
<ng-container i18n>Gather Recent Data</ng-container>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="onGatherMax()">
|
||||||
|
<ng-container i18n>Gather All Data</ng-container>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="onGatherProfileData()">
|
||||||
|
<ng-container i18n>Gather Profile Data</ng-container>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
|
<button
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[matMenuTriggerFor]="assetProfileActionsMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #assetProfileActionsMenu="matMenu" xPosition="before">
|
||||||
<button
|
<button
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
(click)="onGatherSymbol({dataSource: element.dataSource, symbol: element.symbol})"
|
(click)="onGatherSymbol({dataSource: element.dataSource, symbol: element.symbol})"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
|
||||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
@ -43,7 +42,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private adminService: AdminService,
|
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
@ -162,35 +160,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onGather7Days() {
|
|
||||||
this.adminService
|
|
||||||
.gather7Days()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public onGatherMax() {
|
|
||||||
this.adminService
|
|
||||||
.gatherMax()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public onGatherProfileData() {
|
|
||||||
this.adminService
|
|
||||||
.gatherProfileData()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
public onReadOnlyModeChange(aEvent: MatSlideToggleChange) {
|
public onReadOnlyModeChange(aEvent: MatSlideToggleChange) {
|
||||||
this.putAdminSetting({
|
this.putAdminSetting({
|
||||||
key: PROPERTY_IS_READ_ONLY_MODE,
|
key: PROPERTY_IS_READ_ONLY_MODE,
|
||||||
|
@ -27,53 +27,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex my-3">
|
|
||||||
<div class="w-50" i18n>Data Management</div>
|
|
||||||
<div class="w-50">
|
|
||||||
<div class="overflow-hidden">
|
|
||||||
<div class="mb-2">
|
|
||||||
<button
|
|
||||||
color="accent"
|
|
||||||
mat-flat-button
|
|
||||||
(click)="onGather7Days()"
|
|
||||||
>
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1"
|
|
||||||
name="cloud-download-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span i18n>Gather Recent Data</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<button
|
|
||||||
color="accent"
|
|
||||||
mat-flat-button
|
|
||||||
(click)="onGatherMax()"
|
|
||||||
>
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1"
|
|
||||||
name="cloud-download-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span i18n>Gather All Data</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="mb-2 mr-2"
|
|
||||||
color="accent"
|
|
||||||
mat-flat-button
|
|
||||||
(click)="onGatherProfileData()"
|
|
||||||
>
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1"
|
|
||||||
name="cloud-download-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span i18n>Gather Profile Data</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="align-items-start d-flex my-3">
|
<div class="align-items-start d-flex my-3">
|
||||||
<div class="w-50" i18n>Exchange Rates</div>
|
<div class="w-50" i18n>Exchange Rates</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
<mat-label i18n>Compare with...</mat-label>
|
<mat-label i18n>Compare with...</mat-label>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="benchmark"
|
name="benchmark"
|
||||||
|
[disabled]="user?.subscription?.type === 'Basic'"
|
||||||
[value]="benchmark"
|
[value]="benchmark"
|
||||||
(selectionChange)="onChangeBenchmark($event.value)"
|
(selectionChange)="onChangeBenchmark($event.value)"
|
||||||
>
|
>
|
||||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
|
import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
|
||||||
@ -12,6 +13,7 @@ import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
GfPremiumIndicatorModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
NgxSkeletonLoaderModule,
|
NgxSkeletonLoaderModule,
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
|
@ -38,6 +38,7 @@ import {
|
|||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
import { addDays, format, isAfter, parseISO, subDays } from 'date-fns';
|
import { addDays, format, isAfter, parseISO, subDays } from 'date-fns';
|
||||||
|
import { last } from 'lodash';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-investment-chart',
|
selector: 'gf-investment-chart',
|
||||||
@ -53,6 +54,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
@Input() groupBy: GroupBy;
|
@Input() groupBy: GroupBy;
|
||||||
@Input() historicalDataItems: LineChartItem[] = [];
|
@Input() historicalDataItems: LineChartItem[] = [];
|
||||||
@Input() isInPercent = false;
|
@Input() isInPercent = false;
|
||||||
|
@Input() isLoading = false;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() range: DateRange = 'max';
|
@Input() range: DateRange = 'max';
|
||||||
@Input() savingsRate = 0;
|
@Input() savingsRate = 0;
|
||||||
@ -60,9 +62,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
@ViewChild('chartCanvas') chartCanvas;
|
@ViewChild('chartCanvas') chartCanvas;
|
||||||
|
|
||||||
public chart: Chart<any>;
|
public chart: Chart<any>;
|
||||||
public isLoading = true;
|
private investments: InvestmentItem[];
|
||||||
|
private values: LineChartItem[];
|
||||||
private data: InvestmentItem[];
|
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
Chart.register(
|
Chart.register(
|
||||||
@ -92,34 +93,49 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private initialize() {
|
private initialize() {
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
// Create a clone
|
// Create a clone
|
||||||
this.data = this.benchmarkDataItems.map((item) => Object.assign({}, item));
|
this.investments = this.benchmarkDataItems.map((item) =>
|
||||||
|
Object.assign({}, item)
|
||||||
|
);
|
||||||
|
this.values = this.historicalDataItems.map((item) =>
|
||||||
|
Object.assign({}, item)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.groupBy && this.investments?.length > 0) {
|
||||||
|
let date: string;
|
||||||
|
|
||||||
if (!this.groupBy && this.data?.length > 0) {
|
|
||||||
if (this.range === 'max') {
|
if (this.range === 'max') {
|
||||||
// Extend chart by 5% of days in market (before)
|
// Extend chart by 5% of days in market (before)
|
||||||
const firstItem = this.data[0];
|
date = format(
|
||||||
this.data.unshift({
|
subDays(
|
||||||
...firstItem,
|
parseISO(this.investments[0].date),
|
||||||
date: format(
|
this.daysInMarket * 0.05 || 90
|
||||||
subDays(parseISO(firstItem.date), this.daysInMarket * 0.05 || 90),
|
|
||||||
DATE_FORMAT
|
|
||||||
),
|
),
|
||||||
|
DATE_FORMAT
|
||||||
|
);
|
||||||
|
this.investments.unshift({
|
||||||
|
date,
|
||||||
investment: 0
|
investment: 0
|
||||||
});
|
});
|
||||||
|
this.values.unshift({
|
||||||
|
date,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extend chart by 5% of days in market (after)
|
// Extend chart by 5% of days in market (after)
|
||||||
const lastItem = this.data[this.data.length - 1];
|
date = format(
|
||||||
this.data.push({
|
addDays(
|
||||||
...lastItem,
|
parseDate(last(this.investments).date),
|
||||||
date: format(
|
this.daysInMarket * 0.05 || 90
|
||||||
addDays(parseDate(lastItem.date), this.daysInMarket * 0.05 || 90),
|
),
|
||||||
DATE_FORMAT
|
DATE_FORMAT
|
||||||
)
|
);
|
||||||
|
this.investments.push({
|
||||||
|
date,
|
||||||
|
investment: last(this.investments).investment
|
||||||
});
|
});
|
||||||
|
this.values.push({ date, value: last(this.values).value });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
@ -131,7 +147,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
||||||
borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
||||||
borderWidth: this.groupBy ? 0 : 1,
|
borderWidth: this.groupBy ? 0 : 1,
|
||||||
data: this.data.map(({ date, investment }) => {
|
data: this.investments.map(({ date, investment }) => {
|
||||||
return {
|
return {
|
||||||
x: parseDate(date),
|
x: parseDate(date),
|
||||||
y: this.isInPercent ? investment * 100 : investment
|
y: this.isInPercent ? investment * 100 : investment
|
||||||
@ -151,7 +167,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
{
|
{
|
||||||
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
data: this.historicalDataItems.map(({ date, value }) => {
|
data: this.values.map(({ date, value }) => {
|
||||||
return {
|
return {
|
||||||
x: parseDate(date),
|
x: parseDate(date),
|
||||||
y: this.isInPercent ? value * 100 : value
|
y: this.isInPercent ? value * 100 : value
|
||||||
@ -159,7 +175,15 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
fill: false,
|
fill: false,
|
||||||
label: $localize`Total Amount`,
|
label: $localize`Total Amount`,
|
||||||
pointRadius: 0
|
pointRadius: 0,
|
||||||
|
segment: {
|
||||||
|
borderColor: (context: unknown) =>
|
||||||
|
this.isInFuture(
|
||||||
|
context,
|
||||||
|
`rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.67)`
|
||||||
|
),
|
||||||
|
borderDash: (context: unknown) => this.isInFuture(context, [2, 2])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@ -273,8 +297,6 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTooltipPluginConfiguration() {
|
private getTooltipPluginConfiguration() {
|
||||||
|
@ -22,9 +22,6 @@
|
|||||||
<div class="row px-3 py-1">
|
<div class="row px-3 py-1">
|
||||||
<div class="d-flex flex-grow-1" i18n>Sell</div>
|
<div class="d-flex flex-grow-1" i18n>Sell</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<span *ngIf="summary?.totalSell || summary?.totalSell === 0" class="mr-1"
|
|
||||||
>-</span
|
|
||||||
>
|
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
[currency]="baseCurrency"
|
[currency]="baseCurrency"
|
||||||
|
@ -86,7 +86,7 @@ export class AuthGuard implements CanActivate {
|
|||||||
}, 300);
|
}, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve(false);
|
resolve(true);
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
state.url.startsWith('/home') &&
|
state.url.startsWith('/home') &&
|
||||||
|
@ -96,6 +96,20 @@
|
|||||||
title="Ghostfolio is an independent & bootstrapped business"
|
title="Ghostfolio is an independent & bootstrapped business"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
*ngIf="!hasPermissionForSubscription"
|
||||||
|
class="d-flex justify-content-center"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://www.buymeacoffee.com/ghostfolio"
|
||||||
|
target="_blank"
|
||||||
|
title="Support Ghostfolio"
|
||||||
|
><img
|
||||||
|
class="mb-2"
|
||||||
|
src="../assets/images/button-buy-me-a-coffee.png"
|
||||||
|
width="180"
|
||||||
|
/></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -177,7 +191,7 @@
|
|||||||
<a
|
<a
|
||||||
class="py-2 w-100"
|
class="py-2 w-100"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-stroked-button
|
mat-flat-button
|
||||||
[routerLink]="['/faq']"
|
[routerLink]="['/faq']"
|
||||||
>FAQ</a
|
>FAQ</a
|
||||||
>
|
>
|
||||||
@ -189,7 +203,7 @@
|
|||||||
<a
|
<a
|
||||||
class="py-2 w-100"
|
class="py-2 w-100"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-stroked-button
|
mat-flat-button
|
||||||
[routerLink]="['/about', 'changelog']"
|
[routerLink]="['/about', 'changelog']"
|
||||||
>Changelog & License</a
|
>Changelog & License</a
|
||||||
>
|
>
|
||||||
@ -198,7 +212,7 @@
|
|||||||
<a
|
<a
|
||||||
class="py-2 w-100"
|
class="py-2 w-100"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-stroked-button
|
mat-flat-button
|
||||||
[routerLink]="['/about', 'privacy-policy']"
|
[routerLink]="['/about', 'privacy-policy']"
|
||||||
>Privacy Policy</a
|
>Privacy Policy</a
|
||||||
>
|
>
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
Manage your wealth like a boss
|
Manage your wealth like a boss
|
||||||
</h1>
|
</h1>
|
||||||
<p class="lead mb-4">
|
<p class="lead mb-4">
|
||||||
Ghostfolio is a privacy-first, open source dashboard to manage your
|
Ghostfolio is a privacy-first, open source dashboard for your personal
|
||||||
personal finances. Break down your asset allocation, know your net worth
|
finances. Break down your asset allocation, know your net worth and make
|
||||||
and make solid, data-driven investment decisions.
|
solid, data-driven investment decisions.
|
||||||
</p>
|
</p>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<a
|
<a
|
||||||
|
@ -114,9 +114,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.outro-inner-container {
|
.outro-inner-container {
|
||||||
div {
|
display: none;
|
||||||
background-image: url('/assets/intro-dark.jpg') !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.video {
|
.video {
|
||||||
|
@ -2,12 +2,12 @@ import { NgModule } from '@angular/core';
|
|||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
import { TransactionsPageComponent } from './transactions-page.component';
|
import { ActivitiesPageComponent } from './activities-page.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
component: TransactionsPageComponent,
|
component: ActivitiesPageComponent,
|
||||||
path: '',
|
path: '',
|
||||||
title: $localize`Activities`
|
title: $localize`Activities`
|
||||||
}
|
}
|
||||||
@ -17,4 +17,4 @@ const routes: Routes = [
|
|||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule]
|
||||||
})
|
})
|
||||||
export class TransactionsPageRoutingModule {}
|
export class ActivitiesPageRoutingModule {}
|
@ -10,35 +10,35 @@ import { PositionDetailDialog } from '@ghostfolio/client/components/position/pos
|
|||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { isArray } from 'lodash';
|
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog/create-or-update-transaction-dialog.component';
|
import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog/create-or-update-activity-dialog.component';
|
||||||
import { ImportTransactionDialog } from './import-transaction-dialog/import-transaction-dialog.component';
|
import { ImportActivitiesDialog } from './import-activities-dialog/import-activities-dialog.component';
|
||||||
|
import { ImportActivitiesDialogParams } from './import-activities-dialog/interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-transactions-page',
|
selector: 'gf-activities-page',
|
||||||
styleUrls: ['./transactions-page.scss'],
|
styleUrls: ['./activities-page.scss'],
|
||||||
templateUrl: './transactions-page.html'
|
templateUrl: './activities-page.html'
|
||||||
})
|
})
|
||||||
export class TransactionsPageComponent implements OnDestroy, OnInit {
|
export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||||
public activities: Activity[];
|
public activities: Activity[];
|
||||||
public defaultAccountId: string;
|
public defaultAccountId: string;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateActivity: boolean;
|
||||||
public hasPermissionToDeleteOrder: boolean;
|
public hasPermissionToDeleteActivity: boolean;
|
||||||
public hasPermissionToImportOrders: boolean;
|
public hasPermissionToImportActivities: boolean;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
@ -51,24 +51,22 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
private icsService: IcsService,
|
private icsService: IcsService,
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private importTransactionsService: ImportTransactionsService,
|
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private snackBar: MatSnackBar,
|
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
this.routeQueryParams = route.queryParams
|
this.routeQueryParams = route.queryParams
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
if (params['createDialog']) {
|
if (params['createDialog']) {
|
||||||
this.openCreateTransactionDialog();
|
this.openCreateActivityDialog();
|
||||||
} else if (params['editDialog']) {
|
} else if (params['editDialog']) {
|
||||||
if (this.activities) {
|
if (this.activities) {
|
||||||
const transaction = this.activities.find(({ id }) => {
|
const activity = this.activities.find(({ id }) => {
|
||||||
return id === params['transactionId'];
|
return id === params['activityId'];
|
||||||
});
|
});
|
||||||
|
|
||||||
this.openUpdateTransactionDialog(transaction);
|
this.openUpdateActivityDialog(activity);
|
||||||
} else {
|
} else {
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
}
|
}
|
||||||
@ -96,7 +94,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
.subscribe((aId) => {
|
.subscribe((aId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
|
|
||||||
this.hasPermissionToImportOrders =
|
this.hasPermissionToImportActivities =
|
||||||
hasPermission(globalPermissions, permissions.enableImport) &&
|
hasPermission(globalPermissions, permissions.enableImport) &&
|
||||||
!this.hasImpersonationId;
|
!this.hasImpersonationId;
|
||||||
});
|
});
|
||||||
@ -121,7 +119,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
.subscribe(({ activities }) => {
|
.subscribe(({ activities }) => {
|
||||||
this.activities = activities;
|
this.activities = activities;
|
||||||
|
|
||||||
if (this.hasPermissionToCreateOrder && this.activities?.length <= 0) {
|
if (
|
||||||
|
this.hasPermissionToCreateActivity &&
|
||||||
|
this.activities?.length <= 0
|
||||||
|
) {
|
||||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,11 +130,11 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCloneTransaction(aActivity: Activity) {
|
public onCloneActivity(aActivity: Activity) {
|
||||||
this.openCreateTransactionDialog(aActivity);
|
this.openCreateActivityDialog(aActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteTransaction(aId: string) {
|
public onDeleteActivity(aId: string) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.deleteOrder(aId)
|
.deleteOrder(aId)
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
@ -183,98 +184,30 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onImport() {
|
public onImport() {
|
||||||
const input = document.createElement('input');
|
const dialogRef = this.dialog.open(ImportActivitiesDialog, {
|
||||||
input.accept = 'application/JSON, .csv';
|
data: <ImportActivitiesDialogParams>{
|
||||||
input.type = 'file';
|
deviceType: this.deviceType,
|
||||||
|
user: this.user
|
||||||
input.onchange = (event) => {
|
},
|
||||||
this.snackBar.open('⏳' + $localize`Importing data...`);
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
|
||||||
// Getting the file reference
|
|
||||||
const file = (event.target as HTMLInputElement).files[0];
|
|
||||||
|
|
||||||
// Setting up the reader
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsText(file, 'UTF-8');
|
|
||||||
|
|
||||||
reader.onload = async (readerEvent) => {
|
|
||||||
const fileContent = readerEvent.target.result as string;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (file.name.endsWith('.json')) {
|
|
||||||
const content = JSON.parse(fileContent);
|
|
||||||
|
|
||||||
if (!isArray(content.activities)) {
|
|
||||||
if (isArray(content.orders)) {
|
|
||||||
this.handleImportError({
|
|
||||||
activities: [],
|
|
||||||
error: {
|
|
||||||
error: {
|
|
||||||
message: [`orders needs to be renamed to activities`]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.importTransactionsService.importJson({
|
|
||||||
content: content.activities
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.handleImportSuccess();
|
dialogRef
|
||||||
} catch (error) {
|
.afterClosed()
|
||||||
console.error(error);
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
this.handleImportError({ error, activities: content.activities });
|
.subscribe(() => {
|
||||||
}
|
this.fetchActivities();
|
||||||
|
|
||||||
return;
|
|
||||||
} else if (file.name.endsWith('.csv')) {
|
|
||||||
try {
|
|
||||||
await this.importTransactionsService.importCsv({
|
|
||||||
fileContent,
|
|
||||||
userAccounts: this.user.accounts
|
|
||||||
});
|
|
||||||
|
|
||||||
this.handleImportSuccess();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
this.handleImportError({
|
|
||||||
activities: error?.activities ?? [],
|
|
||||||
error: {
|
|
||||||
error: { message: error?.error?.message ?? [error?.message] }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
public onUpdateActivity(aActivity: OrderModel) {
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
this.handleImportError({
|
|
||||||
activities: [],
|
|
||||||
error: { error: { message: ['Unexpected format'] } }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
public onUpdateTransaction(aTransaction: OrderModel) {
|
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { editDialog: true, transactionId: aTransaction.id }
|
queryParams: { activityId: aActivity.id, editDialog: true }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public openUpdateTransactionDialog(activity: Activity): void {
|
public openUpdateActivityDialog(activity: Activity): void {
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateActivityDialog, {
|
||||||
data: {
|
data: {
|
||||||
activity,
|
activity,
|
||||||
accounts: this.user?.accounts?.filter((account) => {
|
accounts: this.user?.accounts?.filter((account) => {
|
||||||
@ -312,41 +245,14 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleImportError({
|
private openCreateActivityDialog(aActivity?: Activity): void {
|
||||||
activities,
|
|
||||||
error
|
|
||||||
}: {
|
|
||||||
activities: any[];
|
|
||||||
error: any;
|
|
||||||
}) {
|
|
||||||
this.snackBar.dismiss();
|
|
||||||
|
|
||||||
this.dialog.open(ImportTransactionDialog, {
|
|
||||||
data: {
|
|
||||||
activities,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
messages: error?.error?.message
|
|
||||||
},
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleImportSuccess() {
|
|
||||||
this.fetchActivities();
|
|
||||||
|
|
||||||
this.snackBar.open('✅' + $localize`Import has been completed`, undefined, {
|
|
||||||
duration: 3000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private openCreateTransactionDialog(aActivity?: Activity): void {
|
|
||||||
this.userService
|
this.userService
|
||||||
.get()
|
.get()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((user) => {
|
.subscribe((user) => {
|
||||||
this.updateUser(user);
|
this.updateUser(user);
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateActivityDialog, {
|
||||||
data: {
|
data: {
|
||||||
accounts: this.user?.accounts?.filter((account) => {
|
accounts: this.user?.accounts?.filter((account) => {
|
||||||
return account.accountType === 'SECURITIES';
|
return account.accountType === 'SECURITIES';
|
||||||
@ -434,11 +340,11 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
return account.isDefault;
|
return account.isDefault;
|
||||||
})?.id;
|
})?.id;
|
||||||
|
|
||||||
this.hasPermissionToCreateOrder = hasPermission(
|
this.hasPermissionToCreateActivity = hasPermission(
|
||||||
this.user.permissions,
|
this.user.permissions,
|
||||||
permissions.createOrder
|
permissions.createOrder
|
||||||
);
|
);
|
||||||
this.hasPermissionToDeleteOrder = hasPermission(
|
this.hasPermissionToDeleteActivity = hasPermission(
|
||||||
this.user.permissions,
|
this.user.permissions,
|
||||||
permissions.deleteOrder
|
permissions.deleteOrder
|
||||||
);
|
);
|
@ -6,14 +6,14 @@
|
|||||||
[activities]="activities"
|
[activities]="activities"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
|
||||||
[hasPermissionToExportActivities]="!hasImpersonationId"
|
[hasPermissionToExportActivities]="!hasImpersonationId"
|
||||||
[hasPermissionToImportActivities]="hasPermissionToImportOrders"
|
[hasPermissionToImportActivities]="hasPermissionToImportActivities"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView"
|
[showActions]="!hasImpersonationId && hasPermissionToDeleteActivity && !user.settings.isRestrictedView"
|
||||||
(activityDeleted)="onDeleteTransaction($event)"
|
(activityDeleted)="onDeleteActivity($event)"
|
||||||
(activityToClone)="onCloneTransaction($event)"
|
(activityToClone)="onCloneActivity($event)"
|
||||||
(activityToUpdate)="onUpdateTransaction($event)"
|
(activityToUpdate)="onUpdateActivity($event)"
|
||||||
(export)="onExport($event)"
|
(export)="onExport($event)"
|
||||||
(exportDrafts)="onExportDrafts($event)"
|
(exportDrafts)="onExportDrafts($event)"
|
||||||
(import)="onImport()"
|
(import)="onImport()"
|
||||||
@ -22,15 +22,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
*ngIf="!hasImpersonationId && hasPermissionToCreateOrder && !user.settings.isRestrictedView"
|
*ngIf="!hasImpersonationId && hasPermissionToCreateActivity && !user.settings.isRestrictedView"
|
||||||
class="fab-container"
|
class="fab-container"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="align-items-center d-flex justify-content-center"
|
class="align-items-center d-flex justify-content-center"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-fab
|
mat-fab
|
||||||
[routerLink]="[]"
|
|
||||||
[queryParams]="{ createDialog: true }"
|
[queryParams]="{ createDialog: true }"
|
||||||
|
[routerLink]="[]"
|
||||||
>
|
>
|
||||||
<ion-icon name="add-outline" size="large"></ion-icon>
|
<ion-icon name="add-outline" size="large"></ion-icon>
|
||||||
</a>
|
</a>
|
@ -0,0 +1,29 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
||||||
|
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||||
|
|
||||||
|
import { ActivitiesPageRoutingModule } from './activities-page-routing.module';
|
||||||
|
import { ActivitiesPageComponent } from './activities-page.component';
|
||||||
|
import { GfCreateOrUpdateActivityDialogModule } from './create-or-update-activity-dialog/create-or-update-activity-dialog.module';
|
||||||
|
import { GfImportActivitiesDialogModule } from './import-activities-dialog/import-activities-dialog.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [ActivitiesPageComponent],
|
||||||
|
imports: [
|
||||||
|
ActivitiesPageRoutingModule,
|
||||||
|
CommonModule,
|
||||||
|
GfActivitiesTableModule,
|
||||||
|
GfCreateOrUpdateActivityDialogModule,
|
||||||
|
GfImportActivitiesDialogModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatSnackBarModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
|
providers: [ImportActivitiesService],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class ActivitiesPageModule {}
|
@ -14,6 +14,7 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
|||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { AssetClass, AssetSubClass, Type } from '@prisma/client';
|
import { AssetClass, AssetSubClass, Type } from '@prisma/client';
|
||||||
import { isUUID } from 'class-validator';
|
import { isUUID } from 'class-validator';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
@ -27,21 +28,25 @@ import {
|
|||||||
takeUntil
|
takeUntil
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
|
import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'h-100' },
|
host: { class: 'h-100' },
|
||||||
selector: 'gf-create-or-update-transaction-dialog',
|
selector: 'gf-create-or-update-activity-dialog',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
styleUrls: ['./create-or-update-transaction-dialog.scss'],
|
styleUrls: ['./create-or-update-activity-dialog.scss'],
|
||||||
templateUrl: 'create-or-update-transaction-dialog.html'
|
templateUrl: 'create-or-update-activity-dialog.html'
|
||||||
})
|
})
|
||||||
export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||||
@ViewChild('autocomplete') autocomplete;
|
@ViewChild('autocomplete') autocomplete;
|
||||||
|
|
||||||
public activityForm: FormGroup;
|
public activityForm: FormGroup;
|
||||||
public assetClasses = Object.keys(AssetClass);
|
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
|
||||||
public assetSubClasses = Object.keys(AssetSubClass);
|
return { id: assetClass, label: translate(assetClass) };
|
||||||
|
});
|
||||||
|
public assetSubClasses = Object.keys(AssetSubClass).map((assetSubClass) => {
|
||||||
|
return { id: assetSubClass, label: translate(assetSubClass) };
|
||||||
|
});
|
||||||
public currencies: string[] = [];
|
public currencies: string[] = [];
|
||||||
public currentMarketPrice = null;
|
public currentMarketPrice = null;
|
||||||
public filteredLookupItems: LookupItem[];
|
public filteredLookupItems: LookupItem[];
|
||||||
@ -55,10 +60,10 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTransactionDialogParams,
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateActivityDialogParams,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private dateAdapter: DateAdapter<any>,
|
private dateAdapter: DateAdapter<any>,
|
||||||
public dialogRef: MatDialogRef<CreateOrUpdateTransactionDialog>,
|
public dialogRef: MatDialogRef<CreateOrUpdateActivityDialog>,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
@Inject(MAT_DATE_LOCALE) private locale: string
|
@Inject(MAT_DATE_LOCALE) private locale: string
|
||||||
) {}
|
) {}
|
@ -4,17 +4,17 @@
|
|||||||
(keyup.enter)="activityForm.valid && onSubmit()"
|
(keyup.enter)="activityForm.valid && onSubmit()"
|
||||||
(ngSubmit)="onSubmit()"
|
(ngSubmit)="onSubmit()"
|
||||||
>
|
>
|
||||||
<h1 *ngIf="data.activity.id" mat-dialog-title i18n>Update activity</h1>
|
<h1 *ngIf="data.activity.id" i18n mat-dialog-title>Update activity</h1>
|
||||||
<h1 *ngIf="!data.activity.id" mat-dialog-title i18n>Add activity</h1>
|
<h1 *ngIf="!data.activity.id" i18n mat-dialog-title>Add activity</h1>
|
||||||
<div class="flex-grow-1" mat-dialog-content>
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Type</mat-label>
|
<mat-label i18n>Type</mat-label>
|
||||||
<mat-select formControlName="type">
|
<mat-select formControlName="type">
|
||||||
<mat-option value="BUY" i18n>BUY</mat-option>
|
<mat-option i18n value="BUY">BUY</mat-option>
|
||||||
<mat-option value="DIVIDEND" i18n>DIVIDEND</mat-option>
|
<mat-option i18n value="DIVIDEND">DIVIDEND</mat-option>
|
||||||
<mat-option value="ITEM" i18n>ITEM</mat-option>
|
<mat-option i18n value="ITEM">ITEM</mat-option>
|
||||||
<mat-option value="SELL" i18n>SELL</mat-option>
|
<mat-option i18n value="SELL">SELL</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@ -156,8 +156,8 @@
|
|||||||
<mat-option [value]="null"></mat-option>
|
<mat-option [value]="null"></mat-option>
|
||||||
<mat-option
|
<mat-option
|
||||||
*ngFor="let assetClass of assetClasses"
|
*ngFor="let assetClass of assetClasses"
|
||||||
[value]="assetClass"
|
[value]="assetClass.id"
|
||||||
>{{ assetClass }}</mat-option
|
>{{ assetClass.label }}</mat-option
|
||||||
>
|
>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@ -171,8 +171,8 @@
|
|||||||
<mat-option [value]="null"></mat-option>
|
<mat-option [value]="null"></mat-option>
|
||||||
<mat-option
|
<mat-option
|
||||||
*ngFor="let assetSubClass of assetSubClasses"
|
*ngFor="let assetSubClass of assetSubClasses"
|
||||||
[value]="assetSubClass"
|
[value]="assetSubClass.id"
|
||||||
>{{ assetSubClass }}</mat-option
|
>{{ assetSubClass.label }}</mat-option
|
||||||
>
|
>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
@ -13,10 +13,10 @@ import { MatSelectModule } from '@angular/material/select';
|
|||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component';
|
import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [CreateOrUpdateTransactionDialog],
|
declarations: [CreateOrUpdateActivityDialog],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfSymbolModule,
|
GfSymbolModule,
|
||||||
@ -35,4 +35,4 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
|
|||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfCreateOrUpdateTransactionDialogModule {}
|
export class GfCreateOrUpdateActivityDialogModule {}
|
@ -2,7 +2,7 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf
|
|||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
import { Account } from '@prisma/client';
|
import { Account } from '@prisma/client';
|
||||||
|
|
||||||
export interface CreateOrUpdateTransactionDialogParams {
|
export interface CreateOrUpdateActivityDialogParams {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
accounts: Account[];
|
accounts: Account[];
|
||||||
activity: Activity;
|
activity: Activity;
|
@ -0,0 +1,176 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Inject,
|
||||||
|
OnDestroy
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
||||||
|
import { isArray } from 'lodash';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
import { ImportActivitiesDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
selector: 'gf-import-activities-dialog',
|
||||||
|
styleUrls: ['./import-activities-dialog.scss'],
|
||||||
|
templateUrl: 'import-activities-dialog.html'
|
||||||
|
})
|
||||||
|
export class ImportActivitiesDialog implements OnDestroy {
|
||||||
|
public details: any[] = [];
|
||||||
|
public errorMessages: string[] = [];
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams,
|
||||||
|
public dialogRef: MatDialogRef<ImportActivitiesDialog>,
|
||||||
|
private importActivitiesService: ImportActivitiesService,
|
||||||
|
private snackBar: MatSnackBar
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public ngOnInit() {}
|
||||||
|
|
||||||
|
public onCancel(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onImport() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.accept = 'application/JSON, .csv';
|
||||||
|
input.type = 'file';
|
||||||
|
|
||||||
|
input.onchange = (event) => {
|
||||||
|
this.snackBar.open('⏳ ' + $localize`Importing data...`);
|
||||||
|
|
||||||
|
// Getting the file reference
|
||||||
|
const file = (event.target as HTMLInputElement).files[0];
|
||||||
|
|
||||||
|
// Setting up the reader
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsText(file, 'UTF-8');
|
||||||
|
|
||||||
|
reader.onload = async (readerEvent) => {
|
||||||
|
const fileContent = readerEvent.target.result as string;
|
||||||
|
|
||||||
|
console.log(fileContent);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (file.name.endsWith('.json')) {
|
||||||
|
const content = JSON.parse(fileContent);
|
||||||
|
|
||||||
|
if (!isArray(content.activities)) {
|
||||||
|
if (isArray(content.orders)) {
|
||||||
|
this.handleImportError({
|
||||||
|
activities: [],
|
||||||
|
error: {
|
||||||
|
error: {
|
||||||
|
message: [`orders needs to be renamed to activities`]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.importActivitiesService.importJson({
|
||||||
|
content: content.activities
|
||||||
|
});
|
||||||
|
|
||||||
|
this.handleImportSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
this.handleImportError({ error, activities: content.activities });
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else if (file.name.endsWith('.csv')) {
|
||||||
|
try {
|
||||||
|
await this.importActivitiesService.importCsv({
|
||||||
|
fileContent,
|
||||||
|
userAccounts: this.data.user.accounts
|
||||||
|
});
|
||||||
|
|
||||||
|
this.handleImportSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
this.handleImportError({
|
||||||
|
activities: error?.activities ?? [],
|
||||||
|
error: {
|
||||||
|
error: { message: error?.error?.message ?? [error?.message] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
this.handleImportError({
|
||||||
|
activities: [],
|
||||||
|
error: { error: { message: ['Unexpected format'] } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onReset() {
|
||||||
|
this.details = [];
|
||||||
|
this.errorMessages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleImportError({
|
||||||
|
activities,
|
||||||
|
error
|
||||||
|
}: {
|
||||||
|
activities: any[];
|
||||||
|
error: any;
|
||||||
|
}) {
|
||||||
|
this.snackBar.dismiss();
|
||||||
|
|
||||||
|
this.errorMessages = error?.error?.message;
|
||||||
|
|
||||||
|
for (const message of this.errorMessages) {
|
||||||
|
if (message.includes('activities.')) {
|
||||||
|
let [index] = message.split(' ');
|
||||||
|
index = index.replace('activities.', '');
|
||||||
|
[index] = index.split('.');
|
||||||
|
|
||||||
|
this.details.push(activities[index]);
|
||||||
|
} else {
|
||||||
|
this.details.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleImportSuccess() {
|
||||||
|
this.snackBar.open(
|
||||||
|
'✅ ' + $localize`Import has been completed`,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
duration: 3000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
<gf-dialog-header
|
||||||
|
mat-dialog-title
|
||||||
|
[deviceType]="data.deviceType"
|
||||||
|
[title]="errorMessages.length === 0 ? 'Import Activities' : 'Import Activities Error'"
|
||||||
|
(closeButtonClicked)="onCancel()"
|
||||||
|
></gf-dialog-header>
|
||||||
|
|
||||||
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
|
<ng-container *ngIf="errorMessages.length === 0">
|
||||||
|
<div class="d-flex justify-content-center flex-column">
|
||||||
|
<button
|
||||||
|
class="py-3"
|
||||||
|
color="primary"
|
||||||
|
mat-stroked-button
|
||||||
|
(click)="onImport()"
|
||||||
|
>
|
||||||
|
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||||
|
<span i18n>Choose File</span>
|
||||||
|
</button>
|
||||||
|
<p class="mb-0 mt-4 text-center">
|
||||||
|
<span class="mr-1" i18n>The following file formats are supported:</span>
|
||||||
|
<a
|
||||||
|
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||||
|
target="_blank"
|
||||||
|
>CSV</a
|
||||||
|
>
|
||||||
|
<span class="mx-1" i18n>or</span>
|
||||||
|
<a
|
||||||
|
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||||
|
target="_blank"
|
||||||
|
>JSON</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="errorMessages.length > 0">
|
||||||
|
<mat-accordion displayMode="flat">
|
||||||
|
<mat-expansion-panel
|
||||||
|
*ngFor="let message of errorMessages; let i = index"
|
||||||
|
[disabled]="!details[i]"
|
||||||
|
>
|
||||||
|
<mat-expansion-panel-header class="pl-1">
|
||||||
|
<mat-panel-title>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="align-items-center d-flex mr-2">
|
||||||
|
<ion-icon name="warning-outline"></ion-icon>
|
||||||
|
</div>
|
||||||
|
<div>{{ message }}</div>
|
||||||
|
</div>
|
||||||
|
</mat-panel-title>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
<pre
|
||||||
|
*ngIf="details[i]"
|
||||||
|
class="m-0"
|
||||||
|
><code>{{ details[i] | json }}</code></pre>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
</mat-accordion>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button mat-button (click)="onReset()">
|
||||||
|
<ion-icon class="mr-2" name="arrow-back-outline"></ion-icon>
|
||||||
|
<span i18n>Back</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<gf-dialog-footer
|
||||||
|
mat-dialog-actions
|
||||||
|
[deviceType]="data.deviceType"
|
||||||
|
(closeButtonClicked)="onCancel()"
|
||||||
|
></gf-dialog-footer>
|
@ -6,10 +6,10 @@ import { MatExpansionModule } from '@angular/material/expansion';
|
|||||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||||
|
|
||||||
import { ImportTransactionDialog } from './import-transaction-dialog.component';
|
import { ImportActivitiesDialog } from './import-activities-dialog.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [ImportTransactionDialog],
|
declarations: [ImportActivitiesDialog],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfDialogFooterModule,
|
GfDialogFooterModule,
|
||||||
@ -20,4 +20,4 @@ import { ImportTransactionDialog } from './import-transaction-dialog.component';
|
|||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfImportTransactionDialogModule {}
|
export class GfImportActivitiesDialogModule {}
|
@ -1,6 +1,10 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(var(--palette-primary-500), 1);
|
||||||
|
}
|
||||||
|
|
||||||
.mat-expansion-panel {
|
.mat-expansion-panel {
|
||||||
background: none;
|
background: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
@ -0,0 +1,6 @@
|
|||||||
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
export interface ImportActivitiesDialogParams {
|
||||||
|
deviceType: string;
|
||||||
|
user: User;
|
||||||
|
}
|
@ -19,6 +19,7 @@ import {
|
|||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Market, ToggleOption } from '@ghostfolio/common/types';
|
import { Market, ToggleOption } from '@ghostfolio/common/types';
|
||||||
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { Account, AssetClass, DataSource } from '@prisma/client';
|
import { Account, AssetClass, DataSource } from '@prisma/client';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
@ -174,7 +175,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
for (const assetClass of Object.keys(AssetClass)) {
|
for (const assetClass of Object.keys(AssetClass)) {
|
||||||
assetClassFilters.push({
|
assetClassFilters.push({
|
||||||
id: assetClass,
|
id: assetClass,
|
||||||
label: assetClass,
|
label: translate(assetClass),
|
||||||
type: 'ASSET_CLASS'
|
type: 'ASSET_CLASS'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
public investments: InvestmentItem[];
|
public investments: InvestmentItem[];
|
||||||
public investmentsByMonth: InvestmentItem[];
|
public investmentsByMonth: InvestmentItem[];
|
||||||
public isLoadingBenchmarkComparator: boolean;
|
public isLoadingBenchmarkComparator: boolean;
|
||||||
|
public isLoadingInvestmentChart: boolean;
|
||||||
public mode: GroupBy = 'month';
|
public mode: GroupBy = 'month';
|
||||||
public modeOptions: ToggleOption[] = [
|
public modeOptions: ToggleOption[] = [
|
||||||
{ label: $localize`Monthly`, value: 'month' }
|
{ label: $localize`Monthly`, value: 'month' }
|
||||||
@ -125,6 +126,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.isLoadingBenchmarkComparator = true;
|
this.isLoadingBenchmarkComparator = true;
|
||||||
|
this.isLoadingInvestmentChart = true;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioPerformance({
|
.fetchPortfolioPerformance({
|
||||||
@ -156,6 +158,8 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isLoadingInvestmentChart = false;
|
||||||
|
|
||||||
this.updateBenchmarkDataItems();
|
this.updateBenchmarkDataItems();
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
|
@ -125,6 +125,7 @@
|
|||||||
[daysInMarket]="daysInMarket"
|
[daysInMarket]="daysInMarket"
|
||||||
[historicalDataItems]="performanceDataItems"
|
[historicalDataItems]="performanceDataItems"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
|
[isLoading]="isLoadingBenchmarkComparator"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[range]="user?.settings?.dateRange"
|
[range]="user?.settings?.dateRange"
|
||||||
></gf-investment-chart>
|
></gf-investment-chart>
|
||||||
|
@ -3,7 +3,13 @@
|
|||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>FIRE</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>FIRE</h3>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="mb-3" i18n>Calculator</h4>
|
<h4 class="align-items-center d-flex mb-3">
|
||||||
|
<span i18n>Calculator</span
|
||||||
|
><gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
></gf-premium-indicator>
|
||||||
|
</h4>
|
||||||
<gf-fire-calculator
|
<gf-fire-calculator
|
||||||
[colorScheme]="user?.settings?.colorScheme"
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[currency]="user?.settings?.baseCurrency"
|
[currency]="user?.settings?.baseCurrency"
|
||||||
@ -18,7 +24,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 i18n>4% Rule</h4>
|
<h4 class="align-items-center d-flex">
|
||||||
|
<span i18n>4% Rule</span
|
||||||
|
><gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
></gf-premium-indicator>
|
||||||
|
</h4>
|
||||||
<div *ngIf="isLoading">
|
<div *ngIf="isLoading">
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
animation="pulse"
|
animation="pulse"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { GfFireCalculatorModule } from '@ghostfolio/ui/fire-calculator';
|
import { GfFireCalculatorModule } from '@ghostfolio/ui/fire-calculator';
|
||||||
|
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ import { FirePageComponent } from './fire-page.component';
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FirePageRoutingModule,
|
FirePageRoutingModule,
|
||||||
GfFireCalculatorModule,
|
GfFireCalculatorModule,
|
||||||
|
GfPremiumIndicatorModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
NgxSkeletonLoaderModule
|
NgxSkeletonLoaderModule
|
||||||
],
|
],
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { AssetClass, DataSource } from '@prisma/client';
|
import { AssetClass, DataSource } from '@prisma/client';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
@ -130,7 +131,7 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
|||||||
for (const assetClass of Object.keys(AssetClass)) {
|
for (const assetClass of Object.keys(AssetClass)) {
|
||||||
assetClassFilters.push({
|
assetClassFilters.push({
|
||||||
id: assetClass,
|
id: assetClass,
|
||||||
label: assetClass,
|
label: translate(assetClass),
|
||||||
type: 'ASSET_CLASS'
|
type: 'ASSET_CLASS'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -40,13 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
<div class="col-xs-12 col-md-6 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<h4 class="align-items-center d-flex">
|
<h4 i18n>Allocations</h4>
|
||||||
<span i18n>Allocations</span>
|
|
||||||
<gf-premium-indicator
|
|
||||||
*ngIf="user?.subscription?.type === 'Basic'"
|
|
||||||
class="ml-1"
|
|
||||||
></gf-premium-indicator>
|
|
||||||
</h4>
|
|
||||||
<div class="flex-grow-1" i18n>
|
<div class="flex-grow-1" i18n>
|
||||||
Check the allocations of your portfolio by account, asset class,
|
Check the allocations of your portfolio by account, asset class,
|
||||||
currency, sector and region.
|
currency, sector and region.
|
||||||
@ -65,13 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
<div class="col-xs-12 col-md-6 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<h4 class="align-items-center d-flex">
|
<h4 i18n>Analysis</h4>
|
||||||
<span i18n>Analysis</span>
|
|
||||||
<gf-premium-indicator
|
|
||||||
*ngIf="user?.subscription?.type === 'Basic'"
|
|
||||||
class="ml-1"
|
|
||||||
></gf-premium-indicator>
|
|
||||||
</h4>
|
|
||||||
<div class="flex-grow-1" i18n>
|
<div class="flex-grow-1" i18n>
|
||||||
Ghostfolio Analysis visualizes your portfolio and shows your top and
|
Ghostfolio Analysis visualizes your portfolio and shows your top and
|
||||||
bottom performers.
|
bottom performers.
|
||||||
@ -90,13 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
<div class="col-xs-12 col-md-6 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<h4 class="align-items-center d-flex">
|
<h4>X-ray</h4>
|
||||||
<span>X-ray</span>
|
|
||||||
<gf-premium-indicator
|
|
||||||
*ngIf="user?.subscription?.type === 'Basic'"
|
|
||||||
class="ml-1"
|
|
||||||
></gf-premium-indicator>
|
|
||||||
</h4>
|
|
||||||
<div class="flex-grow-1" i18n>
|
<div class="flex-grow-1" i18n>
|
||||||
Ghostfolio X-ray uses static analysis to identify potential issues and
|
Ghostfolio X-ray uses static analysis to identify potential issues and
|
||||||
risks in your portfolio.
|
risks in your portfolio.
|
||||||
@ -111,13 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-6 mb-3">
|
<div class="col-xs-12 col-md-6 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<h4 class="align-items-center d-flex">
|
<h4>FIRE</h4>
|
||||||
<span i18n>FIRE</span>
|
|
||||||
<gf-premium-indicator
|
|
||||||
*ngIf="user?.subscription?.type === 'Basic'"
|
|
||||||
class="ml-1"
|
|
||||||
></gf-premium-indicator>
|
|
||||||
</h4>
|
|
||||||
<div class="flex-grow-1" i18n>
|
<div class="flex-grow-1" i18n>
|
||||||
Ghostfolio FIRE calculates metrics for the
|
Ghostfolio FIRE calculates metrics for the
|
||||||
<i>Financial Independence, Retire Early</i> lifestyle.
|
<i>Financial Independence, Retire Early</i> lifestyle.
|
||||||
|
@ -3,7 +3,6 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
|
||||||
|
|
||||||
import { PortfolioPageRoutingModule } from './portfolio-page-routing.module';
|
import { PortfolioPageRoutingModule } from './portfolio-page-routing.module';
|
||||||
import { PortfolioPageComponent } from './portfolio-page.component';
|
import { PortfolioPageComponent } from './portfolio-page.component';
|
||||||
@ -12,7 +11,6 @@ import { PortfolioPageComponent } from './portfolio-page.component';
|
|||||||
declarations: [PortfolioPageComponent],
|
declarations: [PortfolioPageComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfPremiumIndicatorModule,
|
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
PortfolioPageRoutingModule,
|
PortfolioPageRoutingModule,
|
||||||
|
@ -14,21 +14,39 @@
|
|||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="m-0">Currency Cluster Risks</h4>
|
<h4 class="align-items-center d-flex m-0">
|
||||||
|
<span>Currency Cluster Risks</span
|
||||||
|
><gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
></gf-premium-indicator>
|
||||||
|
</h4>
|
||||||
<gf-rules
|
<gf-rules
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
[rules]="currencyClusterRiskRules"
|
[rules]="currencyClusterRiskRules"
|
||||||
></gf-rules>
|
></gf-rules>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="m-0">Account Cluster Risks</h4>
|
<h4 class="align-items-center d-flex m-0">
|
||||||
|
<span>Account Cluster Risks</span
|
||||||
|
><gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
></gf-premium-indicator>
|
||||||
|
</h4>
|
||||||
<gf-rules
|
<gf-rules
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
[rules]="accountClusterRiskRules"
|
[rules]="accountClusterRiskRules"
|
||||||
></gf-rules>
|
></gf-rules>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="m-0">Fees</h4>
|
<h4 class="align-items-center d-flex m-0">
|
||||||
|
<span>Fees</span
|
||||||
|
><gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
></gf-premium-indicator>
|
||||||
|
</h4>
|
||||||
<gf-rules
|
<gf-rules
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
[rules]="feeRules"
|
[rules]="feeRules"
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { RulesModule } from '@ghostfolio/client/components/rules/rules.module';
|
import { RulesModule } from '@ghostfolio/client/components/rules/rules.module';
|
||||||
|
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
import { ReportPageRoutingModule } from './report-page-routing.module';
|
import { ReportPageRoutingModule } from './report-page-routing.module';
|
||||||
import { ReportPageComponent } from './report-page.component';
|
import { ReportPageComponent } from './report-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [ReportPageComponent],
|
declarations: [ReportPageComponent],
|
||||||
imports: [CommonModule, ReportPageRoutingModule, RulesModule],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfPremiumIndicatorModule,
|
||||||
|
ReportPageRoutingModule,
|
||||||
|
RulesModule
|
||||||
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class ReportPageModule {}
|
export class ReportPageModule {}
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
Inject,
|
|
||||||
OnDestroy
|
|
||||||
} from '@angular/core';
|
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
|
|
||||||
import { ImportTransactionDialogParams } from './interfaces/interfaces';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
selector: 'gf-import-transaction-dialog',
|
|
||||||
styleUrls: ['./import-transaction-dialog.scss'],
|
|
||||||
templateUrl: 'import-transaction-dialog.html'
|
|
||||||
})
|
|
||||||
export class ImportTransactionDialog implements OnDestroy {
|
|
||||||
public details: any[] = [];
|
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
@Inject(MAT_DIALOG_DATA) public data: ImportTransactionDialogParams,
|
|
||||||
public dialogRef: MatDialogRef<ImportTransactionDialog>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public ngOnInit() {
|
|
||||||
for (const message of this.data.messages) {
|
|
||||||
if (message.includes('activities.')) {
|
|
||||||
let [index] = message.split(' ');
|
|
||||||
index = index.replace('activities.', '');
|
|
||||||
[index] = index.split('.');
|
|
||||||
|
|
||||||
this.details.push(this.data.activities[index]);
|
|
||||||
} else {
|
|
||||||
this.details.push('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public onCancel(): void {
|
|
||||||
this.dialogRef.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
|
||||||
this.unsubscribeSubject.next();
|
|
||||||
this.unsubscribeSubject.complete();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
<gf-dialog-header
|
|
||||||
mat-dialog-title
|
|
||||||
title="Import Activities Error"
|
|
||||||
[deviceType]="data.deviceType"
|
|
||||||
(closeButtonClicked)="onCancel()"
|
|
||||||
></gf-dialog-header>
|
|
||||||
|
|
||||||
<div class="flex-grow-1" mat-dialog-content>
|
|
||||||
<mat-accordion displayMode="flat">
|
|
||||||
<mat-expansion-panel
|
|
||||||
*ngFor="let message of data.messages; let i = index"
|
|
||||||
[disabled]="!details[i]"
|
|
||||||
>
|
|
||||||
<mat-expansion-panel-header class="pl-1">
|
|
||||||
<mat-panel-title>
|
|
||||||
<div class="d-flex">
|
|
||||||
<div class="align-items-center d-flex mr-2">
|
|
||||||
<ion-icon name="warning-outline"></ion-icon>
|
|
||||||
</div>
|
|
||||||
<div>{{ message }}</div>
|
|
||||||
</div>
|
|
||||||
</mat-panel-title>
|
|
||||||
</mat-expansion-panel-header>
|
|
||||||
<pre
|
|
||||||
*ngIf="details[i]"
|
|
||||||
class="m-0"
|
|
||||||
><code>{{ details[i] | json }}</code></pre>
|
|
||||||
</mat-expansion-panel>
|
|
||||||
</mat-accordion>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<gf-dialog-footer
|
|
||||||
mat-dialog-actions
|
|
||||||
[deviceType]="data.deviceType"
|
|
||||||
(closeButtonClicked)="onCancel()"
|
|
||||||
></gf-dialog-footer>
|
|
@ -1,5 +0,0 @@
|
|||||||
export interface ImportTransactionDialogParams {
|
|
||||||
activities: any[];
|
|
||||||
deviceType: string;
|
|
||||||
messages: string[];
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
|
||||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
|
||||||
|
|
||||||
import { GfCreateOrUpdateTransactionDialogModule } from './create-or-update-transaction-dialog/create-or-update-transaction-dialog.module';
|
|
||||||
import { GfImportTransactionDialogModule } from './import-transaction-dialog/import-transaction-dialog.module';
|
|
||||||
import { TransactionsPageRoutingModule } from './transactions-page-routing.module';
|
|
||||||
import { TransactionsPageComponent } from './transactions-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [TransactionsPageComponent],
|
|
||||||
exports: [],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
GfActivitiesTableModule,
|
|
||||||
GfCreateOrUpdateTransactionDialogModule,
|
|
||||||
GfImportTransactionDialogModule,
|
|
||||||
MatButtonModule,
|
|
||||||
MatSnackBarModule,
|
|
||||||
RouterModule,
|
|
||||||
TransactionsPageRoutingModule
|
|
||||||
],
|
|
||||||
providers: [ImportTransactionsService],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class TransactionsPageModule {}
|
|
@ -32,8 +32,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.intro-container {
|
.intro-container {
|
||||||
.intro {
|
display: none;
|
||||||
background-image: url('/assets/intro-dark.jpg') !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,13 @@ import {
|
|||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
||||||
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
|
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
|
import {
|
||||||
|
AssetClass,
|
||||||
|
AssetSubClass,
|
||||||
|
DataSource,
|
||||||
|
Order as OrderModel
|
||||||
|
} from '@prisma/client';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { cloneDeep, groupBy } from 'lodash';
|
import { cloneDeep, groupBy } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@ -232,6 +238,19 @@ export class DataService {
|
|||||||
response.summary.firstOrderDate
|
response.summary.firstOrderDate
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.holdings) {
|
||||||
|
for (const symbol of Object.keys(response.holdings)) {
|
||||||
|
response.holdings[symbol].assetClass = translate(
|
||||||
|
response.holdings[symbol].assetClass
|
||||||
|
);
|
||||||
|
|
||||||
|
response.holdings[symbol].assetSubClass = translate(
|
||||||
|
response.holdings[symbol].assetSubClass
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -285,6 +304,20 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.SymbolProfile) {
|
||||||
|
if (data.SymbolProfile.assetClass) {
|
||||||
|
data.SymbolProfile.assetClass = <AssetClass>(
|
||||||
|
translate(data.SymbolProfile.assetClass)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.SymbolProfile.assetSubClass) {
|
||||||
|
data.SymbolProfile.assetSubClass = <AssetSubClass>(
|
||||||
|
translate(data.SymbolProfile.assetSubClass)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { Account, DataSource, Type } from '@prisma/client';
|
import { Account, DataSource, Type } from '@prisma/client';
|
||||||
import { parse } from 'date-fns';
|
import { isMatch, parse, parseISO } from 'date-fns';
|
||||||
import { isFinite } from 'lodash';
|
import { isFinite } from 'lodash';
|
||||||
import { parse as csvToJson } from 'papaparse';
|
import { parse as csvToJson } from 'papaparse';
|
||||||
import { EMPTY } from 'rxjs';
|
import { EMPTY } from 'rxjs';
|
||||||
@ -11,7 +11,7 @@ import { catchError } from 'rxjs/operators';
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ImportTransactionsService {
|
export class ImportActivitiesService {
|
||||||
private static ACCOUNT_KEYS = ['account', 'accountid'];
|
private static ACCOUNT_KEYS = ['account', 'accountid'];
|
||||||
private static CURRENCY_KEYS = ['ccy', 'currency'];
|
private static CURRENCY_KEYS = ['ccy', 'currency'];
|
||||||
private static DATA_SOURCE_KEYS = ['datasource'];
|
private static DATA_SOURCE_KEYS = ['datasource'];
|
||||||
@ -90,7 +90,7 @@ export class ImportTransactionsService {
|
|||||||
}) {
|
}) {
|
||||||
item = this.lowercaseKeys(item);
|
item = this.lowercaseKeys(item);
|
||||||
|
|
||||||
for (const key of ImportTransactionsService.ACCOUNT_KEYS) {
|
for (const key of ImportActivitiesService.ACCOUNT_KEYS) {
|
||||||
if (item[key]) {
|
if (item[key]) {
|
||||||
return userAccounts.find((account) => {
|
return userAccounts.find((account) => {
|
||||||
return (
|
return (
|
||||||
@ -115,7 +115,7 @@ export class ImportTransactionsService {
|
|||||||
}) {
|
}) {
|
||||||
item = this.lowercaseKeys(item);
|
item = this.lowercaseKeys(item);
|
||||||
|
|
||||||
for (const key of ImportTransactionsService.CURRENCY_KEYS) {
|
for (const key of ImportActivitiesService.CURRENCY_KEYS) {
|
||||||
if (item[key]) {
|
if (item[key]) {
|
||||||
return item[key];
|
return item[key];
|
||||||
}
|
}
|
||||||
@ -130,7 +130,7 @@ export class ImportTransactionsService {
|
|||||||
private parseDataSource({ item }: { item: any }) {
|
private parseDataSource({ item }: { item: any }) {
|
||||||
item = this.lowercaseKeys(item);
|
item = this.lowercaseKeys(item);
|
||||||
|
|
||||||
for (const key of ImportTransactionsService.DATA_SOURCE_KEYS) {
|
for (const key of ImportActivitiesService.DATA_SOURCE_KEYS) {
|
||||||
if (item[key]) {
|
if (item[key]) {
|
||||||
return DataSource[item[key].toUpperCase()];
|
return DataSource[item[key].toUpperCase()];
|
||||||
}
|
}
|
||||||
@ -151,15 +151,17 @@ export class ImportTransactionsService {
|
|||||||
item = this.lowercaseKeys(item);
|
item = this.lowercaseKeys(item);
|
||||||
let date: string;
|
let date: string;
|
||||||
|
|
||||||
for (const key of ImportTransactionsService.DATE_KEYS) {
|
for (const key of ImportActivitiesService.DATE_KEYS) {
|
||||||
if (item[key]) {
|
if (item[key]) {
|
||||||
try {
|
if (isMatch(item[key], 'dd-MM-yyyy')) {
|
||||||
date = parse(item[key], 'dd-MM-yyyy', new Date()).toISOString();
|
date = parse(item[key], 'dd-MM-yyyy', new Date()).toISOString();
|
||||||
} catch {}
|
} else if (isMatch(item[key], 'dd/MM/yyyy')) {
|
||||||
|
|
||||||
try {
|
|
||||||
date = parse(item[key], 'dd/MM/yyyy', new Date()).toISOString();
|
date = parse(item[key], 'dd/MM/yyyy', new Date()).toISOString();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
date = parseISO(item[key]).toISOString();
|
||||||
} catch {}
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
if (date) {
|
if (date) {
|
||||||
return date;
|
return date;
|
||||||
@ -184,7 +186,7 @@ export class ImportTransactionsService {
|
|||||||
}) {
|
}) {
|
||||||
item = this.lowercaseKeys(item);
|
item = this.lowercaseKeys(item);
|
||||||
|
|
||||||
for (const key of ImportTransactionsService.FEE_KEYS) {
|
for (const key of ImportActivitiesService.FEE_KEYS) {
|
||||||
if (isFinite(item[key])) {
|
if (isFinite(item[key])) {
|
||||||
return item[key];
|
return item[key];
|
||||||
}
|
}
|
||||||
@ -207,7 +209,7 @@ export class ImportTransactionsService {
|
|||||||
}) {
|
}) {
|
||||||
item = this.lowercaseKeys(item);
|
item = this.lowercaseKeys(item);
|
||||||
|
|
||||||
for (const key of ImportTransactionsService.QUANTITY_KEYS) {
|
for (const key of ImportActivitiesService.QUANTITY_KEYS) {
|
||||||
if (isFinite(item[key])) {
|
if (isFinite(item[key])) {
|
||||||
return item[key];
|
return item[key];
|
||||||
}
|
}
|
||||||
@ -230,7 +232,7 @@ export class ImportTransactionsService {
|
|||||||
}) {
|
}) {
|
||||||
item = this.lowercaseKeys(item);
|
item = this.lowercaseKeys(item);
|
||||||
|
|
||||||
for (const key of ImportTransactionsService.SYMBOL_KEYS) {
|
for (const key of ImportActivitiesService.SYMBOL_KEYS) {
|
||||||
if (item[key]) {
|
if (item[key]) {
|
||||||
return item[key];
|
return item[key];
|
||||||
}
|
}
|
||||||
@ -253,7 +255,7 @@ export class ImportTransactionsService {
|
|||||||
}) {
|
}) {
|
||||||
item = this.lowercaseKeys(item);
|
item = this.lowercaseKeys(item);
|
||||||
|
|
||||||
for (const key of ImportTransactionsService.TYPE_KEYS) {
|
for (const key of ImportActivitiesService.TYPE_KEYS) {
|
||||||
if (item[key]) {
|
if (item[key]) {
|
||||||
switch (item[key].toLowerCase()) {
|
switch (item[key].toLowerCase()) {
|
||||||
case 'buy':
|
case 'buy':
|
||||||
@ -287,7 +289,7 @@ export class ImportTransactionsService {
|
|||||||
}) {
|
}) {
|
||||||
item = this.lowercaseKeys(item);
|
item = this.lowercaseKeys(item);
|
||||||
|
|
||||||
for (const key of ImportTransactionsService.UNIT_PRICE_KEYS) {
|
for (const key of ImportActivitiesService.UNIT_PRICE_KEYS) {
|
||||||
if (isFinite(item[key])) {
|
if (isFinite(item[key])) {
|
||||||
return item[key];
|
return item[key];
|
||||||
}
|
}
|
BIN
apps/client/src/assets/images/button-buy-me-a-coffee.png
Normal file
BIN
apps/client/src/assets/images/button-buy-me-a-coffee.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
Before Width: | Height: | Size: 152 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,7 @@ $mat-css-light-theme-selector: '.is-light-theme';
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--dark-background: rgb(39, 39, 39);
|
--dark-background: rgb(25, 25, 25);
|
||||||
--font-family-sans-serif: Roboto, 'Helvetica Neue', sans-serif;
|
--font-family-sans-serif: Roboto, 'Helvetica Neue', sans-serif;
|
||||||
--light-background: rgb(255, 255, 255);
|
--light-background: rgb(255, 255, 255);
|
||||||
}
|
}
|
||||||
@ -90,6 +90,10 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-paginator {
|
||||||
|
background-color: rgba(var(--palette-foreground-base-dark), 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
.svgMap-tooltip {
|
.svgMap-tooltip {
|
||||||
background: var(--dark-background);
|
background: var(--dark-background);
|
||||||
|
|
||||||
@ -102,6 +106,15 @@ body {
|
|||||||
.mat-select-placeholder {
|
.mat-select-placeholder {
|
||||||
color: rgba(var(--light-primary-text));
|
color: rgba(var(--light-primary-text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mat-select-disabled {
|
||||||
|
.mat-select-placeholder {
|
||||||
|
color: rgba(
|
||||||
|
var(--palette-foreground-disabled-text-dark),
|
||||||
|
var(--palette-foreground-disabled-text-dark-alpha)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,6 +224,14 @@ ngx-skeleton-loader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-paginator {
|
||||||
|
background-color: rgba(var(--palette-foreground-base-light), 0.02);
|
||||||
|
|
||||||
|
.mat-paginator-page-size {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.no-min-width {
|
.no-min-width {
|
||||||
min-width: unset !important;
|
min-width: unset !important;
|
||||||
}
|
}
|
||||||
@ -239,4 +260,13 @@ ngx-skeleton-loader {
|
|||||||
.mat-select-placeholder {
|
.mat-select-placeholder {
|
||||||
color: rgba(var(--dark-primary-text));
|
color: rgba(var(--dark-primary-text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mat-select-disabled {
|
||||||
|
.mat-select-placeholder {
|
||||||
|
color: rgba(
|
||||||
|
var(--palette-foreground-disabled-text),
|
||||||
|
var(--palette-foreground-disabled-text-alpha)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
28
apps/ui-e2e/project.json
Normal file
28
apps/ui-e2e/project.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "apps/ui-e2e/src",
|
||||||
|
"projectType": "application",
|
||||||
|
"targets": {
|
||||||
|
"e2e": {
|
||||||
|
"executor": "@nrwl/cypress:cypress",
|
||||||
|
"options": {
|
||||||
|
"cypressConfig": "apps/ui-e2e/cypress.json",
|
||||||
|
"devServerTarget": "ui:storybook",
|
||||||
|
"tsConfig": "apps/ui-e2e/tsconfig.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"devServerTarget": "ui:storybook:ci"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nrwl/linter:eslint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["apps/ui-e2e/**/*.{js,ts}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [],
|
||||||
|
"implicitDependencies": ["ui"]
|
||||||
|
}
|
22
libs/common/project.json
Normal file
22
libs/common/project.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "libs/common/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"targets": {
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nrwl/linter:eslint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["libs/common/**/*.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nrwl/jest:jest",
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/libs/common"],
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "libs/common/jest.config.ts",
|
||||||
|
"passWithNoTests": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
@ -40,6 +40,7 @@ export const DATA_GATHERING_QUEUE_PRIORITY_HIGH = 1;
|
|||||||
|
|
||||||
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
|
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
|
||||||
export const DEFAULT_LANGUAGE_CODE = 'en';
|
export const DEFAULT_LANGUAGE_CODE = 'en';
|
||||||
|
export const DEFAULT_PAGE_SIZE = 50;
|
||||||
|
|
||||||
export const GATHER_ASSET_PROFILE_PROCESS = 'GATHER_ASSET_PROFILE';
|
export const GATHER_ASSET_PROFILE_PROCESS = 'GATHER_ASSET_PROFILE';
|
||||||
export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = {
|
export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = {
|
||||||
|
57
libs/ui/project.json
Normal file
57
libs/ui/project.json
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "library",
|
||||||
|
"generators": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sourceRoot": "libs/ui/src",
|
||||||
|
"prefix": "gf",
|
||||||
|
"targets": {
|
||||||
|
"test": {
|
||||||
|
"executor": "@nrwl/jest:jest",
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/libs/ui"],
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "libs/ui/jest.config.ts",
|
||||||
|
"passWithNoTests": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nrwl/linter:eslint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["libs/ui/src/**/*.ts", "libs/ui/src/**/*.html"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storybook": {
|
||||||
|
"executor": "@storybook/angular:start-storybook",
|
||||||
|
"options": {
|
||||||
|
"port": 4400,
|
||||||
|
"configDir": "libs/ui/.storybook",
|
||||||
|
"browserTarget": "ui:build-storybook",
|
||||||
|
"compodoc": false
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"quiet": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build-storybook": {
|
||||||
|
"executor": "@storybook/angular:build-storybook",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"outputDir": "dist/storybook/ui",
|
||||||
|
"configDir": "libs/ui/.storybook",
|
||||||
|
"browserTarget": "ui:build-storybook",
|
||||||
|
"compodoc": false
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"quiet": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
@ -18,6 +18,7 @@ import {
|
|||||||
} from '@angular/material/autocomplete';
|
} from '@angular/material/autocomplete';
|
||||||
import { MatChipInputEvent } from '@angular/material/chips';
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
import { Filter, FilterGroup } from '@ghostfolio/common/interfaces';
|
import { Filter, FilterGroup } from '@ghostfolio/common/interfaces';
|
||||||
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { groupBy } from 'lodash';
|
import { groupBy } from 'lodash';
|
||||||
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -136,7 +137,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
for (const type of Object.keys(filterGroupsMap)) {
|
for (const type of Object.keys(filterGroupsMap)) {
|
||||||
filterGroups.push({
|
filterGroups.push({
|
||||||
name: <Filter['type']>type,
|
name: <Filter['type']>translate(type),
|
||||||
filters: filterGroupsMap[type]
|
filters: filterGroupsMap[type]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,10 @@
|
|||||||
<div class="activities">
|
<div class="activities">
|
||||||
<table
|
<table
|
||||||
class="gf-table w-100"
|
class="gf-table w-100"
|
||||||
|
mat-table
|
||||||
matSort
|
matSort
|
||||||
matSortActive="date"
|
matSortActive="date"
|
||||||
matSortDirection="desc"
|
matSortDirection="desc"
|
||||||
mat-table
|
|
||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
>
|
>
|
||||||
<ng-container matColumnDef="count">
|
<ng-container matColumnDef="count">
|
||||||
@ -27,7 +27,11 @@
|
|||||||
class="d-none d-lg-table-cell px-1 text-right"
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
mat-cell
|
mat-cell
|
||||||
>
|
>
|
||||||
{{ dataSource.data.length - i }}
|
{{
|
||||||
|
dataSource.data.length > pageSize
|
||||||
|
? dataSource.data.length - pageSize * pageIndex - i
|
||||||
|
: dataSource.data.length - i
|
||||||
|
}}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
*matFooterCellDef
|
*matFooterCellDef
|
||||||
@ -51,7 +55,7 @@
|
|||||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
<ng-container i18n>Type</ng-container>
|
<ng-container i18n>Type</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" mat-cell class="px-1">
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<div
|
<div
|
||||||
class="d-inline-flex p-1 type-badge"
|
class="d-inline-flex p-1 type-badge"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
@ -389,6 +393,10 @@
|
|||||||
<tr
|
<tr
|
||||||
*matRowDef="let row; columns: displayedColumns"
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
mat-row
|
mat-row
|
||||||
|
[ngClass]="{
|
||||||
|
'cursor-pointer':
|
||||||
|
hasPermissionToOpenDetails && !row.isDraft && row.type !== 'ITEM'
|
||||||
|
}"
|
||||||
(click)="
|
(click)="
|
||||||
hasPermissionToOpenDetails &&
|
hasPermissionToOpenDetails &&
|
||||||
!row.isDraft &&
|
!row.isDraft &&
|
||||||
@ -398,10 +406,6 @@
|
|||||||
symbol: row.SymbolProfile.symbol
|
symbol: row.SymbolProfile.symbol
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
[ngClass]="{
|
|
||||||
'cursor-pointer':
|
|
||||||
hasPermissionToOpenDetails && !row.isDraft && row.type !== 'ITEM'
|
|
||||||
}"
|
|
||||||
></tr>
|
></tr>
|
||||||
<tr
|
<tr
|
||||||
*matFooterRowDef="displayedColumns"
|
*matFooterRowDef="displayedColumns"
|
||||||
@ -411,6 +415,18 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<mat-paginator
|
||||||
|
showFirstLastButtons="true"
|
||||||
|
[ngClass]="{
|
||||||
|
'd-none':
|
||||||
|
isLoading ||
|
||||||
|
dataSource.data.length === 0 ||
|
||||||
|
dataSource.data.length <= pageSize
|
||||||
|
}"
|
||||||
|
[pageSize]="pageSize"
|
||||||
|
(page)="onChangePage($event)"
|
||||||
|
></mat-paginator>
|
||||||
|
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
*ngIf="isLoading"
|
*ngIf="isLoading"
|
||||||
animation="pulse"
|
animation="pulse"
|
||||||
|
@ -8,10 +8,12 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
|
||||||
import { getDateFormatString } from '@ghostfolio/common/helper';
|
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||||
import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
@ -37,6 +39,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
@Input() hasPermissionToImportActivities: boolean;
|
@Input() hasPermissionToImportActivities: boolean;
|
||||||
@Input() hasPermissionToOpenDetails = true;
|
@Input() hasPermissionToOpenDetails = true;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
|
@Input() pageSize = DEFAULT_PAGE_SIZE;
|
||||||
@Input() showActions: boolean;
|
@Input() showActions: boolean;
|
||||||
@Input() showSymbolColumn = true;
|
@Input() showSymbolColumn = true;
|
||||||
|
|
||||||
@ -47,6 +50,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
@Output() exportDrafts = new EventEmitter<string[]>();
|
@Output() exportDrafts = new EventEmitter<string[]>();
|
||||||
@Output() import = new EventEmitter<void>();
|
@Output() import = new EventEmitter<void>();
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
public allFilters: Filter[];
|
public allFilters: Filter[];
|
||||||
@ -59,6 +63,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
public isAfter = isAfter;
|
public isAfter = isAfter;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public isUUID = isUUID;
|
public isUUID = isUUID;
|
||||||
|
public pageIndex = 0;
|
||||||
public placeholder = '';
|
public placeholder = '';
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
public searchKeywords: string[] = [];
|
public searchKeywords: string[] = [];
|
||||||
@ -119,12 +124,20 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
return contains;
|
return contains;
|
||||||
};
|
};
|
||||||
|
this.dataSource.paginator = this.paginator;
|
||||||
this.dataSource.sort = this.sort;
|
this.dataSource.sort = this.sort;
|
||||||
|
|
||||||
this.updateFilters();
|
this.updateFilters();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onChangePage(page: PageEvent) {
|
||||||
|
this.pageIndex = page.pageIndex;
|
||||||
|
|
||||||
|
this.totalFees = this.getTotalFees();
|
||||||
|
this.totalValue = this.getTotalValue();
|
||||||
|
}
|
||||||
|
|
||||||
public onCloneActivity(aActivity: OrderWithAccount) {
|
public onCloneActivity(aActivity: OrderWithAccount) {
|
||||||
this.activityToClone.emit(aActivity);
|
this.activityToClone.emit(aActivity);
|
||||||
}
|
}
|
||||||
@ -231,6 +244,21 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
return Object.values(fieldValueMap);
|
return Object.values(fieldValueMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPaginatedData() {
|
||||||
|
if (this.dataSource.data.length > this.pageSize) {
|
||||||
|
const sortedData = this.dataSource.sortData(
|
||||||
|
this.dataSource.filteredData,
|
||||||
|
this.dataSource.sort
|
||||||
|
);
|
||||||
|
|
||||||
|
return sortedData.slice(
|
||||||
|
this.pageIndex * this.pageSize,
|
||||||
|
(this.pageIndex + 1) * this.pageSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.dataSource.filteredData;
|
||||||
|
}
|
||||||
|
|
||||||
private getSearchableFieldValues(activities: OrderWithAccount[]): Filter[] {
|
private getSearchableFieldValues(activities: OrderWithAccount[]): Filter[] {
|
||||||
const fieldValueMap: { [id: string]: Filter } = {};
|
const fieldValueMap: { [id: string]: Filter } = {};
|
||||||
|
|
||||||
@ -243,8 +271,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
private getTotalFees() {
|
private getTotalFees() {
|
||||||
let totalFees = new Big(0);
|
let totalFees = new Big(0);
|
||||||
|
const paginatedData = this.getPaginatedData();
|
||||||
for (const activity of this.dataSource.filteredData) {
|
for (const activity of paginatedData) {
|
||||||
if (isNumber(activity.feeInBaseCurrency)) {
|
if (isNumber(activity.feeInBaseCurrency)) {
|
||||||
totalFees = totalFees.plus(activity.feeInBaseCurrency);
|
totalFees = totalFees.plus(activity.feeInBaseCurrency);
|
||||||
} else {
|
} else {
|
||||||
@ -257,8 +285,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
private getTotalValue() {
|
private getTotalValue() {
|
||||||
let totalValue = new Big(0);
|
let totalValue = new Big(0);
|
||||||
|
const paginatedData = this.getPaginatedData();
|
||||||
for (const activity of this.dataSource.filteredData) {
|
for (const activity of paginatedData) {
|
||||||
if (isNumber(activity.valueInBaseCurrency)) {
|
if (isNumber(activity.valueInBaseCurrency)) {
|
||||||
if (activity.type === 'BUY' || activity.type === 'ITEM') {
|
if (activity.type === 'BUY' || activity.type === 'ITEM') {
|
||||||
totalValue = totalValue.plus(activity.valueInBaseCurrency);
|
totalValue = totalValue.plus(activity.valueInBaseCurrency);
|
||||||
|
@ -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 { MatPaginatorModule } from '@angular/material/paginator';
|
||||||
import { MatSortModule } from '@angular/material/sort';
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
@ -26,6 +27,7 @@ import { ActivitiesTableComponent } from './activities-table.component';
|
|||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
|
MatPaginatorModule,
|
||||||
MatSortModule,
|
MatSortModule,
|
||||||
MatTableModule,
|
MatTableModule,
|
||||||
NgxSkeletonLoaderModule,
|
NgxSkeletonLoaderModule,
|
||||||
|
38
libs/ui/src/lib/i18n.ts
Normal file
38
libs/ui/src/lib/i18n.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import '@angular/localize/init';
|
||||||
|
|
||||||
|
const locales = {
|
||||||
|
ACCOUNT: $localize`Account`,
|
||||||
|
ASSET_CLASS: $localize`Asset Class`,
|
||||||
|
EMERGENCY_FUND: $localize`Emergency Fund`,
|
||||||
|
OTHER: $localize`Other`,
|
||||||
|
SYMBOL: $localize`Symbol`,
|
||||||
|
TAG: $localize`Tag`,
|
||||||
|
|
||||||
|
// enum AssetClass
|
||||||
|
CASH: $localize`Cash`,
|
||||||
|
COMMODITY: $localize`Commodity`,
|
||||||
|
EQUITY: $localize`Equity`,
|
||||||
|
FIXED_INCOME: $localize`Fixed Income`,
|
||||||
|
REAL_ESTATE: $localize`Real Estate`,
|
||||||
|
|
||||||
|
// enum AssetSubClass
|
||||||
|
BOND: $localize`Bond`,
|
||||||
|
CRYPTOCURRENCY: $localize`Cryptocurrency`,
|
||||||
|
ETF: $localize`ETF`,
|
||||||
|
MUTUALFUND: $localize`Mutual Fund`,
|
||||||
|
PRECIOUS_METAL: $localize`Precious Metal`,
|
||||||
|
PRIVATE_EQUITY: $localize`Private Equity`,
|
||||||
|
STOCK: $localize`Stock`,
|
||||||
|
|
||||||
|
// Continents
|
||||||
|
Africa: $localize`Africa`,
|
||||||
|
Asia: $localize`Asia`,
|
||||||
|
Europe: $localize`Europe`,
|
||||||
|
'North America': $localize`North America`,
|
||||||
|
Oceania: $localize`Oceania`,
|
||||||
|
'South America': $localize`South America`
|
||||||
|
};
|
||||||
|
|
||||||
|
export function translate(aKey: string) {
|
||||||
|
return locales[aKey] ?? aKey;
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import '@angular/localize/init';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Meta, Story, moduleMetadata } from '@storybook/angular';
|
import { Meta, Story, moduleMetadata } from '@storybook/angular';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
@ -15,6 +15,7 @@ import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
|||||||
import { getTextColor } from '@ghostfolio/common/helper';
|
import { getTextColor } from '@ghostfolio/common/helper';
|
||||||
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { ColorScheme } from '@ghostfolio/common/types';
|
import { ColorScheme } from '@ghostfolio/common/types';
|
||||||
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { ChartConfiguration, Tooltip } from 'chart.js';
|
import { ChartConfiguration, Tooltip } from 'chart.js';
|
||||||
@ -365,12 +366,12 @@ export class PortfolioProportionChartComponent
|
|||||||
let symbol = context.chart.data.labels?.[labelIndex] ?? '';
|
let symbol = context.chart.data.labels?.[labelIndex] ?? '';
|
||||||
|
|
||||||
if (symbol === this.OTHER_KEY) {
|
if (symbol === this.OTHER_KEY) {
|
||||||
symbol = 'Other';
|
symbol = $localize`Other`;
|
||||||
} else if (symbol === UNKNOWN_KEY) {
|
} else if (symbol === UNKNOWN_KEY) {
|
||||||
symbol = 'No data available';
|
symbol = $localize`No data available`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = this.positions[<string>symbol]?.name;
|
const name = translate(this.positions[<string>symbol]?.name);
|
||||||
|
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (const item of context.dataset.data) {
|
for (const item of context.dataset.data) {
|
||||||
@ -380,7 +381,7 @@ export class PortfolioProportionChartComponent
|
|||||||
const percentage = (context.parsed * 100) / sum;
|
const percentage = (context.parsed * 100) / sum;
|
||||||
|
|
||||||
if (<number>context.raw === Number.MAX_SAFE_INTEGER) {
|
if (<number>context.raw === Number.MAX_SAFE_INTEGER) {
|
||||||
return 'No data available';
|
return $localize`No data available`;
|
||||||
} else if (this.isInPercent) {
|
} else if (this.isInPercent) {
|
||||||
return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`];
|
return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`];
|
||||||
} else {
|
} else {
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
class="mb-0 text-truncate value"
|
class="mb-0 text-truncate value"
|
||||||
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
||||||
>
|
>
|
||||||
{{ formattedValue | titlecase }}
|
{{ formattedValue }}
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
38
nx.json
38
nx.json
@ -1,14 +1,4 @@
|
|||||||
{
|
{
|
||||||
"implicitDependencies": {
|
|
||||||
"angular.json": "*",
|
|
||||||
"package.json": {
|
|
||||||
"dependencies": "*",
|
|
||||||
"devDependencies": "*"
|
|
||||||
},
|
|
||||||
"tsconfig.base.json": "*",
|
|
||||||
".eslintrc.json": "*",
|
|
||||||
"nx.json": "*"
|
|
||||||
},
|
|
||||||
"affected": {
|
"affected": {
|
||||||
"defaultBase": "origin/main"
|
"defaultBase": "origin/main"
|
||||||
},
|
},
|
||||||
@ -46,7 +36,33 @@
|
|||||||
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
||||||
"targetDefaults": {
|
"targetDefaults": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"]
|
"dependsOn": ["^build"],
|
||||||
|
"inputs": ["production", "^production"]
|
||||||
|
},
|
||||||
|
"e2e": {
|
||||||
|
"inputs": ["default", "^production"]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"]
|
||||||
|
},
|
||||||
|
"build-storybook": {
|
||||||
|
"inputs": ["default", "^production", "{workspaceRoot}/.storybook/**/*"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"namedInputs": {
|
||||||
|
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
||||||
|
"sharedGlobals": [
|
||||||
|
"{workspaceRoot}/angular.json",
|
||||||
|
"{workspaceRoot}/tsconfig.base.json",
|
||||||
|
"{workspaceRoot}/nx.json"
|
||||||
|
],
|
||||||
|
"production": [
|
||||||
|
"default",
|
||||||
|
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
|
||||||
|
"!{projectRoot}/tsconfig.spec.json",
|
||||||
|
"!{projectRoot}/jest.config.[jt]s",
|
||||||
|
"!{projectRoot}/.storybook/**/*",
|
||||||
|
"!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
55
package.json
55
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.205.1",
|
"version": "1.209.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -28,7 +28,7 @@
|
|||||||
"database:validate": "prisma validate",
|
"database:validate": "prisma validate",
|
||||||
"dep-graph": "nx dep-graph",
|
"dep-graph": "nx dep-graph",
|
||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"extract-locales": "ng extract-i18n client --output-path ./apps/client/src/locales",
|
"extract-locales": "nx run client:extract-i18n --output-path ./apps/client/src/locales",
|
||||||
"format": "nx format:write",
|
"format": "nx format:write",
|
||||||
"format:check": "nx format:check",
|
"format:check": "nx format:check",
|
||||||
"format:write": "nx format:write",
|
"format:write": "nx format:write",
|
||||||
@ -40,15 +40,15 @@
|
|||||||
"postinstall": "prisma generate && ngcc --properties es2020 browser module main",
|
"postinstall": "prisma generate && ngcc --properties es2020 browser module main",
|
||||||
"replace-placeholders-in-build": "node ./replace.build.js",
|
"replace-placeholders-in-build": "node ./replace.build.js",
|
||||||
"start": "node dist/apps/api/main",
|
"start": "node dist/apps/api/main",
|
||||||
"start:client": "ng serve client --configuration=development-en --hmr -o",
|
"start:client": "nx run client:serve --configuration=development-en --hmr -o",
|
||||||
"start:prod": "yarn database:migrate && yarn database:seed && node main",
|
"start:prod": "yarn database:migrate && yarn database:seed && node main",
|
||||||
"start:server": "nx serve api --watch",
|
"start:server": "nx run api:serve --watch",
|
||||||
"start:storybook": "nx run ui:storybook",
|
"start:storybook": "nx run ui:storybook",
|
||||||
"test": "nx test",
|
"test": "nx test",
|
||||||
"test:single": "nx test --test-file portfolio-calculator-novn-buy-and-sell-partially.spec.ts",
|
"test:single": "nx test --test-file portfolio-calculator-novn-buy-and-sell.spec.ts",
|
||||||
"ts-node": "ts-node",
|
"ts-node": "ts-node",
|
||||||
"update": "nx migrate latest",
|
"update": "nx migrate latest",
|
||||||
"watch:server": "nx build api --watch",
|
"watch:server": "nx run api:build --watch",
|
||||||
"watch:test": "nx test --watch",
|
"watch:test": "nx test --watch",
|
||||||
"workspace-generator": "nx workspace-generator"
|
"workspace-generator": "nx workspace-generator"
|
||||||
},
|
},
|
||||||
@ -72,15 +72,15 @@
|
|||||||
"@dfinity/principal": "0.12.1",
|
"@dfinity/principal": "0.12.1",
|
||||||
"@dinero.js/currencies": "2.0.0-alpha.8",
|
"@dinero.js/currencies": "2.0.0-alpha.8",
|
||||||
"@nestjs/bull": "0.5.5",
|
"@nestjs/bull": "0.5.5",
|
||||||
"@nestjs/common": "9.0.7",
|
"@nestjs/common": "9.1.4",
|
||||||
"@nestjs/config": "2.2.0",
|
"@nestjs/config": "2.2.0",
|
||||||
"@nestjs/core": "9.0.7",
|
"@nestjs/core": "9.1.4",
|
||||||
"@nestjs/jwt": "9.0.0",
|
"@nestjs/jwt": "9.0.0",
|
||||||
"@nestjs/passport": "9.0.0",
|
"@nestjs/passport": "9.0.0",
|
||||||
"@nestjs/platform-express": "9.0.7",
|
"@nestjs/platform-express": "9.1.4",
|
||||||
"@nestjs/schedule": "2.1.0",
|
"@nestjs/schedule": "2.1.0",
|
||||||
"@nestjs/serve-static": "3.0.0",
|
"@nestjs/serve-static": "3.0.0",
|
||||||
"@nrwl/angular": "14.6.4",
|
"@nrwl/angular": "15.0.0",
|
||||||
"@prisma/client": "4.4.0",
|
"@prisma/client": "4.4.0",
|
||||||
"@simplewebauthn/browser": "5.2.1",
|
"@simplewebauthn/browser": "5.2.1",
|
||||||
"@simplewebauthn/server": "5.2.1",
|
"@simplewebauthn/server": "5.2.1",
|
||||||
@ -131,24 +131,24 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "14.2.1",
|
"@angular-devkit/build-angular": "14.2.1",
|
||||||
"@angular-eslint/eslint-plugin": "14.0.3",
|
"@angular-eslint/eslint-plugin": "14.0.4",
|
||||||
"@angular-eslint/eslint-plugin-template": "14.0.3",
|
"@angular-eslint/eslint-plugin-template": "14.0.4",
|
||||||
"@angular-eslint/template-parser": "14.0.3",
|
"@angular-eslint/template-parser": "14.0.4",
|
||||||
"@angular/cli": "14.2.1",
|
"@angular/cli": "14.2.1",
|
||||||
"@angular/compiler-cli": "14.2.0",
|
"@angular/compiler-cli": "14.2.0",
|
||||||
"@angular/language-service": "14.2.0",
|
"@angular/language-service": "14.2.0",
|
||||||
"@angular/localize": "14.2.0",
|
"@angular/localize": "14.2.0",
|
||||||
"@nestjs/schematics": "9.0.1",
|
"@nestjs/schematics": "9.0.3",
|
||||||
"@nestjs/testing": "9.0.7",
|
"@nestjs/testing": "9.1.4",
|
||||||
"@nrwl/cli": "14.6.4",
|
"@nrwl/cli": "15.0.0",
|
||||||
"@nrwl/cypress": "14.6.4",
|
"@nrwl/cypress": "15.0.0",
|
||||||
"@nrwl/eslint-plugin-nx": "14.6.4",
|
"@nrwl/eslint-plugin-nx": "15.0.0",
|
||||||
"@nrwl/jest": "14.6.4",
|
"@nrwl/jest": "15.0.0",
|
||||||
"@nrwl/nest": "14.6.4",
|
"@nrwl/nest": "15.0.0",
|
||||||
"@nrwl/node": "14.6.4",
|
"@nrwl/node": "15.0.0",
|
||||||
"@nrwl/nx-cloud": "14.6.1",
|
"@nrwl/nx-cloud": "15.0.0",
|
||||||
"@nrwl/storybook": "14.6.4",
|
"@nrwl/storybook": "15.0.0",
|
||||||
"@nrwl/workspace": "14.6.4",
|
"@nrwl/workspace": "15.0.0",
|
||||||
"@simplewebauthn/typescript-types": "5.2.1",
|
"@simplewebauthn/typescript-types": "5.2.1",
|
||||||
"@storybook/addon-essentials": "6.5.9",
|
"@storybook/addon-essentials": "6.5.9",
|
||||||
"@storybook/angular": "6.5.9",
|
"@storybook/angular": "6.5.9",
|
||||||
@ -179,7 +179,7 @@
|
|||||||
"jest": "28.1.3",
|
"jest": "28.1.3",
|
||||||
"jest-environment-jsdom": "28.1.1",
|
"jest-environment-jsdom": "28.1.1",
|
||||||
"jest-preset-angular": "12.2.2",
|
"jest-preset-angular": "12.2.2",
|
||||||
"nx": "14.6.4",
|
"nx": "15.0.0",
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
"prettier-plugin-organize-attributes": "0.0.5",
|
"prettier-plugin-organize-attributes": "0.0.5",
|
||||||
"replace-in-file": "6.2.0",
|
"replace-in-file": "6.2.0",
|
||||||
@ -187,7 +187,7 @@
|
|||||||
"ts-jest": "28.0.8",
|
"ts-jest": "28.0.8",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"tslib": "2.0.0",
|
"tslib": "2.0.0",
|
||||||
"typescript": "4.7.3"
|
"typescript": "4.8.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@ -200,5 +200,8 @@
|
|||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "node prisma/seed.js"
|
"seed": "node prisma/seed.js"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"rxjs": "7.5.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
test/import/ok-novn-buy-and-sell.json
Normal file
28
test/import/ok-novn-buy-and-sell.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"date": "2022-07-21T21:28:05.857Z",
|
||||||
|
"version": "dev"
|
||||||
|
},
|
||||||
|
"activities": [
|
||||||
|
{
|
||||||
|
"fee": 0,
|
||||||
|
"quantity": 2,
|
||||||
|
"type": "SELL",
|
||||||
|
"unitPrice": 85.73,
|
||||||
|
"currency": "CHF",
|
||||||
|
"dataSource": "YAHOO",
|
||||||
|
"date": "2022-04-07T22:00:00.000Z",
|
||||||
|
"symbol": "NOVN.SW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fee": 0,
|
||||||
|
"quantity": 2,
|
||||||
|
"type": "BUY",
|
||||||
|
"unitPrice": 75.8,
|
||||||
|
"currency": "CHF",
|
||||||
|
"dataSource": "YAHOO",
|
||||||
|
"date": "2022-03-06T23:00:00.000Z",
|
||||||
|
"symbol": "NOVN.SW"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Reference in New Issue
Block a user