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/),
|
||||
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
|
||||
|
||||
### Added
|
||||
@ -166,7 +307,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- Added support to export accounts
|
||||
- Added suport to import accounts
|
||||
- Added support to import accounts
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -200,7 +200,9 @@ Set the header for each request as follows:
|
||||
"Authorization": "Bearer eyJh..."
|
||||
```
|
||||
|
||||
You can get the _Bearer Token_ via `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`.
|
||||
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
|
||||
|
||||
|
@ -2,13 +2,14 @@
|
||||
export default {
|
||||
displayName: 'api',
|
||||
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json'
|
||||
}
|
||||
},
|
||||
globals: {},
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': 'ts-jest'
|
||||
'^.+\\.[tj]s$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json'
|
||||
}
|
||||
]
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../../coverage/apps/api',
|
||||
|
@ -107,7 +107,10 @@ export class AdminController {
|
||||
dataSource,
|
||||
symbol
|
||||
},
|
||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||
{
|
||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||
jobId: `${dataSource}-${symbol}}`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -138,7 +141,10 @@ export class AdminController {
|
||||
dataSource,
|
||||
symbol
|
||||
},
|
||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||
{
|
||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||
jobId: `${dataSource}-${symbol}}`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -167,7 +173,10 @@ export class AdminController {
|
||||
dataSource,
|
||||
symbol
|
||||
},
|
||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||
{
|
||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||
jobId: `${dataSource}-${symbol}}`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -100,6 +100,7 @@ export class AdminService {
|
||||
dataSource,
|
||||
marketDataItemCount,
|
||||
symbol,
|
||||
assetClass: 'CASH',
|
||||
countriesCount: 0,
|
||||
sectorsCount: 0
|
||||
};
|
||||
@ -186,8 +187,11 @@ export class AdminService {
|
||||
]);
|
||||
|
||||
return {
|
||||
assetProfile,
|
||||
marketData
|
||||
marketData,
|
||||
assetProfile: assetProfile ?? {
|
||||
symbol,
|
||||
currency: '-'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -33,8 +33,11 @@ export class AuthController {
|
||||
private readonly webAuthService: WebAuthService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Get('anonymous/:accessToken')
|
||||
public async accessTokenLogin(
|
||||
public async accessTokenLoginGet(
|
||||
@Param('accessToken') accessToken: string
|
||||
): Promise<OAuthResponse> {
|
||||
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')
|
||||
@UseGuards(AuthGuard('google'))
|
||||
public googleLogin() {
|
||||
@ -81,13 +101,13 @@ export class AuthController {
|
||||
}
|
||||
}
|
||||
|
||||
@Get('internet-identity/:principalId')
|
||||
@Post('internet-identity')
|
||||
public async internetIdentityLogin(
|
||||
@Param('principalId') principalId: string
|
||||
@Body() body: { principalId: string }
|
||||
): Promise<OAuthResponse> {
|
||||
try {
|
||||
const authToken = await this.authService.validateInternetIdentityLogin(
|
||||
principalId
|
||||
body.principalId
|
||||
);
|
||||
return { authToken };
|
||||
} catch {
|
||||
|
@ -87,6 +87,13 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
) {
|
||||
featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png';
|
||||
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 (
|
||||
|
@ -13,6 +13,7 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { ImportController } from './import.controller';
|
||||
import { ImportService } from './import.service';
|
||||
import { PlatformModule } from '@ghostfolio/api/services/platform/platform.module';
|
||||
|
||||
@Module({
|
||||
controllers: [ImportController],
|
||||
@ -24,6 +25,7 @@ import { ImportService } from './import.service';
|
||||
DataProviderModule,
|
||||
ExchangeRateDataModule,
|
||||
OrderModule,
|
||||
PlatformModule,
|
||||
PortfolioModule,
|
||||
PrismaModule,
|
||||
RedisCacheModule,
|
||||
|
@ -6,6 +6,7 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.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 { parseDate } from '@ghostfolio/common/helper';
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
@ -14,7 +15,7 @@ import {
|
||||
OrderWithAccount
|
||||
} from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SymbolProfile } from '@prisma/client';
|
||||
import { Prisma, SymbolProfile } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
@ -26,6 +27,7 @@ export class ImportService {
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly orderService: OrderService,
|
||||
private readonly platformService: PlatformService,
|
||||
private readonly portfolioService: PortfolioService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {}
|
||||
@ -118,15 +120,18 @@ export class ImportService {
|
||||
const accountIdMapping: { [oldAccountId: string]: string } = {};
|
||||
|
||||
if (!isDryRun && accountsDto?.length) {
|
||||
const existingAccounts = await this.accountService.accounts({
|
||||
where: {
|
||||
id: {
|
||||
in: accountsDto.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
const [existingAccounts, existingPlatforms] = await Promise.all([
|
||||
this.accountService.accounts({
|
||||
where: {
|
||||
id: {
|
||||
in: accountsDto.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}),
|
||||
this.platformService.get()
|
||||
]);
|
||||
|
||||
for (const account of accountsDto) {
|
||||
// Check if there is any existing account with the same ID
|
||||
@ -146,19 +151,24 @@ export class ImportService {
|
||||
delete account.id;
|
||||
}
|
||||
|
||||
const newAccountObject = {
|
||||
let accountObject: Prisma.AccountCreateInput = {
|
||||
...account,
|
||||
User: { connect: { id: userId } }
|
||||
};
|
||||
|
||||
if (platformId) {
|
||||
Object.assign(newAccountObject, {
|
||||
if (
|
||||
existingPlatforms.some(({ id }) => {
|
||||
return id === platformId;
|
||||
})
|
||||
) {
|
||||
accountObject = {
|
||||
...accountObject,
|
||||
Platform: { connect: { id: platformId } }
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const newAccount = await this.accountService.createAccount(
|
||||
newAccountObject,
|
||||
accountObject,
|
||||
userId
|
||||
);
|
||||
|
||||
@ -254,6 +264,7 @@ export class ImportService {
|
||||
countries: null,
|
||||
createdAt: undefined,
|
||||
id: undefined,
|
||||
isin: null,
|
||||
name: null,
|
||||
scraperConfiguration: null,
|
||||
sectors: null,
|
||||
|
@ -22,6 +22,7 @@ import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
||||
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||
import { permissions } from '@ghostfolio/common/permissions';
|
||||
import { SubscriptionOffer } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bent from 'bent';
|
||||
@ -304,19 +305,17 @@ export class InfoService {
|
||||
return statistics;
|
||||
}
|
||||
|
||||
private async getSubscriptions(): Promise<Subscription[]> {
|
||||
private async getSubscriptions(): Promise<{
|
||||
[offer in SubscriptionOffer]: Subscription;
|
||||
}> {
|
||||
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let subscriptions: Subscription[] = [];
|
||||
|
||||
const stripeConfig = (await this.prismaService.property.findUnique({
|
||||
where: { key: PROPERTY_STRIPE_CONFIG }
|
||||
})) ?? { value: '{}' };
|
||||
|
||||
subscriptions = [JSON.parse(stripeConfig.value)];
|
||||
|
||||
return subscriptions;
|
||||
return JSON.parse(stripeConfig.value);
|
||||
}
|
||||
}
|
||||
|
@ -118,7 +118,10 @@ export class OrderService {
|
||||
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||
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());
|
||||
|
@ -86,7 +86,7 @@ describe('PortfolioCalculator', () => {
|
||||
netPerformanceInPercentage: 13.100263852242744,
|
||||
netPerformance: 19.86,
|
||||
totalInvestment: 0,
|
||||
value: 19.86
|
||||
value: 0
|
||||
});
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
|
@ -182,10 +182,10 @@ export class PortfolioCalculator {
|
||||
return isBefore(parseDate(transactionPoint.date), end);
|
||||
}) ?? [];
|
||||
|
||||
const firstIndex = transactionPointsBeforeEndDate.length;
|
||||
const currencies: { [symbol: string]: string } = {};
|
||||
const dates: Date[] = [];
|
||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||
const currencies: { [symbol: string]: string } = {};
|
||||
const firstIndex = transactionPointsBeforeEndDate.length;
|
||||
|
||||
let day = start;
|
||||
|
||||
@ -235,87 +235,100 @@ export class PortfolioCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
const netPerformanceValuesBySymbol: {
|
||||
[symbol: string]: { [date: string]: Big };
|
||||
const valuesByDate: {
|
||||
[date: string]: {
|
||||
maxTotalInvestmentValue: Big;
|
||||
totalCurrentValue: Big;
|
||||
totalInvestmentValue: Big;
|
||||
totalNetPerformanceValue: Big;
|
||||
};
|
||||
} = {};
|
||||
|
||||
const investmentValuesBySymbol: {
|
||||
[symbol: string]: { [date: string]: Big };
|
||||
const valuesBySymbol: {
|
||||
[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)) {
|
||||
const { investmentValues, maxInvestmentValues, netPerformanceValues } =
|
||||
this.getSymbolMetrics({
|
||||
end,
|
||||
marketSymbolMap,
|
||||
start,
|
||||
step,
|
||||
symbol,
|
||||
isChartMode: true
|
||||
});
|
||||
const {
|
||||
currentValues,
|
||||
investmentValues,
|
||||
maxInvestmentValues,
|
||||
netPerformanceValues
|
||||
} = this.getSymbolMetrics({
|
||||
end,
|
||||
marketSymbolMap,
|
||||
start,
|
||||
step,
|
||||
symbol,
|
||||
isChartMode: true
|
||||
});
|
||||
|
||||
netPerformanceValuesBySymbol[symbol] = netPerformanceValues;
|
||||
investmentValuesBySymbol[symbol] = investmentValues;
|
||||
maxInvestmentValuesBySymbol[symbol] = maxInvestmentValues;
|
||||
valuesBySymbol[symbol] = {
|
||||
currentValues,
|
||||
investmentValues,
|
||||
maxInvestmentValues,
|
||||
netPerformanceValues
|
||||
};
|
||||
}
|
||||
|
||||
for (const currentDate of dates) {
|
||||
const dateString = format(currentDate, DATE_FORMAT);
|
||||
|
||||
for (const symbol of Object.keys(netPerformanceValuesBySymbol)) {
|
||||
totalNetPerformanceValues[dateString] =
|
||||
totalNetPerformanceValues[dateString] ?? new Big(0);
|
||||
for (const symbol of Object.keys(valuesBySymbol)) {
|
||||
const symbolValues = valuesBySymbol[symbol];
|
||||
|
||||
if (netPerformanceValuesBySymbol[symbol]?.[dateString]) {
|
||||
totalNetPerformanceValues[dateString] = totalNetPerformanceValues[
|
||||
dateString
|
||||
].add(netPerformanceValuesBySymbol[symbol][dateString]);
|
||||
}
|
||||
const currentValue =
|
||||
symbolValues.currentValues?.[dateString] ?? new Big(0);
|
||||
const investmentValue =
|
||||
symbolValues.investmentValues?.[dateString] ?? new Big(0);
|
||||
const maxInvestmentValue =
|
||||
symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0);
|
||||
const netPerformanceValue =
|
||||
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0);
|
||||
|
||||
totalInvestmentValues[dateString] =
|
||||
totalInvestmentValues[dateString] ?? new Big(0);
|
||||
|
||||
maxTotalInvestmentValues[dateString] =
|
||||
maxTotalInvestmentValues[dateString] ?? new Big(0);
|
||||
|
||||
if (investmentValuesBySymbol[symbol]?.[dateString]) {
|
||||
totalInvestmentValues[dateString] = totalInvestmentValues[
|
||||
dateString
|
||||
].add(investmentValuesBySymbol[symbol][dateString]);
|
||||
}
|
||||
|
||||
if (maxInvestmentValuesBySymbol[symbol]?.[dateString]) {
|
||||
maxTotalInvestmentValues[dateString] = maxTotalInvestmentValues[
|
||||
dateString
|
||||
].add(maxInvestmentValuesBySymbol[symbol][dateString]);
|
||||
}
|
||||
valuesByDate[dateString] = {
|
||||
totalCurrentValue: (
|
||||
valuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
|
||||
).add(currentValue),
|
||||
totalInvestmentValue: (
|
||||
valuesByDate[dateString]?.totalInvestmentValue ?? new Big(0)
|
||||
).add(investmentValue),
|
||||
maxTotalInvestmentValue: (
|
||||
valuesByDate[dateString]?.maxTotalInvestmentValue ?? new Big(0)
|
||||
).add(maxInvestmentValue),
|
||||
totalNetPerformanceValue: (
|
||||
valuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0)
|
||||
).add(netPerformanceValue)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(totalNetPerformanceValues).map((date) => {
|
||||
const netPerformanceInPercentage = maxTotalInvestmentValues[date].eq(0)
|
||||
return Object.entries(valuesByDate).map(([date, values]) => {
|
||||
const {
|
||||
maxTotalInvestmentValue,
|
||||
totalCurrentValue,
|
||||
totalInvestmentValue,
|
||||
totalNetPerformanceValue
|
||||
} = values;
|
||||
|
||||
const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0)
|
||||
? 0
|
||||
: totalNetPerformanceValues[date]
|
||||
.div(maxTotalInvestmentValues[date])
|
||||
: totalNetPerformanceValue
|
||||
.div(maxTotalInvestmentValue)
|
||||
.mul(100)
|
||||
.toNumber();
|
||||
|
||||
return {
|
||||
date,
|
||||
netPerformanceInPercentage,
|
||||
netPerformance: totalNetPerformanceValues[date].toNumber(),
|
||||
totalInvestment: totalInvestmentValues[date].toNumber(),
|
||||
value: totalInvestmentValues[date]
|
||||
.plus(totalNetPerformanceValues[date])
|
||||
.toNumber()
|
||||
netPerformance: totalNetPerformanceValue.toNumber(),
|
||||
totalInvestment: totalInvestmentValue.toNumber(),
|
||||
value: totalCurrentValue.toNumber()
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -906,12 +919,16 @@ export class PortfolioCalculator {
|
||||
|
||||
if (orders.length <= 0) {
|
||||
return {
|
||||
currentValues: {},
|
||||
grossPerformance: new Big(0),
|
||||
grossPerformancePercentage: new Big(0),
|
||||
hasErrors: false,
|
||||
initialValue: new Big(0),
|
||||
investmentValues: {},
|
||||
maxInvestmentValues: {},
|
||||
netPerformance: new Big(0),
|
||||
netPerformancePercentage: new Big(0),
|
||||
grossPerformance: new Big(0),
|
||||
grossPerformancePercentage: new Big(0)
|
||||
netPerformanceValues: {}
|
||||
};
|
||||
}
|
||||
|
||||
@ -946,6 +963,7 @@ export class PortfolioCalculator {
|
||||
let grossPerformanceFromSells = new Big(0);
|
||||
let initialValue: Big;
|
||||
let investmentAtStartDate: Big;
|
||||
const currentValues: { [date: string]: Big } = {};
|
||||
const investmentValues: { [date: string]: Big } = {};
|
||||
const maxInvestmentValues: { [date: string]: Big } = {};
|
||||
let lastAveragePrice = new Big(0);
|
||||
@ -1164,6 +1182,7 @@ export class PortfolioCalculator {
|
||||
}
|
||||
|
||||
if (isChartMode && i > indexOfStartOrder) {
|
||||
currentValues[order.date] = valueOfInvestment;
|
||||
netPerformanceValues[order.date] = grossPerformance
|
||||
.minus(grossPerformanceAtStartDate)
|
||||
.minus(fees.minus(feesAtStartDate));
|
||||
@ -1261,15 +1280,16 @@ export class PortfolioCalculator {
|
||||
}
|
||||
|
||||
return {
|
||||
initialValue,
|
||||
currentValues,
|
||||
grossPerformancePercentage,
|
||||
initialValue,
|
||||
investmentValues,
|
||||
maxInvestmentValues,
|
||||
netPerformancePercentage,
|
||||
netPerformanceValues,
|
||||
grossPerformance: totalGrossPerformance,
|
||||
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
|
||||
netPerformance: totalNetPerformance,
|
||||
grossPerformance: totalGrossPerformance
|
||||
netPerformance: totalNetPerformance
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -37,8 +37,7 @@ import {
|
||||
PortfolioSummary,
|
||||
Position,
|
||||
TimelinePosition,
|
||||
UserSettings,
|
||||
UserWithSettings
|
||||
UserSettings
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import type {
|
||||
@ -47,7 +46,8 @@ import type {
|
||||
GroupBy,
|
||||
Market,
|
||||
OrderWithAccount,
|
||||
RequestWithUser
|
||||
RequestWithUser,
|
||||
UserWithSettings
|
||||
} from '@ghostfolio/common/types';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
|
@ -4,9 +4,9 @@ import {
|
||||
DEFAULT_LANGUAGE_CODE,
|
||||
PROPERTY_STRIPE_CONFIG
|
||||
} from '@ghostfolio/common/config';
|
||||
import { UserWithSettings } from '@ghostfolio/common/interfaces';
|
||||
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 { Subscription } from '@prisma/client';
|
||||
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) {
|
||||
const latestSubscription = aSubscriptions.reduce((a, b) => {
|
||||
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
|
||||
@ -131,12 +133,14 @@ export class SubscriptionService {
|
||||
|
||||
return {
|
||||
expiresAt: latestSubscription.expiresAt,
|
||||
offer: latestSubscription.price === 0 ? 'default' : 'renewal',
|
||||
type: isBefore(new Date(), latestSubscription.expiresAt)
|
||||
? SubscriptionType.Premium
|
||||
: SubscriptionType.Basic
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
offer: 'default',
|
||||
type: SubscriptionType.Basic
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||
|
||||
export interface LookupItem {
|
||||
assetClass: AssetClass;
|
||||
assetSubClass: AssetSubClass;
|
||||
currency: string;
|
||||
dataSource: DataSource;
|
||||
name: string;
|
||||
|
@ -1,15 +1,18 @@
|
||||
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 { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
Inject,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
@ -21,7 +24,10 @@ import { SymbolService } from './symbol.service';
|
||||
|
||||
@Controller('symbol')
|
||||
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
|
||||
@ -33,7 +39,10 @@ export class SymbolController {
|
||||
@Query() { query = '' }
|
||||
): Promise<{ items: LookupItem[] }> {
|
||||
try {
|
||||
return this.symbolService.lookup(query.toLowerCase());
|
||||
return this.symbolService.lookup({
|
||||
query: query.toLowerCase(),
|
||||
user: this.request.user
|
||||
});
|
||||
} catch {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
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: [] };
|
||||
|
||||
if (!aQuery) {
|
||||
if (!query) {
|
||||
return results;
|
||||
}
|
||||
|
||||
try {
|
||||
const { items } = await this.dataProviderService.search(aQuery);
|
||||
const { items } = await this.dataProviderService.search({
|
||||
query,
|
||||
user
|
||||
});
|
||||
results.items = items;
|
||||
return results;
|
||||
} catch (error) {
|
||||
|
@ -3,17 +3,20 @@ import type {
|
||||
DateRange,
|
||||
ViewMode
|
||||
} from '@ghostfolio/common/types';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsISO8601,
|
||||
IsIn,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString
|
||||
} from 'class-validator';
|
||||
|
||||
export class UpdateUserSettingDto {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
annualInterestRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
baseCurrency?: string;
|
||||
|
@ -4,16 +4,13 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
|
||||
import {
|
||||
User as IUser,
|
||||
UserSettings,
|
||||
UserWithSettings
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
getPermissions,
|
||||
hasRole,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Role, User } from '@prisma/client';
|
||||
import { sortBy } from 'lodash';
|
||||
|
@ -45,7 +45,10 @@ export class CronService {
|
||||
dataSource,
|
||||
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 {
|
||||
DATA_GATHERING_QUEUE,
|
||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS,
|
||||
QUEUE_JOB_STATUS_LIST
|
||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
@ -11,7 +10,7 @@ import { InjectQueue } from '@nestjs/bull';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
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 { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
|
||||
@ -34,17 +33,14 @@ export class DataGatheringService {
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {}
|
||||
|
||||
public async addJobToQueue(name: string, data: any, options?: JobOptions) {
|
||||
const hasJob = await this.hasJob(name, data);
|
||||
public async addJobToQueue(name: string, data: any, opts?: JobOptions) {
|
||||
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() {
|
||||
@ -152,10 +148,11 @@ export class DataGatheringService {
|
||||
countries,
|
||||
currency,
|
||||
dataSource,
|
||||
isin,
|
||||
name,
|
||||
sectors,
|
||||
url
|
||||
} = assetProfiles[symbol];
|
||||
} = assetProfile;
|
||||
|
||||
try {
|
||||
await this.prismaService.symbolProfile.upsert({
|
||||
@ -165,6 +162,7 @@ export class DataGatheringService {
|
||||
countries,
|
||||
currency,
|
||||
dataSource,
|
||||
isin,
|
||||
name,
|
||||
sectors,
|
||||
symbol,
|
||||
@ -175,6 +173,7 @@ export class DataGatheringService {
|
||||
assetSubClass,
|
||||
countries,
|
||||
currency,
|
||||
isin,
|
||||
name,
|
||||
sectors,
|
||||
url
|
||||
@ -206,17 +205,22 @@ export class DataGatheringService {
|
||||
}
|
||||
|
||||
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
||||
for (const { dataSource, date, symbol } of aSymbolsWithStartDate) {
|
||||
await this.addJobToQueue(
|
||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS,
|
||||
{
|
||||
dataSource,
|
||||
date,
|
||||
symbol
|
||||
},
|
||||
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
|
||||
);
|
||||
}
|
||||
await this.addJobsToQueue(
|
||||
aSymbolsWithStartDate.map(({ dataSource, date, symbol }) => {
|
||||
return {
|
||||
data: {
|
||||
dataSource,
|
||||
date,
|
||||
symbol
|
||||
},
|
||||
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[]> {
|
||||
@ -233,7 +237,7 @@ export class DataGatheringService {
|
||||
return {
|
||||
dataSource,
|
||||
symbol,
|
||||
date: startDate
|
||||
date: min([startDate, subYears(new Date(), 10)])
|
||||
};
|
||||
});
|
||||
|
||||
@ -338,18 +342,4 @@ export class DataGatheringService {
|
||||
|
||||
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[] }> {
|
||||
let items: LookupItem[] = [];
|
||||
|
||||
if (aQuery.length <= 2) {
|
||||
return { items };
|
||||
}
|
||||
|
||||
try {
|
||||
const get = bent(
|
||||
`${this.URL}/search?query=${aQuery}`,
|
||||
@ -180,6 +176,8 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
return {
|
||||
name,
|
||||
symbol,
|
||||
assetClass: AssetClass.CASH,
|
||||
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
||||
currency: this.baseCurrency,
|
||||
dataSource: this.getName()
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import bent from 'bent';
|
||||
const getJSON = bent('json');
|
||||
|
||||
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 countriesMapping = {
|
||||
'Russian Federation': 'Russia'
|
||||
@ -32,17 +32,29 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
return response;
|
||||
}
|
||||
|
||||
const result = await getJSON(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/${symbol}.json`
|
||||
const profile = await getJSON(
|
||||
`${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(() => {
|
||||
return getJSON(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/${
|
||||
symbol.split('.')[0]
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${
|
||||
symbol.split('.')?.[0]
|
||||
}.json`
|
||||
);
|
||||
});
|
||||
|
||||
if (result.weight < 0.95) {
|
||||
if (holdings?.weight < 0.95) {
|
||||
// Skip if data is inaccurate
|
||||
return response;
|
||||
}
|
||||
@ -52,7 +64,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
(response.countries as unknown as Country[]).length === 0
|
||||
) {
|
||||
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;
|
||||
|
||||
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 = [];
|
||||
for (const [name, value] of Object.entries<any>(result.sectors)) {
|
||||
for (const [name, value] of Object.entries<any>(
|
||||
holdings?.sectors ?? {}
|
||||
)) {
|
||||
response.sectors.push({
|
||||
name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name,
|
||||
weight: value.weight
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
|
||||
@ -260,26 +261,51 @@ export class DataProviderService {
|
||||
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[] }>[] = [];
|
||||
let lookupItems: LookupItem[] = [];
|
||||
|
||||
for (const dataSource of this.configurationService.get('DATA_SOURCES')) {
|
||||
promises.push(
|
||||
this.getDataProvider(DataSource[dataSource]).search(aQuery)
|
||||
);
|
||||
if (query?.length < 2) {
|
||||
return { items: lookupItems };
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
searchResults.forEach((searchResult) => {
|
||||
lookupItems = lookupItems.concat(searchResult.items);
|
||||
searchResults.forEach(({ items }) => {
|
||||
if (items?.length > 0) {
|
||||
lookupItems = lookupItems.concat(items);
|
||||
}
|
||||
});
|
||||
|
||||
const filteredItems = lookupItems.filter((lookupItem) => {
|
||||
// Only allow symbols with supported currency
|
||||
return lookupItem.currency ? true : false;
|
||||
});
|
||||
const filteredItems = lookupItems
|
||||
.filter((lookupItem) => {
|
||||
// Only allow symbols with supported currency
|
||||
return lookupItem.currency ? true : false;
|
||||
})
|
||||
.sort(({ name: name1 }, { name: name2 }) => {
|
||||
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
|
||||
});
|
||||
|
||||
return {
|
||||
items: filteredItems
|
||||
@ -295,4 +321,9 @@ export class DataProviderService {
|
||||
|
||||
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,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
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 { format } from 'date-fns';
|
||||
import { format, isToday } from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
export class EodHistoricalDataService implements DataProviderInterface {
|
||||
@ -19,8 +23,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
private readonly URL = 'https://eodhistoricaldata.com/api';
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {
|
||||
this.apiKey = this.configurationService.get('EOD_HISTORICAL_DATA_API_KEY');
|
||||
}
|
||||
@ -32,8 +35,15 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
public async getAssetProfile(
|
||||
aSymbol: string
|
||||
): Promise<Partial<SymbolProfile>> {
|
||||
const [searchResult] = await this.getSearchResult(aSymbol);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
const [response, symbolProfiles] = await Promise.all([
|
||||
const [realTimeResponse, searchResponse] = await Promise.all([
|
||||
get(),
|
||||
this.symbolProfileService.getSymbolProfiles(
|
||||
aSymbols.map((symbol) => {
|
||||
return {
|
||||
symbol,
|
||||
dataSource: DataSource.EOD_HISTORICAL_DATA
|
||||
};
|
||||
})
|
||||
)
|
||||
this.search(aSymbols[0])
|
||||
]);
|
||||
|
||||
const quotes = aSymbols.length === 1 ? [response] : response;
|
||||
const quotes =
|
||||
aSymbols.length === 1 ? [realTimeResponse] : realTimeResponse;
|
||||
|
||||
return quotes.reduce((result, item, index, array) => {
|
||||
result[item.code] = {
|
||||
currency: symbolProfiles.find((symbolProfile) => {
|
||||
return symbolProfile.symbol === item.code;
|
||||
})?.currency,
|
||||
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
||||
marketPrice: item.close,
|
||||
marketState: 'delayed'
|
||||
};
|
||||
return quotes.reduce(
|
||||
(
|
||||
result: { [symbol: string]: IDataProviderResponse },
|
||||
{ close, code, timestamp }
|
||||
) => {
|
||||
result[code] = {
|
||||
currency: searchResponse?.items[0]?.currency,
|
||||
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
||||
marketPrice: close,
|
||||
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
|
||||
};
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.error(error, 'EodHistoricalDataService');
|
||||
}
|
||||
@ -156,6 +164,117 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
}
|
||||
|
||||
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[] }> {
|
||||
const items = await this.prismaService.symbolProfile.findMany({
|
||||
select: {
|
||||
assetClass: true,
|
||||
assetSubClass: true,
|
||||
currency: true,
|
||||
dataSource: true,
|
||||
name: true,
|
||||
|
@ -165,6 +165,8 @@ export class ManualService implements DataProviderInterface {
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
let items = await this.prismaService.symbolProfile.findMany({
|
||||
select: {
|
||||
assetClass: true,
|
||||
assetSubClass: true,
|
||||
currency: true,
|
||||
dataSource: true,
|
||||
name: true,
|
||||
|
@ -101,9 +101,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
modules: ['price', 'summaryProfile', 'topHoldings']
|
||||
});
|
||||
|
||||
const { assetClass, assetSubClass } = this.parseAssetClass(
|
||||
assetProfile.price
|
||||
);
|
||||
const { assetClass, assetSubClass } = this.parseAssetClass({
|
||||
quoteType: assetProfile.price.quoteType,
|
||||
shortName: assetProfile.price.shortName
|
||||
});
|
||||
|
||||
response.assetClass = assetClass;
|
||||
response.assetSubClass = assetSubClass;
|
||||
@ -408,7 +409,14 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
marketDataItem.symbol
|
||||
);
|
||||
|
||||
const { assetClass, assetSubClass } = this.parseAssetClass({
|
||||
quoteType: quote.quoteType,
|
||||
shortName: quote.shortname
|
||||
});
|
||||
|
||||
items.push({
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
symbol,
|
||||
currency: marketDataItem.currency,
|
||||
dataSource: this.getName(),
|
||||
@ -484,14 +492,20 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
return value;
|
||||
}
|
||||
|
||||
private parseAssetClass(aPrice: Price): {
|
||||
private parseAssetClass({
|
||||
quoteType,
|
||||
shortName
|
||||
}: {
|
||||
quoteType: string;
|
||||
shortName: string;
|
||||
}): {
|
||||
assetClass: AssetClass;
|
||||
assetSubClass: AssetSubClass;
|
||||
} {
|
||||
let assetClass: AssetClass;
|
||||
let assetSubClass: AssetSubClass;
|
||||
|
||||
switch (aPrice?.quoteType?.toLowerCase()) {
|
||||
switch (quoteType?.toLowerCase()) {
|
||||
case 'cryptocurrency':
|
||||
assetClass = AssetClass.CASH;
|
||||
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
|
||||
@ -509,10 +523,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
assetSubClass = AssetSubClass.COMMODITY;
|
||||
|
||||
if (
|
||||
aPrice?.shortName?.toLowerCase()?.startsWith('gold') ||
|
||||
aPrice?.shortName?.toLowerCase()?.startsWith('palladium') ||
|
||||
aPrice?.shortName?.toLowerCase()?.startsWith('platinum') ||
|
||||
aPrice?.shortName?.toLowerCase()?.startsWith('silver')
|
||||
shortName?.toLowerCase()?.startsWith('gold') ||
|
||||
shortName?.toLowerCase()?.startsWith('palladium') ||
|
||||
shortName?.toLowerCase()?.startsWith('platinum') ||
|
||||
shortName?.toLowerCase()?.startsWith('silver')
|
||||
) {
|
||||
assetSubClass = AssetSubClass.PRECIOUS_METAL;
|
||||
}
|
||||
|
@ -183,8 +183,29 @@ export class ExchangeRateDataService {
|
||||
if (marketData?.marketPrice) {
|
||||
factor = marketData?.marketPrice;
|
||||
} else {
|
||||
// TODO: Get from data provider service or calculate indirectly via base currency
|
||||
// and market data
|
||||
// Calculate indirectly via base currency
|
||||
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',
|
||||
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$'
|
||||
}
|
||||
},
|
||||
globals: {},
|
||||
coverageDirectory: '../../coverage/apps/client',
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
@ -16,7 +11,13 @@ export default {
|
||||
'jest-preset-angular/build/serializers/html-comment'
|
||||
],
|
||||
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$)'],
|
||||
preset: '../../jest.preset.js'
|
||||
|
@ -130,6 +130,13 @@ const routes: Routes = [
|
||||
'./pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.module'
|
||||
).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',
|
||||
loadChildren: () =>
|
||||
|
@ -7,10 +7,10 @@ import {
|
||||
MAT_DATE_LOCALE,
|
||||
MatNativeDateModule
|
||||
} from '@angular/material/core';
|
||||
import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete';
|
||||
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
|
||||
import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar';
|
||||
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
OnInit,
|
||||
Output
|
||||
} 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 { Access } from '@ghostfolio/common/interfaces';
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
|
||||
import { AccessTableComponent } from './access-table.component';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-dialog-content {
|
||||
.mat-mdc-dialog-content {
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,7 @@ import {
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
|
@ -3,17 +3,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-table {
|
||||
td {
|
||||
&.mat-footer-cell {
|
||||
border-top: 1px solid
|
||||
rgba(
|
||||
var(--palette-foreground-divider),
|
||||
var(--palette-foreground-divider-alpha)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-table {
|
||||
th {
|
||||
::ng-deep {
|
||||
.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,
|
||||
ViewChild
|
||||
} 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 { Router } from '@angular/router';
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||
|
@ -2,10 +2,7 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form class="align-items-center d-flex" [formGroup]="filterForm">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="compact-with-outline without-hint w-100"
|
||||
>
|
||||
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||
<mat-select formControlName="status">
|
||||
<mat-option></mat-option>
|
||||
<mat-option
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
|
||||
import { AdminJobsComponent } from './admin-jobs.component';
|
||||
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
OnInit,
|
||||
Output
|
||||
} 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 {
|
||||
DATE_FORMAT,
|
||||
|
@ -6,10 +6,7 @@ import {
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<form class="d-flex flex-column h-100">
|
||||
<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">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Date</mat-label>
|
||||
|
@ -5,7 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
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';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-dialog-content {
|
||||
.mat-mdc-dialog-content {
|
||||
max-height: unset;
|
||||
|
||||
.mat-mdc-button {
|
||||
|
@ -6,8 +6,8 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
|
@ -152,7 +152,7 @@
|
||||
mat-menu-item
|
||||
(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
|
||||
mat-menu-item
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-dialog-content {
|
||||
.mat-mdc-dialog-content {
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
||||
|
@ -7,13 +7,11 @@ import {
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import {
|
||||
AdminMarketDataDetails,
|
||||
EnhancedSymbolProfile,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
@ -33,7 +31,7 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
|
||||
})
|
||||
export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
public assetClass: string;
|
||||
public assetProfile: EnhancedSymbolProfile;
|
||||
public assetProfile: AdminMarketDataDetails['assetProfile'];
|
||||
public assetProfileForm = this.formBuilder.group({
|
||||
comment: '',
|
||||
symbolMapping: ''
|
||||
|
@ -27,7 +27,7 @@
|
||||
[disabled]="assetProfileForm.dirty"
|
||||
(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
|
||||
mat-menu-item
|
||||
|
@ -2,10 +2,10 @@ import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
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 { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { DataService } from '@ghostfolio/client/services/data.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({
|
||||
key: PROPERTY_IS_READ_ONLY_MODE,
|
||||
value: aEvent.checked ? true : undefined
|
||||
});
|
||||
}
|
||||
|
||||
public onEnableUserSignupModeChange(aEvent: MatSlideToggleChange) {
|
||||
public onEnableUserSignupModeChange(aEvent: MatCheckboxChange) {
|
||||
this.putAdminSetting({
|
||||
key: PROPERTY_IS_USER_SIGNUP_ENABLED,
|
||||
value: aEvent.checked ? undefined : false
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="d-flex my-3">
|
||||
<div class="w-50" i18n>User Count</div>
|
||||
@ -51,7 +51,7 @@
|
||||
<td>
|
||||
<button
|
||||
*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
|
||||
(click)="onDeleteCurrency(exchangeRate.label2)"
|
||||
>
|
||||
@ -101,21 +101,21 @@
|
||||
<div class="d-flex my-3">
|
||||
<div class="w-50" i18n>User Signup</div>
|
||||
<div class="w-50">
|
||||
<mat-slide-toggle
|
||||
<mat-checkbox
|
||||
color="primary"
|
||||
[checked]="info.globalPermissions.includes(permissions.createUserAccount)"
|
||||
(change)="onEnableUserSignupModeChange($event)"
|
||||
></mat-slide-toggle>
|
||||
></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionToToggleReadOnlyMode" class="d-flex my-3">
|
||||
<div class="w-50" i18n>Read-only Mode</div>
|
||||
<div class="w-50">
|
||||
<mat-slide-toggle
|
||||
<mat-checkbox
|
||||
color="primary"
|
||||
[checked]="info?.isReadOnlyMode"
|
||||
(change)="onReadOnlyModeChange($event)"
|
||||
></mat-slide-toggle>
|
||||
></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
|
||||
@ -124,7 +124,7 @@
|
||||
<div *ngIf="info?.systemMessage">
|
||||
<span>{{ info.systemMessage }}</span>
|
||||
<button
|
||||
class="mini-icon mx-1 no-min-width px-2"
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
(click)="onDeleteSystemMessage()"
|
||||
>
|
||||
@ -159,7 +159,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="mini-icon mx-1 no-min-width px-2"
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
(click)="onDeleteCoupon(coupon.code)"
|
||||
>
|
||||
@ -172,7 +172,7 @@
|
||||
<form #couponForm="ngForm" class="align-items-center d-flex">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="compact-with-outline mr-2 without-hint"
|
||||
class="mr-2 without-hint"
|
||||
>
|
||||
<mat-select
|
||||
name="duration"
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||
import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
@ -18,9 +18,9 @@ import { AdminOverviewComponent } from './admin-overview.component';
|
||||
FormsModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatCardModule,
|
||||
MatSelectModule,
|
||||
MatSlideToggleModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
providers: [CacheService],
|
||||
|
@ -3,26 +3,8 @@
|
||||
:host {
|
||||
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 {
|
||||
.mat-form-field {
|
||||
.mat-mdc-form-field {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import {
|
||||
@ -18,6 +18,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './admin-users.html'
|
||||
})
|
||||
export class AdminUsersComponent implements OnDestroy, OnInit {
|
||||
public defaultDateFormat: string;
|
||||
public getEmojiFlag = getEmojiFlag;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public info: InfoItem;
|
||||
@ -43,6 +44,10 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.defaultDateFormat = getDateFormatString(
|
||||
this.user.settings.locale
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -4,44 +4,47 @@
|
||||
<div class="users">
|
||||
<table class="gf-table">
|
||||
<thead>
|
||||
<tr class="mat-header-row">
|
||||
<th class="mat-header-cell px-1 py-2 text-right">#</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>User</th>
|
||||
<tr class="mat-mdc-header-row">
|
||||
<th class="mat-mdc-header-cell px-1 py-2 text-right">#</th>
|
||||
<th class="mat-mdc-header-cell px-1 py-2" i18n>User</th>
|
||||
<th
|
||||
*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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</th>
|
||||
<th
|
||||
*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>
|
||||
</th>
|
||||
<th
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="mat-header-cell px-1 py-2"
|
||||
class="mat-mdc-header-cell px-1 py-2"
|
||||
i18n
|
||||
>
|
||||
Last Request
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2"></th>
|
||||
<th class="mat-mdc-header-cell px-1 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let userItem of users; let i = index" class="mat-row">
|
||||
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<tr
|
||||
*ngFor="let userItem of users; let i = index"
|
||||
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">
|
||||
<span class="d-none d-sm-inline-block text-monospace"
|
||||
>{{ userItem.id }}</span
|
||||
@ -53,28 +56,29 @@
|
||||
*ngIf="userItem?.subscription?.type === 'Premium'"
|
||||
class="ml-1"
|
||||
[enableLink]="false"
|
||||
[title]="userItem.subscription.expiresAt | date: defaultDateFormat"
|
||||
></gf-premium-indicator>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="mat-cell px-1 py-2"
|
||||
class="mat-mdc-cell px-1 py-2"
|
||||
>
|
||||
<span class="h5" [title]="userItem.country"
|
||||
>{{ getEmojiFlag(userItem.country) }}</span
|
||||
>
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<td class="mat-mdc-cell px-1 py-2">
|
||||
{{ formatDistanceToNow(userItem.createdAt) }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
<td class="mat-mdc-cell px-1 py-2 text-right">
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="userItem.accountCount"
|
||||
></gf-value>
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
<td class="mat-mdc-cell px-1 py-2 text-right">
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
@ -83,7 +87,7 @@
|
||||
</td>
|
||||
<td
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="mat-cell px-1 py-2 text-right"
|
||||
class="mat-mdc-cell px-1 py-2 text-right"
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
@ -94,11 +98,11 @@
|
||||
</td>
|
||||
<td
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="mat-cell px-1 py-2"
|
||||
class="mat-mdc-cell px-1 py-2"
|
||||
>
|
||||
{{ formatDistanceToNow(userItem.lastActivity) }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<td class="mat-mdc-cell px-1 py-2">
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
|
@ -9,8 +9,8 @@
|
||||
table {
|
||||
min-width: 100%;
|
||||
|
||||
.mat-row,
|
||||
.mat-header-row {
|
||||
.mat-mdc-row,
|
||||
.mat-mdc-header-row {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
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 { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
|
@ -3,5 +3,5 @@
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 0;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
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';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
OnChanges,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
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';
|
||||
@ -56,8 +56,8 @@ export class HeaderComponent implements OnChanges {
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((id) => {
|
||||
this.impersonationId = id;
|
||||
.subscribe((impersonationId) => {
|
||||
this.impersonationId = impersonationId;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
||||
@ -78,8 +78,8 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((aId) => {
|
||||
this.hasImpersonationId = !!aId;
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
});
|
||||
|
||||
this.update();
|
||||
|
@ -9,8 +9,8 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
|
||||
<mat-card class="p-0">
|
||||
<mat-card-content>
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content class="p-0">
|
||||
<gf-positions
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
|
||||
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
||||
|
@ -66,8 +66,8 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((aId) => {
|
||||
this.hasImpersonationId = !!aId;
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
|
||||
.chart-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import {
|
||||
MatLegacySnackBar as MatSnackBar,
|
||||
MatLegacySnackBarRef as MatSnackBarRef,
|
||||
LegacyTextOnlySnackBar as TextOnlySnackBar
|
||||
} from '@angular/material/legacy-snack-bar';
|
||||
MatSnackBar,
|
||||
MatSnackBarRef,
|
||||
TextOnlySnackBar
|
||||
} from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
@ -69,8 +69,8 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((aId) => {
|
||||
this.hasImpersonationId = !!aId;
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Summary</h3>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||
<mat-card class="h-100">
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<gf-portfolio-summary
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
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 { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
||||
|
||||
|
@ -208,8 +208,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
|
||||
if (
|
||||
this.savingsRate &&
|
||||
// @ts-ignore
|
||||
this.chart.options.plugins.annotation.annotations.savingsRate
|
||||
) {
|
||||
// @ts-ignore
|
||||
this.chart.options.plugins.annotation.annotations.savingsRate.value =
|
||||
this.savingsRate;
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { MatLegacyCheckboxChange as MatCheckboxChange } from '@angular/material/legacy-checkbox';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
|
||||
import {
|
||||
|
@ -4,9 +4,9 @@
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-header>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<div class="py-3" mat-dialog-content>
|
||||
<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>
|
||||
<textarea
|
||||
cdkTextareaAutosize
|
||||
@ -45,7 +45,7 @@
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<div class="flex-grow-1">
|
||||
<mat-checkbox i18n (change)="onChangeStaySignedIn($event)"
|
||||
<mat-checkbox color="primary" i18n (change)="onChangeStaySignedIn($event)"
|
||||
>Stay signed in</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
|
@ -2,11 +2,11 @@ import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
|
||||
import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.component';
|
||||
|
@ -1,23 +1,3 @@
|
||||
:host {
|
||||
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 {
|
||||
display: block;
|
||||
|
||||
.mat-dialog-content {
|
||||
.mat-mdc-dialog-content {
|
||||
max-height: unset;
|
||||
|
||||
gf-line-chart {
|
||||
|
@ -6,10 +6,7 @@ import {
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
|
@ -257,9 +257,11 @@
|
||||
<div *ngIf="tags?.length > 0" class="row">
|
||||
<div class="col">
|
||||
<div class="h5" i18n>Tags</div>
|
||||
<mat-chip-list>
|
||||
<mat-chip *ngFor="let tag of tags">{{ tag.name }}</mat-chip>
|
||||
</mat-chip-list>
|
||||
<mat-chip-listbox>
|
||||
<mat-chip-option *ngFor="let tag of tags" disabled
|
||||
>{{ tag.name }}</mat-chip-option
|
||||
>
|
||||
</mat-chip-listbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
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 { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfTrendIndicatorModule } from '@ghostfolio/ui/trend-indicator';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
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 { GfPositionModule } from '../position/position.module';
|
||||
|
@ -3,11 +3,14 @@
|
||||
<div class="col">
|
||||
<mat-card
|
||||
*ngIf="hasPermissionToCreateOrder && rules === null"
|
||||
appearance="outlined"
|
||||
class="my-2 text-center"
|
||||
>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator>
|
||||
<mat-card-content>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator
|
||||
></mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true"></gf-rule>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
|
||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||
|
||||
|
@ -1,8 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';
|
||||
|
||||
|
@ -17,17 +17,21 @@
|
||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||
<span i18n>Portfolio Summary</span>
|
||||
</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">
|
||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||
<span i18n>Performance Benchmarks</span>
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||
<span i18n>Allocations</span>
|
||||
<span i18n>FIRE Calculator</span>
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||
<span i18n>FIRE Calculator</span>
|
||||
<span i18n>Professional Data Provider</span>
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-dialog-content {
|
||||
.mat-mdc-dialog-content {
|
||||
max-height: unset;
|
||||
|
||||
ion-icon[name='checkmark-circle-outline'] {
|
||||
|
@ -6,6 +6,7 @@
|
||||
<mat-radio-button
|
||||
*ngFor="let option of options"
|
||||
[disabled]="isLoading"
|
||||
[ngClass]="{ 'cursor-pointer': !isLoading }"
|
||||
[value]="option.value"
|
||||
>{{ option.label }}</mat-radio-button
|
||||
>
|
||||
|
@ -1,26 +1,24 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-radio-button {
|
||||
.mat-mdc-radio-button {
|
||||
border-radius: 1rem;
|
||||
margin: 0 0.25rem;
|
||||
padding: 0.15rem 0.75rem;
|
||||
|
||||
&.mat-radio-checked {
|
||||
&.mat-mdc-radio-checked {
|
||||
background-color: rgba(var(--dark-dividers));
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.mat-radio-container {
|
||||
.mdc-radio {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mat-radio-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mat-radio-label-content {
|
||||
label {
|
||||
color: rgba(var(--dark-primary-text), 1);
|
||||
cursor: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@ -28,14 +26,14 @@
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.mat-radio-button {
|
||||
&.mat-radio-checked {
|
||||
.mat-mdc-radio-button {
|
||||
&.mat-mdc-radio-checked {
|
||||
background-color: rgba(var(--light-dividers));
|
||||
border: 1px solid rgba(var(--light-disabled-text));
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.mat-radio-label-content {
|
||||
label {
|
||||
color: rgba(var(--light-primary-text), 1);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
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';
|
||||
|
||||
|
@ -8,10 +8,10 @@ import {
|
||||
} from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
MatLegacySnackBar as MatSnackBar,
|
||||
MatLegacySnackBarRef as MatSnackBarRef,
|
||||
LegacyTextOnlySnackBar as TextOnlySnackBar
|
||||
} from '@angular/material/legacy-snack-bar';
|
||||
MatSnackBar,
|
||||
MatSnackBarRef,
|
||||
TextOnlySnackBar
|
||||
} from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
|
@ -34,7 +34,7 @@
|
||||
<a [routerLink]="['/features']">feature</a>, please join the
|
||||
Ghostfolio
|
||||
<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"
|
||||
>Slack community</a
|
||||
>, tweet to
|
||||
@ -42,7 +42,7 @@
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Tweet to Ghostfolio on Twitter"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="hasPermissionForSubscription"
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>, send an e-mail to
|
||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||
>hi@ghostfol.io</a
|
||||
@ -65,6 +65,7 @@
|
||||
<ion-icon name="logo-twitter"></ion-icon>
|
||||
</a>
|
||||
<a
|
||||
*ngIf="user?.subscription?.type === 'Premium'"
|
||||
class="mx-2"
|
||||
href="mailto:hi@ghostfol.io"
|
||||
mat-icon-button
|
||||
@ -74,7 +75,7 @@
|
||||
</a>
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
href="https://ghostfolio.slack.com"
|
||||
mat-icon-button
|
||||
title="Join the Ghostfolio Slack channel"
|
||||
>
|
||||
@ -147,10 +148,7 @@
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<a
|
||||
class="d-block"
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
>
|
||||
<a class="d-block" href="https://ghostfolio.slack.com">
|
||||
<gf-value
|
||||
size="large"
|
||||
[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