Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
f332aea9b4 | |||
7a9fd18407 | |||
ca08d3154a | |||
01d4ae8757 | |||
43ce2786c1 | |||
de2092c4d2 | |||
435a180e54 | |||
0ad30ffabe | |||
0cc5e558f1 | |||
63b183cc6f | |||
10bae24c5c | |||
0e29278e96 | |||
2db46e5bbf | |||
e757e90e5a | |||
184ddc6209 | |||
e3662a143c | |||
25afd7e07b | |||
7fceaa1350 | |||
7c8530483c | |||
539d3ff754 | |||
9d28b63da6 | |||
24abbd85e6 | |||
b6f395fd3b | |||
04d894cf88 | |||
b4d2c4109e | |||
823093f4d7 | |||
56bf422407 | |||
df0e9ad03b | |||
0e3702c2be | |||
11136ae4f8 | |||
2e6a7d5a91 | |||
83845c256a | |||
34c9703716 | |||
48903238c5 | |||
57a14bd945 | |||
4fd0622114 |
36
.github/workflows/build-code.yml
vendored
Normal file
36
.github/workflows/build-code.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
name: Build code
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node_version:
|
||||||
|
- 16
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node_version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node_version }}
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: yarn format:check
|
||||||
|
|
||||||
|
- name: Execute tests
|
||||||
|
run: yarn test
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: yarn build:all
|
49
.github/workflows/docker-image.yml
vendored
Normal file
49
.github/workflows/docker-image.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
name: Docker image CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: ghostfolio/ghostfolio
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.output.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
30
.travis.yml
30
.travis.yml
@ -1,30 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
git:
|
|
||||||
depth: false
|
|
||||||
node_js:
|
|
||||||
- 16
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
cache: yarn
|
|
||||||
|
|
||||||
if: (type = pull_request) OR (tag IS present)
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- stage: Install dependencies
|
|
||||||
if: type = pull_request
|
|
||||||
script: yarn --frozen-lockfile
|
|
||||||
- stage: Check formatting
|
|
||||||
if: type = pull_request
|
|
||||||
script: yarn format:check
|
|
||||||
- stage: Execute tests
|
|
||||||
if: type = pull_request
|
|
||||||
script: yarn test
|
|
||||||
- stage: Build application
|
|
||||||
if: type = pull_request
|
|
||||||
script: yarn build:all
|
|
||||||
- stage: Build and publish docker image
|
|
||||||
if: tag IS present
|
|
||||||
script: ./publish-docker-image.sh
|
|
68
CHANGELOG.md
68
CHANGELOG.md
@ -5,6 +5,74 @@ 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.185.0 - 30.08.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a skeleton loader to the market mood component in the markets overview
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved the build pipeline from _Travis_ to _GitHub Actions_
|
||||||
|
- Increased the caching of the benchmarks
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Disabled the language selector for the demo user
|
||||||
|
|
||||||
|
## 1.184.2 - 28.08.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the alias to the `Access` database schema
|
||||||
|
- Added support for translated time distances
|
||||||
|
- Added a _GitHub Action_ to create an `arm64` docker image
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the missing assets during the local development
|
||||||
|
|
||||||
|
## 1.183.0 - 24.08.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a filter by asset sub class for the asset profiles in the admin control
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
|
||||||
|
## 1.182.0 - 23.08.2022
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Extended and made the columns of the asset profiles sortable in the admin control
|
||||||
|
- Moved the asset profile details in the admin control panel to a dialog
|
||||||
|
|
||||||
|
## 1.181.2 - 21.08.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a language selector to the account page
|
||||||
|
- Added support for translated labels in the value component
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Integrated the commands `database:setup` and `database:migrate` into the container start
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed a division by zero error in the benchmarks calculation
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply manual data migration (`yarn database:migrate`) is not needed anymore
|
||||||
|
|
||||||
## 1.180.1 - 18.08.2022
|
## 1.180.1 - 18.08.2022
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
20
Dockerfile
20
Dockerfile
@ -1,7 +1,6 @@
|
|||||||
FROM node:16-alpine as builder
|
FROM --platform=$BUILDPLATFORM node:16-slim as builder
|
||||||
|
|
||||||
# Build application and add additional files
|
# Build application and add additional files
|
||||||
|
|
||||||
WORKDIR /ghostfolio
|
WORKDIR /ghostfolio
|
||||||
|
|
||||||
# Only add basic files without the application itself to avoid rebuilding
|
# Only add basic files without the application itself to avoid rebuilding
|
||||||
@ -10,9 +9,16 @@ COPY ./CHANGELOG.md CHANGELOG.md
|
|||||||
COPY ./LICENSE LICENSE
|
COPY ./LICENSE LICENSE
|
||||||
COPY ./package.json package.json
|
COPY ./package.json package.json
|
||||||
COPY ./yarn.lock yarn.lock
|
COPY ./yarn.lock yarn.lock
|
||||||
|
COPY ./.yarnrc .yarnrc
|
||||||
COPY ./prisma/schema.prisma prisma/schema.prisma
|
COPY ./prisma/schema.prisma prisma/schema.prisma
|
||||||
|
|
||||||
RUN apk add --no-cache python3 g++ make openssl git
|
RUN apt update && apt install -y \
|
||||||
|
git \
|
||||||
|
g++ \
|
||||||
|
make \
|
||||||
|
openssl \
|
||||||
|
python3 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
|
|
||||||
# See https://github.com/nrwl/nx/issues/6586 for further details
|
# See https://github.com/nrwl/nx/issues/6586 for further details
|
||||||
@ -45,8 +51,12 @@ COPY package.json /ghostfolio/dist/apps/api
|
|||||||
RUN yarn database:generate-typings
|
RUN yarn database:generate-typings
|
||||||
|
|
||||||
# Image to run, copy everything needed from builder
|
# Image to run, copy everything needed from builder
|
||||||
FROM node:16-alpine
|
FROM node:16-slim
|
||||||
|
RUN apt update && apt install -y \
|
||||||
|
openssl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
|
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
|
||||||
WORKDIR /ghostfolio/apps/api
|
WORKDIR /ghostfolio/apps/api
|
||||||
EXPOSE 3333
|
EXPOSE 3333
|
||||||
CMD [ "node", "main" ]
|
CMD [ "yarn", "start:prod" ]
|
||||||
|
26
README.md
26
README.md
@ -12,13 +12,11 @@
|
|||||||
<strong>Open Source Wealth Management Software</strong>
|
<strong>Open Source Wealth Management Software</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://ghostfol.io"><strong>Ghostfol.io</strong></a> | <a href="https://ghostfol.io/demo"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/faq"><strong>FAQ</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
<a href="https://ghostfol.io"><strong>Ghostfol.io</strong></a> | <a href="https://ghostfol.io/en/demo"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/en/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/en/faq"><strong>FAQ</strong></a> | <a href="https://ghostfol.io/en/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="#contributing">
|
<a href="#contributing">
|
||||||
<img src="https://img.shields.io/badge/contributions-welcome-orange.svg"/></a>
|
<img src="https://img.shields.io/badge/contributions-welcome-orange.svg"/></a>
|
||||||
<a href="https://travis-ci.com/github/ghostfolio/ghostfolio" rel="nofollow">
|
|
||||||
<img src="https://travis-ci.com/ghostfolio/ghostfolio.svg?branch=main" alt="Build Status"/></a>
|
|
||||||
<a href="https://www.gnu.org/licenses/agpl-3.0" rel="nofollow">
|
<a href="https://www.gnu.org/licenses/agpl-3.0" rel="nofollow">
|
||||||
<img src="https://img.shields.io/badge/License-AGPL%20v3-blue.svg" alt="License: AGPL v3"/></a>
|
<img src="https://img.shields.io/badge/License-AGPL%20v3-blue.svg" alt="License: AGPL v3"/></a>
|
||||||
</p>
|
</p>
|
||||||
@ -33,7 +31,7 @@
|
|||||||
|
|
||||||
## Ghostfolio Premium
|
## Ghostfolio Premium
|
||||||
|
|
||||||
Our official **[Ghostfolio Premium](https://ghostfol.io/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
|
Our official **[Ghostfolio Premium](https://ghostfol.io/en/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
|
||||||
|
|
||||||
If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the [Self-hosting](#self-hosting) section.
|
If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the [Self-hosting](#self-hosting) section.
|
||||||
|
|
||||||
@ -114,14 +112,6 @@ Run the following command to start the Docker images from [Docker Hub](https://h
|
|||||||
docker-compose --env-file ./.env -f docker/docker-compose.yml up -d
|
docker-compose --env-file ./.env -f docker/docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Setup Database
|
|
||||||
|
|
||||||
Run the following command to setup the database once Ghostfolio is running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:setup
|
|
||||||
```
|
|
||||||
|
|
||||||
#### b. Build and run environment
|
#### b. Build and run environment
|
||||||
|
|
||||||
Run the following commands to build and start the Docker images:
|
Run the following commands to build and start the Docker images:
|
||||||
@ -131,14 +121,6 @@ docker-compose --env-file ./.env -f docker/docker-compose.build.yml build
|
|||||||
docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Setup Database
|
|
||||||
|
|
||||||
Run the following command to setup the database once Ghostfolio is running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose --env-file ./.env -f docker/docker-compose.build.yml exec ghostfolio yarn database:setup
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Fetch Historical Data
|
#### Fetch Historical Data
|
||||||
|
|
||||||
Open http://localhost:3333 in your browser and accomplish these steps:
|
Open http://localhost:3333 in your browser and accomplish these steps:
|
||||||
@ -151,7 +133,7 @@ Open http://localhost:3333 in your browser and accomplish these steps:
|
|||||||
|
|
||||||
1. Increase the version of the `ghostfolio/ghostfolio` Docker image in `docker/docker-compose.yml`
|
1. Increase the version of the `ghostfolio/ghostfolio` Docker image in `docker/docker-compose.yml`
|
||||||
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
|
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
|
||||||
1. Then, run the following command to keep your database schema in sync: `docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:migrate`
|
At each start, the container will automatically apply the database schema migrations if needed.
|
||||||
|
|
||||||
### Run with _Unraid_ (Community)
|
### Run with _Unraid_ (Community)
|
||||||
|
|
||||||
@ -275,7 +257,7 @@ Ghostfolio is **100% free** and **open source**. We encourage and support an act
|
|||||||
|
|
||||||
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
|
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
|
||||||
|
|
||||||
If you like to support this project, get **[Ghostfolio Premium](https://ghostfol.io/pricing)** or **[Buy me a coffee](https://www.buymeacoffee.com/ghostfolio)**.
|
If you like to support this project, get **[Ghostfolio Premium](https://ghostfol.io/en/pricing)** or **[Buy me a coffee](https://www.buymeacoffee.com/ghostfolio)**.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -128,6 +128,10 @@
|
|||||||
"namedChunks": true
|
"namedChunks": true
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
|
"development-de": {
|
||||||
|
"baseHref": "/de/",
|
||||||
|
"localize": ["de"]
|
||||||
|
},
|
||||||
"development-en": {
|
"development-en": {
|
||||||
"baseHref": "/en/",
|
"baseHref": "/en/",
|
||||||
"localize": ["en"]
|
"localize": ["en"]
|
||||||
@ -170,6 +174,9 @@
|
|||||||
"proxyConfig": "apps/client/proxy.conf.json"
|
"proxyConfig": "apps/client/proxy.conf.json"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
|
"development-de": {
|
||||||
|
"browserTarget": "client:build:development-de"
|
||||||
|
},
|
||||||
"development-en": {
|
"development-en": {
|
||||||
"browserTarget": "client:build:development-en"
|
"browserTarget": "client:build:development-en"
|
||||||
},
|
},
|
||||||
|
@ -42,14 +42,16 @@ export class AccessController {
|
|||||||
return accessesWithGranteeUser.map((access) => {
|
return accessesWithGranteeUser.map((access) => {
|
||||||
if (access.GranteeUser) {
|
if (access.GranteeUser) {
|
||||||
return {
|
return {
|
||||||
granteeAlias: access.GranteeUser?.alias,
|
alias: access.alias,
|
||||||
|
grantee: access.GranteeUser?.id,
|
||||||
id: access.id,
|
id: access.id,
|
||||||
type: 'RESTRICTED_VIEW'
|
type: 'RESTRICTED_VIEW'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
granteeAlias: 'Public',
|
alias: access.alias,
|
||||||
|
grantee: 'Public',
|
||||||
id: access.id,
|
id: access.id,
|
||||||
type: 'PUBLIC'
|
type: 'PUBLIC'
|
||||||
};
|
};
|
||||||
@ -71,6 +73,10 @@ export class AccessController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.accessService.createAccess({
|
return this.accessService.createAccess({
|
||||||
|
alias: data.alias || undefined,
|
||||||
|
GranteeUser: data.granteeUserId
|
||||||
|
? { connect: { id: data.granteeUserId } }
|
||||||
|
: undefined,
|
||||||
User: { connect: { id: this.request.user.id } }
|
User: { connect: { id: this.request.user.id } }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1 +1,11 @@
|
|||||||
export class CreateAccessDto {}
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateAccessDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
alias?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
granteeUserId?: string;
|
||||||
|
}
|
||||||
|
@ -8,7 +8,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
AdminMarketDataDetails
|
AdminMarketDataDetails,
|
||||||
|
Filter
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
@ -22,6 +23,7 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
|
Query,
|
||||||
UseGuards
|
UseGuards
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
@ -226,7 +228,9 @@ export class AdminController {
|
|||||||
|
|
||||||
@Get('market-data')
|
@Get('market-data')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getMarketData(): Promise<AdminMarketData> {
|
public async getMarketData(
|
||||||
|
@Query('assetSubClasses') filterByAssetSubClasses?: string
|
||||||
|
): Promise<AdminMarketData> {
|
||||||
if (
|
if (
|
||||||
!hasPermission(
|
!hasPermission(
|
||||||
this.request.user.permissions,
|
this.request.user.permissions,
|
||||||
@ -239,7 +243,18 @@ export class AdminController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.adminService.getMarketData();
|
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? [];
|
||||||
|
|
||||||
|
const filters: Filter[] = [
|
||||||
|
...assetSubClasses.map((assetSubClass) => {
|
||||||
|
return <Filter>{
|
||||||
|
id: assetSubClass,
|
||||||
|
type: 'ASSET_SUB_CLASS'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
return this.adminService.getMarketData(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('market-data/:dataSource/:symbol')
|
@Get('market-data/:dataSource/:symbol')
|
||||||
|
@ -11,11 +11,13 @@ import {
|
|||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
AdminMarketDataDetails,
|
AdminMarketDataDetails,
|
||||||
AdminMarketDataItem,
|
AdminMarketDataItem,
|
||||||
|
Filter,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Property } from '@prisma/client';
|
import { AssetSubClass, Prisma, Property } from '@prisma/client';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
|
import { groupBy } from 'lodash';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
@ -63,14 +65,27 @@ export class AdminService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMarketData(): Promise<AdminMarketData> {
|
public async getMarketData(filters?: Filter[]): Promise<AdminMarketData> {
|
||||||
|
const where: Prisma.SymbolProfileWhereInput = {};
|
||||||
|
|
||||||
|
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
|
||||||
|
filters,
|
||||||
|
(filter) => {
|
||||||
|
return filter.type;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const marketData = await this.prismaService.marketData.groupBy({
|
const marketData = await this.prismaService.marketData.groupBy({
|
||||||
_count: true,
|
_count: true,
|
||||||
by: ['dataSource', 'symbol']
|
by: ['dataSource', 'symbol']
|
||||||
});
|
});
|
||||||
|
|
||||||
const currencyPairsToGather: AdminMarketDataItem[] =
|
let currencyPairsToGather: AdminMarketDataItem[] = [];
|
||||||
this.exchangeRateDataService
|
|
||||||
|
if (filtersByAssetSubClass) {
|
||||||
|
where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id];
|
||||||
|
} else {
|
||||||
|
currencyPairsToGather = this.exchangeRateDataService
|
||||||
.getCurrencyPairs()
|
.getCurrencyPairs()
|
||||||
.map(({ dataSource, symbol }) => {
|
.map(({ dataSource, symbol }) => {
|
||||||
const marketDataItemCount =
|
const marketDataItemCount =
|
||||||
@ -84,17 +99,24 @@ export class AdminService {
|
|||||||
return {
|
return {
|
||||||
dataSource,
|
dataSource,
|
||||||
marketDataItemCount,
|
marketDataItemCount,
|
||||||
symbol
|
symbol,
|
||||||
|
countriesCount: 0,
|
||||||
|
sectorsCount: 0
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const symbolProfilesToGather: AdminMarketDataItem[] = (
|
const symbolProfilesToGather: AdminMarketDataItem[] = (
|
||||||
await this.prismaService.symbolProfile.findMany({
|
await this.prismaService.symbolProfile.findMany({
|
||||||
|
where,
|
||||||
orderBy: [{ symbol: 'asc' }],
|
orderBy: [{ symbol: 'asc' }],
|
||||||
select: {
|
select: {
|
||||||
_count: {
|
_count: {
|
||||||
select: { Order: true }
|
select: { Order: true }
|
||||||
},
|
},
|
||||||
|
assetClass: true,
|
||||||
|
assetSubClass: true,
|
||||||
|
countries: true,
|
||||||
dataSource: true,
|
dataSource: true,
|
||||||
Order: {
|
Order: {
|
||||||
orderBy: [{ date: 'asc' }],
|
orderBy: [{ date: 'asc' }],
|
||||||
@ -102,10 +124,14 @@ export class AdminService {
|
|||||||
take: 1
|
take: 1
|
||||||
},
|
},
|
||||||
scraperConfiguration: true,
|
scraperConfiguration: true,
|
||||||
|
sectors: true,
|
||||||
symbol: true
|
symbol: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
).map((symbolProfile) => {
|
).map((symbolProfile) => {
|
||||||
|
const countriesCount = symbolProfile.countries
|
||||||
|
? Object.keys(symbolProfile.countries).length
|
||||||
|
: 0;
|
||||||
const marketDataItemCount =
|
const marketDataItemCount =
|
||||||
marketData.find((marketDataItem) => {
|
marketData.find((marketDataItem) => {
|
||||||
return (
|
return (
|
||||||
@ -113,10 +139,17 @@ export class AdminService {
|
|||||||
marketDataItem.symbol === symbolProfile.symbol
|
marketDataItem.symbol === symbolProfile.symbol
|
||||||
);
|
);
|
||||||
})?._count ?? 0;
|
})?._count ?? 0;
|
||||||
|
const sectorsCount = symbolProfile.sectors
|
||||||
|
? Object.keys(symbolProfile.sectors).length
|
||||||
|
: 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
countriesCount,
|
||||||
marketDataItemCount,
|
marketDataItemCount,
|
||||||
|
sectorsCount,
|
||||||
activityCount: symbolProfile._count.Order,
|
activityCount: symbolProfile._count.Order,
|
||||||
|
assetClass: symbolProfile.assetClass,
|
||||||
|
assetSubClass: symbolProfile.assetSubClass,
|
||||||
dataSource: symbolProfile.dataSource,
|
dataSource: symbolProfile.dataSource,
|
||||||
date: symbolProfile.Order?.[0]?.date,
|
date: symbolProfile.Order?.[0]?.date,
|
||||||
symbol: symbolProfile.symbol
|
symbol: symbolProfile.symbol
|
||||||
|
@ -1,30 +1,20 @@
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { BenchmarkResponse } from '@ghostfolio/common/interfaces';
|
||||||
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
|
|
||||||
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
|
|
||||||
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||||
|
|
||||||
import { BenchmarkService } from './benchmark.service';
|
import { BenchmarkService } from './benchmark.service';
|
||||||
|
|
||||||
@Controller('benchmark')
|
@Controller('benchmark')
|
||||||
export class BenchmarkController {
|
export class BenchmarkController {
|
||||||
public constructor(
|
public constructor(private readonly benchmarkService: BenchmarkService) {}
|
||||||
private readonly benchmarkService: BenchmarkService,
|
|
||||||
private readonly propertyService: PropertyService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async getBenchmark(): Promise<BenchmarkResponse> {
|
public async getBenchmark(): Promise<BenchmarkResponse> {
|
||||||
const benchmarkAssets: UniqueAsset[] =
|
|
||||||
((await this.propertyService.getByKey(
|
|
||||||
PROPERTY_BENCHMARKS
|
|
||||||
)) as UniqueAsset[]) ?? [];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
benchmarks: await this.benchmarkService.getBenchmarks(benchmarkAssets)
|
benchmarks: await this.benchmarkService.getBenchmarks()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||||
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
|
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
|
||||||
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
import ms from 'ms';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BenchmarkService {
|
export class BenchmarkService {
|
||||||
@ -13,15 +16,17 @@ export class BenchmarkService {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly marketDataService: MarketDataService,
|
private readonly marketDataService: MarketDataService,
|
||||||
|
private readonly propertyService: PropertyService,
|
||||||
private readonly redisCacheService: RedisCacheService,
|
private readonly redisCacheService: RedisCacheService,
|
||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getBenchmarks(
|
public async getBenchmarks({ useCache = true } = {}): Promise<
|
||||||
benchmarkAssets: UniqueAsset[]
|
BenchmarkResponse['benchmarks']
|
||||||
): Promise<BenchmarkResponse['benchmarks']> {
|
> {
|
||||||
let benchmarks: BenchmarkResponse['benchmarks'];
|
let benchmarks: BenchmarkResponse['benchmarks'];
|
||||||
|
|
||||||
|
if (useCache) {
|
||||||
try {
|
try {
|
||||||
benchmarks = JSON.parse(
|
benchmarks = JSON.parse(
|
||||||
await this.redisCacheService.get(this.CACHE_KEY_BENCHMARKS)
|
await this.redisCacheService.get(this.CACHE_KEY_BENCHMARKS)
|
||||||
@ -31,7 +36,12 @@ export class BenchmarkService {
|
|||||||
return benchmarks;
|
return benchmarks;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const benchmarkAssets: UniqueAsset[] =
|
||||||
|
((await this.propertyService.getByKey(
|
||||||
|
PROPERTY_BENCHMARKS
|
||||||
|
)) as UniqueAsset[]) ?? [];
|
||||||
const promises: Promise<number>[] = [];
|
const promises: Promise<number>[] = [];
|
||||||
|
|
||||||
const [quotes, assetProfiles] = await Promise.all([
|
const [quotes, assetProfiles] = await Promise.all([
|
||||||
@ -48,9 +58,13 @@ export class BenchmarkService {
|
|||||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||||
const { marketPrice } = quotes[benchmarkAssets[index].symbol];
|
const { marketPrice } = quotes[benchmarkAssets[index].symbol];
|
||||||
|
|
||||||
const performancePercentFromAllTimeHigh = new Big(marketPrice)
|
let performancePercentFromAllTimeHigh = new Big(0);
|
||||||
|
|
||||||
|
if (allTimeHigh) {
|
||||||
|
performancePercentFromAllTimeHigh = new Big(marketPrice)
|
||||||
.div(allTimeHigh)
|
.div(allTimeHigh)
|
||||||
.minus(1);
|
.minus(1);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
marketCondition: this.getMarketCondition(
|
marketCondition: this.getMarketCondition(
|
||||||
@ -72,7 +86,8 @@ export class BenchmarkService {
|
|||||||
|
|
||||||
await this.redisCacheService.set(
|
await this.redisCacheService.set(
|
||||||
this.CACHE_KEY_BENCHMARKS,
|
this.CACHE_KEY_BENCHMARKS,
|
||||||
JSON.stringify(benchmarks)
|
JSON.stringify(benchmarks),
|
||||||
|
ms('4 hours') / 1000
|
||||||
);
|
);
|
||||||
|
|
||||||
return benchmarks;
|
return benchmarks;
|
||||||
|
@ -349,6 +349,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
const portfolioPublicDetails: PortfolioPublicDetails = {
|
const portfolioPublicDetails: PortfolioPublicDetails = {
|
||||||
hasDetails,
|
hasDetails,
|
||||||
|
alias: access.alias,
|
||||||
holdings: {}
|
holdings: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,6 +9,10 @@ export class UpdateUserSettingDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
isRestrictedView?: boolean;
|
isRestrictedView?: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
language?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
@ -62,7 +62,7 @@ export class UserService {
|
|||||||
tags,
|
tags,
|
||||||
access: access.map((accessItem) => {
|
access: access.map((accessItem) => {
|
||||||
return {
|
return {
|
||||||
alias: accessItem.User.alias,
|
alias: accessItem.alias,
|
||||||
id: accessItem.id
|
id: accessItem.id
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@ -7,7 +7,7 @@ import { Module } from '@nestjs/common';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
exports: [TwitterBotService],
|
exports: [TwitterBotService],
|
||||||
imports: [BenchmarkModule, ConfigurationModule, PropertyModule, SymbolModule],
|
imports: [BenchmarkModule, ConfigurationModule, SymbolModule],
|
||||||
providers: [TwitterBotService]
|
providers: [TwitterBotService]
|
||||||
})
|
})
|
||||||
export class TwitterBotModule {}
|
export class TwitterBotModule {}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
||||||
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
|
||||||
import {
|
import {
|
||||||
PROPERTY_BENCHMARKS,
|
|
||||||
ghostfolioFearAndGreedIndexDataSource,
|
ghostfolioFearAndGreedIndexDataSource,
|
||||||
ghostfolioFearAndGreedIndexSymbol
|
ghostfolioFearAndGreedIndexSymbol
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
@ -11,7 +9,6 @@ import {
|
|||||||
resolveFearAndGreedIndex,
|
resolveFearAndGreedIndex,
|
||||||
resolveMarketCondition
|
resolveMarketCondition
|
||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { isWeekend } from 'date-fns';
|
import { isWeekend } from 'date-fns';
|
||||||
import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2';
|
import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2';
|
||||||
@ -23,7 +20,6 @@ export class TwitterBotService {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly benchmarkService: BenchmarkService,
|
private readonly benchmarkService: BenchmarkService,
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly propertyService: PropertyService,
|
|
||||||
private readonly symbolService: SymbolService
|
private readonly symbolService: SymbolService
|
||||||
) {
|
) {
|
||||||
this.twitterClient = new TwitterApi({
|
this.twitterClient = new TwitterApi({
|
||||||
@ -82,14 +78,9 @@ export class TwitterBotService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getBenchmarkListing(aMax: number) {
|
private async getBenchmarkListing(aMax: number) {
|
||||||
const benchmarkAssets: UniqueAsset[] =
|
const benchmarks = await this.benchmarkService.getBenchmarks({
|
||||||
((await this.propertyService.getByKey(
|
useCache: false
|
||||||
PROPERTY_BENCHMARKS
|
});
|
||||||
)) as UniqueAsset[]) ?? [];
|
|
||||||
|
|
||||||
const benchmarks = await this.benchmarkService.getBenchmarks(
|
|
||||||
benchmarkAssets
|
|
||||||
);
|
|
||||||
|
|
||||||
const benchmarkListing: string[] = [];
|
const benchmarkListing: string[] = [];
|
||||||
|
|
||||||
|
@ -2,5 +2,13 @@
|
|||||||
"/api": {
|
"/api": {
|
||||||
"target": "http://localhost:3333",
|
"target": "http://localhost:3333",
|
||||||
"secure": false
|
"secure": false
|
||||||
|
},
|
||||||
|
"/assets": {
|
||||||
|
"target": "http://localhost:3333",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
|
"/ionicons": {
|
||||||
|
"target": "http://localhost:3333",
|
||||||
|
"secure": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,8 @@
|
|||||||
class="cursor-pointer d-inline-block info-message px-3 py-2"
|
class="cursor-pointer d-inline-block info-message px-3 py-2"
|
||||||
(click)="onCreateAccount()"
|
(click)="onCreateAccount()"
|
||||||
>
|
>
|
||||||
<span i18n>You are using the Live Demo.</span>
|
<span>You are using the Live Demo.</span>
|
||||||
<span class="a ml-2" i18n>Create Account</span>
|
<span class="a ml-2">Create Account</span>
|
||||||
</div></a
|
</div></a
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||||
<ng-container matColumnDef="granteeAlias">
|
<ng-container matColumnDef="alias">
|
||||||
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
{{ element.alias }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="grantee">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
{{ element.granteeAlias }}
|
{{ element.grantee }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
@ -43,8 +50,8 @@
|
|||||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||||
<button i18n mat-menu-item (click)="onDeleteAccess(element.id)">
|
<button mat-menu-item (click)="onDeleteAccess(element.id)">
|
||||||
Revoke
|
<ng-container i18n>Revoke</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
|
@ -33,7 +33,7 @@ export class AccessTableComponent implements OnChanges, OnInit {
|
|||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
this.displayedColumns = ['granteeAlias', 'type', 'details'];
|
this.displayedColumns = ['alias', 'grantee', 'type', 'details'];
|
||||||
|
|
||||||
if (this.showActions) {
|
if (this.showActions) {
|
||||||
this.displayedColumns.push('actions');
|
this.displayedColumns.push('actions');
|
||||||
|
@ -21,18 +21,10 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value size="medium" [value]="accountType">Account Type</gf-value>
|
||||||
label="Account Type"
|
|
||||||
size="medium"
|
|
||||||
[value]="accountType"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value size="medium" [value]="platformName">Platform</gf-value>
|
||||||
label="Platform"
|
|
||||||
size="medium"
|
|
||||||
[value]="platformName"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -19,13 +19,8 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="currency">
|
<ng-container matColumnDef="currency">
|
||||||
<th
|
<th *matHeaderCellDef class="d-none d-lg-table-cell px-1" mat-header-cell>
|
||||||
*matHeaderCellDef
|
<ng-container i18n>Currency</ng-container>
|
||||||
class="d-none d-lg-table-cell px-1"
|
|
||||||
i18n
|
|
||||||
mat-header-cell
|
|
||||||
>
|
|
||||||
Currency
|
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||||
{{ element.currency }}
|
{{ element.currency }}
|
||||||
@ -36,13 +31,8 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="platform">
|
<ng-container matColumnDef="platform">
|
||||||
<th
|
<th *matHeaderCellDef class="d-none d-lg-table-cell px-1" mat-header-cell>
|
||||||
*matHeaderCellDef
|
<ng-container i18n>Platform</ng-container>
|
||||||
class="d-none d-lg-table-cell px-1"
|
|
||||||
i18n
|
|
||||||
mat-header-cell
|
|
||||||
>
|
|
||||||
Platform
|
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
@ -81,10 +71,9 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell px-1 text-right"
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
>
|
>
|
||||||
Cash Balance
|
<ng-container i18n>Cash Balance</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td
|
<td
|
||||||
*matCellDef="let element"
|
*matCellDef="let element"
|
||||||
@ -116,10 +105,9 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell px-1 text-right"
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
>
|
>
|
||||||
Value
|
<ng-container i18n>Value</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td
|
<td
|
||||||
*matCellDef="let element"
|
*matCellDef="let element"
|
||||||
@ -151,10 +139,9 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-lg-none d-xl-none px-1 text-right"
|
class="d-lg-none d-xl-none px-1 text-right"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
>
|
>
|
||||||
Value
|
<ng-container i18n>Value</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td
|
<td
|
||||||
*matCellDef="let element"
|
*matCellDef="let element"
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<table class="gf-table w-100">
|
<table class="gf-table w-100">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="mat-header-row">
|
<tr class="mat-header-row">
|
||||||
<th class="mat-header-cell px-1 py-2 text-right" i18n>#</th>
|
<th class="mat-header-cell px-1 py-2 text-right">#</th>
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>Type</th>
|
<th class="mat-header-cell px-1 py-2" i18n>Type</th>
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th>
|
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th>
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>Data Source</th>
|
<th class="mat-header-cell px-1 py-2" i18n>Data Source</th>
|
||||||
@ -105,19 +105,18 @@
|
|||||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||||
<button i18n mat-menu-item (click)="onViewData(job.data)">
|
<button mat-menu-item (click)="onViewData(job.data)">
|
||||||
View Data
|
<ng-container i18n>View Data</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
i18n
|
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[disabled]="job.stacktrace?.length <= 0"
|
[disabled]="job.stacktrace?.length <= 0"
|
||||||
(click)="onViewStacktrace(job.stacktrace)"
|
(click)="onViewStacktrace(job.stacktrace)"
|
||||||
>
|
>
|
||||||
View Stacktrace
|
<ng-container i18n>View Stacktrace</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button i18n mat-menu-item (click)="onDeleteJob(job.id)">
|
<button mat-menu-item (click)="onDeleteJob(job.id)">
|
||||||
Delete Job
|
<ng-container i18n>Delete Job</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
|
@ -43,8 +43,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="justify-content-end" mat-dialog-actions>
|
<div class="justify-content-end" mat-dialog-actions>
|
||||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||||
<button color="primary" i18n mat-flat-button (click)="onUpdate()">
|
<button color="primary" mat-flat-button (click)="onUpdate()">
|
||||||
Save
|
<ng-container i18n>Save</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -3,17 +3,27 @@ import {
|
|||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit,
|
||||||
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatSort } from '@angular/material/sort';
|
||||||
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.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';
|
||||||
import { getDateFormatString } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getDateFormatString } from '@ghostfolio/common/helper';
|
||||||
import { UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
||||||
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { AssetSubClass, DataSource } from '@prisma/client';
|
||||||
|
import { format, parseISO } from 'date-fns';
|
||||||
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { AssetProfileDialog } from './asset-profile-dialog/asset-profile-dialog.component';
|
||||||
|
import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@ -22,11 +32,46 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './admin-market-data.html'
|
templateUrl: './admin-market-data.html'
|
||||||
})
|
})
|
||||||
export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
public activeFilters: Filter[] = [];
|
||||||
|
public allFilters: Filter[] = [
|
||||||
|
AssetSubClass.BOND,
|
||||||
|
AssetSubClass.COMMODITY,
|
||||||
|
AssetSubClass.CRYPTOCURRENCY,
|
||||||
|
AssetSubClass.ETF,
|
||||||
|
AssetSubClass.MUTUALFUND,
|
||||||
|
AssetSubClass.PRECIOUS_METAL,
|
||||||
|
AssetSubClass.PRIVATE_EQUITY,
|
||||||
|
AssetSubClass.STOCK
|
||||||
|
].map((id) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
label: id,
|
||||||
|
type: 'ASSET_SUB_CLASS'
|
||||||
|
};
|
||||||
|
});
|
||||||
public currentDataSource: DataSource;
|
public currentDataSource: DataSource;
|
||||||
public currentSymbol: string;
|
public currentSymbol: string;
|
||||||
|
public dataSource: MatTableDataSource<AdminMarketDataItem> =
|
||||||
|
new MatTableDataSource();
|
||||||
public defaultDateFormat: string;
|
public defaultDateFormat: string;
|
||||||
public marketData: AdminMarketDataItem[] = [];
|
public deviceType: string;
|
||||||
public marketDataDetails: MarketData[] = [];
|
public displayedColumns = [
|
||||||
|
'symbol',
|
||||||
|
'dataSource',
|
||||||
|
'assetClass',
|
||||||
|
'assetSubClass',
|
||||||
|
'date',
|
||||||
|
'activityCount',
|
||||||
|
'marketDataItemCount',
|
||||||
|
'countriesCount',
|
||||||
|
'sectorsCount',
|
||||||
|
'actions'
|
||||||
|
];
|
||||||
|
public filters$ = new Subject<Filter[]>();
|
||||||
|
public isLoading = false;
|
||||||
|
public placeholder = '';
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -35,8 +80,29 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
private adminService: AdminService,
|
private adminService: AdminService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
|
private deviceService: DeviceDetectorService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
|
this.route.queryParams
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((params) => {
|
||||||
|
if (
|
||||||
|
params['assetProfileDialog'] &&
|
||||||
|
params['dataSource'] &&
|
||||||
|
params['dateOfFirstActivity'] &&
|
||||||
|
params['symbol']
|
||||||
|
) {
|
||||||
|
this.openAssetProfileDialog({
|
||||||
|
dataSource: params['dataSource'],
|
||||||
|
dateOfFirstActivity: params['dateOfFirstActivity'],
|
||||||
|
symbol: params['symbol']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
@ -51,7 +117,31 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.fetchAdminMarketData();
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
|
this.filters$
|
||||||
|
.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((filters) => {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.activeFilters = filters;
|
||||||
|
this.placeholder =
|
||||||
|
this.activeFilters.length <= 0 ? $localize`Filter by...` : '';
|
||||||
|
|
||||||
|
return this.dataService.fetchAdminMarketData({
|
||||||
|
filters: this.activeFilters
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
takeUntil(this.unsubscribeSubject)
|
||||||
|
)
|
||||||
|
.subscribe(({ marketData }) => {
|
||||||
|
this.dataSource = new MatTableDataSource(marketData);
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
||||||
@ -75,54 +165,60 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
.subscribe(() => {});
|
.subscribe(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onMarketDataChanged(withRefresh: boolean = false) {
|
public onOpenAssetProfileDialog({
|
||||||
if (withRefresh) {
|
dataSource,
|
||||||
this.fetchAdminMarketData();
|
dateOfFirstActivity,
|
||||||
this.fetchAdminMarketDataBySymbol({
|
symbol
|
||||||
dataSource: this.currentDataSource,
|
}: UniqueAsset & { dateOfFirstActivity: string }) {
|
||||||
symbol: this.currentSymbol
|
this.router.navigate([], {
|
||||||
|
queryParams: {
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
assetProfileDialog: true,
|
||||||
|
dateOfFirstActivity: format(parseISO(dateOfFirstActivity), DATE_FORMAT)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public setCurrentProfile({ dataSource, symbol }: UniqueAsset) {
|
|
||||||
this.marketDataDetails = [];
|
|
||||||
|
|
||||||
if (this.currentSymbol === symbol) {
|
|
||||||
this.currentDataSource = undefined;
|
|
||||||
this.currentSymbol = '';
|
|
||||||
} else {
|
|
||||||
this.currentDataSource = dataSource;
|
|
||||||
this.currentSymbol = symbol;
|
|
||||||
|
|
||||||
this.fetchAdminMarketDataBySymbol({ dataSource, symbol });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchAdminMarketData() {
|
private openAssetProfileDialog({
|
||||||
this.dataService
|
dataSource,
|
||||||
.fetchAdminMarketData()
|
dateOfFirstActivity,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
dateOfFirstActivity: string;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ marketData }) => {
|
.subscribe((user) => {
|
||||||
this.marketData = marketData;
|
this.user = user;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
const dialogRef = this.dialog.open(AssetProfileDialog, {
|
||||||
|
autoFocus: false,
|
||||||
|
data: <AssetProfileDialogParams>{
|
||||||
|
dataSource,
|
||||||
|
dateOfFirstActivity,
|
||||||
|
symbol,
|
||||||
|
deviceType: this.deviceType,
|
||||||
|
locale: this.user?.settings?.locale
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
private fetchAdminMarketDataBySymbol({ dataSource, symbol }: UniqueAsset) {
|
dialogRef
|
||||||
this.adminService
|
.afterClosed()
|
||||||
.fetchAdminMarketDataBySymbol({ dataSource, symbol })
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ marketData }) => {
|
.subscribe(() => {
|
||||||
this.marketDataDetails = marketData;
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,108 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<table class="gf-table w-100">
|
<gf-activities-filter
|
||||||
<thead>
|
[allFilters]="allFilters"
|
||||||
<tr class="mat-header-row">
|
[isLoading]="isLoading"
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th>
|
[placeholder]="placeholder"
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>Data Source</th>
|
(valueChanged)="filters$.next($event)"
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>First Activity</th>
|
></gf-activities-filter>
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>Activity Count</th>
|
</div>
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>Historical Data</th>
|
</div>
|
||||||
<th class="mat-header-cell px-1 py-2"></th>
|
<div class="row">
|
||||||
</tr>
|
<div class="col">
|
||||||
</thead>
|
<table
|
||||||
<tbody>
|
class="gf-table w-100"
|
||||||
<ng-container *ngFor="let item of marketData; let i = index">
|
matSort
|
||||||
<tr
|
matSortActive="symbol"
|
||||||
class="cursor-pointer mat-row"
|
matSortDirection="asc"
|
||||||
(click)="setCurrentProfile({ dataSource: item.dataSource, symbol: item.symbol })"
|
mat-table
|
||||||
|
[dataSource]="dataSource"
|
||||||
>
|
>
|
||||||
<td class="mat-cell px-1 py-2">{{ item.symbol }}</td>
|
<ng-container matColumnDef="symbol">
|
||||||
<td class="mat-cell px-1 py-2">{{ item.dataSource }}</td>
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
<td class="mat-cell px-1 py-2">
|
<ng-container i18n>Symbol</ng-container>
|
||||||
{{ (item.date | date: defaultDateFormat) ?? '' }}
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
{{ element.symbol }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell px-1 py-2">{{ item.activityCount }}</td>
|
</ng-container>
|
||||||
<td class="mat-cell px-1 py-2">{{ item.marketDataItemCount }}</td>
|
|
||||||
<td class="mat-cell px-1 py-2">
|
<ng-container matColumnDef="dataSource">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
|
<ng-container i18n>Data Source</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
{{ element.dataSource }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="assetClass">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
|
<ng-container i18n>Asset Class</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
{{ element.assetClass }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="assetSubClass">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
|
<ng-container i18n>Asset Sub Class</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
{{ element.assetSubClass }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="date">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
|
<ng-container i18n>First Activity</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
{{ (element.date | date: defaultDateFormat) ?? '' }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="activityCount">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
|
<ng-container i18n>Activity Count</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
|
{{ element.activityCount }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="marketDataItemCount">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
|
<ng-container i18n>Historical Data</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
|
{{ element.marketDataItemCount }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="countriesCount">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
|
<ng-container i18n>Countries Count</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
|
{{ element.countriesCount }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="sectorsCount">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
|
<ng-container i18n>Sectors Count</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
|
{{ element.sectorsCount }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
|
||||||
|
<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
|
||||||
@ -36,44 +113,35 @@
|
|||||||
</button>
|
</button>
|
||||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||||
<button
|
<button
|
||||||
i18n
|
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
(click)="onGatherSymbol({dataSource: item.dataSource, symbol: item.symbol})"
|
(click)="onGatherSymbol({dataSource: element.dataSource, symbol: element.symbol})"
|
||||||
>
|
>
|
||||||
Gather Data
|
<ng-container i18n>Gather Data</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
i18n
|
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
(click)="onGatherProfileDataBySymbol({dataSource: item.dataSource, symbol: item.symbol})"
|
(click)="onGatherProfileDataBySymbol({dataSource: element.dataSource, symbol: element.symbol})"
|
||||||
>
|
>
|
||||||
Gather Profile Data
|
<ng-container i18n>Gather Profile Data</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
i18n
|
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[disabled]="item.activityCount !== 0"
|
[disabled]="element.activityCount !== 0"
|
||||||
(click)="onDeleteProfileData({dataSource: item.dataSource, symbol: item.symbol})"
|
(click)="onDeleteProfileData({dataSource: element.dataSource, symbol: element.symbol})"
|
||||||
>
|
>
|
||||||
Delete Profile Data
|
<ng-container i18n>Delete</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr *ngIf="currentSymbol === item.symbol" class="mat-row">
|
|
||||||
<td class="p-1" colspan="6">
|
|
||||||
<gf-admin-market-data-detail
|
|
||||||
[dataSource]="item.dataSource"
|
|
||||||
[dateOfFirstActivity]="item.date"
|
|
||||||
[locale]="user?.settings?.locale"
|
|
||||||
[marketData]="marketDataDetails"
|
|
||||||
[symbol]="item.symbol"
|
|
||||||
(marketDataChanged)="onMarketDataChanged($event)"
|
|
||||||
></gf-admin-market-data-detail>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</tbody>
|
|
||||||
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||||
|
<tr
|
||||||
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
|
class="cursor-pointer"
|
||||||
|
mat-row
|
||||||
|
(click)="onOpenAssetProfileDialog({ dateOfFirstActivity: row.date, dataSource: row.dataSource, symbol: row.symbol })"
|
||||||
|
></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,17 +2,23 @@ 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 { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
||||||
|
|
||||||
import { AdminMarketDataComponent } from './admin-market-data.component';
|
import { AdminMarketDataComponent } from './admin-market-data.component';
|
||||||
|
import { GfAssetProfileDialogModule } from './asset-profile-dialog/assset-profile-dialog.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AdminMarketDataComponent],
|
declarations: [AdminMarketDataComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfAdminMarketDataDetailModule,
|
GfActivitiesFilterModule,
|
||||||
|
GfAssetProfileDialogModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatMenuModule
|
MatMenuModule,
|
||||||
|
MatSortModule,
|
||||||
|
MatTableModule
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.mat-dialog-content {
|
||||||
|
max-height: unset;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Inject,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
import { MarketData } from '@prisma/client';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { AssetProfileDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'd-flex flex-column h-100' },
|
||||||
|
selector: 'gf-asset-profile-dialog',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
templateUrl: 'asset-profile-dialog.html',
|
||||||
|
styleUrls: ['./asset-profile-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||||
|
public marketDataDetails: MarketData[] = [];
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private adminService: AdminService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
public dialogRef: MatDialogRef<AssetProfileDialog>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onClose(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onMarketDataChanged(withRefresh: boolean = false) {
|
||||||
|
if (withRefresh) {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchAdminMarketDataBySymbol({ dataSource, symbol }: UniqueAsset) {
|
||||||
|
this.adminService
|
||||||
|
.fetchAdminMarketDataBySymbol({ dataSource, symbol })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ marketData }) => {
|
||||||
|
this.marketDataDetails = marketData;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize() {
|
||||||
|
this.fetchAdminMarketDataBySymbol({
|
||||||
|
dataSource: this.data.dataSource,
|
||||||
|
symbol: this.data.symbol
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
<gf-dialog-header
|
||||||
|
mat-dialog-title
|
||||||
|
position="center"
|
||||||
|
[deviceType]="data.deviceType"
|
||||||
|
[title]="data.symbol"
|
||||||
|
(closeButtonClicked)="onClose()"
|
||||||
|
></gf-dialog-header>
|
||||||
|
|
||||||
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
|
<gf-admin-market-data-detail
|
||||||
|
[dataSource]="data.dataSource"
|
||||||
|
[dateOfFirstActivity]="data.dateOfFirstActivity"
|
||||||
|
[locale]="data.locale"
|
||||||
|
[marketData]="marketDataDetails"
|
||||||
|
[symbol]="data.symbol"
|
||||||
|
(marketDataChanged)="onMarketDataChanged($event)"
|
||||||
|
></gf-admin-market-data-detail>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<gf-dialog-footer
|
||||||
|
mat-dialog-actions
|
||||||
|
[deviceType]="data.deviceType"
|
||||||
|
(closeButtonClicked)="onClose()"
|
||||||
|
></gf-dialog-footer>
|
@ -0,0 +1,23 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
||||||
|
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||||
|
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||||
|
|
||||||
|
import { AssetProfileDialog } from './asset-profile-dialog.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AssetProfileDialog],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfAdminMarketDataDetailModule,
|
||||||
|
GfDialogFooterModule,
|
||||||
|
GfDialogHeaderModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatDialogModule
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class GfAssetProfileDialogModule {}
|
@ -0,0 +1,9 @@
|
|||||||
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface AssetProfileDialogParams {
|
||||||
|
dateOfFirstActivity: string;
|
||||||
|
dataSource: DataSource;
|
||||||
|
deviceType: string;
|
||||||
|
locale: string;
|
||||||
|
symbol: string;
|
||||||
|
}
|
@ -8,7 +8,7 @@
|
|||||||
<div class="w-50">{{ userCount }}</div>
|
<div class="w-50">{{ userCount }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex my-3">
|
<div class="d-flex my-3">
|
||||||
<div class="w-50" i18n>Transaction Count</div>
|
<div class="w-50" i18n>Activity Count</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<ng-container *ngIf="transactionCount">
|
<ng-container *ngIf="transactionCount">
|
||||||
{{ transactionCount }} ({{ transactionCount / userCount | number
|
{{ transactionCount }} ({{ transactionCount / userCount | number
|
||||||
@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex my-3">
|
<div class="d-flex my-3">
|
||||||
<div class="w-50" i18n>Data Gathering</div>
|
<div class="w-50" i18n>Data Management</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
|
@ -7,17 +7,17 @@
|
|||||||
<tr class="mat-header-row">
|
<tr class="mat-header-row">
|
||||||
<th class="mat-header-cell px-1 py-2 text-right">#</th>
|
<th class="mat-header-cell px-1 py-2 text-right">#</th>
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>User</th>
|
<th class="mat-header-cell px-1 py-2" i18n>User</th>
|
||||||
<th class="mat-header-cell px-1 py-2 text-right" i18n>
|
<th class="mat-header-cell px-1 py-2 text-right">
|
||||||
Registration
|
<ng-container i18n>Registration</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<th class="mat-header-cell px-1 py-2 text-right" i18n>
|
<th class="mat-header-cell px-1 py-2 text-right">
|
||||||
Accounts
|
<ng-container i18n>Accounts</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<th class="mat-header-cell px-1 py-2 text-right" i18n>
|
<th class="mat-header-cell px-1 py-2 text-right">
|
||||||
Activities
|
<ng-container i18n>Activities</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<th class="mat-header-cell px-1 py-2 text-right" i18n>
|
<th class="mat-header-cell px-1 py-2 text-right">
|
||||||
Engagement per Day
|
<ng-container i18n>Engagement per Day</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>Last Request</th>
|
<th class="mat-header-cell px-1 py-2" i18n>Last Request</th>
|
||||||
<th class="mat-header-cell px-1 py-2"></th>
|
<th class="mat-header-cell px-1 py-2"></th>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<div class="align-items-center d-flex flex-row">
|
<div class="position-relative">
|
||||||
|
<div class="align-items-center d-flex flex-row" [hidden]="!fearAndGreedIndex">
|
||||||
<div class="h2 mb-0 mr-2">{{ fearAndGreedIndexEmoji }}</div>
|
<div class="h2 mb-0 mr-2">{{ fearAndGreedIndexEmoji }}</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="h4 mb-0">
|
<div class="h4 mb-0">
|
||||||
@ -11,3 +12,12 @@
|
|||||||
<small class="d-block" i18n>Current Market Mood</small>
|
<small class="d-block" i18n>Current Market Mood</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
*ngIf="!fearAndGreedIndex"
|
||||||
|
animation="pulse"
|
||||||
|
class="position-absolute w-100"
|
||||||
|
[theme]="{
|
||||||
|
height: '100%'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
</div>
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
ngx-skeleton-loader {
|
||||||
|
bottom: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { FearAndGreedIndexComponent } from './fear-and-greed-index.component';
|
import { FearAndGreedIndexComponent } from './fear-and-greed-index.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [FearAndGreedIndexComponent],
|
declarations: [FearAndGreedIndexComponent],
|
||||||
exports: [FearAndGreedIndexComponent],
|
exports: [FearAndGreedIndexComponent],
|
||||||
imports: [CommonModule]
|
imports: [CommonModule, NgxSkeletonLoaderModule]
|
||||||
})
|
})
|
||||||
export class GfFearAndGreedIndexModule {}
|
export class GfFearAndGreedIndexModule {}
|
||||||
|
@ -285,17 +285,16 @@
|
|||||||
mat-flat-button
|
mat-flat-button
|
||||||
><ion-icon name="logo-github"></ion-icon
|
><ion-icon name="logo-github"></ion-icon
|
||||||
></a>
|
></a>
|
||||||
<button class="mx-1" i18n mat-flat-button (click)="openLoginDialog()">
|
<button class="mx-1" mat-flat-button (click)="openLoginDialog()">
|
||||||
Sign In
|
<ng-container i18n>Sign in</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
*ngIf="currentRoute !== 'register' && !info?.isReadOnlyMode"
|
*ngIf="currentRoute !== 'register' && !info?.isReadOnlyMode"
|
||||||
class="d-none d-sm-block"
|
class="d-none d-sm-block"
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[routerLink]="['/register']"
|
[routerLink]="['/register']"
|
||||||
>Get Started
|
><ng-container i18n>Get started</ng-container>
|
||||||
</a>
|
</a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
|
@ -47,7 +47,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
private settingsStorageService: SettingsStorageService,
|
private settingsStorageService: SettingsStorageService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
route.queryParams
|
this.route.queryParams
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
if (
|
if (
|
||||||
|
@ -21,6 +21,8 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
export class HomeMarketComponent implements OnDestroy, OnInit {
|
export class HomeMarketComponent implements OnDestroy, OnInit {
|
||||||
public benchmarks: Benchmark[];
|
public benchmarks: Benchmark[];
|
||||||
public fearAndGreedIndex: number;
|
public fearAndGreedIndex: number;
|
||||||
|
public fearLabel = $localize`Fear`;
|
||||||
|
public greedLabel = $localize`Greed`;
|
||||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||||
public historicalData: HistoricalDataItem[];
|
public historicalData: HistoricalDataItem[];
|
||||||
public info: InfoItem;
|
public info: InfoItem;
|
||||||
|
@ -9,18 +9,17 @@
|
|||||||
class="mb-3"
|
class="mb-3"
|
||||||
symbol="Fear & Greed Index"
|
symbol="Fear & Greed Index"
|
||||||
yMax="100"
|
yMax="100"
|
||||||
yMaxLabel="Greed"
|
|
||||||
yMin="0"
|
yMin="0"
|
||||||
yMinLabel="Fear"
|
|
||||||
[historicalDataItems]="historicalData"
|
[historicalDataItems]="historicalData"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[showXAxis]="true"
|
[showXAxis]="true"
|
||||||
[showYAxis]="true"
|
[showYAxis]="true"
|
||||||
|
[yMaxLabel]="greedLabel"
|
||||||
|
[yMinLabel]="fearLabel"
|
||||||
></gf-line-chart>
|
></gf-line-chart>
|
||||||
<gf-fear-and-greed-index
|
<gf-fear-and-greed-index
|
||||||
class="d-flex justify-content-center"
|
class="d-flex justify-content-center"
|
||||||
[fearAndGreedIndex]="fearAndGreedIndex"
|
[fearAndGreedIndex]="fearAndGreedIndex"
|
||||||
[hidden]="isLoading"
|
|
||||||
></gf-fear-and-greed-index>
|
></gf-fear-and-greed-index>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings"
|
[hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings"
|
||||||
[isLoading]="isLoading"
|
[isLoading]="isLoading"
|
||||||
|
[language]="user?.settings?.language"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[summary]="summary"
|
[summary]="summary"
|
||||||
(emergencyFundChanged)="onChangeEmergencyFund($event)"
|
(emergencyFundChanged)="onChangeEmergencyFund($event)"
|
||||||
|
@ -122,7 +122,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
data: this.investments.map((position) => {
|
data: this.investments.map((position) => {
|
||||||
return position.investment;
|
return position.investment;
|
||||||
}),
|
}),
|
||||||
label: 'Investment',
|
label: $localize`Deposit`,
|
||||||
segment: {
|
segment: {
|
||||||
borderColor: (context: unknown) =>
|
borderColor: (context: unknown) =>
|
||||||
this.isInFuture(
|
this.isInFuture(
|
||||||
|
@ -49,12 +49,11 @@
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="!data.accessToken"
|
[disabled]="!data.accessToken"
|
||||||
[mat-dialog-close]="data"
|
[mat-dialog-close]="data"
|
||||||
>
|
>
|
||||||
Sign in
|
<ng-container i18n>Sign in</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
Output
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { getDateFnsLocale } from '@ghostfolio/common/helper';
|
||||||
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
|
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
|||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
@Input() hasPermissionToUpdateUserSettings: boolean;
|
@Input() hasPermissionToUpdateUserSettings: boolean;
|
||||||
@Input() isLoading: boolean;
|
@Input() isLoading: boolean;
|
||||||
|
@Input() language: string;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() summary: PortfolioSummary;
|
@Input() summary: PortfolioSummary;
|
||||||
|
|
||||||
@ -34,7 +36,9 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
|||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
if (this.summary) {
|
if (this.summary) {
|
||||||
if (this.summary.firstOrderDate) {
|
if (this.summary.firstOrderDate) {
|
||||||
this.timeInMarket = formatDistanceToNow(this.summary.firstOrderDate);
|
this.timeInMarket = formatDistanceToNow(this.summary.firstOrderDate, {
|
||||||
|
locale: getDateFnsLocale(this.language)
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.timeInMarket = '-';
|
this.timeInMarket = '-';
|
||||||
}
|
}
|
||||||
|
@ -35,112 +35,124 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Change"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[currency]="data.baseCurrency"
|
[currency]="data.baseCurrency"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="netPerformance"
|
[value]="netPerformance"
|
||||||
></gf-value>
|
>Change</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Performance"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="netPerformancePercent"
|
[value]="netPerformancePercent"
|
||||||
></gf-value>
|
>Performance</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Average Unit Price"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[currency]="SymbolProfile?.currency"
|
[currency]="SymbolProfile?.currency"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="averagePrice"
|
[value]="averagePrice"
|
||||||
></gf-value>
|
>Average Unit Price</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Market Price"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[currency]="SymbolProfile?.currency"
|
[currency]="SymbolProfile?.currency"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="marketPrice"
|
[value]="marketPrice"
|
||||||
></gf-value>
|
>Market Price</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Minimum Price"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[currency]="SymbolProfile?.currency"
|
[currency]="SymbolProfile?.currency"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[ngClass]="{ 'text-danger': minPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
|
[ngClass]="{ 'text-danger': minPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
|
||||||
[value]="minPrice"
|
[value]="minPrice"
|
||||||
></gf-value>
|
>Minimum Price</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Maximum Price"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[currency]="SymbolProfile?.currency"
|
[currency]="SymbolProfile?.currency"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[ngClass]="{ 'text-success': maxPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
|
[ngClass]="{ 'text-success': maxPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
|
||||||
[value]="maxPrice"
|
[value]="maxPrice"
|
||||||
></gf-value>
|
>Maximum Price</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Quantity"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[precision]="quantityPrecision"
|
[precision]="quantityPrecision"
|
||||||
[value]="quantity"
|
[value]="quantity"
|
||||||
></gf-value>
|
>Quantity</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Investment"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[currency]="data.baseCurrency"
|
[currency]="data.baseCurrency"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="investment"
|
[value]="investment"
|
||||||
></gf-value>
|
>Investment</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="First Buy Date"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[isDate]="true"
|
[isDate]="true"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="firstBuyDate"
|
[value]="firstBuyDate"
|
||||||
></gf-value>
|
>First Buy Date</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[label]="transactionCount === 1 ? 'Transaction' : 'Transactions'"
|
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="transactionCount"
|
[value]="transactionCount"
|
||||||
></gf-value>
|
>Transactions</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Asset Class"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[hidden]="!SymbolProfile?.assetClass"
|
[hidden]="!SymbolProfile?.assetClass"
|
||||||
[value]="SymbolProfile?.assetClass"
|
[value]="SymbolProfile?.assetClass"
|
||||||
></gf-value>
|
>Asset Class</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Asset Sub Class"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[hidden]="!SymbolProfile?.assetSubClass"
|
[hidden]="!SymbolProfile?.assetSubClass"
|
||||||
[value]="SymbolProfile?.assetSubClass"
|
[value]="SymbolProfile?.assetSubClass"
|
||||||
></gf-value>
|
>Asset Sub Class</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="SymbolProfile?.countries?.length > 0 || SymbolProfile?.sectors?.length > 0"
|
*ngIf="SymbolProfile?.countries?.length > 0 || SymbolProfile?.sectors?.length > 0"
|
||||||
@ -150,22 +162,24 @@
|
|||||||
>
|
>
|
||||||
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
|
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Sector"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="SymbolProfile.sectors[0].name"
|
[value]="SymbolProfile.sectors[0].name"
|
||||||
></gf-value>
|
>Sector</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
*ngIf="SymbolProfile?.countries?.length === 1"
|
*ngIf="SymbolProfile?.countries?.length === 1"
|
||||||
class="col-6 mb-3"
|
class="col-6 mb-3"
|
||||||
>
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Country"
|
i18n
|
||||||
size="medium"
|
size="medium"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="SymbolProfile.countries[0].name"
|
[value]="SymbolProfile.countries[0].name"
|
||||||
></gf-value>
|
>Country</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #charts>
|
<ng-template #charts>
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="symbol">
|
<ng-container matColumnDef="symbol">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
Symbol
|
<ng-container i18n>Symbol</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<span [title]="element.name">{{ element.symbol | gfSymbol }}</span>
|
<span [title]="element.name">{{ element.symbol | gfSymbol }}</span>
|
||||||
@ -30,11 +30,10 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell px-1"
|
class="d-none d-lg-table-cell px-1"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Name
|
<ng-container i18n>Name</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||||
<ng-container *ngIf="element.name !== element.symbol">{{
|
<ng-container *ngIf="element.name !== element.symbol">{{
|
||||||
@ -47,11 +46,10 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Value
|
<ng-container i18n>Value</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td class="d-none d-lg-table-cell px-1" mat-cell *matCellDef="let element">
|
<td class="d-none d-lg-table-cell px-1" mat-cell *matCellDef="let element">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
@ -68,11 +66,10 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="justify-content-end px-1"
|
class="justify-content-end px-1"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Allocation
|
<ng-container i18n>Allocation</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
@ -89,10 +86,9 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell px-1 text-right"
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
>
|
>
|
||||||
Performance
|
<ng-container i18n>Performance</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
@ -137,8 +133,8 @@
|
|||||||
*ngIf="dataSource.data.length > pageSize && !isLoading"
|
*ngIf="dataSource.data.length > pageSize && !isLoading"
|
||||||
class="my-3 text-center"
|
class="my-3 text-center"
|
||||||
>
|
>
|
||||||
<button i18n mat-stroked-button (click)="onShowAllPositions()">
|
<button mat-stroked-button (click)="onShowAllPositions()">
|
||||||
Show all
|
<ng-container i18n>Show all</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -72,7 +72,13 @@ export class AuthGuard implements CanActivate {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe((user) => {
|
.subscribe((user) => {
|
||||||
if (
|
const userLanguage = user?.settings?.language;
|
||||||
|
|
||||||
|
if (userLanguage && document.documentElement.lang !== userLanguage) {
|
||||||
|
window.location.href = `../${userLanguage}`;
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
} else if (
|
||||||
state.url.startsWith('/home') &&
|
state.url.startsWith('/home') &&
|
||||||
user.settings.viewMode === ViewMode.ZEN
|
user.settings.viewMode === ViewMode.ZEN
|
||||||
) {
|
) {
|
||||||
|
@ -56,7 +56,9 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
|||||||
if (!this.snackBarRef) {
|
if (!this.snackBarRef) {
|
||||||
if (this.info.isReadOnlyMode) {
|
if (this.info.isReadOnlyMode) {
|
||||||
this.snackBarRef = this.snackBar.open(
|
this.snackBarRef = this.snackBar.open(
|
||||||
$localize`This feature is currently unavailable. Please try again later.`,
|
$localize`This feature is currently unavailable.` +
|
||||||
|
' ' +
|
||||||
|
$localize`Please try again later.`,
|
||||||
undefined,
|
undefined,
|
||||||
{ duration: 6000 }
|
{ duration: 6000 }
|
||||||
);
|
);
|
||||||
@ -81,7 +83,9 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
|||||||
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
|
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
|
||||||
if (!this.snackBarRef) {
|
if (!this.snackBarRef) {
|
||||||
this.snackBarRef = this.snackBar.open(
|
this.snackBarRef = this.snackBar.open(
|
||||||
$localize`Oops! Something went wrong. Please try again later.`,
|
$localize`Oops! Something went wrong.` +
|
||||||
|
' ' +
|
||||||
|
$localize`Please try again later.`,
|
||||||
$localize`Okay`,
|
$localize`Okay`,
|
||||||
{ duration: 6000 }
|
{ duration: 6000 }
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>About Ghostfolio</h3>
|
<h3 class="d-flex justify-content-center mb-3">About Ghostfolio</h3>
|
||||||
<div class="about-container">
|
<div class="about-container">
|
||||||
<p>
|
<p>
|
||||||
Ghostfolio is a lightweight wealth management application for
|
Ghostfolio is a lightweight wealth management application for
|
||||||
@ -21,7 +21,7 @@
|
|||||||
<ng-container *ngIf="version">
|
<ng-container *ngIf="version">
|
||||||
This instance is running Ghostfolio {{ version }}.
|
This instance is running Ghostfolio {{ version }}.
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="hasPermissionForStatistics" i18n
|
<ng-container *ngIf="hasPermissionForStatistics"
|
||||||
>Check the system status at
|
>Check the system status at
|
||||||
<a href="https://status.ghostfol.io" title="Ghostfolio status"
|
<a href="https://status.ghostfol.io" title="Ghostfolio status"
|
||||||
>status.ghostfol.io</a
|
>status.ghostfol.io</a
|
||||||
@ -102,33 +102,36 @@
|
|||||||
|
|
||||||
<div *ngIf="hasPermissionForStatistics" class="mb-5 row">
|
<div *ngIf="hasPermissionForStatistics" class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="mb-3 text-center" i18n>Ghostfolio in Numbers</h3>
|
<h3 class="mb-3 text-center">Ghostfolio in Numbers</h3>
|
||||||
<mat-card>
|
<mat-card>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Active Users"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
subLabel="(Last 24 hours)"
|
subLabel="(Last 24 hours)"
|
||||||
[value]="statistics?.activeUsers1d ?? '-'"
|
[value]="statistics?.activeUsers1d ?? '-'"
|
||||||
></gf-value>
|
>Active Users</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="New Users"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
subLabel="(Last 30 days)"
|
subLabel="(Last 30 days)"
|
||||||
[value]="statistics?.newUsers30d ?? '-'"
|
[value]="statistics?.newUsers30d ?? '-'"
|
||||||
></gf-value>
|
>New Users</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Active Users"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
subLabel="(Last 30 days)"
|
subLabel="(Last 30 days)"
|
||||||
[value]="statistics?.activeUsers30d ?? '-'"
|
[value]="statistics?.activeUsers30d ?? '-'"
|
||||||
></gf-value>
|
>Active Users</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<a
|
<a
|
||||||
@ -136,10 +139,11 @@
|
|||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
>
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Users in Slack community"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
[value]="statistics?.slackCommunityUsers ?? '-'"
|
[value]="statistics?.slackCommunityUsers ?? '-'"
|
||||||
></gf-value>
|
>Users in Slack community</gf-value
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
@ -148,10 +152,11 @@
|
|||||||
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
||||||
>
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Contributors on GitHub"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
[value]="statistics?.gitHubContributors ?? '-'"
|
[value]="statistics?.gitHubContributors ?? '-'"
|
||||||
></gf-value>
|
>Contributors on GitHub</gf-value
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
@ -160,10 +165,11 @@
|
|||||||
href="https://github.com/ghostfolio/ghostfolio/stargazers"
|
href="https://github.com/ghostfolio/ghostfolio/stargazers"
|
||||||
>
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Stars on GitHub"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
[value]="statistics?.gitHubStargazers ?? '-'"
|
[value]="statistics?.gitHubStargazers ?? '-'"
|
||||||
></gf-value>
|
>Stars on GitHub</gf-value
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -177,7 +183,6 @@
|
|||||||
<a
|
<a
|
||||||
class="py-2 w-100"
|
class="py-2 w-100"
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-stroked-button
|
mat-stroked-button
|
||||||
[routerLink]="['/faq']"
|
[routerLink]="['/faq']"
|
||||||
>FAQ</a
|
>FAQ</a
|
||||||
@ -190,7 +195,6 @@
|
|||||||
<a
|
<a
|
||||||
class="py-2 w-100"
|
class="py-2 w-100"
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-stroked-button
|
mat-stroked-button
|
||||||
[routerLink]="['/about', 'changelog']"
|
[routerLink]="['/about', 'changelog']"
|
||||||
>Changelog & License</a
|
>Changelog & License</a
|
||||||
@ -200,7 +204,6 @@
|
|||||||
<a
|
<a
|
||||||
class="py-2 w-100"
|
class="py-2 w-100"
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-stroked-button
|
mat-stroked-button
|
||||||
[routerLink]="['/about', 'privacy-policy']"
|
[routerLink]="['/about', 'privacy-policy']"
|
||||||
>Privacy Policy</a
|
>Privacy Policy</a
|
||||||
@ -210,7 +213,6 @@
|
|||||||
<a
|
<a
|
||||||
class="py-2 w-100"
|
class="py-2 w-100"
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[routerLink]="['/blog']"
|
[routerLink]="['/blog']"
|
||||||
>Blog</a
|
>Blog</a
|
||||||
|
@ -53,6 +53,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
public hasPermissionToDeleteAccess: boolean;
|
public hasPermissionToDeleteAccess: boolean;
|
||||||
public hasPermissionToUpdateViewMode: boolean;
|
public hasPermissionToUpdateViewMode: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
|
public language = document.documentElement.lang;
|
||||||
public locales = ['de', 'de-CH', 'en-GB', 'en-US'];
|
public locales = ['de', 'de-CH', 'en-GB', 'en-US'];
|
||||||
public price: number;
|
public price: number;
|
||||||
public priceId: string;
|
public priceId: string;
|
||||||
@ -162,6 +163,14 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
this.user = user;
|
this.user = user;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
|
|
||||||
|
if (aKey === 'language') {
|
||||||
|
if (aValue) {
|
||||||
|
window.location.href = `../${aValue}/account`;
|
||||||
|
} else {
|
||||||
|
window.location.href = `../`;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -307,6 +316,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
const dialogRef = this.dialog.open(CreateOrUpdateAccessDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateAccessDialog, {
|
||||||
data: {
|
data: {
|
||||||
access: {
|
access: {
|
||||||
|
alias: '',
|
||||||
type: 'PUBLIC'
|
type: 'PUBLIC'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -322,7 +332,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
if (access) {
|
if (access) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.postAccess({})
|
.postAccess({ alias: access.alias })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
@ -31,11 +31,10 @@
|
|||||||
<ng-container *ngIf="hasPermissionForSubscription">
|
<ng-container *ngIf="hasPermissionForSubscription">
|
||||||
<button
|
<button
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
(click)="onCheckout(priceId)"
|
(click)="onCheckout(priceId)"
|
||||||
>
|
>
|
||||||
Upgrade
|
<ng-container i18n>Upgrade</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<div *ngIf="price" class="mt-1">
|
<div *ngIf="price" class="mt-1">
|
||||||
<ng-container *ngIf="coupon"
|
<ng-container *ngIf="coupon"
|
||||||
@ -91,8 +90,8 @@
|
|||||||
<div class="d-flex mt-4 py-1">
|
<div class="d-flex mt-4 py-1">
|
||||||
<form #changeUserSettingsForm="ngForm" class="w-100">
|
<form #changeUserSettingsForm="ngForm" class="w-100">
|
||||||
<div class="d-flex mb-2">
|
<div class="d-flex mb-2">
|
||||||
<div class="align-items-center d-flex pt-1 pt-1 w-50" i18n>
|
<div class="align-items-center d-flex pt-1 pt-1 w-50">
|
||||||
Base Currency
|
<ng-container i18n>Base Currency</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<div class="pl-1 w-50">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
@ -111,11 +110,31 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="align-items-center d-flex mb-2">
|
||||||
|
<div class="pr-1 w-50">
|
||||||
|
<div i18n>Language</div>
|
||||||
|
<div class="hint-text text-muted" i18n>Beta</div>
|
||||||
|
</div>
|
||||||
|
<div class="pl-1 w-50">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-select
|
||||||
|
name="language"
|
||||||
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
|
[value]="language"
|
||||||
|
(selectionChange)="onChangeUserSetting('language', $event.value)"
|
||||||
|
>
|
||||||
|
<mat-option [value]="null"></mat-option>
|
||||||
|
<mat-option value="de">Deutsch</mat-option>
|
||||||
|
<mat-option value="en">English</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="align-items-center d-flex mb-2">
|
<div class="align-items-center d-flex mb-2">
|
||||||
<div class="pr-1 w-50">
|
<div class="pr-1 w-50">
|
||||||
<div i18n>Locale</div>
|
<div i18n>Locale</div>
|
||||||
<div class="hint-text text-muted" i18n>
|
<div class="hint-text text-muted">
|
||||||
Date and number format
|
<ng-container i18n>Date and number format</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<div class="pl-1 w-50">
|
||||||
@ -137,8 +156,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="align-items-center d-flex pr-1 pt-1 w-50" i18n>
|
<div class="align-items-center d-flex pr-1 pt-1 w-50">
|
||||||
View Mode
|
<ng-container i18n>View Mode</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-1 w-50">
|
<div class="pl-1 w-50">
|
||||||
<div class="align-items-center d-flex overflow-hidden">
|
<div class="align-items-center d-flex overflow-hidden">
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
<form #addAccessForm="ngForm" class="d-flex flex-column h-100">
|
<form #addAccessForm="ngForm" class="d-flex flex-column h-100">
|
||||||
<h1 i18n mat-dialog-title>Grant access</h1>
|
<h1 i18n mat-dialog-title>Grant access</h1>
|
||||||
<div class="flex-grow-1" mat-dialog-content>
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Alias</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="alias"
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="data.access.alias"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<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>
|
||||||
@ -14,12 +25,11 @@
|
|||||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||||
<button
|
<button
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="!addAccessForm.form.valid"
|
[disabled]="!addAccessForm.form.valid"
|
||||||
[mat-dialog-close]="data"
|
[mat-dialog-close]="data"
|
||||||
>
|
>
|
||||||
Save
|
<ng-container i18n>Save</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -4,6 +4,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
|
||||||
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.component';
|
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.component';
|
||||||
@ -16,6 +17,7 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.com
|
|||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
]
|
]
|
||||||
|
@ -66,12 +66,11 @@
|
|||||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||||
<button
|
<button
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="!addAccountForm.form.valid"
|
[disabled]="!addAccountForm.form.valid"
|
||||||
[mat-dialog-close]="data"
|
[mat-dialog-close]="data"
|
||||||
>
|
>
|
||||||
Save
|
<ng-container i18n>Save</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,36 +1,34 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="mb-3 text-center" i18n>Frequently Asked Questions (FAQ)</h3>
|
<h3 class="mb-3 text-center">Frequently Asked Questions (FAQ)</h3>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n>What is Ghostfolio?</mat-card-title>
|
<mat-card-title>What is Ghostfolio?</mat-card-title>
|
||||||
<mat-card-content i18n>
|
<mat-card-content>
|
||||||
Ghostfolio is a lightweight, open source wealth management application
|
Ghostfolio is a lightweight, open source wealth management application
|
||||||
for individuals to keep track of their net worth. The software
|
for individuals to keep track of their net worth. The software
|
||||||
empowers you to make solid, data-driven investment decisions.
|
empowers you to make solid, data-driven investment decisions.
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n
|
<mat-card-title
|
||||||
>What assets can I track with Ghostfolio?</mat-card-title
|
>What assets can I track with Ghostfolio?</mat-card-title
|
||||||
>
|
>
|
||||||
<mat-card-content i18n>
|
<mat-card-content>
|
||||||
With Ghostfolio, you can keep track of various assets like stocks,
|
With Ghostfolio, you can keep track of various assets like stocks,
|
||||||
ETFs or cryptocurrencies.
|
ETFs or cryptocurrencies.
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n
|
<mat-card-title>What else is included in Ghostfolio?</mat-card-title>
|
||||||
>What else is included in Ghostfolio?</mat-card-title
|
<mat-card-content>
|
||||||
>
|
|
||||||
<mat-card-content i18n>
|
|
||||||
Please find a feature overview to manage your wealth
|
Please find a feature overview to manage your wealth
|
||||||
<a [routerLink]="['/features']">here</a>.
|
<a [routerLink]="['/features']">here</a>.
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n>How do I start?</mat-card-title>
|
<mat-card-title>How do I start?</mat-card-title>
|
||||||
<mat-card-content i18n>
|
<mat-card-content>
|
||||||
You can sign up via the “<a [routerLink]="['/register']"
|
You can sign up via the “<a [routerLink]="['/register']"
|
||||||
>Get Started</a
|
>Get Started</a
|
||||||
>” button at the top of the page. You have multiple options to join
|
>” button at the top of the page. You have multiple options to join
|
||||||
@ -42,8 +40,8 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n>Can I use Ghostfolio anonymously?</mat-card-title>
|
<mat-card-title>Can I use Ghostfolio anonymously?</mat-card-title>
|
||||||
<mat-card-content i18n>
|
<mat-card-content>
|
||||||
Yes, the authentication systems (via security token or
|
Yes, the authentication systems (via security token or
|
||||||
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
|
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
|
||||||
>Internet Identity</a
|
>Internet Identity</a
|
||||||
@ -52,8 +50,8 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n>How can Ghostfolio be free?</mat-card-title>
|
<mat-card-title>How can Ghostfolio be free?</mat-card-title>
|
||||||
<mat-card-content i18n
|
<mat-card-content
|
||||||
>This project is driven by the efforts of contributors from around the
|
>This project is driven by the efforts of contributors from around the
|
||||||
world. The
|
world. The
|
||||||
<a href="https://github.com/ghostfolio/ghostfolio">source code</a> is
|
<a href="https://github.com/ghostfolio/ghostfolio">source code</a> is
|
||||||
@ -64,16 +62,16 @@
|
|||||||
>
|
>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n>Is it really free?</mat-card-title>
|
<mat-card-title>Is it really free?</mat-card-title>
|
||||||
<mat-card-content i18n
|
<mat-card-content
|
||||||
>Yes, it is! Our
|
>Yes, it is! Our
|
||||||
<a [routerLink]="['/pricing']">pricing page</a> details everything you
|
<a [routerLink]="['/pricing']">pricing page</a> details everything you
|
||||||
get for free.</mat-card-content
|
get for free.</mat-card-content
|
||||||
>
|
>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n>What is Ghostfolio Premium?</mat-card-title>
|
<mat-card-title>What is Ghostfolio Premium?</mat-card-title>
|
||||||
<mat-card-content i18n
|
<mat-card-content
|
||||||
><a [routerLink]="['/pricing']">Ghostfolio Premium</a> is a fully
|
><a [routerLink]="['/pricing']">Ghostfolio Premium</a> is a fully
|
||||||
managed Ghostfolio cloud offering for ambitious investors. The revenue
|
managed Ghostfolio cloud offering for ambitious investors. The revenue
|
||||||
is used to cover the hosting infrastructure. It is the Open Source
|
is used to cover the hosting infrastructure. It is the Open Source
|
||||||
@ -81,8 +79,8 @@
|
|||||||
>
|
>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n>Can I start with a trial version?</mat-card-title>
|
<mat-card-title>Can I start with a trial version?</mat-card-title>
|
||||||
<mat-card-content i18n
|
<mat-card-content
|
||||||
>Yes, you can try
|
>Yes, you can try
|
||||||
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> by signing up
|
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> by signing up
|
||||||
for Ghostfolio and applying for a trial (see “My Ghostfolio”). It’s
|
for Ghostfolio and applying for a trial (see “My Ghostfolio”). It’s
|
||||||
@ -91,8 +89,8 @@
|
|||||||
>
|
>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n>Which devices are supported?</mat-card-title>
|
<mat-card-title>Which devices are supported?</mat-card-title>
|
||||||
<mat-card-content i18n
|
<mat-card-content
|
||||||
>Ghostfolio works in every modern web browser on smartphones, tablets
|
>Ghostfolio works in every modern web browser on smartphones, tablets
|
||||||
and desktop computers (where you have even more analysis options and
|
and desktop computers (where you have even more analysis options and
|
||||||
statistics). For Android users, there is a dedicated Ghostfolio app
|
statistics). For Android users, there is a dedicated Ghostfolio app
|
||||||
@ -104,10 +102,10 @@
|
|||||||
>
|
>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n
|
<mat-card-title
|
||||||
>Ghostfolio sounds cool, how can I get involved?</mat-card-title
|
>Ghostfolio sounds cool, how can I get involved?</mat-card-title
|
||||||
>
|
>
|
||||||
<mat-card-content i18n
|
<mat-card-content
|
||||||
>Any support for Ghostfolio is welcome. Be it with a
|
>Any support for Ghostfolio is welcome. Be it with a
|
||||||
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> subscription to
|
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> subscription to
|
||||||
finance the hosting, a positive rating in the
|
finance the hosting, a positive rating in the
|
||||||
@ -124,8 +122,8 @@
|
|||||||
>
|
>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-title i18n>Got any other questions?</mat-card-title>
|
<mat-card-title>Got any other questions?</mat-card-title>
|
||||||
<mat-card-content i18n
|
<mat-card-content
|
||||||
>Join the Ghostfolio
|
>Join the Ghostfolio
|
||||||
<a
|
<a
|
||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-flex justify-content-center mb-3 text-center" i18n>
|
<h3 class="d-flex justify-content-center mb-3 text-center">Features</h3>
|
||||||
Features
|
|
||||||
</h3>
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p>
|
<p>
|
||||||
Check out the numerous features of <strong>Ghostfolio</strong> to
|
Check out the numerous features of <strong>Ghostfolio</strong> to
|
||||||
@ -14,7 +12,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 i18n>Stocks</h4>
|
<h4>Stocks</h4>
|
||||||
<p class="m-0">Keep track of your stock purchases and sales.</p>
|
<p class="m-0">Keep track of your stock purchases and sales.</p>
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@ -22,7 +20,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 i18n>ETFs</h4>
|
<h4>ETFs</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Are you into ETFs (Exchange Traded Funds)? Track your ETF
|
Are you into ETFs (Exchange Traded Funds)? Track your ETF
|
||||||
investments.
|
investments.
|
||||||
@ -33,7 +31,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 i18n>Bonds</h4>
|
<h4>Bonds</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Manage your investment in bonds and other assets with fixed
|
Manage your investment in bonds and other assets with fixed
|
||||||
income.
|
income.
|
||||||
@ -44,7 +42,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 i18n>Cryptocurrencies</h4>
|
<h4>Cryptocurrencies</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Keep track of your Bitcoin and Altcoin holdings.
|
Keep track of your Bitcoin and Altcoin holdings.
|
||||||
</p>
|
</p>
|
||||||
@ -54,7 +52,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 i18n>Dividend</h4>
|
<h4>Dividend</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Are you building a dividend portfolio? Track your dividend in
|
Are you building a dividend portfolio? Track your dividend in
|
||||||
Ghostfolio.
|
Ghostfolio.
|
||||||
@ -65,7 +63,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 class="align-items-center d-flex" i18n>Wealth Items</h4>
|
<h4 class="align-items-center d-flex">Wealth Items</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Track all your treasuries, be it your luxury watch or rare
|
Track all your treasuries, be it your luxury watch or rare
|
||||||
trading cards.
|
trading cards.
|
||||||
@ -76,7 +74,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 class="align-items-center d-flex" i18n>Emergency Fund</h4>
|
<h4 class="align-items-center d-flex">Emergency Fund</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Define your emergency fund you are comfortable with for
|
Define your emergency fund you are comfortable with for
|
||||||
difficult times.
|
difficult times.
|
||||||
@ -87,7 +85,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 class="align-items-center d-flex" i18n>Import and Export</h4>
|
<h4 class="align-items-center d-flex">Import and Export</h4>
|
||||||
<p class="m-0">Import and export your investment activities.</p>
|
<p class="m-0">Import and export your investment activities.</p>
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@ -95,7 +93,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 i18n>Multi-Accounts</h4>
|
<h4>Multi-Accounts</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Keep an eye on all your accounts across multiple platforms
|
Keep an eye on all your accounts across multiple platforms
|
||||||
(multi-banking).
|
(multi-banking).
|
||||||
@ -107,7 +105,7 @@
|
|||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 class="align-items-center d-flex">
|
<h4 class="align-items-center d-flex">
|
||||||
<span i18n>Portfolio Calculations</span>
|
<span>Portfolio Calculations</span>
|
||||||
<gf-premium-indicator
|
<gf-premium-indicator
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="hasPermissionForSubscription"
|
||||||
class="ml-1"
|
class="ml-1"
|
||||||
@ -125,7 +123,7 @@
|
|||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 class="align-items-center d-flex">
|
<h4 class="align-items-center d-flex">
|
||||||
<span i18n>Portfolio Allocations</span>
|
<span>Portfolio Allocations</span>
|
||||||
<gf-premium-indicator
|
<gf-premium-indicator
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="hasPermissionForSubscription"
|
||||||
class="ml-1"
|
class="ml-1"
|
||||||
@ -141,7 +139,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 class="align-items-center d-flex" i18n>Dark Mode</h4>
|
<h4 class="align-items-center d-flex">Dark Mode</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Ghostfolio automatically switches to a dark color theme based on
|
Ghostfolio automatically switches to a dark color theme based on
|
||||||
your operating system's preferences.
|
your operating system's preferences.
|
||||||
@ -152,7 +150,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 class="align-items-center d-flex" i18n>Zen Mode</h4>
|
<h4 class="align-items-center d-flex">Zen Mode</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Keep calm and activate Zen Mode if the markets are going crazy.
|
Keep calm and activate Zen Mode if the markets are going crazy.
|
||||||
</p>
|
</p>
|
||||||
@ -166,7 +164,7 @@
|
|||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 class="align-items-center d-flex">
|
<h4 class="align-items-center d-flex">
|
||||||
<span i18n>Market Mood</span>
|
<span>Market Mood</span>
|
||||||
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
|
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
|
||||||
</h4>
|
</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
@ -181,7 +179,7 @@
|
|||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 class="align-items-center d-flex">
|
<h4 class="align-items-center d-flex">
|
||||||
<span i18n>Static Analysis</span>
|
<span>Static Analysis</span>
|
||||||
<gf-premium-indicator
|
<gf-premium-indicator
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="hasPermissionForSubscription"
|
||||||
class="ml-1"
|
class="ml-1"
|
||||||
@ -197,7 +195,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 i18n>Community</h4>
|
<h4>Community</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Join the Ghostfolio
|
Join the Ghostfolio
|
||||||
<a
|
<a
|
||||||
@ -214,7 +212,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 i18n>Open Source Software</h4>
|
<h4>Open Source Software</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
The source code is fully available as
|
The source code is fully available as
|
||||||
<a
|
<a
|
||||||
@ -232,7 +230,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div *ngIf="!user" class="row">
|
<div *ngIf="!user" class="row">
|
||||||
<div class="col mt-3 text-center">
|
<div class="col mt-3 text-center">
|
||||||
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
|
<a color="primary" mat-flat-button [routerLink]="['/register']">
|
||||||
Get Started
|
Get Started
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
<h1 class="font-weight-bold intro my-5" i18n>
|
<h1 class="font-weight-bold intro my-5">
|
||||||
Manage your wealth like a boss
|
Manage your wealth like a boss
|
||||||
</h1>
|
</h1>
|
||||||
<div>
|
<div>
|
||||||
@ -29,19 +29,13 @@
|
|||||||
<a
|
<a
|
||||||
class="d-inline-block"
|
class="d-inline-block"
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[routerLink]="['/register']"
|
[routerLink]="['/register']"
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
</a>
|
</a>
|
||||||
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
|
<div class="d-inline-block mx-3 text-muted">or</div>
|
||||||
<a
|
<a class="d-inline-block" mat-stroked-button [routerLink]="['/demo']">
|
||||||
class="d-inline-block"
|
|
||||||
i18n
|
|
||||||
mat-stroked-button
|
|
||||||
[routerLink]="['/demo']"
|
|
||||||
>
|
|
||||||
Live Demo
|
Live Demo
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -107,7 +101,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="mt-4 text-center">
|
<div class="mt-4 text-center">
|
||||||
<a [routerLink]="['/about']" i18n mat-stroked-button
|
<a [routerLink]="['/about']" mat-stroked-button
|
||||||
>Learn more about Ghostfolio</a
|
>Learn more about Ghostfolio</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -162,16 +156,11 @@
|
|||||||
Join now or check out the example account
|
Join now or check out the example account
|
||||||
</p>
|
</p>
|
||||||
<div class="py-2 text-center">
|
<div class="py-2 text-center">
|
||||||
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
|
<a color="primary" mat-flat-button [routerLink]="['/register']">
|
||||||
Get Started
|
Get Started
|
||||||
</a>
|
</a>
|
||||||
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
|
<div class="d-inline-block mx-3 text-muted">or</div>
|
||||||
<a
|
<a class="d-inline-block" mat-stroked-button [routerLink]="['/demo']">
|
||||||
class="d-inline-block"
|
|
||||||
i18n
|
|
||||||
mat-stroked-button
|
|
||||||
[routerLink]="['/demo']"
|
|
||||||
>
|
|
||||||
Live Demo
|
Live Demo
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,8 +54,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
};
|
};
|
||||||
public period = 'current';
|
public period = 'current';
|
||||||
public periodOptions: ToggleOption[] = [
|
public periodOptions: ToggleOption[] = [
|
||||||
{ label: 'Initial', value: 'original' },
|
{ label: $localize`Initial`, value: 'original' },
|
||||||
{ label: 'Current', value: 'current' }
|
{ label: $localize`Current`, value: 'current' }
|
||||||
];
|
];
|
||||||
public placeholder = '';
|
public placeholder = '';
|
||||||
public portfolioDetails: PortfolioDetails;
|
public portfolioDetails: PortfolioDetails;
|
||||||
@ -85,7 +85,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private readonly SEARCH_PLACEHOLDER = 'Filter by account or tag...';
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -133,7 +132,9 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.activeFilters = filters;
|
this.activeFilters = filters;
|
||||||
this.placeholder =
|
this.placeholder =
|
||||||
this.activeFilters.length <= 0 ? this.SEARCH_PLACEHOLDER : '';
|
this.activeFilters.length <= 0
|
||||||
|
? $localize`Filter by account or tag...`
|
||||||
|
: '';
|
||||||
|
|
||||||
return this.dataService.fetchPortfolioDetails({
|
return this.dataService.fetchPortfolioDetails({
|
||||||
filters: this.activeFilters
|
filters: this.activeFilters
|
||||||
|
@ -94,8 +94,8 @@
|
|||||||
<div class="col-md-12 allocations-by-symbol">
|
<div class="col-md-12 allocations-by-symbol">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="overflow-hidden w-100">
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
<mat-card-title class="align-items-center d-flex text-truncate" i18n>
|
<mat-card-title class="align-items-center d-flex text-truncate">
|
||||||
By Holding</mat-card-title
|
<ng-container i18n>By Holding</ng-container></mat-card-title
|
||||||
>
|
>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
@ -233,27 +233,30 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Developed Markets"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[value]="markets?.developedMarkets?.value"
|
[value]="markets?.developedMarkets?.value"
|
||||||
></gf-value>
|
>Developed Markets</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Emerging Markets"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[value]="markets?.emergingMarkets?.value"
|
[value]="markets?.emergingMarkets?.value"
|
||||||
></gf-value>
|
>Emerging Markets</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Other Markets"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[value]="markets?.otherMarkets?.value"
|
[value]="markets?.otherMarkets?.value"
|
||||||
></gf-value>
|
>Other Markets</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
@ -26,8 +26,8 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
public investmentsByMonth: InvestmentItem[];
|
public investmentsByMonth: InvestmentItem[];
|
||||||
public mode: GroupBy;
|
public mode: GroupBy;
|
||||||
public modeOptions: ToggleOption[] = [
|
public modeOptions: ToggleOption[] = [
|
||||||
{ label: 'Monthly', value: 'month' },
|
{ label: $localize`Monthly`, value: 'month' },
|
||||||
{ label: 'Accumulating', value: undefined }
|
{ label: $localize`Accumulating`, value: undefined }
|
||||||
];
|
];
|
||||||
public top3: Position[];
|
public top3: Position[];
|
||||||
public user: User;
|
public user: User;
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title class="align-items-center d-flex" i18n
|
<mat-card-title class="align-items-center d-flex" i18n
|
||||||
>Top 3</mat-card-title
|
>Top</mat-card-title
|
||||||
>
|
>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
@ -88,7 +88,7 @@
|
|||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title class="align-items-center d-flex" i18n
|
<mat-card-title class="align-items-center d-flex" i18n
|
||||||
>Bottom 3</mat-card-title
|
>Bottom</mat-card-title
|
||||||
>
|
>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
|
@ -38,7 +38,6 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
|||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private readonly SEARCH_PLACEHOLDER = 'Filter by account or tag...';
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -84,7 +83,9 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.activeFilters = filters;
|
this.activeFilters = filters;
|
||||||
this.placeholder =
|
this.placeholder =
|
||||||
this.activeFilters.length <= 0 ? this.SEARCH_PLACEHOLDER : '';
|
this.activeFilters.length <= 0
|
||||||
|
? $localize`Filter by account or tag...`
|
||||||
|
: '';
|
||||||
|
|
||||||
return this.dataService.fetchPortfolioDetails({
|
return this.dataService.fetchPortfolioDetails({
|
||||||
filters: this.activeFilters
|
filters: this.activeFilters
|
||||||
|
@ -91,7 +91,7 @@
|
|||||||
<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 class="align-items-center d-flex">
|
||||||
<span i18n>X-ray</span>
|
<span>X-ray</span>
|
||||||
<gf-premium-indicator
|
<gf-premium-indicator
|
||||||
*ngIf="user?.subscription?.type === 'Basic'"
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
class="ml-1"
|
class="ml-1"
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="align-items-center d-flex justify-content-center mb-3" i18n>
|
<h3 class="align-items-center d-flex justify-content-center mb-3">
|
||||||
X-ray
|
X-ray
|
||||||
</h3>
|
</h3>
|
||||||
<p class="mb-4" i18n>
|
<p class="mb-4">
|
||||||
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.
|
||||||
<span class="d-none"
|
<span class="d-none"
|
||||||
@ -14,21 +14,21 @@
|
|||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="m-0" i18n>Currency Cluster Risks</h4>
|
<h4 class="m-0">Currency Cluster Risks</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" i18n>Account Cluster Risks</h4>
|
<h4 class="m-0">Account Cluster Risks</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" i18n>Fees</h4>
|
<h4 class="m-0">Fees</h4>
|
||||||
<gf-rules
|
<gf-rules
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
[rules]="feeRules"
|
[rules]="feeRules"
|
||||||
|
@ -166,7 +166,7 @@
|
|||||||
[ngClass]="{ 'd-none': activityForm.controls['type']?.value !== 'ITEM' }"
|
[ngClass]="{ 'd-none': activityForm.controls['type']?.value !== 'ITEM' }"
|
||||||
>
|
>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Asset Sub-Class</mat-label>
|
<mat-label i18n>Asset Sub Class</mat-label>
|
||||||
<mat-select formControlName="assetSubClass">
|
<mat-select formControlName="assetSubClass">
|
||||||
<mat-option [value]="null"></mat-option>
|
<mat-option [value]="null"></mat-option>
|
||||||
<mat-option
|
<mat-option
|
||||||
@ -201,12 +201,11 @@
|
|||||||
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||||
<button
|
<button
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
type="submit"
|
type="submit"
|
||||||
[disabled]="!activityForm.valid"
|
[disabled]="!activityForm.valid"
|
||||||
>
|
>
|
||||||
Save
|
<ng-container i18n>Save</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-flex justify-content-center mb-3 text-center" i18n>
|
<h3 class="d-flex justify-content-center mb-3 text-center">
|
||||||
Pricing Plans
|
Pricing Plans
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@ -20,7 +20,7 @@
|
|||||||
<div class="col-xs-12 col-md-4 mb-3">
|
<div class="col-xs-12 col-md-4 mb-3">
|
||||||
<mat-card class="d-flex flex-column h-100">
|
<mat-card class="d-flex flex-column h-100">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 i18n>Open Source</h4>
|
<h4>Open Source</h4>
|
||||||
<p>
|
<p>
|
||||||
For tech-savvy investors who prefer to run
|
For tech-savvy investors who prefer to run
|
||||||
<strong>Ghostfolio</strong> on their own infrastructure.
|
<strong>Ghostfolio</strong> on their own infrastructure.
|
||||||
@ -73,7 +73,7 @@
|
|||||||
[ngClass]="{ 'active': user?.subscription?.type === 'Basic' }"
|
[ngClass]="{ 'active': user?.subscription?.type === 'Basic' }"
|
||||||
>
|
>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 class="align-items-center d-flex" i18n>Basic</h4>
|
<h4 class="align-items-center d-flex">Basic</h4>
|
||||||
<p>
|
<p>
|
||||||
For new investors who are just getting started with trading.
|
For new investors who are just getting started with trading.
|
||||||
</p>
|
</p>
|
||||||
@ -124,7 +124,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h4 class="align-items-center d-flex">
|
<h4 class="align-items-center d-flex">
|
||||||
<span i18n>Premium</span>
|
<span>Premium</span>
|
||||||
<gf-premium-indicator
|
<gf-premium-indicator
|
||||||
class="ml-1"
|
class="ml-1"
|
||||||
[enableLink]="false"
|
[enableLink]="false"
|
||||||
@ -186,7 +186,7 @@
|
|||||||
>{{ baseCurrency }} <strong
|
>{{ baseCurrency }} <strong
|
||||||
>{{ price }}</strong
|
>{{ price }}</strong
|
||||||
></ng-container
|
></ng-container
|
||||||
> <span i18n>per year</span></span
|
> <span>per year</span></span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@ -196,14 +196,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div *ngIf="user?.subscription?.type === 'Basic'" class="row">
|
<div *ngIf="user?.subscription?.type === 'Basic'" class="row">
|
||||||
<div class="col mt-3 text-center">
|
<div class="col mt-3 text-center">
|
||||||
<a color="primary" i18n mat-flat-button [routerLink]="['/account']">
|
<a color="primary" mat-flat-button [routerLink]="['/account']">
|
||||||
Upgrade Plan
|
Upgrade Plan
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!user" class="row">
|
<div *ngIf="!user" class="row">
|
||||||
<div class="col mt-3 text-center">
|
<div class="col mt-3 text-center">
|
||||||
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
|
<a color="primary" mat-flat-button [routerLink]="['/register']">
|
||||||
Get Started
|
Get Started
|
||||||
</a>
|
</a>
|
||||||
<p class="text-muted"><small>It's free</small></p>
|
<p class="text-muted"><small>It's free</small></p>
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="h4 mb-3 text-center" i18n>
|
<h3 class="h4 mb-3 text-center" i18n>
|
||||||
Hello, someone has shared a <strong>Portfolio</strong> with you!
|
Hello, {{ portfolioPublicDetails?.alias ?? 'someone' }} has shared a
|
||||||
|
<strong>Portfolio</strong> with you!
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -82,27 +83,30 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Developed Markets"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[value]="markets?.developedMarkets?.value"
|
[value]="markets?.developedMarkets?.value"
|
||||||
></gf-value>
|
>Developed Markets</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Emerging Markets"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[value]="markets?.emergingMarkets?.value"
|
[value]="markets?.emergingMarkets?.value"
|
||||||
></gf-value>
|
>Emerging Markets</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Other Markets"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[value]="markets?.otherMarkets?.value"
|
[value]="markets?.otherMarkets?.value"
|
||||||
></gf-value>
|
>Other Markets</gf-value
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
@ -129,8 +133,8 @@
|
|||||||
Ghostfolio empowers you to keep track of your wealth.
|
Ghostfolio empowers you to keep track of your wealth.
|
||||||
</p>
|
</p>
|
||||||
<div class="py-2 text-center">
|
<div class="py-2 text-center">
|
||||||
<a color="primary" href="https://ghostfol.io" i18n mat-flat-button>
|
<a color="primary" href="https://ghostfol.io" mat-flat-button>
|
||||||
Get Started
|
<ng-container i18n>Get Started</ng-container>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,12 +20,11 @@
|
|||||||
<button
|
<button
|
||||||
class="d-inline-block"
|
class="d-inline-block"
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="!demoAuthToken || info?.isReadOnlyMode"
|
[disabled]="!demoAuthToken || info?.isReadOnlyMode"
|
||||||
(click)="createAccount()"
|
(click)="createAccount()"
|
||||||
>
|
>
|
||||||
Create Account
|
<ng-container i18n>Create Account</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<ng-container *ngIf="hasPermissionForSocialLogin">
|
<ng-container *ngIf="hasPermissionForSocialLogin">
|
||||||
<div class="my-3 text-muted" i18n>or</div>
|
<div class="my-3 text-muted" i18n>or</div>
|
||||||
|
@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
|
|||||||
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
|
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ component: WebauthnPageComponent, path: '', title: $localize`Login` }
|
{ component: WebauthnPageComponent, path: '', title: $localize`Sign in` }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -14,21 +14,20 @@
|
|||||||
*ngIf="hasError"
|
*ngIf="hasError"
|
||||||
class="align-items-center col d-flex flex-column justify-content-center"
|
class="align-items-center col d-flex flex-column justify-content-center"
|
||||||
>
|
>
|
||||||
<h1 class="d-flex h5 justify-content-center mb-0 text-center" i18n>
|
<h1 class="d-flex h5 justify-content-center mb-0 text-center">
|
||||||
Oops, authentication has failed.
|
<ng-container i18n>Oops, authentication has failed.</ng-container>
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<button
|
||||||
class="mb-3 mt-4"
|
class="mb-3 mt-4"
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
(click)="signIn()"
|
(click)="signIn()"
|
||||||
>
|
>
|
||||||
Try again
|
<ng-container i18n>Try again</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<div class="text-muted" i18n>or</div>
|
<div class="text-muted"><ng-container i18n>or</ng-container></div>
|
||||||
<button class="mt-1" i18n mat-flat-button (click)="deregisterDevice()">
|
<button class="mt-1" mat-flat-button (click)="deregisterDevice()">
|
||||||
Go back to Home Page
|
<ng-container i18n>Go back to Home Page</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -133,8 +133,32 @@ export class DataService {
|
|||||||
return this.http.get<AdminData>('/api/v1/admin');
|
return this.http.get<AdminData>('/api/v1/admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchAdminMarketData() {
|
public fetchAdminMarketData({ filters }: { filters?: Filter[] }) {
|
||||||
return this.http.get<AdminMarketData>('/api/v1/admin/market-data');
|
let params = new HttpParams();
|
||||||
|
|
||||||
|
if (filters?.length > 0) {
|
||||||
|
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
|
||||||
|
filters,
|
||||||
|
(filter) => {
|
||||||
|
return filter.type;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filtersByAssetSubClass) {
|
||||||
|
params = params.append(
|
||||||
|
'assetSubClasses',
|
||||||
|
filtersByAssetSubClass
|
||||||
|
.map(({ id }) => {
|
||||||
|
return id;
|
||||||
|
})
|
||||||
|
.join(',')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<AdminMarketData>('/api/v1/admin/market-data', {
|
||||||
|
params
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteAccess(aId: string) {
|
public deleteAccess(aId: string) {
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
|||||||
import * as currencies from '@dinero.js/currencies';
|
import * as currencies from '@dinero.js/currencies';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
||||||
|
import { de } from 'date-fns/locale';
|
||||||
|
|
||||||
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
||||||
import { Benchmark } from './interfaces';
|
import { Benchmark } from './interfaces';
|
||||||
@ -56,6 +57,14 @@ export function getCssVariable(aCssVariable: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDateFnsLocale(aLanguageCode: string) {
|
||||||
|
if (aLanguageCode === 'de') {
|
||||||
|
return de;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function getDateFormatString(aLocale?: string) {
|
export function getDateFormatString(aLocale?: string) {
|
||||||
const formatObject = new Intl.DateTimeFormat(aLocale).formatToParts(
|
const formatObject = new Intl.DateTimeFormat(aLocale).formatToParts(
|
||||||
new Date()
|
new Date()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export interface Access {
|
export interface Access {
|
||||||
granteeAlias: string;
|
alias?: string;
|
||||||
|
grantee: string;
|
||||||
id: string;
|
id: string;
|
||||||
type: 'PUBLIC' | 'RESTRICTED_VIEW';
|
type: 'PUBLIC' | 'RESTRICTED_VIEW';
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import { DataSource } from '@prisma/client';
|
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
export interface AdminMarketData {
|
export interface AdminMarketData {
|
||||||
marketData: AdminMarketDataItem[];
|
marketData: AdminMarketDataItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminMarketDataItem {
|
export interface AdminMarketDataItem {
|
||||||
|
assetClass?: AssetClass;
|
||||||
|
assetSubClass?: AssetSubClass;
|
||||||
|
countriesCount: number;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
date?: Date;
|
date?: Date;
|
||||||
marketDataItemCount?: number;
|
marketDataItemCount: number;
|
||||||
|
sectorsCount: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export interface Filter {
|
export interface Filter {
|
||||||
id: string;
|
id: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'ACCOUNT' | 'ASSET_CLASS' | 'SYMBOL' | 'TAG';
|
type: 'ACCOUNT' | 'ASSET_CLASS' | 'ASSET_SUB_CLASS' | 'SYMBOL' | 'TAG';
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
export interface PortfolioPublicDetails {
|
export interface PortfolioPublicDetails {
|
||||||
|
alias?: string;
|
||||||
hasDetails: boolean;
|
hasDetails: boolean;
|
||||||
holdings: {
|
holdings: {
|
||||||
[symbol: string]: Pick<
|
[symbol: string]: Pick<
|
||||||
|
@ -3,6 +3,7 @@ import { ViewMode } from '@prisma/client';
|
|||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
baseCurrency?: string;
|
baseCurrency?: string;
|
||||||
isRestrictedView?: boolean;
|
isRestrictedView?: boolean;
|
||||||
|
language?: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
viewMode?: ViewMode;
|
viewMode?: ViewMode;
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,8 @@
|
|||||||
></td>
|
></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="date">
|
<ng-container matColumnDef="date">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
Date
|
<ng-container i18n>Date</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
@ -48,8 +48,8 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="type">
|
<ng-container matColumnDef="type">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
Type
|
<ng-container i18n>Type</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" mat-cell class="px-1">
|
<td *matCellDef="let element" mat-cell class="px-1">
|
||||||
<div
|
<div
|
||||||
@ -80,8 +80,8 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="symbol">
|
<ng-container matColumnDef="symbol">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
Symbol
|
<ng-container i18n>Symbol</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
@ -103,11 +103,10 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell px-1"
|
class="d-none d-lg-table-cell px-1"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Currency
|
<ng-container i18n>Currency</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td
|
<td
|
||||||
*matCellDef="let element"
|
*matCellDef="let element"
|
||||||
@ -125,11 +124,10 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Quantity
|
<ng-container i18n>Quantity</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td
|
<td
|
||||||
*matCellDef="let element"
|
*matCellDef="let element"
|
||||||
@ -155,11 +153,10 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Unit Price
|
<ng-container i18n>Unit Price</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td
|
<td
|
||||||
*matCellDef="let element"
|
*matCellDef="let element"
|
||||||
@ -185,11 +182,10 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Fee
|
<ng-container i18n>Fee</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td
|
<td
|
||||||
*matCellDef="let element"
|
*matCellDef="let element"
|
||||||
@ -219,11 +215,10 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Value
|
<ng-container i18n>Value</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td
|
<td
|
||||||
*matCellDef="let element"
|
*matCellDef="let element"
|
||||||
@ -255,11 +250,10 @@
|
|||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-lg-none d-xl-none justify-content-end px-1"
|
class="d-lg-none d-xl-none justify-content-end px-1"
|
||||||
i18n
|
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Value
|
<ng-container i18n>Value</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-lg-none d-xl-none px-1" mat-cell>
|
<td *matCellDef="let element" class="d-lg-none d-xl-none px-1" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
|
@ -65,8 +65,6 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
public totalFees: number;
|
public totalFees: number;
|
||||||
public totalValue: number;
|
public totalValue: number;
|
||||||
|
|
||||||
private readonly SEARCH_PLACEHOLDER =
|
|
||||||
'Filter by account, currency, symbol or type...';
|
|
||||||
private readonly SEARCH_STRING_SEPARATOR = ',';
|
private readonly SEARCH_STRING_SEPARATOR = ',';
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
@ -289,7 +287,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.placeholder =
|
this.placeholder =
|
||||||
lowercaseSearchKeywords.length <= 0 ? this.SEARCH_PLACEHOLDER : '';
|
lowercaseSearchKeywords.length <= 0
|
||||||
|
? $localize`Filter by account, currency, symbol or type...`
|
||||||
|
: '';
|
||||||
|
|
||||||
this.searchKeywords = filters.map((filter) => {
|
this.searchKeywords = filters.map((filter) => {
|
||||||
return filter.label;
|
return filter.label;
|
||||||
|
@ -31,17 +31,18 @@
|
|||||||
step="0.25"
|
step="0.25"
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
<span class="ml-2" i18n matSuffix>%</span>
|
<span class="ml-2" matSuffix>%</span>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Projected Total Amount"
|
i18n
|
||||||
size="large"
|
size="large"
|
||||||
[currency]="currency"
|
[currency]="currency"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="projectedTotalAmount"
|
[value]="projectedTotalAmount"
|
||||||
></gf-value>
|
>Projected Total Amount</gf-value
|
||||||
|
>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9 text-center">
|
<div class="col-md-9 text-center">
|
||||||
|
@ -273,7 +273,7 @@ export class FireCalculatorComponent
|
|||||||
const datasetDeposit = {
|
const datasetDeposit = {
|
||||||
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||||
data: [],
|
data: [],
|
||||||
label: 'Deposit'
|
label: $localize`Deposit`
|
||||||
};
|
};
|
||||||
|
|
||||||
const datasetInterest = {
|
const datasetInterest = {
|
||||||
@ -283,7 +283,7 @@ export class FireCalculatorComponent
|
|||||||
.lighten(0.5)
|
.lighten(0.5)
|
||||||
.hex(),
|
.hex(),
|
||||||
data: [],
|
data: [],
|
||||||
label: 'Interest'
|
label: $localize`Interest`
|
||||||
};
|
};
|
||||||
|
|
||||||
const datasetSavings = {
|
const datasetSavings = {
|
||||||
@ -293,7 +293,7 @@ export class FireCalculatorComponent
|
|||||||
.lighten(0.25)
|
.lighten(0.25)
|
||||||
.hex(),
|
.hex(),
|
||||||
data: [],
|
data: [],
|
||||||
label: 'Savings'
|
label: $localize`Savings`
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let period = 1; period <= t; period++) {
|
for (let period = 1; period <= t; period++) {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
<ng-template #label><ng-content></ng-content></ng-template>
|
||||||
<ng-container *ngIf="value || value === 0 || value === null">
|
<ng-container *ngIf="value || value === 0 || value === null">
|
||||||
<div
|
<div
|
||||||
class="d-flex"
|
class="d-flex"
|
||||||
@ -43,13 +44,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="label">
|
<ng-container>
|
||||||
<div *ngIf="size === 'large'">
|
<div *ngIf="size === 'large'">
|
||||||
<span class="h6">{{ label }}</span>
|
<span class="h6"
|
||||||
|
><ng-container *ngTemplateOutlet="label"></ng-container
|
||||||
|
></span>
|
||||||
<span *ngIf="subLabel" class="text-muted"> {{ subLabel }}</span>
|
<span *ngIf="subLabel" class="text-muted"> {{ subLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
<small *ngIf="size !== 'large'">
|
<small *ngIf="size !== 'large'">
|
||||||
{{ label }}
|
<ng-container *ngTemplateOutlet="label"></ng-container>
|
||||||
</small>
|
</small>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -31,7 +31,6 @@ Currency.args = {
|
|||||||
|
|
||||||
export const Label = Template.bind({});
|
export const Label = Template.bind({});
|
||||||
Label.args = {
|
Label.args = {
|
||||||
label: 'Label',
|
|
||||||
locale: 'en-US',
|
locale: 'en-US',
|
||||||
value: 7.25
|
value: 7.25
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,6 @@ export class ValueComponent implements OnChanges {
|
|||||||
@Input() isCurrency = false;
|
@Input() isCurrency = false;
|
||||||
@Input() isDate = false;
|
@Input() isDate = false;
|
||||||
@Input() isPercent = false;
|
@Input() isPercent = false;
|
||||||
@Input() label = '';
|
|
||||||
@Input() locale = getLocale();
|
@Input() locale = getLocale();
|
||||||
@Input() position = '';
|
@Input() position = '';
|
||||||
@Input() precision: number | undefined;
|
@Input() precision: number | undefined;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.180.1",
|
"version": "1.185.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -14,10 +14,9 @@
|
|||||||
"affected:test": "nx affected:test",
|
"affected:test": "nx affected:test",
|
||||||
"angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng",
|
"angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng",
|
||||||
"build:all": "nx run api:build:production && nx run client:build:production --localize && yarn replace-placeholders-in-build",
|
"build:all": "nx run api:build:production && nx run client:build:production --localize && yarn replace-placeholders-in-build",
|
||||||
"build:dev": "nx run api:build && nx run client:build && yarn replace-placeholders-in-build",
|
"build:dev": "nx run api:build && nx run client:build --localize && yarn replace-placeholders-in-build",
|
||||||
"build:storybook": "nx run ui:build-storybook",
|
"build:storybook": "nx run ui:build-storybook",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"database:baseline": "sh ./prisma/baseline.sh",
|
|
||||||
"database:format-schema": "prisma format",
|
"database:format-schema": "prisma format",
|
||||||
"database:generate-typings": "prisma generate",
|
"database:generate-typings": "prisma generate",
|
||||||
"database:gui": "prisma studio",
|
"database:gui": "prisma studio",
|
||||||
@ -25,7 +24,7 @@
|
|||||||
"database:migrate": "prisma migrate deploy",
|
"database:migrate": "prisma migrate deploy",
|
||||||
"database:push": "prisma db push",
|
"database:push": "prisma db push",
|
||||||
"database:seed": "prisma db seed",
|
"database:seed": "prisma db seed",
|
||||||
"database:setup": "yarn database:push && yarn database:seed && yarn database:baseline",
|
"database:setup": "yarn database:push && yarn database:seed",
|
||||||
"database:validate": "prisma validate",
|
"database:validate": "prisma validate",
|
||||||
"dep-graph": "nx dep-graph",
|
"dep-graph": "nx dep-graph",
|
||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
@ -42,7 +41,7 @@
|
|||||||
"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": "ng serve client --configuration=development-en --hmr -o",
|
||||||
"start:prod": "node apps/api/main",
|
"start:prod": "yarn database:migrate && yarn database:seed && node main",
|
||||||
"start:server": "nx serve api --watch",
|
"start:server": "nx serve api --watch",
|
||||||
"start:storybook": "nx run ui:storybook",
|
"start:storybook": "nx run ui:storybook",
|
||||||
"test": "nx test",
|
"test": "nx test",
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# List all migration scripts based on the directory name and mark the migration as "applied"
|
|
||||||
|
|
||||||
for directory in ./prisma/migrations/*/; do
|
|
||||||
migration=$(echo "$directory" | sed 's/.\/prisma\/migrations\///' | sed 's/\///')
|
|
||||||
yarn prisma migrate resolve --applied $migration
|
|
||||||
done
|
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Access" ADD COLUMN "alias" TEXT;
|
@ -1,7 +1,7 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
previewFeatures = []
|
previewFeatures = []
|
||||||
binaryTargets = ["debian-openssl-1.1.x", "native"]
|
binaryTargets = ["debian-openssl-1.1.x", "linux-arm64-openssl-1.1.x", "native"]
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
@ -10,6 +10,7 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Access {
|
model Access {
|
||||||
|
alias String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
granteeUserId String?
|
granteeUserId String?
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
set -xe
|
|
||||||
echo "$DOCKER_HUB_ACCESS_TOKEN" | docker login -u "$DOCKER_HUB_USERNAME" --password-stdin
|
|
||||||
|
|
||||||
docker build -t ghostfolio/ghostfolio:$TRAVIS_TAG -t ghostfolio/ghostfolio:latest .
|
|
||||||
docker push ghostfolio/ghostfolio --all-tags
|
|
Reference in New Issue
Block a user