Compare commits
63 Commits
Author | SHA1 | Date | |
---|---|---|---|
f2d206262e | |||
1ed5690b33 | |||
4451514ec5 | |||
8f73f85276 | |||
9a5d7b664b | |||
7d2d1d971a | |||
d111493eed | |||
e975f92a96 | |||
739cb4242d | |||
a37eebc9f1 | |||
e92730879e | |||
464973f9b0 | |||
f6228c099f | |||
a57fdfb2bb | |||
24716f0561 | |||
3453100afd | |||
84de2c0c68 | |||
1b7b082003 | |||
1928c2c2cc | |||
52e7a7886d | |||
36298b217e | |||
9bce57894e | |||
9d6bb325cd | |||
a5f833c612 | |||
732b14c6ab | |||
b74a042da8 | |||
d55c052f57 | |||
864f585efa | |||
6d56146054 | |||
2c9f29a3c6 | |||
9bef2e960c | |||
17b8c41673 | |||
f0afbd7346 | |||
5dc7429f6a | |||
7b39b32293 | |||
e5b5a9e7e9 | |||
1f3511368a | |||
b37df2c84f | |||
f92ba54060 | |||
a3bbd4030e | |||
4b30da2d92 | |||
93d082afbb | |||
0c85380dbf | |||
fb576376dc | |||
ff111d4c6c | |||
bc6e9a8b68 | |||
bd1963ec26 | |||
a0bec9e97f | |||
c45df20d88 | |||
fa1d669633 | |||
1009b462e9 | |||
b404858904 | |||
7ec033577f | |||
c8ca82b803 | |||
5db2faa17d | |||
1605fb8d48 | |||
b6a7804a26 | |||
de31381fd9 | |||
0d92b8d8bb | |||
7c6ff776d9 | |||
e37a34ed6c | |||
c4d9c00f92 | |||
3af8be89e3 |
143
CHANGELOG.md
143
CHANGELOG.md
@ -5,6 +5,147 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 1.254.0 - 2023-04-14
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the queue jobs implementation by adding in bulk
|
||||||
|
- Improved the queue jobs implementation by introducing unique job ids
|
||||||
|
|
||||||
|
## 1.253.0 - 2023-04-14
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Reduced the execution interval of the data gathering to every 12 hours
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the background color of dialogs in dark mode
|
||||||
|
|
||||||
|
## 1.252.2 - 2023-04-11
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Deprecated the `auth` endpoint of the login with _Security Token_ (`GET`)
|
||||||
|
|
||||||
|
## 1.252.1 - 2023-04-10
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed the slide toggles to checkboxes on the account page
|
||||||
|
- Changed the slide toggles to checkboxes in the admin control panel
|
||||||
|
- Decreased the density of the theme
|
||||||
|
- Migrated the style of various components to `@angular/material` `15` (mdc)
|
||||||
|
- Upgraded `@angular/cdk` and `@angular/material` from version `15.2.5` to `15.2.6`
|
||||||
|
- Upgraded `bull` from version `4.10.2` to `4.10.4`
|
||||||
|
|
||||||
|
## 1.251.0 - 2023-04-07
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the activities import for `csv` files exported by _Interactive Brokers_
|
||||||
|
- Improved the rendering of the chart ticks (`0.5K` → `500`)
|
||||||
|
- Increased the historical market data gathering of currency pairs to 10+ years
|
||||||
|
- Improved the content of the Frequently Asked Questions (FAQ) page
|
||||||
|
- Improved the content of the pricing page
|
||||||
|
- Changed the `auth` endpoint of the login with _Security Token_ from `GET` to `POST`
|
||||||
|
- Changed the `auth` endpoint of the _Internet Identity_ login provider from `GET` to `POST`
|
||||||
|
- Migrated the style of the `libs` components to `@angular/material` `15` (mdc)
|
||||||
|
- `ActivitiesFilterComponent`
|
||||||
|
- `ActivitiesTableComponent`
|
||||||
|
- `BenchmarkComponent`
|
||||||
|
- `HoldingsTableComponent`
|
||||||
|
- Upgraded `angular` from version `15.1.5` to `15.2.5`
|
||||||
|
- Upgraded `Nx` from version `15.7.2` to `15.9.2`
|
||||||
|
|
||||||
|
## 1.250.0 - 2023-04-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for multiple subscription offers
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the portfolio evolution chart (ignore first item)
|
||||||
|
- Improved the accounts import by handling the platform
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with more than 50 activities in the activities import (`dryRun`)
|
||||||
|
|
||||||
|
## 1.249.0 - 2023-03-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the testimonial section on the landing page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the loading state of the value component on the allocations page
|
||||||
|
- Improved the value component by always showing the label (also while loading)
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the algebraic sign in the value component
|
||||||
|
|
||||||
|
## 1.248.0 - 2023-03-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a blog post: _Ghostfolio reaches 1’000 Stars on GitHub_
|
||||||
|
- Added a breadcrumb navigation to the blog post pages
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refactored the calculation of the chart
|
||||||
|
- Hid the platform selector if no platforms are available in the create or update account dialog
|
||||||
|
- Upgraded `ng-extract-i18n-merge` from version `2.5.0` to `2.6.0`
|
||||||
|
|
||||||
|
## 1.247.0 - 2023-03-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the asset and asset sub class to the search functionality
|
||||||
|
- Added the subscription expiration date to the users table of the admin control panel
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated the URL of the Ghostfolio Slack channel
|
||||||
|
- Upgraded `prisma` from version `4.10.1` to `4.11.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the total amount calculation in the portfolio evolution chart
|
||||||
|
|
||||||
|
## 1.246.0 - 2023-03-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for asset and asset sub class to the `EOD_HISTORICAL_DATA` data source type
|
||||||
|
- Added `isin` to the asset profile model
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Extended the _Trackinsight_ data enhancer for asset profile data by `isin`
|
||||||
|
- Improved the language localization for _Gather Data_
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the border color in the _FIRE_ calculator (dark mode)
|
||||||
|
|
||||||
|
## 1.245.0 - 2023-03-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the search functionality for the `EOD_HISTORICAL_DATA` data source type
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the usability of the _FIRE_ calculator
|
||||||
|
- Improved the exchange rate service for a specific date used in activities with a manual currency
|
||||||
|
- Upgraded `ngx-device-detector` from version `3.0.0` to `5.0.1`
|
||||||
|
|
||||||
## 1.244.0 - 2023-03-09
|
## 1.244.0 - 2023-03-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -166,7 +307,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added support to export accounts
|
- Added support to export accounts
|
||||||
- Added suport to import accounts
|
- Added support to import accounts
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -200,7 +200,9 @@ Set the header for each request as follows:
|
|||||||
"Authorization": "Bearer eyJh..."
|
"Authorization": "Bearer eyJh..."
|
||||||
```
|
```
|
||||||
|
|
||||||
You can get the _Bearer Token_ via `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`.
|
You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anonymous` (Body: `{ accessToken: <INSERT_SECURITY_TOKEN_OF_ACCOUNT> }`)
|
||||||
|
|
||||||
|
Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`.
|
||||||
|
|
||||||
### Import Activities
|
### Import Activities
|
||||||
|
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
export default {
|
export default {
|
||||||
displayName: 'api',
|
displayName: 'api',
|
||||||
|
|
||||||
globals: {
|
globals: {},
|
||||||
'ts-jest': {
|
transform: {
|
||||||
|
'^.+\\.[tj]s$': [
|
||||||
|
'ts-jest',
|
||||||
|
{
|
||||||
tsconfig: '<rootDir>/tsconfig.spec.json'
|
tsconfig: '<rootDir>/tsconfig.spec.json'
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
transform: {
|
|
||||||
'^.+\\.[tj]s$': 'ts-jest'
|
|
||||||
},
|
},
|
||||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||||
coverageDirectory: '../../coverage/apps/api',
|
coverageDirectory: '../../coverage/apps/api',
|
||||||
|
@ -107,7 +107,10 @@ export class AdminController {
|
|||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
},
|
},
|
||||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
{
|
||||||
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||||
|
jobId: `${dataSource}-${symbol}}`
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +141,10 @@ export class AdminController {
|
|||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
},
|
},
|
||||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
{
|
||||||
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||||
|
jobId: `${dataSource}-${symbol}}`
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,7 +173,10 @@ export class AdminController {
|
|||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
},
|
},
|
||||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
{
|
||||||
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||||
|
jobId: `${dataSource}-${symbol}}`
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +100,7 @@ export class AdminService {
|
|||||||
dataSource,
|
dataSource,
|
||||||
marketDataItemCount,
|
marketDataItemCount,
|
||||||
symbol,
|
symbol,
|
||||||
|
assetClass: 'CASH',
|
||||||
countriesCount: 0,
|
countriesCount: 0,
|
||||||
sectorsCount: 0
|
sectorsCount: 0
|
||||||
};
|
};
|
||||||
@ -186,8 +187,11 @@ export class AdminService {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assetProfile,
|
marketData,
|
||||||
marketData
|
assetProfile: assetProfile ?? {
|
||||||
|
symbol,
|
||||||
|
currency: '-'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,8 +33,11 @@ export class AuthController {
|
|||||||
private readonly webAuthService: WebAuthService
|
private readonly webAuthService: WebAuthService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
@Get('anonymous/:accessToken')
|
@Get('anonymous/:accessToken')
|
||||||
public async accessTokenLogin(
|
public async accessTokenLoginGet(
|
||||||
@Param('accessToken') accessToken: string
|
@Param('accessToken') accessToken: string
|
||||||
): Promise<OAuthResponse> {
|
): Promise<OAuthResponse> {
|
||||||
try {
|
try {
|
||||||
@ -50,6 +53,23 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('anonymous')
|
||||||
|
public async accessTokenLogin(
|
||||||
|
@Body() body: { accessToken: string }
|
||||||
|
): Promise<OAuthResponse> {
|
||||||
|
try {
|
||||||
|
const authToken = await this.authService.validateAnonymousLogin(
|
||||||
|
body.accessToken
|
||||||
|
);
|
||||||
|
return { authToken };
|
||||||
|
} catch {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('google')
|
@Get('google')
|
||||||
@UseGuards(AuthGuard('google'))
|
@UseGuards(AuthGuard('google'))
|
||||||
public googleLogin() {
|
public googleLogin() {
|
||||||
@ -81,13 +101,13 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('internet-identity/:principalId')
|
@Post('internet-identity')
|
||||||
public async internetIdentityLogin(
|
public async internetIdentityLogin(
|
||||||
@Param('principalId') principalId: string
|
@Body() body: { principalId: string }
|
||||||
): Promise<OAuthResponse> {
|
): Promise<OAuthResponse> {
|
||||||
try {
|
try {
|
||||||
const authToken = await this.authService.validateInternetIdentityLogin(
|
const authToken = await this.authService.validateInternetIdentityLogin(
|
||||||
principalId
|
body.principalId
|
||||||
);
|
);
|
||||||
return { authToken };
|
return { authToken };
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -87,6 +87,13 @@ export class FrontendMiddleware implements NestMiddleware {
|
|||||||
) {
|
) {
|
||||||
featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png';
|
featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png';
|
||||||
title = `Ghostfolio meets Umbrel - ${title}`;
|
title = `Ghostfolio meets Umbrel - ${title}`;
|
||||||
|
} else if (
|
||||||
|
request.path.startsWith(
|
||||||
|
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
featureGraphicPath = 'assets/images/blog/1000-stars-on-github.jpg';
|
||||||
|
title = `Ghostfolio reaches 1’000 Stars on GitHub - ${title}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -13,6 +13,7 @@ import { Module } from '@nestjs/common';
|
|||||||
|
|
||||||
import { ImportController } from './import.controller';
|
import { ImportController } from './import.controller';
|
||||||
import { ImportService } from './import.service';
|
import { ImportService } from './import.service';
|
||||||
|
import { PlatformModule } from '@ghostfolio/api/services/platform/platform.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [ImportController],
|
controllers: [ImportController],
|
||||||
@ -24,6 +25,7 @@ import { ImportService } from './import.service';
|
|||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
OrderModule,
|
OrderModule,
|
||||||
|
PlatformModule,
|
||||||
PortfolioModule,
|
PortfolioModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
|
@ -6,6 +6,7 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
|||||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { PlatformService } from '@ghostfolio/api/services/platform/platform.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
@ -14,7 +15,7 @@ import {
|
|||||||
OrderWithAccount
|
OrderWithAccount
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { SymbolProfile } from '@prisma/client';
|
import { Prisma, SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
|
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
@ -26,6 +27,7 @@ export class ImportService {
|
|||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly orderService: OrderService,
|
private readonly orderService: OrderService,
|
||||||
|
private readonly platformService: PlatformService,
|
||||||
private readonly portfolioService: PortfolioService,
|
private readonly portfolioService: PortfolioService,
|
||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
) {}
|
) {}
|
||||||
@ -118,7 +120,8 @@ export class ImportService {
|
|||||||
const accountIdMapping: { [oldAccountId: string]: string } = {};
|
const accountIdMapping: { [oldAccountId: string]: string } = {};
|
||||||
|
|
||||||
if (!isDryRun && accountsDto?.length) {
|
if (!isDryRun && accountsDto?.length) {
|
||||||
const existingAccounts = await this.accountService.accounts({
|
const [existingAccounts, existingPlatforms] = await Promise.all([
|
||||||
|
this.accountService.accounts({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: {
|
||||||
in: accountsDto.map(({ id }) => {
|
in: accountsDto.map(({ id }) => {
|
||||||
@ -126,7 +129,9 @@ export class ImportService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}),
|
||||||
|
this.platformService.get()
|
||||||
|
]);
|
||||||
|
|
||||||
for (const account of accountsDto) {
|
for (const account of accountsDto) {
|
||||||
// Check if there is any existing account with the same ID
|
// Check if there is any existing account with the same ID
|
||||||
@ -146,19 +151,24 @@ export class ImportService {
|
|||||||
delete account.id;
|
delete account.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAccountObject = {
|
let accountObject: Prisma.AccountCreateInput = {
|
||||||
...account,
|
...account,
|
||||||
User: { connect: { id: userId } }
|
User: { connect: { id: userId } }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (platformId) {
|
if (
|
||||||
Object.assign(newAccountObject, {
|
existingPlatforms.some(({ id }) => {
|
||||||
|
return id === platformId;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
accountObject = {
|
||||||
|
...accountObject,
|
||||||
Platform: { connect: { id: platformId } }
|
Platform: { connect: { id: platformId } }
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAccount = await this.accountService.createAccount(
|
const newAccount = await this.accountService.createAccount(
|
||||||
newAccountObject,
|
accountObject,
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -254,6 +264,7 @@ export class ImportService {
|
|||||||
countries: null,
|
countries: null,
|
||||||
createdAt: undefined,
|
createdAt: undefined,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
|
isin: null,
|
||||||
name: null,
|
name: null,
|
||||||
scraperConfiguration: null,
|
scraperConfiguration: null,
|
||||||
sectors: null,
|
sectors: null,
|
||||||
|
@ -22,6 +22,7 @@ import { InfoItem } from '@ghostfolio/common/interfaces';
|
|||||||
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
||||||
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import { SubscriptionOffer } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
@ -304,19 +305,17 @@ export class InfoService {
|
|||||||
return statistics;
|
return statistics;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSubscriptions(): Promise<Subscription[]> {
|
private async getSubscriptions(): Promise<{
|
||||||
|
[offer in SubscriptionOffer]: Subscription;
|
||||||
|
}> {
|
||||||
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let subscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
const stripeConfig = (await this.prismaService.property.findUnique({
|
const stripeConfig = (await this.prismaService.property.findUnique({
|
||||||
where: { key: PROPERTY_STRIPE_CONFIG }
|
where: { key: PROPERTY_STRIPE_CONFIG }
|
||||||
})) ?? { value: '{}' };
|
})) ?? { value: '{}' };
|
||||||
|
|
||||||
subscriptions = [JSON.parse(stripeConfig.value)];
|
return JSON.parse(stripeConfig.value);
|
||||||
|
|
||||||
return subscriptions;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,10 @@ export class OrderService {
|
|||||||
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||||
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
||||||
},
|
},
|
||||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
{
|
||||||
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||||
|
jobId: `${data.SymbolProfile.connectOrCreate.create.dataSource}-${data.SymbolProfile.connectOrCreate.create.symbol}}`
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const isDraft = isAfter(data.date as Date, endOfToday());
|
const isDraft = isAfter(data.date as Date, endOfToday());
|
||||||
|
@ -86,7 +86,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
netPerformanceInPercentage: 13.100263852242744,
|
netPerformanceInPercentage: 13.100263852242744,
|
||||||
netPerformance: 19.86,
|
netPerformance: 19.86,
|
||||||
totalInvestment: 0,
|
totalInvestment: 0,
|
||||||
value: 19.86
|
value: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
|
@ -182,10 +182,10 @@ export class PortfolioCalculator {
|
|||||||
return isBefore(parseDate(transactionPoint.date), end);
|
return isBefore(parseDate(transactionPoint.date), end);
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
|
|
||||||
const firstIndex = transactionPointsBeforeEndDate.length;
|
const currencies: { [symbol: string]: string } = {};
|
||||||
const dates: Date[] = [];
|
const dates: Date[] = [];
|
||||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||||
const currencies: { [symbol: string]: string } = {};
|
const firstIndex = transactionPointsBeforeEndDate.length;
|
||||||
|
|
||||||
let day = start;
|
let day = start;
|
||||||
|
|
||||||
@ -235,25 +235,31 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const netPerformanceValuesBySymbol: {
|
const valuesByDate: {
|
||||||
[symbol: string]: { [date: string]: Big };
|
[date: string]: {
|
||||||
|
maxTotalInvestmentValue: Big;
|
||||||
|
totalCurrentValue: Big;
|
||||||
|
totalInvestmentValue: Big;
|
||||||
|
totalNetPerformanceValue: Big;
|
||||||
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
const investmentValuesBySymbol: {
|
const valuesBySymbol: {
|
||||||
[symbol: string]: { [date: string]: Big };
|
[symbol: string]: {
|
||||||
|
currentValues: { [date: string]: Big };
|
||||||
|
investmentValues: { [date: string]: Big };
|
||||||
|
maxInvestmentValues: { [date: string]: Big };
|
||||||
|
netPerformanceValues: { [date: string]: Big };
|
||||||
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
const maxInvestmentValuesBySymbol: {
|
|
||||||
[symbol: string]: { [date: string]: Big };
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
const totalNetPerformanceValues: { [date: string]: Big } = {};
|
|
||||||
const totalInvestmentValues: { [date: string]: Big } = {};
|
|
||||||
const maxTotalInvestmentValues: { [date: string]: Big } = {};
|
|
||||||
|
|
||||||
for (const symbol of Object.keys(symbols)) {
|
for (const symbol of Object.keys(symbols)) {
|
||||||
const { investmentValues, maxInvestmentValues, netPerformanceValues } =
|
const {
|
||||||
this.getSymbolMetrics({
|
currentValues,
|
||||||
|
investmentValues,
|
||||||
|
maxInvestmentValues,
|
||||||
|
netPerformanceValues
|
||||||
|
} = this.getSymbolMetrics({
|
||||||
end,
|
end,
|
||||||
marketSymbolMap,
|
marketSymbolMap,
|
||||||
start,
|
start,
|
||||||
@ -262,60 +268,67 @@ export class PortfolioCalculator {
|
|||||||
isChartMode: true
|
isChartMode: true
|
||||||
});
|
});
|
||||||
|
|
||||||
netPerformanceValuesBySymbol[symbol] = netPerformanceValues;
|
valuesBySymbol[symbol] = {
|
||||||
investmentValuesBySymbol[symbol] = investmentValues;
|
currentValues,
|
||||||
maxInvestmentValuesBySymbol[symbol] = maxInvestmentValues;
|
investmentValues,
|
||||||
|
maxInvestmentValues,
|
||||||
|
netPerformanceValues
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const currentDate of dates) {
|
for (const currentDate of dates) {
|
||||||
const dateString = format(currentDate, DATE_FORMAT);
|
const dateString = format(currentDate, DATE_FORMAT);
|
||||||
|
|
||||||
for (const symbol of Object.keys(netPerformanceValuesBySymbol)) {
|
for (const symbol of Object.keys(valuesBySymbol)) {
|
||||||
totalNetPerformanceValues[dateString] =
|
const symbolValues = valuesBySymbol[symbol];
|
||||||
totalNetPerformanceValues[dateString] ?? new Big(0);
|
|
||||||
|
|
||||||
if (netPerformanceValuesBySymbol[symbol]?.[dateString]) {
|
const currentValue =
|
||||||
totalNetPerformanceValues[dateString] = totalNetPerformanceValues[
|
symbolValues.currentValues?.[dateString] ?? new Big(0);
|
||||||
dateString
|
const investmentValue =
|
||||||
].add(netPerformanceValuesBySymbol[symbol][dateString]);
|
symbolValues.investmentValues?.[dateString] ?? new Big(0);
|
||||||
}
|
const maxInvestmentValue =
|
||||||
|
symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0);
|
||||||
|
const netPerformanceValue =
|
||||||
|
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0);
|
||||||
|
|
||||||
totalInvestmentValues[dateString] =
|
valuesByDate[dateString] = {
|
||||||
totalInvestmentValues[dateString] ?? new Big(0);
|
totalCurrentValue: (
|
||||||
|
valuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
|
||||||
maxTotalInvestmentValues[dateString] =
|
).add(currentValue),
|
||||||
maxTotalInvestmentValues[dateString] ?? new Big(0);
|
totalInvestmentValue: (
|
||||||
|
valuesByDate[dateString]?.totalInvestmentValue ?? new Big(0)
|
||||||
if (investmentValuesBySymbol[symbol]?.[dateString]) {
|
).add(investmentValue),
|
||||||
totalInvestmentValues[dateString] = totalInvestmentValues[
|
maxTotalInvestmentValue: (
|
||||||
dateString
|
valuesByDate[dateString]?.maxTotalInvestmentValue ?? new Big(0)
|
||||||
].add(investmentValuesBySymbol[symbol][dateString]);
|
).add(maxInvestmentValue),
|
||||||
}
|
totalNetPerformanceValue: (
|
||||||
|
valuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0)
|
||||||
if (maxInvestmentValuesBySymbol[symbol]?.[dateString]) {
|
).add(netPerformanceValue)
|
||||||
maxTotalInvestmentValues[dateString] = maxTotalInvestmentValues[
|
};
|
||||||
dateString
|
|
||||||
].add(maxInvestmentValuesBySymbol[symbol][dateString]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.keys(totalNetPerformanceValues).map((date) => {
|
return Object.entries(valuesByDate).map(([date, values]) => {
|
||||||
const netPerformanceInPercentage = maxTotalInvestmentValues[date].eq(0)
|
const {
|
||||||
|
maxTotalInvestmentValue,
|
||||||
|
totalCurrentValue,
|
||||||
|
totalInvestmentValue,
|
||||||
|
totalNetPerformanceValue
|
||||||
|
} = values;
|
||||||
|
|
||||||
|
const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0)
|
||||||
? 0
|
? 0
|
||||||
: totalNetPerformanceValues[date]
|
: totalNetPerformanceValue
|
||||||
.div(maxTotalInvestmentValues[date])
|
.div(maxTotalInvestmentValue)
|
||||||
.mul(100)
|
.mul(100)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date,
|
date,
|
||||||
netPerformanceInPercentage,
|
netPerformanceInPercentage,
|
||||||
netPerformance: totalNetPerformanceValues[date].toNumber(),
|
netPerformance: totalNetPerformanceValue.toNumber(),
|
||||||
totalInvestment: totalInvestmentValues[date].toNumber(),
|
totalInvestment: totalInvestmentValue.toNumber(),
|
||||||
value: totalInvestmentValues[date]
|
value: totalCurrentValue.toNumber()
|
||||||
.plus(totalNetPerformanceValues[date])
|
|
||||||
.toNumber()
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -906,12 +919,16 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
if (orders.length <= 0) {
|
if (orders.length <= 0) {
|
||||||
return {
|
return {
|
||||||
|
currentValues: {},
|
||||||
|
grossPerformance: new Big(0),
|
||||||
|
grossPerformancePercentage: new Big(0),
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
initialValue: new Big(0),
|
initialValue: new Big(0),
|
||||||
|
investmentValues: {},
|
||||||
|
maxInvestmentValues: {},
|
||||||
netPerformance: new Big(0),
|
netPerformance: new Big(0),
|
||||||
netPerformancePercentage: new Big(0),
|
netPerformancePercentage: new Big(0),
|
||||||
grossPerformance: new Big(0),
|
netPerformanceValues: {}
|
||||||
grossPerformancePercentage: new Big(0)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -946,6 +963,7 @@ export class PortfolioCalculator {
|
|||||||
let grossPerformanceFromSells = new Big(0);
|
let grossPerformanceFromSells = new Big(0);
|
||||||
let initialValue: Big;
|
let initialValue: Big;
|
||||||
let investmentAtStartDate: Big;
|
let investmentAtStartDate: Big;
|
||||||
|
const currentValues: { [date: string]: Big } = {};
|
||||||
const investmentValues: { [date: string]: Big } = {};
|
const investmentValues: { [date: string]: Big } = {};
|
||||||
const maxInvestmentValues: { [date: string]: Big } = {};
|
const maxInvestmentValues: { [date: string]: Big } = {};
|
||||||
let lastAveragePrice = new Big(0);
|
let lastAveragePrice = new Big(0);
|
||||||
@ -1164,6 +1182,7 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isChartMode && i > indexOfStartOrder) {
|
if (isChartMode && i > indexOfStartOrder) {
|
||||||
|
currentValues[order.date] = valueOfInvestment;
|
||||||
netPerformanceValues[order.date] = grossPerformance
|
netPerformanceValues[order.date] = grossPerformance
|
||||||
.minus(grossPerformanceAtStartDate)
|
.minus(grossPerformanceAtStartDate)
|
||||||
.minus(fees.minus(feesAtStartDate));
|
.minus(fees.minus(feesAtStartDate));
|
||||||
@ -1261,15 +1280,16 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialValue,
|
currentValues,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
|
initialValue,
|
||||||
investmentValues,
|
investmentValues,
|
||||||
maxInvestmentValues,
|
maxInvestmentValues,
|
||||||
netPerformancePercentage,
|
netPerformancePercentage,
|
||||||
netPerformanceValues,
|
netPerformanceValues,
|
||||||
|
grossPerformance: totalGrossPerformance,
|
||||||
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
|
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
|
||||||
netPerformance: totalNetPerformance,
|
netPerformance: totalNetPerformance
|
||||||
grossPerformance: totalGrossPerformance
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,8 +37,7 @@ import {
|
|||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
TimelinePosition,
|
TimelinePosition,
|
||||||
UserSettings,
|
UserSettings
|
||||||
UserWithSettings
|
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import type {
|
import type {
|
||||||
@ -47,7 +46,8 @@ import type {
|
|||||||
GroupBy,
|
GroupBy,
|
||||||
Market,
|
Market,
|
||||||
OrderWithAccount,
|
OrderWithAccount,
|
||||||
RequestWithUser
|
RequestWithUser,
|
||||||
|
UserWithSettings
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
@ -4,9 +4,9 @@ import {
|
|||||||
DEFAULT_LANGUAGE_CODE,
|
DEFAULT_LANGUAGE_CODE,
|
||||||
PROPERTY_STRIPE_CONFIG
|
PROPERTY_STRIPE_CONFIG
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { UserWithSettings } from '@ghostfolio/common/interfaces';
|
|
||||||
import { Subscription as SubscriptionInterface } from '@ghostfolio/common/interfaces/subscription.interface';
|
import { Subscription as SubscriptionInterface } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||||
|
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Subscription } from '@prisma/client';
|
import { Subscription } from '@prisma/client';
|
||||||
import { addMilliseconds, isBefore } from 'date-fns';
|
import { addMilliseconds, isBefore } from 'date-fns';
|
||||||
@ -123,7 +123,9 @@ export class SubscriptionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSubscription(aSubscriptions: Subscription[]) {
|
public getSubscription(
|
||||||
|
aSubscriptions: Subscription[]
|
||||||
|
): UserWithSettings['subscription'] {
|
||||||
if (aSubscriptions.length > 0) {
|
if (aSubscriptions.length > 0) {
|
||||||
const latestSubscription = aSubscriptions.reduce((a, b) => {
|
const latestSubscription = aSubscriptions.reduce((a, b) => {
|
||||||
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
|
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
|
||||||
@ -131,12 +133,14 @@ export class SubscriptionService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
expiresAt: latestSubscription.expiresAt,
|
expiresAt: latestSubscription.expiresAt,
|
||||||
|
offer: latestSubscription.price === 0 ? 'default' : 'renewal',
|
||||||
type: isBefore(new Date(), latestSubscription.expiresAt)
|
type: isBefore(new Date(), latestSubscription.expiresAt)
|
||||||
? SubscriptionType.Premium
|
? SubscriptionType.Premium
|
||||||
: SubscriptionType.Basic
|
: SubscriptionType.Basic
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
offer: 'default',
|
||||||
type: SubscriptionType.Basic
|
type: SubscriptionType.Basic
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { DataSource } from '@prisma/client';
|
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
export interface LookupItem {
|
export interface LookupItem {
|
||||||
|
assetClass: AssetClass;
|
||||||
|
assetSubClass: AssetSubClass;
|
||||||
currency: string;
|
currency: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
HttpException,
|
HttpException,
|
||||||
|
Inject,
|
||||||
Param,
|
Param,
|
||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UseInterceptors
|
UseInterceptors
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
@ -21,7 +24,10 @@ import { SymbolService } from './symbol.service';
|
|||||||
|
|
||||||
@Controller('symbol')
|
@Controller('symbol')
|
||||||
export class SymbolController {
|
export class SymbolController {
|
||||||
public constructor(private readonly symbolService: SymbolService) {}
|
public constructor(
|
||||||
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
|
private readonly symbolService: SymbolService
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must be before /:symbol
|
* Must be before /:symbol
|
||||||
@ -33,7 +39,10 @@ export class SymbolController {
|
|||||||
@Query() { query = '' }
|
@Query() { query = '' }
|
||||||
): Promise<{ items: LookupItem[] }> {
|
): Promise<{ items: LookupItem[] }> {
|
||||||
try {
|
try {
|
||||||
return this.symbolService.lookup(query.toLowerCase());
|
return this.symbolService.lookup({
|
||||||
|
query: query.toLowerCase(),
|
||||||
|
user: this.request.user
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
|
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
||||||
|
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { format, subDays } from 'date-fns';
|
import { format, subDays } from 'date-fns';
|
||||||
|
|
||||||
@ -79,15 +80,24 @@ export class SymbolService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async lookup({
|
||||||
|
query,
|
||||||
|
user
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
user: UserWithSettings;
|
||||||
|
}): Promise<{ items: LookupItem[] }> {
|
||||||
const results: { items: LookupItem[] } = { items: [] };
|
const results: { items: LookupItem[] } = { items: [] };
|
||||||
|
|
||||||
if (!aQuery) {
|
if (!query) {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { items } = await this.dataProviderService.search(aQuery);
|
const { items } = await this.dataProviderService.search({
|
||||||
|
query,
|
||||||
|
user
|
||||||
|
});
|
||||||
results.items = items;
|
results.items = items;
|
||||||
return results;
|
return results;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -3,17 +3,20 @@ import type {
|
|||||||
DateRange,
|
DateRange,
|
||||||
ViewMode
|
ViewMode
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
import { Type } from 'class-transformer';
|
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsIn,
|
|
||||||
IsISO8601,
|
IsISO8601,
|
||||||
|
IsIn,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString
|
IsString
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
export class UpdateUserSettingDto {
|
export class UpdateUserSettingDto {
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
annualInterestRate?: number;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
baseCurrency?: string;
|
baseCurrency?: string;
|
||||||
|
@ -4,16 +4,13 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||||
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
|
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
|
||||||
import {
|
import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
User as IUser,
|
|
||||||
UserSettings,
|
|
||||||
UserWithSettings
|
|
||||||
} from '@ghostfolio/common/interfaces';
|
|
||||||
import {
|
import {
|
||||||
getPermissions,
|
getPermissions,
|
||||||
hasRole,
|
hasRole,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
|
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Prisma, Role, User } from '@prisma/client';
|
import { Prisma, Role, User } from '@prisma/client';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
|
@ -45,7 +45,10 @@ export class CronService {
|
|||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
},
|
},
|
||||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
{
|
||||||
|
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||||
|
jobId: `${dataSource}-${symbol}}`
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.se
|
|||||||
import {
|
import {
|
||||||
DATA_GATHERING_QUEUE,
|
DATA_GATHERING_QUEUE,
|
||||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
||||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS,
|
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
|
||||||
QUEUE_JOB_STATUS_LIST
|
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
@ -11,7 +10,7 @@ import { InjectQueue } from '@nestjs/bull';
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import { JobOptions, Queue } from 'bull';
|
import { JobOptions, Queue } from 'bull';
|
||||||
import { format, subDays } from 'date-fns';
|
import { format, min, subDays, subYears } from 'date-fns';
|
||||||
|
|
||||||
import { DataProviderService } from './data-provider/data-provider.service';
|
import { DataProviderService } from './data-provider/data-provider.service';
|
||||||
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
|
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
|
||||||
@ -34,17 +33,14 @@ export class DataGatheringService {
|
|||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async addJobToQueue(name: string, data: any, options?: JobOptions) {
|
public async addJobToQueue(name: string, data: any, opts?: JobOptions) {
|
||||||
const hasJob = await this.hasJob(name, data);
|
return this.dataGatheringQueue.add(name, data, opts);
|
||||||
|
|
||||||
if (hasJob) {
|
|
||||||
Logger.log(
|
|
||||||
`Job ${name} with data ${JSON.stringify(data)} already exists.`,
|
|
||||||
'DataGatheringService'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return this.dataGatheringQueue.add(name, data, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async addJobsToQueue(
|
||||||
|
jobs: { data: any; name: string; opts?: JobOptions }[]
|
||||||
|
) {
|
||||||
|
return this.dataGatheringQueue.addBulk(jobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async gather7Days() {
|
public async gather7Days() {
|
||||||
@ -152,10 +148,11 @@ export class DataGatheringService {
|
|||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
isin,
|
||||||
name,
|
name,
|
||||||
sectors,
|
sectors,
|
||||||
url
|
url
|
||||||
} = assetProfiles[symbol];
|
} = assetProfile;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.prismaService.symbolProfile.upsert({
|
await this.prismaService.symbolProfile.upsert({
|
||||||
@ -165,6 +162,7 @@ export class DataGatheringService {
|
|||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
isin,
|
||||||
name,
|
name,
|
||||||
sectors,
|
sectors,
|
||||||
symbol,
|
symbol,
|
||||||
@ -175,6 +173,7 @@ export class DataGatheringService {
|
|||||||
assetSubClass,
|
assetSubClass,
|
||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
|
isin,
|
||||||
name,
|
name,
|
||||||
sectors,
|
sectors,
|
||||||
url
|
url
|
||||||
@ -206,17 +205,22 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
||||||
for (const { dataSource, date, symbol } of aSymbolsWithStartDate) {
|
await this.addJobsToQueue(
|
||||||
await this.addJobToQueue(
|
aSymbolsWithStartDate.map(({ dataSource, date, symbol }) => {
|
||||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
return {
|
||||||
{
|
data: {
|
||||||
dataSource,
|
dataSource,
|
||||||
date,
|
date,
|
||||||
symbol
|
symbol
|
||||||
},
|
},
|
||||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
|
name: GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
||||||
);
|
opts: {
|
||||||
|
...GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS,
|
||||||
|
jobId: `${dataSource}-${symbol}-${format(date, DATE_FORMAT)}`
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
public async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
||||||
@ -233,7 +237,7 @@ export class DataGatheringService {
|
|||||||
return {
|
return {
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
date: startDate
|
date: min([startDate, subYears(new Date(), 10)])
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -338,18 +342,4 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
return [...currencyPairsToGather, ...symbolProfilesToGather];
|
return [...currencyPairsToGather, ...symbolProfilesToGather];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async hasJob(name: string, data: any) {
|
|
||||||
const jobs = await this.dataGatheringQueue.getJobs(
|
|
||||||
QUEUE_JOB_STATUS_LIST.filter((status) => {
|
|
||||||
return status !== 'completed';
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return jobs.some((job) => {
|
|
||||||
return (
|
|
||||||
job.name === name && JSON.stringify(job.data) === JSON.stringify(data)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -163,10 +163,6 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
let items: LookupItem[] = [];
|
let items: LookupItem[] = [];
|
||||||
|
|
||||||
if (aQuery.length <= 2) {
|
|
||||||
return { items };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const get = bent(
|
||||||
`${this.URL}/search?query=${aQuery}`,
|
`${this.URL}/search?query=${aQuery}`,
|
||||||
@ -180,6 +176,8 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
symbol,
|
symbol,
|
||||||
|
assetClass: AssetClass.CASH,
|
||||||
|
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
||||||
currency: this.baseCurrency,
|
currency: this.baseCurrency,
|
||||||
dataSource: this.getName()
|
dataSource: this.getName()
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@ import bent from 'bent';
|
|||||||
const getJSON = bent('json');
|
const getJSON = bent('json');
|
||||||
|
|
||||||
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||||
private static baseUrl = 'https://data.trackinsight.com/holdings';
|
private static baseUrl = 'https://data.trackinsight.com';
|
||||||
private static countries = require('countries-list/dist/countries.json');
|
private static countries = require('countries-list/dist/countries.json');
|
||||||
private static countriesMapping = {
|
private static countriesMapping = {
|
||||||
'Russian Federation': 'Russia'
|
'Russian Federation': 'Russia'
|
||||||
@ -32,17 +32,29 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await getJSON(
|
const profile = await getJSON(
|
||||||
`${TrackinsightDataEnhancerService.baseUrl}/${symbol}.json`
|
`${TrackinsightDataEnhancerService.baseUrl}/data-api/funds/${symbol}.json`
|
||||||
|
).catch(() => {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const isin = profile.isin?.split(';')?.[0];
|
||||||
|
|
||||||
|
if (isin) {
|
||||||
|
response.isin = isin;
|
||||||
|
}
|
||||||
|
|
||||||
|
const holdings = await getJSON(
|
||||||
|
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`
|
||||||
).catch(() => {
|
).catch(() => {
|
||||||
return getJSON(
|
return getJSON(
|
||||||
`${TrackinsightDataEnhancerService.baseUrl}/${
|
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${
|
||||||
symbol.split('.')[0]
|
symbol.split('.')?.[0]
|
||||||
}.json`
|
}.json`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.weight < 0.95) {
|
if (holdings?.weight < 0.95) {
|
||||||
// Skip if data is inaccurate
|
// Skip if data is inaccurate
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -52,7 +64,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
(response.countries as unknown as Country[]).length === 0
|
(response.countries as unknown as Country[]).length === 0
|
||||||
) {
|
) {
|
||||||
response.countries = [];
|
response.countries = [];
|
||||||
for (const [name, value] of Object.entries<any>(result.countries)) {
|
for (const [name, value] of Object.entries<any>(
|
||||||
|
holdings?.countries ?? {}
|
||||||
|
)) {
|
||||||
let countryCode: string;
|
let countryCode: string;
|
||||||
|
|
||||||
for (const [key, country] of Object.entries<any>(
|
for (const [key, country] of Object.entries<any>(
|
||||||
@ -80,7 +94,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
(response.sectors as unknown as Sector[]).length === 0
|
(response.sectors as unknown as Sector[]).length === 0
|
||||||
) {
|
) {
|
||||||
response.sectors = [];
|
response.sectors = [];
|
||||||
for (const [name, value] of Object.entries<any>(result.sectors)) {
|
for (const [name, value] of Object.entries<any>(
|
||||||
|
holdings?.sectors ?? {}
|
||||||
|
)) {
|
||||||
response.sectors.push({
|
response.sectors.push({
|
||||||
name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name,
|
name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name,
|
||||||
weight: value.weight
|
weight: value.weight
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
|
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
|
import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
|
||||||
@ -260,25 +261,50 @@ export class DataProviderService {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search({
|
||||||
|
query,
|
||||||
|
user
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
user: UserWithSettings;
|
||||||
|
}): Promise<{ items: LookupItem[] }> {
|
||||||
const promises: Promise<{ items: LookupItem[] }>[] = [];
|
const promises: Promise<{ items: LookupItem[] }>[] = [];
|
||||||
let lookupItems: LookupItem[] = [];
|
let lookupItems: LookupItem[] = [];
|
||||||
|
|
||||||
for (const dataSource of this.configurationService.get('DATA_SOURCES')) {
|
if (query?.length < 2) {
|
||||||
promises.push(
|
return { items: lookupItems };
|
||||||
this.getDataProvider(DataSource[dataSource]).search(aQuery)
|
}
|
||||||
);
|
|
||||||
|
let dataSources = this.configurationService.get('DATA_SOURCES');
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||||
|
user.subscription.type === 'Basic'
|
||||||
|
) {
|
||||||
|
dataSources = dataSources.filter((dataSource) => {
|
||||||
|
return !this.isPremiumDataSource(DataSource[dataSource]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dataSource of dataSources) {
|
||||||
|
promises.push(this.getDataProvider(DataSource[dataSource]).search(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchResults = await Promise.all(promises);
|
const searchResults = await Promise.all(promises);
|
||||||
|
|
||||||
searchResults.forEach((searchResult) => {
|
searchResults.forEach(({ items }) => {
|
||||||
lookupItems = lookupItems.concat(searchResult.items);
|
if (items?.length > 0) {
|
||||||
|
lookupItems = lookupItems.concat(items);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredItems = lookupItems.filter((lookupItem) => {
|
const filteredItems = lookupItems
|
||||||
|
.filter((lookupItem) => {
|
||||||
// Only allow symbols with supported currency
|
// Only allow symbols with supported currency
|
||||||
return lookupItem.currency ? true : false;
|
return lookupItem.currency ? true : false;
|
||||||
|
})
|
||||||
|
.sort(({ name: name1 }, { name: name2 }) => {
|
||||||
|
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -295,4 +321,9 @@ export class DataProviderService {
|
|||||||
|
|
||||||
throw new Error('No data provider has been found.');
|
throw new Error('No data provider has been found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isPremiumDataSource(aDataSource: DataSource) {
|
||||||
|
const premiumDataSources: DataSource[] = [DataSource.EOD_HISTORICAL_DATA];
|
||||||
|
return premiumDataSources.includes(aDataSource);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,17 @@ import {
|
|||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
import {
|
||||||
|
AssetClass,
|
||||||
|
AssetSubClass,
|
||||||
|
DataSource,
|
||||||
|
SymbolProfile
|
||||||
|
} from '@prisma/client';
|
||||||
import bent from 'bent';
|
import bent from 'bent';
|
||||||
import { format } from 'date-fns';
|
import { format, isToday } from 'date-fns';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EodHistoricalDataService implements DataProviderInterface {
|
export class EodHistoricalDataService implements DataProviderInterface {
|
||||||
@ -19,8 +23,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
private readonly URL = 'https://eodhistoricaldata.com/api';
|
private readonly URL = 'https://eodhistoricaldata.com/api';
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService
|
||||||
private readonly symbolProfileService: SymbolProfileService
|
|
||||||
) {
|
) {
|
||||||
this.apiKey = this.configurationService.get('EOD_HISTORICAL_DATA_API_KEY');
|
this.apiKey = this.configurationService.get('EOD_HISTORICAL_DATA_API_KEY');
|
||||||
}
|
}
|
||||||
@ -32,8 +35,15 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
public async getAssetProfile(
|
public async getAssetProfile(
|
||||||
aSymbol: string
|
aSymbol: string
|
||||||
): Promise<Partial<SymbolProfile>> {
|
): Promise<Partial<SymbolProfile>> {
|
||||||
|
const [searchResult] = await this.getSearchResult(aSymbol);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dataSource: this.getName()
|
assetClass: searchResult?.assetClass,
|
||||||
|
assetSubClass: searchResult?.assetSubClass,
|
||||||
|
currency: searchResult?.currency,
|
||||||
|
dataSource: this.getName(),
|
||||||
|
isin: searchResult?.isin,
|
||||||
|
name: searchResult?.name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,32 +132,30 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
200
|
200
|
||||||
);
|
);
|
||||||
|
|
||||||
const [response, symbolProfiles] = await Promise.all([
|
const [realTimeResponse, searchResponse] = await Promise.all([
|
||||||
get(),
|
get(),
|
||||||
this.symbolProfileService.getSymbolProfiles(
|
this.search(aSymbols[0])
|
||||||
aSymbols.map((symbol) => {
|
|
||||||
return {
|
|
||||||
symbol,
|
|
||||||
dataSource: DataSource.EOD_HISTORICAL_DATA
|
|
||||||
};
|
|
||||||
})
|
|
||||||
)
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const quotes = aSymbols.length === 1 ? [response] : response;
|
const quotes =
|
||||||
|
aSymbols.length === 1 ? [realTimeResponse] : realTimeResponse;
|
||||||
|
|
||||||
return quotes.reduce((result, item, index, array) => {
|
return quotes.reduce(
|
||||||
result[item.code] = {
|
(
|
||||||
currency: symbolProfiles.find((symbolProfile) => {
|
result: { [symbol: string]: IDataProviderResponse },
|
||||||
return symbolProfile.symbol === item.code;
|
{ close, code, timestamp }
|
||||||
})?.currency,
|
) => {
|
||||||
|
result[code] = {
|
||||||
|
currency: searchResponse?.items[0]?.currency,
|
||||||
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
||||||
marketPrice: item.close,
|
marketPrice: close,
|
||||||
marketState: 'delayed'
|
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, {});
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error, 'EodHistoricalDataService');
|
Logger.error(error, 'EodHistoricalDataService');
|
||||||
}
|
}
|
||||||
@ -156,6 +164,117 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
return { items: [] };
|
const searchResult = await this.getSearchResult(aQuery);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: searchResult
|
||||||
|
.filter(({ symbol }) => {
|
||||||
|
return !symbol.toLowerCase().endsWith('forex');
|
||||||
|
})
|
||||||
|
.map(
|
||||||
|
({
|
||||||
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
name,
|
||||||
|
symbol
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
name,
|
||||||
|
symbol
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSearchResult(aQuery: string): Promise<
|
||||||
|
(LookupItem & {
|
||||||
|
assetClass: AssetClass;
|
||||||
|
assetSubClass: AssetSubClass;
|
||||||
|
isin: string;
|
||||||
|
})[]
|
||||||
|
> {
|
||||||
|
let searchResult = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const get = bent(
|
||||||
|
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
|
||||||
|
'GET',
|
||||||
|
'json',
|
||||||
|
200
|
||||||
|
);
|
||||||
|
const response = await get();
|
||||||
|
|
||||||
|
searchResult = response.map(
|
||||||
|
({
|
||||||
|
Code,
|
||||||
|
Currency: currency,
|
||||||
|
Exchange,
|
||||||
|
ISIN: isin,
|
||||||
|
Name: name,
|
||||||
|
Type
|
||||||
|
}) => {
|
||||||
|
const { assetClass, assetSubClass } = this.parseAssetClass({
|
||||||
|
Exchange,
|
||||||
|
Type
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
|
currency,
|
||||||
|
isin,
|
||||||
|
name,
|
||||||
|
dataSource: this.getName(),
|
||||||
|
symbol: `${Code}.${Exchange}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error, 'EodHistoricalDataService');
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseAssetClass({
|
||||||
|
Exchange,
|
||||||
|
Type
|
||||||
|
}: {
|
||||||
|
Exchange: string;
|
||||||
|
Type: string;
|
||||||
|
}): {
|
||||||
|
assetClass: AssetClass;
|
||||||
|
assetSubClass: AssetSubClass;
|
||||||
|
} {
|
||||||
|
let assetClass: AssetClass;
|
||||||
|
let assetSubClass: AssetSubClass;
|
||||||
|
|
||||||
|
switch (Type?.toLowerCase()) {
|
||||||
|
case 'common stock':
|
||||||
|
assetClass = AssetClass.EQUITY;
|
||||||
|
assetSubClass = AssetSubClass.STOCK;
|
||||||
|
break;
|
||||||
|
case 'currency':
|
||||||
|
assetClass = AssetClass.CASH;
|
||||||
|
|
||||||
|
if (Exchange?.toLowerCase() === 'cc') {
|
||||||
|
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'etf':
|
||||||
|
assetClass = AssetClass.EQUITY;
|
||||||
|
assetSubClass = AssetSubClass.ETF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { assetClass, assetSubClass };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,6 +145,8 @@ export class GoogleSheetsService implements DataProviderInterface {
|
|||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
const items = await this.prismaService.symbolProfile.findMany({
|
const items = await this.prismaService.symbolProfile.findMany({
|
||||||
select: {
|
select: {
|
||||||
|
assetClass: true,
|
||||||
|
assetSubClass: true,
|
||||||
currency: true,
|
currency: true,
|
||||||
dataSource: true,
|
dataSource: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
@ -165,6 +165,8 @@ export class ManualService implements DataProviderInterface {
|
|||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
let items = await this.prismaService.symbolProfile.findMany({
|
let items = await this.prismaService.symbolProfile.findMany({
|
||||||
select: {
|
select: {
|
||||||
|
assetClass: true,
|
||||||
|
assetSubClass: true,
|
||||||
currency: true,
|
currency: true,
|
||||||
dataSource: true,
|
dataSource: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
@ -101,9 +101,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
modules: ['price', 'summaryProfile', 'topHoldings']
|
modules: ['price', 'summaryProfile', 'topHoldings']
|
||||||
});
|
});
|
||||||
|
|
||||||
const { assetClass, assetSubClass } = this.parseAssetClass(
|
const { assetClass, assetSubClass } = this.parseAssetClass({
|
||||||
assetProfile.price
|
quoteType: assetProfile.price.quoteType,
|
||||||
);
|
shortName: assetProfile.price.shortName
|
||||||
|
});
|
||||||
|
|
||||||
response.assetClass = assetClass;
|
response.assetClass = assetClass;
|
||||||
response.assetSubClass = assetSubClass;
|
response.assetSubClass = assetSubClass;
|
||||||
@ -408,7 +409,14 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
marketDataItem.symbol
|
marketDataItem.symbol
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { assetClass, assetSubClass } = this.parseAssetClass({
|
||||||
|
quoteType: quote.quoteType,
|
||||||
|
shortName: quote.shortname
|
||||||
|
});
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
symbol,
|
symbol,
|
||||||
currency: marketDataItem.currency,
|
currency: marketDataItem.currency,
|
||||||
dataSource: this.getName(),
|
dataSource: this.getName(),
|
||||||
@ -484,14 +492,20 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseAssetClass(aPrice: Price): {
|
private parseAssetClass({
|
||||||
|
quoteType,
|
||||||
|
shortName
|
||||||
|
}: {
|
||||||
|
quoteType: string;
|
||||||
|
shortName: string;
|
||||||
|
}): {
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
assetSubClass: AssetSubClass;
|
assetSubClass: AssetSubClass;
|
||||||
} {
|
} {
|
||||||
let assetClass: AssetClass;
|
let assetClass: AssetClass;
|
||||||
let assetSubClass: AssetSubClass;
|
let assetSubClass: AssetSubClass;
|
||||||
|
|
||||||
switch (aPrice?.quoteType?.toLowerCase()) {
|
switch (quoteType?.toLowerCase()) {
|
||||||
case 'cryptocurrency':
|
case 'cryptocurrency':
|
||||||
assetClass = AssetClass.CASH;
|
assetClass = AssetClass.CASH;
|
||||||
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
||||||
@ -509,10 +523,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
assetSubClass = AssetSubClass.COMMODITY;
|
assetSubClass = AssetSubClass.COMMODITY;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
aPrice?.shortName?.toLowerCase()?.startsWith('gold') ||
|
shortName?.toLowerCase()?.startsWith('gold') ||
|
||||||
aPrice?.shortName?.toLowerCase()?.startsWith('palladium') ||
|
shortName?.toLowerCase()?.startsWith('palladium') ||
|
||||||
aPrice?.shortName?.toLowerCase()?.startsWith('platinum') ||
|
shortName?.toLowerCase()?.startsWith('platinum') ||
|
||||||
aPrice?.shortName?.toLowerCase()?.startsWith('silver')
|
shortName?.toLowerCase()?.startsWith('silver')
|
||||||
) {
|
) {
|
||||||
assetSubClass = AssetSubClass.PRECIOUS_METAL;
|
assetSubClass = AssetSubClass.PRECIOUS_METAL;
|
||||||
}
|
}
|
||||||
|
@ -183,8 +183,29 @@ export class ExchangeRateDataService {
|
|||||||
if (marketData?.marketPrice) {
|
if (marketData?.marketPrice) {
|
||||||
factor = marketData?.marketPrice;
|
factor = marketData?.marketPrice;
|
||||||
} else {
|
} else {
|
||||||
// TODO: Get from data provider service or calculate indirectly via base currency
|
// Calculate indirectly via base currency
|
||||||
// and market data
|
try {
|
||||||
|
const [
|
||||||
|
{ marketPrice: marketPriceBaseCurrencyFromCurrency },
|
||||||
|
{ marketPrice: marketPriceBaseCurrencyToCurrency }
|
||||||
|
] = await Promise.all([
|
||||||
|
this.marketDataService.get({
|
||||||
|
dataSource,
|
||||||
|
date: aDate,
|
||||||
|
symbol: `${this.baseCurrency}${aFromCurrency}`
|
||||||
|
}),
|
||||||
|
this.marketDataService.get({
|
||||||
|
dataSource,
|
||||||
|
date: aDate,
|
||||||
|
symbol: `${this.baseCurrency}${aToCurrency}`
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Calculate the opposite direction
|
||||||
|
factor =
|
||||||
|
(1 / marketPriceBaseCurrencyFromCurrency) *
|
||||||
|
marketPriceBaseCurrencyToCurrency;
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
apps/api/src/services/platform/platform.module.ts
Normal file
11
apps/api/src/services/platform/platform.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PlatformService } from './platform.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [PlatformService],
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [PlatformService]
|
||||||
|
})
|
||||||
|
export class PlatformModule {}
|
11
apps/api/src/services/platform/platform.service.ts
Normal file
11
apps/api/src/services/platform/platform.service.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PlatformService {
|
||||||
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
public async get() {
|
||||||
|
return this.prismaService.platform.findMany();
|
||||||
|
}
|
||||||
|
}
|
@ -3,12 +3,7 @@ export default {
|
|||||||
displayName: 'client',
|
displayName: 'client',
|
||||||
|
|
||||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||||
globals: {
|
globals: {},
|
||||||
'ts-jest': {
|
|
||||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
|
||||||
stringifyContentPathRegex: '\\.(html|svg)$'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
coverageDirectory: '../../coverage/apps/client',
|
coverageDirectory: '../../coverage/apps/client',
|
||||||
snapshotSerializers: [
|
snapshotSerializers: [
|
||||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||||
@ -16,7 +11,13 @@ export default {
|
|||||||
'jest-preset-angular/build/serializers/html-comment'
|
'jest-preset-angular/build/serializers/html-comment'
|
||||||
],
|
],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular'
|
'^.+.(ts|mjs|js|html)$': [
|
||||||
|
'jest-preset-angular',
|
||||||
|
{
|
||||||
|
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||||
|
stringifyContentPathRegex: '\\.(html|svg)$'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'],
|
transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'],
|
||||||
preset: '../../jest.preset.js'
|
preset: '../../jest.preset.js'
|
||||||
|
@ -130,6 +130,13 @@ const routes: Routes = [
|
|||||||
'./pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.module'
|
'./pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.module'
|
||||||
).then((m) => m.GhostfolioMeetsUmbrelPageModule)
|
).then((m) => m.GhostfolioMeetsUmbrelPageModule)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'blog/2023/03/ghostfolio-reaches-1000-stars-on-github',
|
||||||
|
loadChildren: () =>
|
||||||
|
import(
|
||||||
|
'./pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.module'
|
||||||
|
).then((m) => m.ThousandStarsOnGitHubPageModule)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'demo',
|
path: 'demo',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -7,10 +7,10 @@ import {
|
|||||||
MAT_DATE_LOCALE,
|
MAT_DATE_LOCALE,
|
||||||
MatNativeDateModule
|
MatNativeDateModule
|
||||||
} from '@angular/material/core';
|
} from '@angular/material/core';
|
||||||
import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete';
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
Output
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||||
import { Access } from '@ghostfolio/common/interfaces';
|
import { Access } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
|
||||||
import { AccessTableComponent } from './access-table.component';
|
import { AccessTableComponent } from './access-table.component';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.mat-dialog-content {
|
.mat-mdc-dialog-content {
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,7 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
|
||||||
MatLegacyDialogRef as MatDialogRef
|
|
||||||
} from '@angular/material/legacy-dialog';
|
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||||
|
@ -3,17 +3,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.mat-table {
|
.mat-mdc-table {
|
||||||
td {
|
|
||||||
&.mat-footer-cell {
|
|
||||||
border-top: 1px solid
|
|
||||||
rgba(
|
|
||||||
var(--palette-foreground-divider),
|
|
||||||
var(--palette-foreground-divider-alpha)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
th {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-sort-header-container {
|
.mat-sort-header-container {
|
||||||
@ -23,16 +13,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
|
||||||
.mat-table {
|
|
||||||
td {
|
|
||||||
&.mat-footer-cell {
|
|
||||||
border-top-color: rgba(
|
|
||||||
var(--palette-foreground-divider-dark),
|
|
||||||
var(--palette-foreground-divider-dark-alpha)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Account as AccountModel } from '@prisma/client';
|
import { Account as AccountModel } from '@prisma/client';
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { MatSortModule } from '@angular/material/sort';
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||||
|
@ -2,10 +2,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<form class="align-items-center d-flex" [formGroup]="filterForm">
|
<form class="align-items-center d-flex" [formGroup]="filterForm">
|
||||||
<mat-form-field
|
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||||
appearance="outline"
|
|
||||||
class="compact-with-outline without-hint w-100"
|
|
||||||
>
|
|
||||||
<mat-select formControlName="status">
|
<mat-select formControlName="status">
|
||||||
<mat-option></mat-option>
|
<mat-option></mat-option>
|
||||||
<mat-option
|
<mat-option
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
|
||||||
import { AdminJobsComponent } from './admin-jobs.component';
|
import { AdminJobsComponent } from './admin-jobs.component';
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
Output
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
|
@ -6,10 +6,7 @@ import {
|
|||||||
OnDestroy
|
OnDestroy
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
|
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
|
||||||
import {
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
|
||||||
MatLegacyDialogRef as MatDialogRef
|
|
||||||
} from '@angular/material/legacy-dialog';
|
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<form class="d-flex flex-column h-100">
|
<form class="d-flex flex-column h-100">
|
||||||
<h1 i18n mat-dialog-title>Details for {{ data.symbol }}</h1>
|
<h1 i18n mat-dialog-title>Details for {{ data.symbol }}</h1>
|
||||||
<div class="flex-grow-1 pt-3" mat-dialog-content>
|
<div class="flex-grow-1 py-3" mat-dialog-content>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Date</mat-label>
|
<mat-label i18n>Date</mat-label>
|
||||||
|
@ -5,7 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
|
||||||
import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
|
import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.mat-dialog-content {
|
.mat-mdc-dialog-content {
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
|
|
||||||
.mat-mdc-button {
|
.mat-mdc-button {
|
||||||
|
@ -6,8 +6,8 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
mat-menu-item
|
mat-menu-item
|
||||||
(click)="onGatherSymbol({dataSource: element.dataSource, symbol: element.symbol})"
|
(click)="onGatherSymbol({dataSource: element.dataSource, symbol: element.symbol})"
|
||||||
>
|
>
|
||||||
<ng-container i18n>Gather Data</ng-container>
|
<ng-container i18n>Gather Historical Data</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { MatSortModule } from '@angular/material/sort';
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.mat-dialog-content {
|
.mat-mdc-dialog-content {
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,11 @@ import {
|
|||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormBuilder } from '@angular/forms';
|
import { FormBuilder } from '@angular/forms';
|
||||||
import {
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
|
||||||
MatLegacyDialogRef as MatDialogRef
|
|
||||||
} from '@angular/material/legacy-dialog';
|
|
||||||
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
import {
|
import {
|
||||||
|
AdminMarketDataDetails,
|
||||||
EnhancedSymbolProfile,
|
EnhancedSymbolProfile,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
@ -33,7 +31,7 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
|
|||||||
})
|
})
|
||||||
export class AssetProfileDialog implements OnDestroy, OnInit {
|
export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||||
public assetClass: string;
|
public assetClass: string;
|
||||||
public assetProfile: EnhancedSymbolProfile;
|
public assetProfile: AdminMarketDataDetails['assetProfile'];
|
||||||
public assetProfileForm = this.formBuilder.group({
|
public assetProfileForm = this.formBuilder.group({
|
||||||
comment: '',
|
comment: '',
|
||||||
symbolMapping: ''
|
symbolMapping: ''
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
[disabled]="assetProfileForm.dirty"
|
[disabled]="assetProfileForm.dirty"
|
||||||
(click)="onGatherSymbol({dataSource: data.dataSource, symbol: data.symbol})"
|
(click)="onGatherSymbol({dataSource: data.dataSource, symbol: data.symbol})"
|
||||||
>
|
>
|
||||||
<ng-container i18n>Gather Data</ng-container>
|
<ng-container i18n>Gather Historical Data</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
|
@ -2,10 +2,10 @@ import { TextFieldModule } from '@angular/cdk/text-field';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
||||||
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatLegacySlideToggleChange as MatSlideToggleChange } from '@angular/material/legacy-slide-toggle';
|
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
@ -166,14 +166,14 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onReadOnlyModeChange(aEvent: MatSlideToggleChange) {
|
public onReadOnlyModeChange(aEvent: MatCheckboxChange) {
|
||||||
this.putAdminSetting({
|
this.putAdminSetting({
|
||||||
key: PROPERTY_IS_READ_ONLY_MODE,
|
key: PROPERTY_IS_READ_ONLY_MODE,
|
||||||
value: aEvent.checked ? true : undefined
|
value: aEvent.checked ? true : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onEnableUserSignupModeChange(aEvent: MatSlideToggleChange) {
|
public onEnableUserSignupModeChange(aEvent: MatCheckboxChange) {
|
||||||
this.putAdminSetting({
|
this.putAdminSetting({
|
||||||
key: PROPERTY_IS_USER_SIGNUP_ENABLED,
|
key: PROPERTY_IS_USER_SIGNUP_ENABLED,
|
||||||
value: aEvent.checked ? undefined : false
|
value: aEvent.checked ? undefined : false
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="mb-5 row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<mat-card class="mb-3">
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div class="d-flex my-3">
|
<div class="d-flex my-3">
|
||||||
<div class="w-50" i18n>User Count</div>
|
<div class="w-50" i18n>User Count</div>
|
||||||
@ -51,7 +51,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
*ngIf="customCurrencies.includes(exchangeRate.label2)"
|
*ngIf="customCurrencies.includes(exchangeRate.label2)"
|
||||||
class="mini-icon mx-1 no-min-width px-2"
|
class="h-100 mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
(click)="onDeleteCurrency(exchangeRate.label2)"
|
(click)="onDeleteCurrency(exchangeRate.label2)"
|
||||||
>
|
>
|
||||||
@ -101,21 +101,21 @@
|
|||||||
<div class="d-flex my-3">
|
<div class="d-flex my-3">
|
||||||
<div class="w-50" i18n>User Signup</div>
|
<div class="w-50" i18n>User Signup</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<mat-slide-toggle
|
<mat-checkbox
|
||||||
color="primary"
|
color="primary"
|
||||||
[checked]="info.globalPermissions.includes(permissions.createUserAccount)"
|
[checked]="info.globalPermissions.includes(permissions.createUserAccount)"
|
||||||
(change)="onEnableUserSignupModeChange($event)"
|
(change)="onEnableUserSignupModeChange($event)"
|
||||||
></mat-slide-toggle>
|
></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="hasPermissionToToggleReadOnlyMode" class="d-flex my-3">
|
<div *ngIf="hasPermissionToToggleReadOnlyMode" class="d-flex my-3">
|
||||||
<div class="w-50" i18n>Read-only Mode</div>
|
<div class="w-50" i18n>Read-only Mode</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<mat-slide-toggle
|
<mat-checkbox
|
||||||
color="primary"
|
color="primary"
|
||||||
[checked]="info?.isReadOnlyMode"
|
[checked]="info?.isReadOnlyMode"
|
||||||
(change)="onReadOnlyModeChange($event)"
|
(change)="onReadOnlyModeChange($event)"
|
||||||
></mat-slide-toggle>
|
></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
|
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
|
||||||
@ -124,7 +124,7 @@
|
|||||||
<div *ngIf="info?.systemMessage">
|
<div *ngIf="info?.systemMessage">
|
||||||
<span>{{ info.systemMessage }}</span>
|
<span>{{ info.systemMessage }}</span>
|
||||||
<button
|
<button
|
||||||
class="mini-icon mx-1 no-min-width px-2"
|
class="h-100 mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
(click)="onDeleteSystemMessage()"
|
(click)="onDeleteSystemMessage()"
|
||||||
>
|
>
|
||||||
@ -159,7 +159,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
class="mini-icon mx-1 no-min-width px-2"
|
class="h-100 mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
(click)="onDeleteCoupon(coupon.code)"
|
(click)="onDeleteCoupon(coupon.code)"
|
||||||
>
|
>
|
||||||
@ -172,7 +172,7 @@
|
|||||||
<form #couponForm="ngForm" class="align-items-center d-flex">
|
<form #couponForm="ngForm" class="align-items-center d-flex">
|
||||||
<mat-form-field
|
<mat-form-field
|
||||||
appearance="outline"
|
appearance="outline"
|
||||||
class="compact-with-outline mr-2 without-hint"
|
class="mr-2 without-hint"
|
||||||
>
|
>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="duration"
|
name="duration"
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
@ -18,9 +18,9 @@ import { AdminOverviewComponent } from './admin-overview.component';
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatCheckboxModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatSlideToggleModule,
|
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
],
|
],
|
||||||
providers: [CacheService],
|
providers: [CacheService],
|
||||||
|
@ -3,26 +3,8 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.mat-button {
|
|
||||||
&.mini-icon {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-flat-button {
|
|
||||||
::ng-deep {
|
|
||||||
.mat-button-wrapper {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscription {
|
.subscription {
|
||||||
.mat-form-field {
|
.mat-mdc-form-field {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { getEmojiFlag } from '@ghostfolio/common/helper';
|
import { getDateFormatString, getEmojiFlag } from '@ghostfolio/common/helper';
|
||||||
import { AdminData, InfoItem, User } from '@ghostfolio/common/interfaces';
|
import { AdminData, InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import {
|
import {
|
||||||
@ -18,6 +18,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './admin-users.html'
|
templateUrl: './admin-users.html'
|
||||||
})
|
})
|
||||||
export class AdminUsersComponent implements OnDestroy, OnInit {
|
export class AdminUsersComponent implements OnDestroy, OnInit {
|
||||||
|
public defaultDateFormat: string;
|
||||||
public getEmojiFlag = getEmojiFlag;
|
public getEmojiFlag = getEmojiFlag;
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
public info: InfoItem;
|
public info: InfoItem;
|
||||||
@ -43,6 +44,10 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
|
|||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.defaultDateFormat = getDateFormatString(
|
||||||
|
this.user.settings.locale
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4,44 +4,47 @@
|
|||||||
<div class="users">
|
<div class="users">
|
||||||
<table class="gf-table">
|
<table class="gf-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="mat-header-row">
|
<tr class="mat-mdc-header-row">
|
||||||
<th class="mat-header-cell px-1 py-2 text-right">#</th>
|
<th class="mat-mdc-header-cell px-1 py-2 text-right">#</th>
|
||||||
<th class="mat-header-cell px-1 py-2" i18n>User</th>
|
<th class="mat-mdc-header-cell px-1 py-2" i18n>User</th>
|
||||||
<th
|
<th
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="hasPermissionForSubscription"
|
||||||
class="mat-header-cell px-1 py-2"
|
class="mat-mdc-header-cell px-1 py-2"
|
||||||
>
|
>
|
||||||
<ng-container i18n>Country</ng-container>
|
<ng-container i18n>Country</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<th class="mat-header-cell px-1 py-2">
|
<th class="mat-mdc-header-cell px-1 py-2">
|
||||||
<ng-container i18n>Registration</ng-container>
|
<ng-container i18n>Registration</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<th class="mat-header-cell px-1 py-2 text-right">
|
<th class="mat-mdc-header-cell px-1 py-2 text-right">
|
||||||
<ng-container i18n>Accounts</ng-container>
|
<ng-container i18n>Accounts</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<th class="mat-header-cell px-1 py-2 text-right">
|
<th class="mat-mdc-header-cell px-1 py-2 text-right">
|
||||||
<ng-container i18n>Activities</ng-container>
|
<ng-container i18n>Activities</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="hasPermissionForSubscription"
|
||||||
class="mat-header-cell px-1 py-2 text-right"
|
class="mat-mdc-header-cell px-1 py-2 text-right"
|
||||||
>
|
>
|
||||||
<ng-container i18n>Engagement per Day</ng-container>
|
<ng-container i18n>Engagement per Day</ng-container>
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="hasPermissionForSubscription"
|
||||||
class="mat-header-cell px-1 py-2"
|
class="mat-mdc-header-cell px-1 py-2"
|
||||||
i18n
|
i18n
|
||||||
>
|
>
|
||||||
Last Request
|
Last Request
|
||||||
</th>
|
</th>
|
||||||
<th class="mat-header-cell px-1 py-2"></th>
|
<th class="mat-mdc-header-cell px-1 py-2"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let userItem of users; let i = index" class="mat-row">
|
<tr
|
||||||
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td>
|
*ngFor="let userItem of users; let i = index"
|
||||||
<td class="mat-cell px-1 py-2">
|
class="mat-mdc-row"
|
||||||
|
>
|
||||||
|
<td class="mat-mdc-cell px-1 py-2 text-right">{{ i + 1 }}</td>
|
||||||
|
<td class="mat-mdc-cell px-1 py-2">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span class="d-none d-sm-inline-block text-monospace"
|
<span class="d-none d-sm-inline-block text-monospace"
|
||||||
>{{ userItem.id }}</span
|
>{{ userItem.id }}</span
|
||||||
@ -53,28 +56,29 @@
|
|||||||
*ngIf="userItem?.subscription?.type === 'Premium'"
|
*ngIf="userItem?.subscription?.type === 'Premium'"
|
||||||
class="ml-1"
|
class="ml-1"
|
||||||
[enableLink]="false"
|
[enableLink]="false"
|
||||||
|
[title]="userItem.subscription.expiresAt | date: defaultDateFormat"
|
||||||
></gf-premium-indicator>
|
></gf-premium-indicator>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="hasPermissionForSubscription"
|
||||||
class="mat-cell px-1 py-2"
|
class="mat-mdc-cell px-1 py-2"
|
||||||
>
|
>
|
||||||
<span class="h5" [title]="userItem.country"
|
<span class="h5" [title]="userItem.country"
|
||||||
>{{ getEmojiFlag(userItem.country) }}</span
|
>{{ getEmojiFlag(userItem.country) }}</span
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell px-1 py-2">
|
<td class="mat-mdc-cell px-1 py-2">
|
||||||
{{ formatDistanceToNow(userItem.createdAt) }}
|
{{ formatDistanceToNow(userItem.createdAt) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell px-1 py-2 text-right">
|
<td class="mat-mdc-cell px-1 py-2 text-right">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[value]="userItem.accountCount"
|
[value]="userItem.accountCount"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell px-1 py-2 text-right">
|
<td class="mat-mdc-cell px-1 py-2 text-right">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
@ -83,7 +87,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="hasPermissionForSubscription"
|
||||||
class="mat-cell px-1 py-2 text-right"
|
class="mat-mdc-cell px-1 py-2 text-right"
|
||||||
>
|
>
|
||||||
<gf-value
|
<gf-value
|
||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
@ -94,11 +98,11 @@
|
|||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
*ngIf="hasPermissionForSubscription"
|
*ngIf="hasPermissionForSubscription"
|
||||||
class="mat-cell px-1 py-2"
|
class="mat-mdc-cell px-1 py-2"
|
||||||
>
|
>
|
||||||
{{ formatDistanceToNow(userItem.lastActivity) }}
|
{{ formatDistanceToNow(userItem.lastActivity) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell px-1 py-2">
|
<td class="mat-mdc-cell px-1 py-2">
|
||||||
<button
|
<button
|
||||||
class="mx-1 no-min-width px-2"
|
class="mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
table {
|
table {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
|
||||||
.mat-row,
|
.mat-mdc-row,
|
||||||
.mat-header-row {
|
.mat-mdc-header-row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
@ -3,5 +3,5 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
|
||||||
import { DialogFooterComponent } from './dialog-footer.component';
|
import { DialogFooterComponent } from './dialog-footer.component';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
|
||||||
import { DialogHeaderComponent } from './dialog-header.component';
|
import { DialogHeaderComponent } from './dialog-header.component';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
OnChanges,
|
OnChanges,
|
||||||
Output
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
@ -56,8 +56,8 @@ export class HeaderComponent implements OnChanges {
|
|||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
.onChangeHasImpersonation()
|
.onChangeHasImpersonation()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((id) => {
|
.subscribe((impersonationId) => {
|
||||||
this.impersonationId = id;
|
this.impersonationId = impersonationId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
||||||
@ -78,8 +78,8 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
.onChangeHasImpersonation()
|
.onChangeHasImpersonation()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((aId) => {
|
.subscribe((impersonationId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!impersonationId;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
|
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
|
||||||
<mat-card class="p-0">
|
<mat-card appearance="outlined">
|
||||||
<mat-card-content>
|
<mat-card-content class="p-0">
|
||||||
<gf-positions
|
<gf-positions
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
|
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
|
||||||
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
||||||
|
@ -66,8 +66,8 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
.onChangeHasImpersonation()
|
.onChangeHasImpersonation()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((aId) => {
|
.subscribe((impersonationId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!impersonationId;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
MatLegacySnackBar as MatSnackBar,
|
MatSnackBar,
|
||||||
MatLegacySnackBarRef as MatSnackBarRef,
|
MatSnackBarRef,
|
||||||
LegacyTextOnlySnackBar as TextOnlySnackBar
|
TextOnlySnackBar
|
||||||
} from '@angular/material/legacy-snack-bar';
|
} from '@angular/material/snack-bar';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
@ -69,8 +69,8 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
.onChangeHasImpersonation()
|
.onChangeHasImpersonation()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((aId) => {
|
.subscribe((impersonationId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!impersonationId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Summary</h3>
|
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Summary</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||||
<mat-card class="h-100">
|
<mat-card appearance="outlined">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-summary
|
<gf-portfolio-summary
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
||||||
|
|
||||||
|
@ -208,8 +208,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
this.savingsRate &&
|
this.savingsRate &&
|
||||||
|
// @ts-ignore
|
||||||
this.chart.options.plugins.annotation.annotations.savingsRate
|
this.chart.options.plugins.annotation.annotations.savingsRate
|
||||||
) {
|
) {
|
||||||
|
// @ts-ignore
|
||||||
this.chart.options.plugins.annotation.annotations.savingsRate.value =
|
this.chart.options.plugins.annotation.annotations.savingsRate.value =
|
||||||
this.savingsRate;
|
this.savingsRate;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||||
import { MatLegacyCheckboxChange as MatCheckboxChange } from '@angular/material/legacy-checkbox';
|
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
import {
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
|
||||||
MatLegacyDialogRef as MatDialogRef
|
|
||||||
} from '@angular/material/legacy-dialog';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
|
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
|
||||||
import {
|
import {
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
(closeButtonClicked)="onClose()"
|
(closeButtonClicked)="onClose()"
|
||||||
></gf-dialog-header>
|
></gf-dialog-header>
|
||||||
|
|
||||||
<div mat-dialog-content>
|
<div class="py-3" mat-dialog-content>
|
||||||
<div class="align-items-center d-flex flex-column">
|
<div class="align-items-center d-flex flex-column">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="without-hint w-100">
|
||||||
<mat-label i18n>Security Token</mat-label>
|
<mat-label i18n>Security Token</mat-label>
|
||||||
<textarea
|
<textarea
|
||||||
cdkTextareaAutosize
|
cdkTextareaAutosize
|
||||||
@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div mat-dialog-actions>
|
<div mat-dialog-actions>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<mat-checkbox i18n (change)="onChangeStaySignedIn($event)"
|
<mat-checkbox color="primary" i18n (change)="onChangeStaySignedIn($event)"
|
||||||
>Stay signed in</mat-checkbox
|
>Stay signed in</mat-checkbox
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,11 +2,11 @@ import { TextFieldModule } from '@angular/cdk/text-field';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
|
||||||
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
|
||||||
import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.component';
|
import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.component';
|
||||||
|
@ -1,23 +1,3 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
textarea.mat-input-element.cdk-textarea-autosize {
|
|
||||||
box-sizing: content-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-checkbox {
|
|
||||||
::ng-deep {
|
|
||||||
label {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-form-field {
|
|
||||||
::ng-deep {
|
|
||||||
.mat-form-field-wrapper {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.mat-dialog-content {
|
.mat-mdc-dialog-content {
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
|
|
||||||
gf-line-chart {
|
gf-line-chart {
|
||||||
|
@ -6,10 +6,7 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
|
||||||
MatLegacyDialogRef as MatDialogRef
|
|
||||||
} from '@angular/material/legacy-dialog';
|
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
|
@ -257,9 +257,11 @@
|
|||||||
<div *ngIf="tags?.length > 0" class="row">
|
<div *ngIf="tags?.length > 0" class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="h5" i18n>Tags</div>
|
<div class="h5" i18n>Tags</div>
|
||||||
<mat-chip-list>
|
<mat-chip-listbox>
|
||||||
<mat-chip *ngFor="let tag of tags">{{ tag.name }}</mat-chip>
|
<mat-chip-option *ngFor="let tag of tags" disabled
|
||||||
</mat-chip-list>
|
>{{ tag.name }}</mat-chip-option
|
||||||
|
>
|
||||||
|
</mat-chip-listbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
import { GfTrendIndicatorModule } from '@ghostfolio/ui/trend-indicator';
|
import { GfTrendIndicatorModule } from '@ghostfolio/ui/trend-indicator';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||||
|
|
||||||
import { GfPositionModule } from '../position/position.module';
|
import { GfPositionModule } from '../position/position.module';
|
||||||
|
@ -3,11 +3,14 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<mat-card
|
<mat-card
|
||||||
*ngIf="hasPermissionToCreateOrder && rules === null"
|
*ngIf="hasPermissionToCreateOrder && rules === null"
|
||||||
|
appearance="outlined"
|
||||||
class="my-2 text-center"
|
class="my-2 text-center"
|
||||||
>
|
>
|
||||||
|
<mat-card-content>
|
||||||
<gf-no-transactions-info-indicator
|
<gf-no-transactions-info-indicator
|
||||||
[hasBorder]="false"
|
[hasBorder]="false"
|
||||||
></gf-no-transactions-info-indicator>
|
></gf-no-transactions-info-indicator
|
||||||
|
></mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true"></gf-rule>
|
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true"></gf-rule>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
|
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
|
||||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||||
|
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||||
import {
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
|
||||||
MatLegacyDialogRef as MatDialogRef
|
|
||||||
} from '@angular/material/legacy-dialog';
|
|
||||||
|
|
||||||
import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';
|
import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
|
@ -17,17 +17,21 @@
|
|||||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||||
<span i18n>Portfolio Summary</span>
|
<span i18n>Portfolio Summary</span>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="align-items-center d-flex mb-1">
|
||||||
|
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||||
|
<span i18n>Portfolio Allocations</span>
|
||||||
|
</li>
|
||||||
<li class="align-items-center d-flex mb-1">
|
<li class="align-items-center d-flex mb-1">
|
||||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||||
<span i18n>Performance Benchmarks</span>
|
<span i18n>Performance Benchmarks</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="align-items-center d-flex mb-1">
|
<li class="align-items-center d-flex mb-1">
|
||||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||||
<span i18n>Allocations</span>
|
<span i18n>FIRE Calculator</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="align-items-center d-flex mb-1">
|
<li class="align-items-center d-flex mb-1">
|
||||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||||
<span i18n>FIRE Calculator</span>
|
<span i18n>Professional Data Provider</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="align-items-center d-flex mb-1">
|
<li class="align-items-center d-flex mb-1">
|
||||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.mat-dialog-content {
|
.mat-mdc-dialog-content {
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
|
|
||||||
ion-icon[name='checkmark-circle-outline'] {
|
ion-icon[name='checkmark-circle-outline'] {
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
<mat-radio-button
|
<mat-radio-button
|
||||||
*ngFor="let option of options"
|
*ngFor="let option of options"
|
||||||
[disabled]="isLoading"
|
[disabled]="isLoading"
|
||||||
|
[ngClass]="{ 'cursor-pointer': !isLoading }"
|
||||||
[value]="option.value"
|
[value]="option.value"
|
||||||
>{{ option.label }}</mat-radio-button
|
>{{ option.label }}</mat-radio-button
|
||||||
>
|
>
|
||||||
|
@ -1,26 +1,24 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.mat-radio-button {
|
.mat-mdc-radio-button {
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
margin: 0 0.25rem;
|
margin: 0 0.25rem;
|
||||||
padding: 0.15rem 0.75rem;
|
padding: 0.15rem 0.75rem;
|
||||||
|
|
||||||
&.mat-radio-checked {
|
&.mat-mdc-radio-checked {
|
||||||
background-color: rgba(var(--dark-dividers));
|
background-color: rgba(var(--dark-dividers));
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-radio-container {
|
.mdc-radio {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-radio-label {
|
label {
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-radio-label-content {
|
|
||||||
color: rgba(var(--dark-primary-text), 1);
|
color: rgba(var(--dark-primary-text), 1);
|
||||||
|
cursor: inherit;
|
||||||
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,14 +26,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
:host-context(.is-dark-theme) {
|
||||||
.mat-radio-button {
|
.mat-mdc-radio-button {
|
||||||
&.mat-radio-checked {
|
&.mat-mdc-radio-checked {
|
||||||
background-color: rgba(var(--light-dividers));
|
background-color: rgba(var(--light-dividers));
|
||||||
border: 1px solid rgba(var(--light-disabled-text));
|
border: 1px solid rgba(var(--light-disabled-text));
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-radio-label-content {
|
label {
|
||||||
color: rgba(var(--light-primary-text), 1);
|
color: rgba(var(--light-primary-text), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatLegacyRadioModule as MatRadioModule } from '@angular/material/legacy-radio';
|
import { MatRadioModule } from '@angular/material/radio';
|
||||||
|
|
||||||
import { ToggleComponent } from './toggle.component';
|
import { ToggleComponent } from './toggle.component';
|
||||||
|
|
||||||
|
@ -8,10 +8,10 @@ import {
|
|||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
MatLegacySnackBar as MatSnackBar,
|
MatSnackBar,
|
||||||
MatLegacySnackBarRef as MatSnackBarRef,
|
MatSnackBarRef,
|
||||||
LegacyTextOnlySnackBar as TextOnlySnackBar
|
TextOnlySnackBar
|
||||||
} from '@angular/material/legacy-snack-bar';
|
} from '@angular/material/snack-bar';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
<a [routerLink]="['/features']">feature</a>, please join the
|
<a [routerLink]="['/features']">feature</a>, please join the
|
||||||
Ghostfolio
|
Ghostfolio
|
||||||
<a
|
<a
|
||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
href="https://ghostfolio.slack.com"
|
||||||
title="Join the Ghostfolio Slack community"
|
title="Join the Ghostfolio Slack community"
|
||||||
>Slack community</a
|
>Slack community</a
|
||||||
>, tweet to
|
>, tweet to
|
||||||
@ -42,7 +42,7 @@
|
|||||||
href="https://twitter.com/ghostfolio_"
|
href="https://twitter.com/ghostfolio_"
|
||||||
title="Tweet to Ghostfolio on Twitter"
|
title="Tweet to Ghostfolio on Twitter"
|
||||||
>@ghostfolio_</a
|
>@ghostfolio_</a
|
||||||
><ng-container *ngIf="hasPermissionForSubscription"
|
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||||
>, send an e-mail to
|
>, send an e-mail to
|
||||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||||
>hi@ghostfol.io</a
|
>hi@ghostfol.io</a
|
||||||
@ -65,6 +65,7 @@
|
|||||||
<ion-icon name="logo-twitter"></ion-icon>
|
<ion-icon name="logo-twitter"></ion-icon>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
*ngIf="user?.subscription?.type === 'Premium'"
|
||||||
class="mx-2"
|
class="mx-2"
|
||||||
href="mailto:hi@ghostfol.io"
|
href="mailto:hi@ghostfol.io"
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
@ -74,7 +75,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="mx-2"
|
class="mx-2"
|
||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
href="https://ghostfolio.slack.com"
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
title="Join the Ghostfolio Slack channel"
|
title="Join the Ghostfolio Slack channel"
|
||||||
>
|
>
|
||||||
@ -147,10 +148,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<a
|
<a class="d-block" href="https://ghostfolio.slack.com">
|
||||||
class="d-block"
|
|
||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
|
||||||
>
|
|
||||||
<gf-value
|
<gf-value
|
||||||
size="large"
|
size="large"
|
||||||
[value]="statistics?.slackCommunityUsers ?? '-'"
|
[value]="statistics?.slackCommunityUsers ?? '-'"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user