Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
cc16ba5dc8 | |||
d10227bc39 | |||
4e214c32e8 | |||
49e2862e03 | |||
34e33a2400 | |||
ec9bc984af | |||
2388c494df | |||
d71ab10eed | |||
0e0592180f | |||
60e2aff488 | |||
7b5454e7de | |||
30835ced88 | |||
8897f32bc5 | |||
abaa6b5f27 | |||
2060fcaf0b | |||
fd2408dd62 | |||
31cca024f1 | |||
b535122945 | |||
5113e4e3ad | |||
35e039748f | |||
c6b9e0aa5b | |||
b250491ca5 | |||
61e501c659 | |||
c0f19d56ec | |||
8e2b235b1f | |||
c3407e9b34 | |||
74193e4ee2 | |||
3fe8f9c882 | |||
d130efad47 | |||
109f0ebd70 | |||
069ddcc6b2 | |||
f7bf6e652b | |||
eb059a024a | |||
ad88acff1c | |||
1ff736537c | |||
1fa65e1efd | |||
df6bb489c2 | |||
928a13310d | |||
2384861953 | |||
fe90bda6fb | |||
d4b29ff11c | |||
a0a26cfa58 | |||
1610150427 | |||
cff8acd7b1 | |||
0f36d6cbdb | |||
046e28b521 | |||
aba562cb35 | |||
03f2f33344 | |||
a996dd7ed5 | |||
002b883668 | |||
0b06823893 | |||
2dfd779444 | |||
1824413379 | |||
3332ade3d3 | |||
8d2e110e3d | |||
a8fcf09380 | |||
1071f446a8 |
127
CHANGELOG.md
127
CHANGELOG.md
@ -5,7 +5,132 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.158.0 - 12.06.2022
|
||||
## 1.169.0 - 14.07.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for the cryptocurrency _Songbird_ (`SGB1-USD`)
|
||||
- Added support for the cryptocurrency _Terra 2.0_ (`LUNA2-USD`)
|
||||
- Added a blog post
|
||||
|
||||
### Changed
|
||||
|
||||
- Refreshed the cryptocurrencies list to support more coins by default
|
||||
- Upgraded `date-fns` from version `2.22.1` to `2.28.0`
|
||||
|
||||
## 1.168.0 - 10.07.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Extended the investment timeline grouped by month
|
||||
|
||||
### Changed
|
||||
|
||||
- Handled an occasional currency pair inconsistency in the _Yahoo Finance_ service (`GBP=X` instead of `USDGBP=X`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the content height of the account detail dialog
|
||||
|
||||
## 1.167.0 - 07.07.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added _Markets_ to the public pages
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the _Create Account_ link in the _Live Demo_
|
||||
- Upgraded `ngx-markdown` from version `13.0.0` to `14.0.1`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the _Holdings_ section for users without a subscription
|
||||
|
||||
## 1.166.0 - 30.06.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added an account detail dialog
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the label of the (symbol) search
|
||||
- Refactored the demo account as a route (`/demo`)
|
||||
- Upgraded `nestjs` from version `8.2.3` to `8.4.7`
|
||||
- Upgraded `prisma` from version `3.14.0` to `3.15.2`
|
||||
- Upgraded `yahoo-finance2` from version `2.3.2` to `2.3.3`
|
||||
- Upgraded `zone.js` from version `0.11.4` to `0.11.6`
|
||||
|
||||
## 1.165.0 - 25.06.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added an icon and name column to the positions table
|
||||
- Added a reusable premium indicator component
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved the positions table to a dedicated section (_Holdings_)
|
||||
- Changed the data gathering by symbol endpoint to delete data first
|
||||
|
||||
## 1.164.0 - 23.06.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added the positions table including performance to the public page
|
||||
|
||||
## 1.163.0 - 22.06.2022
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the onboarding for iOS
|
||||
|
||||
## 1.162.0 - 18.06.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added a _Privacy Policy_ page
|
||||
|
||||
### Changed
|
||||
|
||||
- Simplified the header
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the currency inconsistency in the _Yahoo Finance_ service (convert from `ILA` to `ILS`)
|
||||
|
||||
## 1.161.1 - 16.06.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added the vertical hover line to inspect data points in the performance chart on the home page
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the landing page
|
||||
- Upgraded `angular` from version `13.3.6` to `14.0.2`
|
||||
- Upgraded `Nx` from version `14.1.4` to `14.3.5`
|
||||
- Upgraded `storybook` from version `6.4.22` to `6.5.9`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved the error handling of missing market prices
|
||||
|
||||
## 1.160.0 - 15.06.2022
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the `No data provider has been found` error in the search (regression after `envalid` upgrade to `7.3.1` in Ghostfolio `1.157.0`)
|
||||
|
||||
## 1.159.0 - 15.06.2022
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the default `HOST` to `0.0.0.0`
|
||||
- Refactored the endpoint of the public page (filter by equity)
|
||||
|
||||
## 1.158.1 - 12.06.2022
|
||||
|
||||
### Added
|
||||
|
||||
|
@ -12,7 +12,7 @@ COPY ./package.json package.json
|
||||
COPY ./yarn.lock yarn.lock
|
||||
COPY ./prisma/schema.prisma prisma/schema.prisma
|
||||
|
||||
RUN apk add --no-cache python3 g++ make openssl
|
||||
RUN apk add --no-cache python3 g++ make openssl git
|
||||
RUN yarn install
|
||||
|
||||
# See https://github.com/nrwl/nx/issues/6586 for further details
|
||||
@ -22,7 +22,7 @@ RUN node decorate-angular-cli.js
|
||||
COPY ./angular.json angular.json
|
||||
COPY ./nx.json nx.json
|
||||
COPY ./replace.build.js replace.build.js
|
||||
COPY ./jest.preset.ts jest.preset.ts
|
||||
COPY ./jest.preset.js jest.preset.js
|
||||
COPY ./jest.config.ts jest.config.ts
|
||||
COPY ./tsconfig.base.json tsconfig.base.json
|
||||
COPY ./libs libs
|
||||
|
@ -12,7 +12,7 @@
|
||||
<strong>Open Source Wealth Management Software</strong>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
||||
<a href="https://ghostfol.io"><strong>Ghostfol.io</strong></a> | <a href="https://ghostfol.io/demo"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#contributing">
|
||||
@ -35,7 +35,7 @@
|
||||
|
||||
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.
|
||||
|
||||
If you prefer to run Ghostfolio on your own infrastructure (self-hosting), please find further instructions in the section [Run with Docker](#run-with-docker-self-hosting).
|
||||
If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the [Self-hosting](#self-hosting) section.
|
||||
|
||||
## Why Ghostfolio?
|
||||
|
||||
@ -136,7 +136,7 @@ Open http://localhost:3333 in your browser and accomplish these steps:
|
||||
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
|
||||
1. Then, run the following command to keep your database schema in sync: `docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:migrate`
|
||||
|
||||
### Run with _Unraid_ (unofficial)
|
||||
### Run with _Unraid_ (Community)
|
||||
|
||||
Please follow the instructions of the Ghostfolio [Unraid Community App](https://unraid.net/community/apps?q=ghostfolio).
|
||||
|
||||
@ -186,7 +186,7 @@ yarn database:push
|
||||
|
||||
Run `yarn test`
|
||||
|
||||
## Public API (experimental)
|
||||
## Public API
|
||||
|
||||
### Import Activities
|
||||
|
||||
|
30
angular.json
30
angular.json
@ -2,6 +2,7 @@
|
||||
"version": 1,
|
||||
"projects": {
|
||||
"api": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"root": "apps/api",
|
||||
"sourceRoot": "apps/api/src",
|
||||
"projectType": "application",
|
||||
@ -56,6 +57,7 @@
|
||||
"tags": []
|
||||
},
|
||||
"client": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
@ -113,7 +115,7 @@
|
||||
}
|
||||
],
|
||||
"styles": ["apps/client/src/styles.scss"],
|
||||
"scripts": ["node_modules/marked/lib/marked.js"],
|
||||
"scripts": ["node_modules/marked/marked.min.js"],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
@ -189,6 +191,7 @@
|
||||
"tags": []
|
||||
},
|
||||
"client-e2e": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"root": "apps/client-e2e",
|
||||
"sourceRoot": "apps/client-e2e/src",
|
||||
"projectType": "application",
|
||||
@ -211,6 +214,7 @@
|
||||
"implicitDependencies": ["client"]
|
||||
},
|
||||
"common": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"root": "libs/common",
|
||||
"sourceRoot": "libs/common/src",
|
||||
"projectType": "library",
|
||||
@ -233,6 +237,7 @@
|
||||
"tags": []
|
||||
},
|
||||
"ui": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
@ -258,14 +263,12 @@
|
||||
}
|
||||
},
|
||||
"storybook": {
|
||||
"builder": "@nrwl/storybook:storybook",
|
||||
"builder": "@storybook/angular:start-storybook",
|
||||
"options": {
|
||||
"uiFramework": "@storybook/angular",
|
||||
"port": 4400,
|
||||
"config": {
|
||||
"configFolder": "libs/ui/.storybook"
|
||||
},
|
||||
"projectBuildConfig": "ui:build-storybook"
|
||||
"configDir": "libs/ui/.storybook",
|
||||
"browserTarget": "ui:build-storybook",
|
||||
"compodoc": false
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
@ -274,15 +277,13 @@
|
||||
}
|
||||
},
|
||||
"build-storybook": {
|
||||
"builder": "@nrwl/storybook:build",
|
||||
"builder": "@storybook/angular:build-storybook",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"uiFramework": "@storybook/angular",
|
||||
"outputPath": "dist/storybook/ui",
|
||||
"config": {
|
||||
"configFolder": "libs/ui/.storybook"
|
||||
},
|
||||
"projectBuildConfig": "ui:build-storybook"
|
||||
"outputDir": "dist/storybook/ui",
|
||||
"configDir": "libs/ui/.storybook",
|
||||
"browserTarget": "ui:build-storybook",
|
||||
"compodoc": false
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
@ -294,6 +295,7 @@
|
||||
"tags": []
|
||||
},
|
||||
"ui-e2e": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"root": "apps/ui-e2e",
|
||||
"sourceRoot": "apps/ui-e2e/src",
|
||||
"projectType": "application",
|
||||
|
@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
displayName: 'api',
|
||||
|
||||
globals: {
|
||||
@ -13,5 +13,5 @@ module.exports = {
|
||||
coverageDirectory: '../../coverage/apps/api',
|
||||
testTimeout: 10000,
|
||||
testEnvironment: 'node',
|
||||
preset: '../../jest.preset.ts'
|
||||
preset: '../../jest.preset.js'
|
||||
};
|
||||
|
@ -7,7 +7,10 @@ import {
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { Accounts } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type {
|
||||
AccountWithValue,
|
||||
RequestWithUser
|
||||
} from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
@ -123,13 +126,45 @@ export class AccountController {
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getAccountById(@Param('id') id: string): Promise<AccountModel> {
|
||||
return this.accountService.account({
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
});
|
||||
public async getAccountById(
|
||||
@Headers('impersonation-id') impersonationId,
|
||||
@Param('id') id: string
|
||||
): Promise<AccountWithValue> {
|
||||
const impersonationUserId =
|
||||
await this.impersonationService.validateImpersonationId(
|
||||
impersonationId,
|
||||
this.request.user.id
|
||||
);
|
||||
|
||||
let accountsWithAggregations =
|
||||
await this.portfolioService.getAccountsWithAggregations(
|
||||
impersonationUserId || this.request.user.id,
|
||||
[{ id, type: 'ACCOUNT' }]
|
||||
);
|
||||
|
||||
if (
|
||||
impersonationUserId ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
accountsWithAggregations = {
|
||||
...nullifyValuesInObject(accountsWithAggregations, [
|
||||
'totalBalanceInBaseCurrency',
|
||||
'totalValueInBaseCurrency'
|
||||
]),
|
||||
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
|
||||
'balance',
|
||||
'balanceInBaseCurrency',
|
||||
'convertedBalance',
|
||||
'fee',
|
||||
'quantity',
|
||||
'unitPrice',
|
||||
'value',
|
||||
'valueInBaseCurrency'
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
return accountsWithAggregations.accounts[0];
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
@ -3,8 +3,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
|
||||
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { Controller, Get, UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||
|
||||
import { BenchmarkService } from './benchmark.service';
|
||||
|
||||
@ -16,7 +15,6 @@ export class BenchmarkController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getBenchmark(): Promise<BenchmarkResponse> {
|
||||
|
@ -63,6 +63,8 @@ export class InfoService {
|
||||
} else {
|
||||
info.fearAndGreedDataSource = ghostfolioFearAndGreedIndexDataSource;
|
||||
}
|
||||
|
||||
globalPermissions.push(permissions.enableFearAndGreedIndex);
|
||||
}
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) {
|
||||
|
@ -4,6 +4,7 @@ import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/
|
||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { Filter } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
@ -17,6 +18,7 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
@ -66,8 +68,36 @@ export class OrderController {
|
||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getAllOrders(
|
||||
@Headers('impersonation-id') impersonationId
|
||||
@Headers('impersonation-id') impersonationId,
|
||||
@Query('accounts') filterByAccounts?: string,
|
||||
@Query('assetClasses') filterByAssetClasses?: string,
|
||||
@Query('tags') filterByTags?: string
|
||||
): Promise<Activities> {
|
||||
const accountIds = filterByAccounts?.split(',') ?? [];
|
||||
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
||||
const tagIds = filterByTags?.split(',') ?? [];
|
||||
|
||||
const filters: Filter[] = [
|
||||
...accountIds.map((accountId) => {
|
||||
return <Filter>{
|
||||
id: accountId,
|
||||
type: 'ACCOUNT'
|
||||
};
|
||||
}),
|
||||
...assetClasses.map((assetClass) => {
|
||||
return <Filter>{
|
||||
id: assetClass,
|
||||
type: 'ASSET_CLASS'
|
||||
};
|
||||
}),
|
||||
...tagIds.map((tagId) => {
|
||||
return <Filter>{
|
||||
id: tagId,
|
||||
type: 'TAG'
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
const impersonationUserId =
|
||||
await this.impersonationService.validateImpersonationId(
|
||||
impersonationId,
|
||||
@ -76,6 +106,7 @@ export class OrderController {
|
||||
const userCurrency = this.request.user.Settings.currency;
|
||||
|
||||
let activities = await this.orderService.getOrders({
|
||||
filters,
|
||||
userCurrency,
|
||||
includeDrafts: true,
|
||||
userId: impersonationUserId || this.request.user.id
|
||||
|
@ -14,8 +14,11 @@ import {
|
||||
format,
|
||||
isAfter,
|
||||
isBefore,
|
||||
isSameMonth,
|
||||
isSameYear,
|
||||
max,
|
||||
min
|
||||
min,
|
||||
set
|
||||
} from 'date-fns';
|
||||
import { first, flatten, isNumber, sortBy } from 'lodash';
|
||||
|
||||
@ -323,6 +326,46 @@ export class PortfolioCalculator {
|
||||
});
|
||||
}
|
||||
|
||||
public getInvestmentsByMonth(): { date: string; investment: Big }[] {
|
||||
if (this.orders.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const investments = [];
|
||||
let currentDate = parseDate(this.orders[0].date);
|
||||
let investmentByMonth = new Big(0);
|
||||
|
||||
for (const [index, order] of this.orders.entries()) {
|
||||
if (
|
||||
isSameMonth(parseDate(order.date), currentDate) &&
|
||||
isSameYear(parseDate(order.date), currentDate)
|
||||
) {
|
||||
investmentByMonth = investmentByMonth.plus(
|
||||
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
|
||||
);
|
||||
|
||||
if (index === this.orders.length - 1) {
|
||||
investments.push({
|
||||
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
|
||||
investment: investmentByMonth
|
||||
});
|
||||
}
|
||||
} else {
|
||||
investments.push({
|
||||
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
|
||||
investment: investmentByMonth
|
||||
});
|
||||
|
||||
currentDate = parseDate(order.date);
|
||||
investmentByMonth = order.quantity
|
||||
.mul(order.unitPrice)
|
||||
.mul(this.getFactor(order.type));
|
||||
}
|
||||
}
|
||||
|
||||
return investments;
|
||||
}
|
||||
|
||||
public async calculateTimeline(
|
||||
timelineSpecification: TimelineSpecification[],
|
||||
endDate: string
|
||||
|
@ -20,7 +20,12 @@ import {
|
||||
PortfolioReport,
|
||||
PortfolioSummary
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import type {
|
||||
DateRange,
|
||||
GroupBy,
|
||||
RequestWithUser
|
||||
} from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
@ -190,21 +195,35 @@ export class PortfolioController {
|
||||
}
|
||||
}
|
||||
|
||||
const isBasicUser =
|
||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||
this.request.user.subscription.type === 'Basic';
|
||||
let hasDetails = true;
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||
hasDetails = this.request.user.subscription.type === 'Premium';
|
||||
}
|
||||
|
||||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||
holdings[symbol] = {
|
||||
...portfolioPosition,
|
||||
assetClass: hasDetails ? portfolioPosition.assetClass : undefined,
|
||||
assetSubClass: hasDetails ? portfolioPosition.assetSubClass : undefined,
|
||||
countries: hasDetails ? portfolioPosition.countries : [],
|
||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||
markets: hasDetails ? portfolioPosition.markets : undefined,
|
||||
sectors: hasDetails ? portfolioPosition.sectors : []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accounts,
|
||||
hasError,
|
||||
holdings: isBasicUser ? {} : holdings
|
||||
holdings
|
||||
};
|
||||
}
|
||||
|
||||
@Get('investments')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getInvestments(
|
||||
@Headers('impersonation-id') impersonationId: string
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Query('groupBy') groupBy?: GroupBy
|
||||
): Promise<PortfolioInvestments> {
|
||||
if (
|
||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||
@ -216,9 +235,16 @@ export class PortfolioController {
|
||||
);
|
||||
}
|
||||
|
||||
let investments = await this.portfolioService.getInvestments(
|
||||
impersonationId
|
||||
);
|
||||
let investments: InvestmentItem[];
|
||||
|
||||
if (groupBy === 'month') {
|
||||
investments = await this.portfolioService.getInvestments(
|
||||
impersonationId,
|
||||
'month'
|
||||
);
|
||||
} else {
|
||||
investments = await this.portfolioService.getInvestments(impersonationId);
|
||||
}
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
@ -316,7 +342,9 @@ export class PortfolioController {
|
||||
|
||||
const { holdings } = await this.portfolioService.getDetails(
|
||||
access.userId,
|
||||
access.userId
|
||||
access.userId,
|
||||
'max',
|
||||
[{ id: 'EQUITY', type: 'ASSET_CLASS' }]
|
||||
);
|
||||
|
||||
const portfolioPublicDetails: PortfolioPublicDetails = {
|
||||
@ -325,9 +353,6 @@ export class PortfolioController {
|
||||
};
|
||||
|
||||
const totalValue = Object.values(holdings)
|
||||
.filter((holding) => {
|
||||
return holding.assetClass === 'EQUITY';
|
||||
})
|
||||
.map((portfolioPosition) => {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||
@ -338,17 +363,18 @@ export class PortfolioController {
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||
if (portfolioPosition.assetClass === 'EQUITY') {
|
||||
portfolioPublicDetails.holdings[symbol] = {
|
||||
allocationCurrent: portfolioPosition.allocationCurrent,
|
||||
countries: hasDetails ? portfolioPosition.countries : [],
|
||||
currency: portfolioPosition.currency,
|
||||
markets: portfolioPosition.markets,
|
||||
name: portfolioPosition.name,
|
||||
sectors: hasDetails ? portfolioPosition.sectors : [],
|
||||
value: portfolioPosition.value / totalValue
|
||||
};
|
||||
}
|
||||
portfolioPublicDetails.holdings[symbol] = {
|
||||
allocationCurrent: portfolioPosition.value / totalValue,
|
||||
countries: hasDetails ? portfolioPosition.countries : [],
|
||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||
markets: hasDetails ? portfolioPosition.markets : undefined,
|
||||
name: portfolioPosition.name,
|
||||
netPerformancePercent: portfolioPosition.netPerformancePercent,
|
||||
sectors: hasDetails ? portfolioPosition.sectors : [],
|
||||
symbol: portfolioPosition.symbol,
|
||||
url: portfolioPosition.url,
|
||||
value: portfolioPosition.value / totalValue
|
||||
};
|
||||
}
|
||||
|
||||
return portfolioPublicDetails;
|
||||
|
@ -41,6 +41,7 @@ import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.in
|
||||
import type {
|
||||
AccountWithValue,
|
||||
DateRange,
|
||||
GroupBy,
|
||||
Market,
|
||||
OrderWithAccount,
|
||||
RequestWithUser
|
||||
@ -50,6 +51,7 @@ import { REQUEST } from '@nestjs/core';
|
||||
import {
|
||||
AssetClass,
|
||||
DataSource,
|
||||
Prisma,
|
||||
Tag,
|
||||
Type as TypeOfOrder
|
||||
} from '@prisma/client';
|
||||
@ -63,6 +65,7 @@ import {
|
||||
max,
|
||||
parse,
|
||||
parseISO,
|
||||
set,
|
||||
setDayOfYear,
|
||||
startOfDay,
|
||||
subDays,
|
||||
@ -100,14 +103,23 @@ export class PortfolioService {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
|
||||
public async getAccounts(aUserId: string): Promise<AccountWithValue[]> {
|
||||
public async getAccounts(
|
||||
aUserId: string,
|
||||
aFilters?: Filter[]
|
||||
): Promise<AccountWithValue[]> {
|
||||
const where: Prisma.AccountWhereInput = { userId: aUserId };
|
||||
|
||||
if (aFilters?.[0].id && aFilters?.[0].type === 'ACCOUNT') {
|
||||
where.id = aFilters[0].id;
|
||||
}
|
||||
|
||||
const [accounts, details] = await Promise.all([
|
||||
this.accountService.accounts({
|
||||
where,
|
||||
include: { Order: true, Platform: true },
|
||||
orderBy: { name: 'asc' },
|
||||
where: { userId: aUserId }
|
||||
orderBy: { name: 'asc' }
|
||||
}),
|
||||
this.getDetails(aUserId, aUserId)
|
||||
this.getDetails(aUserId, aUserId, undefined, aFilters)
|
||||
]);
|
||||
|
||||
const userCurrency = this.request.user.Settings.currency;
|
||||
@ -145,8 +157,11 @@ export class PortfolioService {
|
||||
});
|
||||
}
|
||||
|
||||
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
|
||||
const accounts = await this.getAccounts(aUserId);
|
||||
public async getAccountsWithAggregations(
|
||||
aUserId: string,
|
||||
aFilters?: Filter[]
|
||||
): Promise<Accounts> {
|
||||
const accounts = await this.getAccounts(aUserId, aFilters);
|
||||
let totalBalanceInBaseCurrency = new Big(0);
|
||||
let totalValueInBaseCurrency = new Big(0);
|
||||
let transactionCount = 0;
|
||||
@ -170,7 +185,8 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
public async getInvestments(
|
||||
aImpersonationId: string
|
||||
aImpersonationId: string,
|
||||
groupBy?: GroupBy
|
||||
): Promise<InvestmentItem[]> {
|
||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||
|
||||
@ -191,28 +207,57 @@ export class PortfolioService {
|
||||
return [];
|
||||
}
|
||||
|
||||
const investments = portfolioCalculator.getInvestments().map((item) => {
|
||||
return {
|
||||
date: item.date,
|
||||
investment: item.investment.toNumber()
|
||||
};
|
||||
});
|
||||
let investments: InvestmentItem[];
|
||||
|
||||
// Add investment of today
|
||||
const investmentOfToday = investments.filter((investment) => {
|
||||
return investment.date === format(new Date(), DATE_FORMAT);
|
||||
});
|
||||
|
||||
if (investmentOfToday.length <= 0) {
|
||||
const pastInvestments = investments.filter((investment) => {
|
||||
return isBefore(parseDate(investment.date), new Date());
|
||||
if (groupBy === 'month') {
|
||||
investments = portfolioCalculator.getInvestmentsByMonth().map((item) => {
|
||||
return {
|
||||
date: item.date,
|
||||
investment: item.investment.toNumber()
|
||||
};
|
||||
});
|
||||
const lastInvestment = pastInvestments[pastInvestments.length - 1];
|
||||
|
||||
investments.push({
|
||||
date: format(new Date(), DATE_FORMAT),
|
||||
investment: lastInvestment?.investment ?? 0
|
||||
// Add investment of current month
|
||||
const dateOfCurrentMonth = format(
|
||||
set(new Date(), { date: 1 }),
|
||||
DATE_FORMAT
|
||||
);
|
||||
const investmentOfCurrentMonth = investments.filter(({ date }) => {
|
||||
return date === dateOfCurrentMonth;
|
||||
});
|
||||
|
||||
if (investmentOfCurrentMonth.length <= 0) {
|
||||
investments.push({
|
||||
date: dateOfCurrentMonth,
|
||||
investment: 0
|
||||
});
|
||||
}
|
||||
} else {
|
||||
investments = portfolioCalculator
|
||||
.getInvestments()
|
||||
.map(({ date, investment }) => {
|
||||
return {
|
||||
date,
|
||||
investment: investment.toNumber()
|
||||
};
|
||||
});
|
||||
|
||||
// Add investment of today
|
||||
const investmentOfToday = investments.filter(({ date }) => {
|
||||
return date === format(new Date(), DATE_FORMAT);
|
||||
});
|
||||
|
||||
if (investmentOfToday.length <= 0) {
|
||||
const pastInvestments = investments.filter(({ date }) => {
|
||||
return isBefore(parseDate(date), new Date());
|
||||
});
|
||||
const lastInvestment = pastInvestments[pastInvestments.length - 1];
|
||||
|
||||
investments.push({
|
||||
date: format(new Date(), DATE_FORMAT),
|
||||
investment: lastInvestment?.investment ?? 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sortBy(investments, (investment) => {
|
||||
@ -273,7 +318,6 @@ export class PortfolioService {
|
||||
.filter((timelineItem) => timelineItem !== null)
|
||||
.map((timelineItem) => ({
|
||||
date: timelineItem.date,
|
||||
marketPrice: timelineItem.value,
|
||||
value: timelineItem.netPerformance.toNumber()
|
||||
}));
|
||||
|
||||
@ -394,7 +438,7 @@ export class PortfolioService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = item.quantity.mul(item.marketPrice);
|
||||
const value = item.quantity.mul(item.marketPrice ?? 0);
|
||||
const symbolProfile = symbolProfileMap[item.symbol];
|
||||
const dataProviderResponse = dataProviderResponses[item.symbol];
|
||||
|
||||
@ -442,6 +486,7 @@ export class PortfolioService {
|
||||
sectors: symbolProfile.sectors,
|
||||
symbol: item.symbol,
|
||||
transactionCount: item.transactionCount,
|
||||
url: symbolProfile.url,
|
||||
value: value.toNumber()
|
||||
};
|
||||
}
|
||||
@ -658,7 +703,7 @@ export class PortfolioService {
|
||||
netPerformancePercent: position.netPerformancePercentage?.toNumber(),
|
||||
quantity: quantity.toNumber(),
|
||||
value: this.exchangeRateDataService.toCurrency(
|
||||
quantity.mul(marketPrice).toNumber(),
|
||||
quantity.mul(marketPrice ?? 0).toNumber(),
|
||||
currency,
|
||||
userCurrency
|
||||
)
|
||||
@ -1290,6 +1335,10 @@ export class PortfolioService {
|
||||
|
||||
if (filters.length === 0) {
|
||||
currentAccounts = await this.accountService.getAccounts(userId);
|
||||
} else if (filters.length === 1 && filters[0].type === 'ACCOUNT') {
|
||||
currentAccounts = await this.accountService.accounts({
|
||||
where: { id: filters[0].id }
|
||||
});
|
||||
} else {
|
||||
const accountIds = uniq(
|
||||
orders.map(({ accountId }) => {
|
||||
|
@ -46,7 +46,6 @@ export class SymbolController {
|
||||
* Must be after /lookup
|
||||
*/
|
||||
@Get(':dataSource/:symbol')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getSymbolData(
|
||||
|
@ -158,10 +158,6 @@ export class UserService {
|
||||
|
||||
let currentPermissions = getPermissions(user.role);
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
|
||||
currentPermissions.push(permissions.accessFearAndGreedIndex);
|
||||
}
|
||||
|
||||
if (user.subscription?.type === 'Premium') {
|
||||
currentPermissions.push(permissions.reportDataGlitch);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,6 @@
|
||||
{
|
||||
"LUNA1": "Terra",
|
||||
"LUNA2": "Terra",
|
||||
"SGB1": "Songbird",
|
||||
"UNI1": "Uniswap"
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ async function bootstrap() {
|
||||
})
|
||||
);
|
||||
|
||||
const host = process.env.HOST || 'localhost';
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
const port = process.env.PORT || 3333;
|
||||
await app.listen(port, host, () => {
|
||||
logLogo();
|
||||
|
@ -15,7 +15,7 @@ export class ConfigurationService {
|
||||
BASE_CURRENCY: str({ default: 'USD' }),
|
||||
CACHE_TTL: num({ default: 1 }),
|
||||
DATA_SOURCE_PRIMARY: str({ default: DataSource.YAHOO }),
|
||||
DATA_SOURCES: json({ default: JSON.stringify([DataSource.YAHOO]) }),
|
||||
DATA_SOURCES: json({ default: [DataSource.YAHOO] }),
|
||||
ENABLE_FEATURE_BLOG: bool({ default: false }),
|
||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
|
||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
|
||||
@ -31,7 +31,7 @@ export class ConfigurationService {
|
||||
GOOGLE_SHEETS_ACCOUNT: str({ default: '' }),
|
||||
GOOGLE_SHEETS_ID: str({ default: '' }),
|
||||
GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }),
|
||||
HOST: host({ default: 'localhost' }),
|
||||
HOST: host({ default: '0.0.0.0' }),
|
||||
JWT_SECRET_KEY: str({}),
|
||||
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
|
||||
MAX_ORDERS_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
|
||||
|
@ -10,6 +10,7 @@ import ms from 'ms';
|
||||
|
||||
import { DataGatheringProcessor } from './data-gathering.processor';
|
||||
import { ExchangeRateDataModule } from './exchange-rate-data.module';
|
||||
import { MarketDataModule } from './market-data.module';
|
||||
import { SymbolProfileModule } from './symbol-profile.module';
|
||||
|
||||
@Module({
|
||||
@ -25,6 +26,7 @@ import { SymbolProfileModule } from './symbol-profile.module';
|
||||
DataEnhancerModule,
|
||||
DataProviderModule,
|
||||
ExchangeRateDataModule,
|
||||
MarketDataModule,
|
||||
PrismaModule,
|
||||
SymbolProfileModule
|
||||
],
|
||||
|
@ -17,6 +17,7 @@ import { DataProviderService } from './data-provider/data-provider.service';
|
||||
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
|
||||
import { ExchangeRateDataService } from './exchange-rate-data.service';
|
||||
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||
import { MarketDataService } from './market-data.service';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Injectable()
|
||||
@ -28,6 +29,7 @@ export class DataGatheringService {
|
||||
private readonly dataGatheringQueue: Queue,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {}
|
||||
@ -56,6 +58,8 @@ export class DataGatheringService {
|
||||
}
|
||||
|
||||
public async gatherSymbol({ dataSource, symbol }: UniqueAsset) {
|
||||
await this.marketDataService.deleteMany({ dataSource, symbol });
|
||||
|
||||
const symbols = (await this.getSymbolsMax()).filter((dataGatheringItem) => {
|
||||
return (
|
||||
dataGatheringItem.dataSource === dataSource &&
|
||||
|
@ -37,10 +37,15 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
}
|
||||
|
||||
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
|
||||
const symbol = aYahooFinanceSymbol.replace(
|
||||
let symbol = aYahooFinanceSymbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
);
|
||||
|
||||
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
|
||||
symbol = `${this.baseCurrency}${symbol}`;
|
||||
}
|
||||
|
||||
return symbol.replace('=X', '');
|
||||
}
|
||||
|
||||
@ -181,6 +186,9 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
if (symbol === 'USDGBp') {
|
||||
// Convert GPB to GBp (pence)
|
||||
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
||||
} else if (symbol === 'USDILA') {
|
||||
// Convert ILS to ILA
|
||||
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
||||
}
|
||||
|
||||
response[symbol][format(historicalItem.date, DATE_FORMAT)] = {
|
||||
@ -243,6 +251,18 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
.mul(100)
|
||||
.toNumber()
|
||||
};
|
||||
} else if (
|
||||
symbol === 'USDILS' &&
|
||||
yahooFinanceSymbols.includes('USDILA=X')
|
||||
) {
|
||||
// Convert ILS to ILA
|
||||
response['USDILA'] = {
|
||||
...response[symbol],
|
||||
currency: 'ILA',
|
||||
marketPrice: new Big(response[symbol].marketPrice)
|
||||
.mul(100)
|
||||
.toNumber()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
BASE_CURRENCY: string;
|
||||
CACHE_TTL: number;
|
||||
DATA_SOURCE_PRIMARY: string;
|
||||
DATA_SOURCES: string | string[]; // string is not correct, error in envalid?
|
||||
DATA_SOURCES: string[];
|
||||
ENABLE_FEATURE_BLOG: boolean;
|
||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
|
||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
|
||||
|
@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
displayName: 'client',
|
||||
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
@ -18,5 +18,5 @@ module.exports = {
|
||||
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular'
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'],
|
||||
preset: '../../jest.preset.ts'
|
||||
preset: '../../jest.preset.js'
|
||||
};
|
||||
|
@ -5,9 +5,6 @@ import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||
import { format, parse } from 'date-fns';
|
||||
|
||||
export class CustomDateAdapter extends NativeDateAdapter {
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
@Inject(MAT_DATE_LOCALE) public locale: string,
|
||||
@Inject(forwardRef(() => MAT_DATE_LOCALE)) matDateLocale: string,
|
||||
|
@ -16,6 +16,13 @@ const routes: Routes = [
|
||||
(m) => m.ChangelogPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'about/privacy-policy',
|
||||
loadChildren: () =>
|
||||
import('./pages/about/privacy-policy/privacy-policy-page.module').then(
|
||||
(m) => m.PrivacyPolicyPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'account',
|
||||
loadChildren: () =>
|
||||
@ -52,6 +59,11 @@ const routes: Routes = [
|
||||
'./pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module'
|
||||
).then((m) => m.HalloGhostfolioPageModule)
|
||||
},
|
||||
{
|
||||
path: 'demo',
|
||||
loadChildren: () =>
|
||||
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
|
||||
},
|
||||
{
|
||||
path: 'en/blog/2021/07/hello-ghostfolio',
|
||||
loadChildren: () =>
|
||||
@ -66,6 +78,13 @@ const routes: Routes = [
|
||||
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
|
||||
).then((m) => m.FirstMonthsInOpenSourcePageModule)
|
||||
},
|
||||
{
|
||||
path: 'en/blog/2022/07/how-do-i-get-my-finances-in-order',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.module'
|
||||
).then((m) => m.HowDoIGetMyFinancesInOrderPageModule)
|
||||
},
|
||||
{
|
||||
path: 'features',
|
||||
loadChildren: () =>
|
||||
@ -78,6 +97,13 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
||||
},
|
||||
{
|
||||
path: 'markets',
|
||||
loadChildren: () =>
|
||||
import('./pages/markets/markets-page.module').then(
|
||||
(m) => m.MarketsPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'p',
|
||||
loadChildren: () =>
|
||||
@ -120,6 +146,13 @@ const routes: Routes = [
|
||||
(m) => m.FirePageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'portfolio/holdings',
|
||||
loadChildren: () =>
|
||||
import('./pages/portfolio/holdings/holdings-page.module').then(
|
||||
(m) => m.HoldingsPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'portfolio/report',
|
||||
loadChildren: () =>
|
||||
|
@ -15,13 +15,17 @@
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2 text-center">
|
||||
<a *ngIf="canCreateAccount" class="text-center" [routerLink]="['/']">
|
||||
<a
|
||||
*ngIf="canCreateAccount"
|
||||
class="text-center"
|
||||
[routerLink]="['/register']"
|
||||
>
|
||||
<div
|
||||
class="cursor-pointer d-inline-block info-message px-3 py-2"
|
||||
(click)="onCreateAccount()"
|
||||
>
|
||||
<span i18n>You are using the Live Demo.</span>
|
||||
<a class="ml-2" href="#" i18n>Create Account</a>
|
||||
<span class="a ml-2" i18n>Create Account</span>
|
||||
</div></a
|
||||
>
|
||||
<div
|
||||
|
@ -17,7 +17,7 @@
|
||||
border-radius: 2rem;
|
||||
font-size: 80%;
|
||||
|
||||
a {
|
||||
.a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import {
|
||||
DateAdapter,
|
||||
MAT_DATE_FORMATS,
|
||||
@ -38,6 +40,8 @@ export function NgxStripeFactory(): string {
|
||||
GfHeaderModule,
|
||||
HttpClientModule,
|
||||
MarkdownModule.forRoot(),
|
||||
MatAutocompleteModule,
|
||||
MatChipsModule,
|
||||
MaterialCssVarsModule.forRoot({
|
||||
darkThemeClass: 'is-dark-theme',
|
||||
isAutoContrast: true,
|
||||
|
@ -10,7 +10,6 @@ import { AccessTableComponent } from './access-table.component';
|
||||
declarations: [AccessTableComponent],
|
||||
exports: [AccessTableComponent],
|
||||
imports: [CommonModule, MatButtonModule, MatMenuModule, MatTableModule],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPortfolioAccessTableModule {}
|
||||
|
@ -0,0 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-dialog-content {
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Inject,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import { AccountType } from '@prisma/client';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { AccountDetailDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
host: { class: 'd-flex flex-column h-100' },
|
||||
selector: 'gf-account-detail-dialog',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: 'account-detail-dialog.html',
|
||||
styleUrls: ['./account-detail-dialog.component.scss']
|
||||
})
|
||||
export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
public accountType: AccountType;
|
||||
public name: string;
|
||||
public orders: OrderWithAccount[];
|
||||
public platformName: string;
|
||||
public user: User;
|
||||
public valueInBaseCurrency: number;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams,
|
||||
private dataService: DataService,
|
||||
public dialogRef: MatDialogRef<AccountDetailDialog>,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.dataService
|
||||
.fetchAccount(this.data.accountId)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
|
||||
this.accountType = accountType;
|
||||
this.name = name;
|
||||
this.platformName = Platform?.name;
|
||||
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchActivities({
|
||||
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }]
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ activities }) => {
|
||||
this.orders = activities;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
public onClose(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public onExport() {
|
||||
this.dataService
|
||||
.fetchExport(
|
||||
this.orders.map((order) => {
|
||||
return order.id;
|
||||
})
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
downloadAsFile({
|
||||
content: data,
|
||||
fileName: `ghostfolio-export-${this.name
|
||||
.replace(/\s+/g, '-')
|
||||
.toLowerCase()}-${format(
|
||||
parseISO(data.meta.date),
|
||||
'yyyyMMddHHmm'
|
||||
)}.json`,
|
||||
format: 'json'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
<gf-dialog-header
|
||||
mat-dialog-title
|
||||
position="center"
|
||||
[deviceType]="data.deviceType"
|
||||
[title]="name"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-header>
|
||||
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div class="container p-0">
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center mb-3">
|
||||
<gf-value
|
||||
size="large"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="valueInBaseCurrency"
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Account Type"
|
||||
size="medium"
|
||||
[value]="accountType"
|
||||
></gf-value>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Platform"
|
||||
size="medium"
|
||||
[value]="platformName"
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="orders?.length > 0" class="row">
|
||||
<div class="col mb-3">
|
||||
<div class="h5 mb-0" i18n>Activities</div>
|
||||
<gf-activities-table
|
||||
[activities]="orders"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="data.deviceType"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="!hasImpersonationId"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToImportActivities]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="false"
|
||||
[showSymbolColumn]="false"
|
||||
(export)="onExport()"
|
||||
></gf-activities-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gf-dialog-footer
|
||||
mat-dialog-actions
|
||||
[deviceType]="data.deviceType"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-footer>
|
@ -0,0 +1,27 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { AccountDetailDialog } from './account-detail-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccountDetailDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfActivitiesTableModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
NgxSkeletonLoaderModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAccountDetailDialogModule {}
|
@ -0,0 +1,5 @@
|
||||
export interface AccountDetailDialogParams {
|
||||
accountId: string;
|
||||
deviceType: string;
|
||||
hasImpersonationId: boolean;
|
||||
}
|
@ -65,7 +65,7 @@
|
||||
<ng-container matColumnDef="transactions">
|
||||
<th *matHeaderCellDef class="px-1 text-right" mat-header-cell>
|
||||
<span class="d-block d-sm-none">#</span>
|
||||
<span class="d-none d-sm-block" i18n>Transactions</span>
|
||||
<span class="d-none d-sm-block" i18n>Activities</span>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
<ng-container *ngIf="element.accountType === 'SECURITIES'">{{
|
||||
@ -212,7 +212,12 @@
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
class="cursor-pointer"
|
||||
mat-row
|
||||
(click)="onOpenAccountDetailDialog(row.id)"
|
||||
></tr>
|
||||
<tr
|
||||
*matFooterRowDef="displayedColumns"
|
||||
mat-footer-row
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
|
||||
@ -39,7 +40,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor() {}
|
||||
public constructor(private router: Router) {}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
@ -75,6 +76,12 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public onOpenAccountDetailDialog(accountId: string) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { accountId, accountDetailDialog: true }
|
||||
});
|
||||
}
|
||||
|
||||
public onUpdateAccount(aAccount: AccountModel) {
|
||||
this.accountToUpdate.emit(aAccount);
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import { AccountsTableComponent } from './accounts-table.component';
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAccountsTableModule {}
|
||||
|
@ -30,9 +30,6 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private adminService: AdminService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@ -52,9 +49,6 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.filterForm = this.formBuilder.group({
|
||||
status: []
|
||||
|
@ -9,7 +9,6 @@ import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/mark
|
||||
declarations: [AdminMarketDataDetailComponent],
|
||||
exports: [AdminMarketDataDetailComponent],
|
||||
imports: [CommonModule, GfLineChartModule, GfMarketDataDetailDialogModule],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminMarketDataDetailModule {}
|
||||
|
@ -11,7 +11,6 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [MarketDataDetailDialog],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
@ -22,7 +21,6 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
|
||||
MatInputModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfMarketDataDetailDialogModule {}
|
||||
|
@ -31,9 +31,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private adminService: AdminService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@ -53,9 +50,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.fetchAdminMarketData();
|
||||
}
|
||||
|
@ -42,9 +42,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private adminService: AdminService,
|
||||
private cacheService: CacheService,
|
||||
@ -78,9 +75,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.fetchAdminData();
|
||||
}
|
||||
|
@ -21,9 +21,6 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
@ -38,9 +35,6 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.fetchAdminData();
|
||||
}
|
||||
|
@ -35,11 +35,10 @@
|
||||
>{{ userItem.alias || (userItem.id | slice:0:5) +
|
||||
'...' }}</span
|
||||
>
|
||||
<ion-icon
|
||||
<gf-premium-indicator
|
||||
*ngIf="userItem?.subscription?.type === 'Premium'"
|
||||
class="ml-1 text-muted"
|
||||
name="diamond-outline"
|
||||
></ion-icon>
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
</div>
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
import { AdminUsersComponent } from './admin-users.component';
|
||||
@ -9,7 +10,13 @@ import { AdminUsersComponent } from './admin-users.component';
|
||||
@NgModule({
|
||||
declarations: [AdminUsersComponent],
|
||||
exports: [],
|
||||
imports: [CommonModule, GfValueModule, MatButtonModule, MatMenuModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfPremiumIndicatorModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminUsersModule {}
|
||||
|
@ -8,7 +8,6 @@ import { DialogFooterComponent } from './dialog-footer.component';
|
||||
declarations: [DialogFooterComponent],
|
||||
exports: [DialogFooterComponent],
|
||||
imports: [CommonModule, MatButtonModule],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfDialogFooterModule {}
|
||||
|
@ -8,7 +8,6 @@ import { DialogHeaderComponent } from './dialog-header.component';
|
||||
declarations: [DialogHeaderComponent],
|
||||
exports: [DialogHeaderComponent],
|
||||
imports: [CommonModule, MatButtonModule],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfDialogHeaderModule {}
|
||||
|
@ -66,7 +66,9 @@
|
||||
>Resources</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
*ngIf="
|
||||
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
|
||||
"
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
@ -203,7 +205,9 @@
|
||||
>Resources</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
*ngIf="
|
||||
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
|
||||
"
|
||||
class="d-block d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
@ -229,13 +233,7 @@
|
||||
mat-button
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo
|
||||
[hideName]="
|
||||
!currentRoute ||
|
||||
currentRoute === 'register' ||
|
||||
currentRoute === 'start'
|
||||
"
|
||||
></gf-logo>
|
||||
<gf-logo [hideName]="currentRoute === 'register'"></gf-logo>
|
||||
</a>
|
||||
<span class="spacer"></span>
|
||||
<a
|
||||
@ -271,6 +269,18 @@
|
||||
[routerLink]="['/pricing']"
|
||||
>Pricing</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionToAccessFearAndGreedIndex"
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'markets',
|
||||
'text-decoration-underline': currentRoute === 'markets'
|
||||
}"
|
||||
[routerLink]="['/markets']"
|
||||
>Markets</a
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1 no-min-width px-1"
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
|
@ -37,6 +37,7 @@ export class HeaderComponent implements OnChanges {
|
||||
public hasPermissionForSocialLogin: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public hasPermissionToAccessAdminControl: boolean;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
public impersonationId: string;
|
||||
public isMenuOpen: boolean;
|
||||
|
||||
@ -73,6 +74,11 @@ export class HeaderComponent implements OnChanges {
|
||||
this.user?.permissions,
|
||||
permissions.accessAdminControl
|
||||
);
|
||||
|
||||
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.enableFearAndGreedIndex
|
||||
);
|
||||
}
|
||||
|
||||
public impersonateAccount(aId: string) {
|
||||
|
@ -21,7 +21,6 @@ import { HeaderComponent } from './header.component';
|
||||
MatToolbarModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfHeaderModule {}
|
||||
|
@ -36,9 +36,6 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
@ -81,9 +78,6 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
|
@ -11,7 +11,6 @@ import { HomeHoldingsComponent } from './home-holdings.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HomeHoldingsComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfPositionDetailDialogModule,
|
||||
@ -21,7 +20,6 @@ import { HomeHoldingsComponent } from './home-holdings.component';
|
||||
MatCardModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfHomeHoldingsModule {}
|
||||
|
@ -30,9 +30,6 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
@ -47,52 +44,49 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.accessFearAndGreedIndex
|
||||
);
|
||||
|
||||
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
||||
this.dataService
|
||||
.fetchSymbolItem({
|
||||
dataSource: this.info.fearAndGreedDataSource,
|
||||
includeHistoricalData: this.numberOfDays,
|
||||
symbol: ghostfolioFearAndGreedIndexSymbol
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ historicalData, marketPrice }) => {
|
||||
this.fearAndGreedIndex = marketPrice;
|
||||
this.historicalData = [
|
||||
...historicalData,
|
||||
{
|
||||
date: resetHours(new Date()).toISOString(),
|
||||
value: marketPrice
|
||||
}
|
||||
];
|
||||
this.isLoading = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
this.dataService
|
||||
.fetchBenchmarks()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ benchmarks }) => {
|
||||
this.benchmarks = benchmarks;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {}
|
||||
public ngOnInit() {
|
||||
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.enableFearAndGreedIndex
|
||||
);
|
||||
|
||||
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
||||
this.dataService
|
||||
.fetchSymbolItem({
|
||||
dataSource: this.info.fearAndGreedDataSource,
|
||||
includeHistoricalData: this.numberOfDays,
|
||||
symbol: ghostfolioFearAndGreedIndexSymbol
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ historicalData, marketPrice }) => {
|
||||
this.fearAndGreedIndex = marketPrice;
|
||||
this.historicalData = [
|
||||
...historicalData,
|
||||
{
|
||||
date: resetHours(new Date()).toISOString(),
|
||||
value: marketPrice
|
||||
}
|
||||
];
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
this.dataService
|
||||
.fetchBenchmarks()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ benchmarks }) => {
|
||||
this.benchmarks = benchmarks;
|
||||
this.isLoading = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
|
@ -28,16 +28,17 @@
|
||||
<div class="mb-3 row">
|
||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||
<gf-benchmark
|
||||
*ngFor="let benchmark of benchmarks"
|
||||
class="py-2"
|
||||
[benchmark]="benchmark"
|
||||
[benchmarks]="benchmarks"
|
||||
[locale]="user?.settings?.locale"
|
||||
></gf-benchmark>
|
||||
<gf-benchmark
|
||||
*ngIf="!benchmarks"
|
||||
class="py-2"
|
||||
[benchmark]="undefined"
|
||||
></gf-benchmark>
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,19 +3,20 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
|
||||
import { GfBenchmarkModule } from '@ghostfolio/ui/benchmark/benchmark.module';
|
||||
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { HomeMarketComponent } from './home-market.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HomeMarketComponent],
|
||||
exports: [],
|
||||
exports: [HomeMarketComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfBenchmarkModule,
|
||||
GfFearAndGreedIndexModule,
|
||||
GfLineChartModule
|
||||
GfLineChartModule,
|
||||
NgxSkeletonLoaderModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfHomeMarketModule {}
|
||||
|
@ -42,9 +42,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
@ -69,9 +66,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
|
@ -4,7 +4,9 @@
|
||||
<div class="row w-100">
|
||||
<div class="chart-container col">
|
||||
<gf-line-chart
|
||||
class="position-absolute"
|
||||
symbol="Performance"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
|
||||
|
@ -10,7 +10,6 @@ import { HomeOverviewComponent } from './home-overview.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HomeOverviewComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfLineChartModule,
|
||||
@ -19,7 +18,6 @@ import { HomeOverviewComponent } from './home-overview.component';
|
||||
GfToggleModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfHomeOverviewModule {}
|
||||
|
@ -25,10 +25,8 @@
|
||||
gf-line-chart {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,9 +21,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
@ -46,9 +43,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
|
@ -8,14 +8,12 @@ import { HomeSummaryComponent } from './home-summary.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HomeSummaryComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfPortfolioSummaryModule,
|
||||
MatCardModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfHomeSummaryModule {}
|
||||
|
@ -22,7 +22,10 @@ import {
|
||||
transformTickToAbbreviation
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import { GroupBy } from '@ghostfolio/common/types';
|
||||
import {
|
||||
BarController,
|
||||
BarElement,
|
||||
Chart,
|
||||
LineController,
|
||||
LineElement,
|
||||
@ -42,6 +45,7 @@ import { addDays, isAfter, parseISO, subDays } from 'date-fns';
|
||||
export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
@Input() currency: string;
|
||||
@Input() daysInMarket: number;
|
||||
@Input() groupBy: GroupBy;
|
||||
@Input() investments: InvestmentItem[];
|
||||
@Input() isInPercent = false;
|
||||
@Input() locale: string;
|
||||
@ -53,6 +57,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
|
||||
public constructor() {
|
||||
Chart.register(
|
||||
BarController,
|
||||
BarElement,
|
||||
LinearScale,
|
||||
LineController,
|
||||
LineElement,
|
||||
@ -78,7 +84,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
private initialize() {
|
||||
this.isLoading = true;
|
||||
|
||||
if (this.investments?.length > 0) {
|
||||
if (!this.groupBy && this.investments?.length > 0) {
|
||||
// Extend chart by 5% of days in market (before)
|
||||
const firstItem = this.investments[0];
|
||||
this.investments.unshift({
|
||||
@ -102,13 +108,14 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: this.investments.map((position) => {
|
||||
return position.date;
|
||||
labels: this.investments.map((investmentItem) => {
|
||||
return investmentItem.date;
|
||||
}),
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderWidth: 2,
|
||||
borderWidth: this.groupBy ? 0 : 2,
|
||||
data: this.investments.map((position) => {
|
||||
return position.investment;
|
||||
}),
|
||||
@ -137,6 +144,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
this.chart = new Chart(this.chartCanvas.nativeElement, {
|
||||
data,
|
||||
options: {
|
||||
animation: false,
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0
|
||||
@ -178,8 +186,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
grid: {
|
||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
||||
color: `rgba(${getTextColor()}, 0.8)`,
|
||||
display: false
|
||||
display: false,
|
||||
drawBorder: false
|
||||
},
|
||||
position: 'right',
|
||||
ticks: {
|
||||
callback: (value: number) => {
|
||||
return transformTickToAbbreviation(value);
|
||||
@ -192,12 +202,12 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
},
|
||||
plugins: [getVerticalHoverLinePlugin(this.chartCanvas)],
|
||||
type: 'line'
|
||||
type: this.groupBy ? 'bar' : 'line'
|
||||
});
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
private getTooltipPluginConfiguration() {
|
||||
|
@ -13,7 +13,6 @@ import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.com
|
||||
|
||||
@NgModule({
|
||||
declarations: [LoginWithAccessTokenDialog],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
@ -26,7 +25,6 @@ import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.com
|
||||
ReactiveFormsModule,
|
||||
TextFieldModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class LoginWithAccessTokenDialogModule {}
|
||||
|
@ -8,7 +8,6 @@ import { PortfolioPerformanceComponent } from './portfolio-performance.component
|
||||
@NgModule({
|
||||
declarations: [PortfolioPerformanceComponent],
|
||||
exports: [PortfolioPerformanceComponent],
|
||||
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
|
||||
providers: []
|
||||
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule]
|
||||
})
|
||||
export class GfPortfolioPerformanceModule {}
|
||||
|
@ -15,7 +15,6 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PositionDetailDialog],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfActivitiesTableModule,
|
||||
@ -29,7 +28,6 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
|
||||
MatDialogModule,
|
||||
NgxSkeletonLoaderModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPositionDetailDialogModule {}
|
||||
|
@ -23,7 +23,6 @@ import { PositionComponent } from './position.component';
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPositionModule {}
|
||||
|
@ -6,12 +6,40 @@
|
||||
mat-table
|
||||
[dataSource]="dataSource"
|
||||
>
|
||||
<ng-container matColumnDef="icon">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<gf-symbol-icon
|
||||
*ngIf="element.url"
|
||||
[tooltip]="element.name"
|
||||
[url]="element.url"
|
||||
></gf-symbol-icon>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="symbol">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
||||
Symbol
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.symbol | gfSymbol }}
|
||||
<span [title]="element.name">{{ element.symbol | gfSymbol }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
<ng-container *ngIf="element.name !== element.symbol">{{
|
||||
element.name
|
||||
}}</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -36,48 +64,6 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="performance">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
i18n
|
||||
mat-header-cell
|
||||
>
|
||||
Performance
|
||||
</th>
|
||||
<td class="d-none d-lg-table-cell px-1" mat-cell *matCellDef="let element">
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.netPerformancePercent"
|
||||
></gf-value>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="allocationInvestment">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="justify-content-end px-1"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Initial Allocation
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<div class="d-flex justify-content-end px-1">
|
||||
<gf-value
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.allocationInvestment"
|
||||
></gf-value>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="allocationCurrent">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
@ -86,7 +72,7 @@
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Current Allocation
|
||||
Allocation
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<div class="d-flex justify-content-end">
|
||||
@ -99,15 +85,39 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<ng-container matColumnDef="performance">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
i18n
|
||||
mat-header-cell
|
||||
>
|
||||
Performance
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.netPerformancePercent"
|
||||
></gf-value>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
mat-row
|
||||
[ngClass]="{
|
||||
'cursor-pointer': !ignoreAssetSubClasses.includes(row.assetSubClass)
|
||||
'cursor-pointer':
|
||||
hasPermissionToShowValues &&
|
||||
!ignoreAssetSubClasses.includes(row.assetSubClass)
|
||||
}"
|
||||
(click)="
|
||||
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
|
||||
hasPermissionToShowValues &&
|
||||
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
|
||||
onOpenPositionDialog({ dataSource: row.dataSource, symbol: row.symbol })
|
||||
"
|
||||
></tr>
|
||||
|
@ -27,7 +27,9 @@ import { Subject, Subscription } from 'rxjs';
|
||||
export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() hasPermissionToShowValues = true;
|
||||
@Input() locale: string;
|
||||
@Input() pageSize = Number.MAX_SAFE_INTEGER;
|
||||
@Input() positions: PortfolioPosition[];
|
||||
|
||||
@Output() transactionDeleted = new EventEmitter<string>();
|
||||
@ -44,7 +46,6 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
ASSET_SUB_CLASS_EMERGENCY_FUND
|
||||
];
|
||||
public isLoading = true;
|
||||
public pageSize = 7;
|
||||
public routeQueryParams: Subscription;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -54,13 +55,14 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.displayedColumns = [
|
||||
'symbol',
|
||||
'value',
|
||||
'performance',
|
||||
'allocationInvestment',
|
||||
'allocationCurrent'
|
||||
];
|
||||
this.displayedColumns = ['icon', 'symbol', 'name'];
|
||||
|
||||
if (this.hasPermissionToShowValues) {
|
||||
this.displayedColumns.push('value');
|
||||
}
|
||||
|
||||
this.displayedColumns.push('allocationCurrent');
|
||||
this.displayedColumns.push('performance');
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
|
@ -35,7 +35,6 @@ import { PositionsTableComponent } from './positions-table.component';
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPositionsTableModule {}
|
||||
|
@ -15,7 +15,6 @@ import { PositionsComponent } from './positions.component';
|
||||
GfPositionModule,
|
||||
MatButtonModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPositionsModule {}
|
||||
|
@ -8,7 +8,6 @@ import { RuleComponent } from './rule.component';
|
||||
declarations: [RuleComponent],
|
||||
exports: [RuleComponent],
|
||||
imports: [CommonModule, NgxSkeletonLoaderModule],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfRuleModule {}
|
||||
|
@ -19,7 +19,6 @@ import { RulesComponent } from './rules.component';
|
||||
MatButtonModule,
|
||||
MatCardModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class RulesModule {}
|
||||
|
@ -7,7 +7,6 @@ import { SymbolIconComponent } from './symbol-icon.component';
|
||||
declarations: [SymbolIconComponent],
|
||||
exports: [SymbolIconComponent],
|
||||
imports: [CommonModule],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfSymbolIconModule {}
|
||||
|
@ -23,7 +23,7 @@ export class ToggleComponent implements OnChanges, OnInit {
|
||||
|
||||
@Output() change = new EventEmitter<Pick<ToggleOption, 'value'>>();
|
||||
|
||||
public option = new FormControl();
|
||||
public option = new FormControl<string>(undefined);
|
||||
|
||||
public constructor() {}
|
||||
|
||||
|
@ -8,7 +8,6 @@ import { ToggleComponent } from './toggle.component';
|
||||
@NgModule({
|
||||
declarations: [ToggleComponent],
|
||||
exports: [ToggleComponent],
|
||||
imports: [CommonModule, MatRadioModule, ReactiveFormsModule],
|
||||
providers: []
|
||||
imports: [CommonModule, MatRadioModule, ReactiveFormsModule]
|
||||
})
|
||||
export class GfToggleModule {}
|
||||
|
@ -7,7 +7,6 @@ import { WorldMapChartComponent } from './world-map-chart.component';
|
||||
@NgModule({
|
||||
declarations: [WorldMapChartComponent],
|
||||
exports: [WorldMapChartComponent],
|
||||
imports: [CommonModule, NgxSkeletonLoaderModule],
|
||||
providers: []
|
||||
imports: [CommonModule, NgxSkeletonLoaderModule]
|
||||
})
|
||||
export class GfWorldMapChartModule {}
|
||||
|
@ -5,22 +5,24 @@ import {
|
||||
Router,
|
||||
RouterStateSnapshot
|
||||
} from '@angular/router';
|
||||
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { ViewMode } from '@prisma/client';
|
||||
import { EMPTY } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { SettingsStorageService } from '../services/settings-storage.service';
|
||||
import { UserService } from '../services/user/user.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthGuard implements CanActivate {
|
||||
private static PUBLIC_PAGE_ROUTES = [
|
||||
'/about',
|
||||
'/about/changelog',
|
||||
'/about/privacy-policy',
|
||||
'/blog',
|
||||
'/de/blog',
|
||||
'/demo',
|
||||
'/en/blog',
|
||||
'/features',
|
||||
'/markets',
|
||||
'/p',
|
||||
'/pricing',
|
||||
'/register',
|
||||
@ -34,11 +36,10 @@ export class AuthGuard implements CanActivate {
|
||||
) {}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
if (route.queryParams?.utm_source) {
|
||||
this.settingsStorageService.setSetting(
|
||||
'utm_source',
|
||||
route.queryParams?.utm_source
|
||||
);
|
||||
const utmSource = route.queryParams?.utm_source;
|
||||
|
||||
if (utmSource) {
|
||||
this.settingsStorageService.setSetting('utm_source', utmSource);
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
@ -46,7 +47,10 @@ export class AuthGuard implements CanActivate {
|
||||
.get()
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
if (route.queryParams?.utm_source) {
|
||||
if (utmSource === 'ios') {
|
||||
this.router.navigate(['/demo']);
|
||||
resolve(false);
|
||||
} else if (utmSource === 'trusted-web-activity') {
|
||||
this.router.navigate(['/register']);
|
||||
resolve(false);
|
||||
} else if (
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
HttpRequest
|
||||
} from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ImpersonationStorageService } from '../services/impersonation-storage.service';
|
||||
@ -18,7 +17,6 @@ const TOKEN_HEADER_KEY = 'Authorization';
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
public constructor(
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private router: Router,
|
||||
private tokenStorageService: TokenStorageService
|
||||
) {}
|
||||
|
||||
|
@ -26,9 +26,6 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
@ -54,9 +51,6 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
this.statistics = statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
|
@ -4,11 +4,11 @@
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>About Ghostfolio</h3>
|
||||
<div class="about-container">
|
||||
<p>
|
||||
<strong>Ghostfolio</strong> is a lightweight wealth management
|
||||
application for individuals to keep track of stocks, ETFs or
|
||||
cryptocurrencies and make solid, data-driven investment decisions. The
|
||||
source code is fully available as open source software (OSS). The
|
||||
project has been initiated by
|
||||
Ghostfolio is a lightweight wealth management application for
|
||||
individuals to keep track of stocks, ETFs or cryptocurrencies and make
|
||||
solid, data-driven investment decisions. The source code is fully
|
||||
available as open source software (OSS). The project has been
|
||||
initiated by
|
||||
<a href="https://dotsilver.ch" title="Website of Thomas Kaul"
|
||||
>Thomas Kaul</a
|
||||
>
|
||||
@ -174,8 +174,8 @@
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
class="col-md-6 col-xs-12 my-2"
|
||||
[ngClass]="{ 'offset-md-3': !hasPermissionForBlog }"
|
||||
class="col-md-4 col-xs-12 my-2"
|
||||
[ngClass]="{ 'offset-md-4': !hasPermissionForBlog }"
|
||||
>
|
||||
<a
|
||||
class="py-2 w-100"
|
||||
@ -186,7 +186,17 @@
|
||||
>Changelog & License</a
|
||||
>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForBlog" class="col-md-6 col-xs-12 my-2">
|
||||
<div *ngIf="hasPermissionForSubscription" class="col-md-4 col-xs-12 my-2">
|
||||
<a
|
||||
class="py-2 w-100"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]="['/about', 'privacy-policy']"
|
||||
>Privacy Policy</a
|
||||
>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForBlog" class="col-md-4 col-xs-12 my-2">
|
||||
<a
|
||||
class="py-2 w-100"
|
||||
color="primary"
|
||||
|
@ -9,7 +9,6 @@ import { AboutPageComponent } from './about-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AboutPageComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
AboutPageRoutingModule,
|
||||
CommonModule,
|
||||
@ -17,7 +16,6 @@ import { AboutPageComponent } from './about-page.component';
|
||||
MatButtonModule,
|
||||
MatCardModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class AboutPageModule {}
|
||||
|
@ -10,9 +10,6 @@ import { Subject } from 'rxjs';
|
||||
export class ChangelogPageComponent implements OnDestroy {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { PrivacyPolicyPageComponent } from './privacy-policy-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: PrivacyPolicyPageComponent, canActivate: [AuthGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
exports: [RouterModule],
|
||||
imports: [RouterModule.forChild(routes)]
|
||||
})
|
||||
export class PrivacyPolicyPageRoutingModule {}
|
@ -0,0 +1,19 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-privacy-policy-page',
|
||||
styleUrls: ['./privacy-policy-page.scss'],
|
||||
templateUrl: './privacy-policy-page.html'
|
||||
})
|
||||
export class PrivacyPolicyPageComponent implements OnDestroy {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Privacy Policy</h3>
|
||||
<markdown [src]="'assets/privacy-policy.md'"></markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,17 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
|
||||
import { PrivacyPolicyPageRoutingModule } from './privacy-policy-page-routing.module';
|
||||
import { PrivacyPolicyPageComponent } from './privacy-policy-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PrivacyPolicyPageComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MarkdownModule.forChild(),
|
||||
PrivacyPolicyPageRoutingModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class PrivacyPolicyPageModule {}
|
@ -0,0 +1,21 @@
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
::ng-deep {
|
||||
markdown {
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
color: rgb(var(--light-primary-text));
|
||||
}
|
@ -63,9 +63,6 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
@ -145,9 +142,6 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
|
@ -12,16 +12,20 @@
|
||||
<div class="pr-1 w-50" i18n>Alias</div>
|
||||
<div class="pl-1 w-50">{{ user.alias }}</div>
|
||||
</div>
|
||||
<div *ngIf="user?.subscription" class="d-flex py-1">
|
||||
<div
|
||||
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
|
||||
class="d-flex py-1"
|
||||
>
|
||||
<div class="pr-1 w-50" i18n>Membership</div>
|
||||
<div class="pl-1 w-50">
|
||||
<div class="align-items-center d-flex mb-1">
|
||||
{{ user?.subscription?.type }}
|
||||
<ion-icon
|
||||
<a [routerLink]="['/pricing']"
|
||||
>{{ user?.subscription?.type }}</a
|
||||
>
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Premium'"
|
||||
class="ml-1 text-muted"
|
||||
name="diamond-outline"
|
||||
></ion-icon>
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
</div>
|
||||
<div *ngIf="user?.subscription?.type === 'Premium'">
|
||||
Valid until {{ user?.subscription?.expiresAt | date:
|
||||
@ -54,11 +58,11 @@
|
||||
class="mr-2 my-2"
|
||||
mat-stroked-button
|
||||
[href]="trySubscriptionMail"
|
||||
><span i18n>Try Premium</span
|
||||
><ion-icon
|
||||
class="ml-1 text-muted"
|
||||
name="diamond-outline"
|
||||
></ion-icon
|
||||
><span i18n>Try Premium</span>
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false"
|
||||
></gf-premium-indicator
|
||||
></a>
|
||||
<a
|
||||
class="mr-2 my-2"
|
||||
@ -170,7 +174,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex mt-4 py-1">
|
||||
<div class="pr-1 w-50" i18n>ID</div>
|
||||
<div class="pr-1 w-50" i18n>User ID</div>
|
||||
<div class="pl-1 w-50">{{ user?.id }}</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
|
@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
import { AccountPageRoutingModule } from './account-page-routing.module';
|
||||
@ -18,13 +19,13 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccountPageComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
AccountPageRoutingModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfCreateOrUpdateAccessDialogModule,
|
||||
GfPortfolioAccessTableModule,
|
||||
GfPremiumIndicatorModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
@ -35,7 +36,6 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
|
||||
MatSlideToggleModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: []
|
||||
]
|
||||
})
|
||||
export class AccountPageModule {}
|
||||
|
@ -2,15 +2,6 @@
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
|
||||
gf-access-table {
|
||||
overflow-x: auto;
|
||||
|
||||
|
@ -10,7 +10,6 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.com
|
||||
|
||||
@NgModule({
|
||||
declarations: [CreateOrUpdateAccessDialog],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
@ -19,7 +18,6 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.com
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
providers: []
|
||||
]
|
||||
})
|
||||
export class GfCreateOrUpdateAccessDialogModule {}
|
||||
|
@ -3,6 +3,8 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
|
||||
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
@ -35,9 +37,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
@ -51,12 +50,17 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['createDialog'] && this.hasPermissionToCreateAccount) {
|
||||
if (params['accountId'] && params['accountDetailDialog']) {
|
||||
this.openAccountDetailDialog(params['accountId']);
|
||||
} else if (
|
||||
params['createDialog'] &&
|
||||
this.hasPermissionToCreateAccount
|
||||
) {
|
||||
this.openCreateAccountDialog();
|
||||
} else if (params['editDialog']) {
|
||||
if (this.accounts) {
|
||||
const account = this.accounts.find((account) => {
|
||||
return account.id === params['transactionId'];
|
||||
return account.id === params['accountId'];
|
||||
});
|
||||
|
||||
this.openUpdateAccountDialog(account);
|
||||
@ -67,9 +71,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
@ -145,7 +146,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
public onUpdateAccount(aAccount: AccountModel) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { editDialog: true, transactionId: aAccount.id }
|
||||
queryParams: { accountId: aAccount.id, editDialog: true }
|
||||
});
|
||||
}
|
||||
|
||||
@ -203,6 +204,26 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openAccountDetailDialog(aAccountId: string) {
|
||||
const dialogRef = this.dialog.open(AccountDetailDialog, {
|
||||
autoFocus: false,
|
||||
data: <AccountDetailDialogParams>{
|
||||
accountId: aAccountId,
|
||||
deviceType: this.deviceType,
|
||||
hasImpersonationId: this.hasImpersonationId
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
}
|
||||
|
||||
private openCreateAccountDialog(): void {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||
data: {
|
||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
|
||||
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||
|
||||
import { AccountsPageRoutingModule } from './accounts-page-routing.module';
|
||||
@ -10,16 +11,15 @@ import { GfCreateOrUpdateAccountDialogModule } from './create-or-update-account-
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccountsPageComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
AccountsPageRoutingModule,
|
||||
CommonModule,
|
||||
GfAccountDetailDialogModule,
|
||||
GfAccountsTableModule,
|
||||
GfCreateOrUpdateAccountDialogModule,
|
||||
MatButtonModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class AccountsPageModule {}
|
||||
|
@ -50,6 +50,17 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="data.account.id">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Account ID</mat-label>
|
||||
<input
|
||||
disabled
|
||||
matInput
|
||||
name="accountId"
|
||||
[(ngModel)]="data.account.id"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="justify-content-end" mat-dialog-actions>
|
||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||
|
@ -11,7 +11,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
||||
|
||||
@NgModule({
|
||||
declarations: [CreateOrUpdateAccountDialog],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
@ -21,7 +20,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
providers: []
|
||||
]
|
||||
})
|
||||
export class GfCreateOrUpdateAccountDialogModule {}
|
||||
|
@ -16,18 +16,12 @@ export class AdminPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(private dataService: DataService) {
|
||||
const { systemMessage } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasMessage = !!systemMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -16,9 +16,6 @@ import { takeUntil } from 'rxjs/operators';
|
||||
export class AuthPageComponent implements OnDestroy, OnInit {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
@ -26,9 +23,6 @@ export class AuthPageComponent implements OnDestroy, OnInit {
|
||||
private tokenStorageService: TokenStorageService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.route.params
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
|
@ -6,8 +6,6 @@ import { AuthPageComponent } from './auth-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AuthPageComponent],
|
||||
exports: [],
|
||||
imports: [AuthPageRoutingModule, CommonModule],
|
||||
providers: []
|
||||
imports: [AuthPageRoutingModule, CommonModule]
|
||||
})
|
||||
export class AuthPageModule {}
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1" i18n>Hallo Ghostfolio 👋</h1>
|
||||
<h1 class="mb-1">Hallo Ghostfolio 👋</h1>
|
||||
<div class="text-muted"><small>31.07.2021</small></div>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user