Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
0b35a3c7a7 | |||
1586cd3a59 | |||
ae763cbb87 | |||
aa72287d54 | |||
d155ab6f28 | |||
913ca71aa5 | |||
1ffde2a27e | |||
fcf0cea982 | |||
ae1968aadf | |||
3e6333ef95 | |||
c69686651e | |||
93b6011ddc | |||
f567e25f27 | |||
5dc538bafb | |||
b4de06fcf0 | |||
27da0eb26e | |||
8ff80c10e5 | |||
5db5d5e79a | |||
12aac101bd | |||
3a66ccdebe | |||
6a722d1bb7 | |||
7c9407d5dc | |||
8abb517ac6 | |||
dec1d89c5c | |||
24e9ecc3e2 | |||
4a1e05b8cd | |||
39d1a85267 | |||
7cb86de7af |
61
CHANGELOG.md
61
CHANGELOG.md
@ -5,6 +5,57 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 2.23.0 - 2023-11-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the benchmarks in the markets overview by 50-Day and 200-Day trends (experimental)
|
||||||
|
- Set up the language localization for Polski (`pl`)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the data source validation in the activities import
|
||||||
|
- Changed _Twitter_ to _𝕏_
|
||||||
|
- Improved selection in the twitter bot service
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Upgraded `ng-extract-i18n-merge` from version `2.7.0` to `2.8.3`
|
||||||
|
- Upgraded `prettier` from version `3.0.3` to `3.1.0`
|
||||||
|
|
||||||
|
## 2.22.0 - 2023-11-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the platform icon to the account selectors in the cash balance transfer from one to another account
|
||||||
|
- Added the platform icon to the account selector of the create or edit activity dialog
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Optimized the style of the carousel component on mobile for the testimonial section on the landing page
|
||||||
|
- Introduced action menus in the overview of the admin control panel
|
||||||
|
- Harmonized the name column in the historical market data table of the admin control panel
|
||||||
|
- Refactored the implementation of the data range functionality (`getRange()`) in the market data service
|
||||||
|
|
||||||
|
## 2.21.0 - 2023-11-09
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Extended the system message
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the unit for the _Zen Mode_ in the overview tab of the home page
|
||||||
|
- Fixed an issue to get quotes in the _Financial Modeling Prep_ service
|
||||||
|
|
||||||
|
## 2.20.0 - 2023-11-08
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Removed the loading indicator of the unit in the overview tab of the home page
|
||||||
|
- Improved the import of historical market data in the admin control panel
|
||||||
|
- Increased the timeout in the health check endpoint for data enhancers
|
||||||
|
- Increased the timeout in the health check endpoint for data providers
|
||||||
|
- Removed the account type from the `Account` database schema
|
||||||
|
|
||||||
## 2.19.0 - 2023-11-06
|
## 2.19.0 - 2023-11-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -149,7 +200,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added support to transfer a part of the cash balance from one to another account
|
- Added support to transfer a part of the cash balance from one to another account
|
||||||
- Extended the markets overview by benchmarks (date of last all time high)
|
- Extended the benchmarks in the markets overview by the date of the last all time high
|
||||||
- Added support to import historical market data in the admin control panel
|
- Added support to import historical market data in the admin control panel
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@ -361,7 +412,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added health check endpoints for data enhancers
|
- Added a health check endpoint for data enhancers
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -537,7 +588,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Improved the usability of the login dialog
|
- Improved the usability of the login dialog
|
||||||
- Disabled the caching in the health check endpoints for data providers
|
- Disabled the caching in the health check endpoint for data providers
|
||||||
- Improved the content of the Frequently Asked Questions (FAQ) page
|
- Improved the content of the Frequently Asked Questions (FAQ) page
|
||||||
- Upgraded `prisma` from version `4.15.0` to `4.16.2`
|
- Upgraded `prisma` from version `4.15.0` to `4.16.2`
|
||||||
|
|
||||||
@ -925,7 +976,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Added a fallback to historical market data if a data provider does not provide live data
|
- Added a fallback to historical market data if a data provider does not provide live data
|
||||||
- Added a general health check endpoint
|
- Added a general health check endpoint
|
||||||
- Added health check endpoints for data providers
|
- Added a health check endpoint for data providers
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -2389,7 +2440,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added the _Ghostfolio_ trailer to the landing page
|
- Added the _Ghostfolio_ trailer to the landing page
|
||||||
- Extended the markets overview by benchmarks (current change to the all time high)
|
- Extended the benchmarks in the markets overview by the current change to the all time high
|
||||||
|
|
||||||
## 1.151.0 - 24.05.2022
|
## 1.151.0 - 24.05.2022
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { AccountType } from '@prisma/client';
|
|
||||||
import { Transform, TransformFnParams } from 'class-transformer';
|
import { Transform, TransformFnParams } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
@ -10,10 +9,6 @@ import {
|
|||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
|
|
||||||
export class CreateAccountDto {
|
export class CreateAccountDto {
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
accountType?: AccountType;
|
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
balance: number;
|
balance: number;
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { AccountType } from '@prisma/client';
|
|
||||||
import { Transform, TransformFnParams } from 'class-transformer';
|
import { Transform, TransformFnParams } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
@ -10,10 +9,6 @@ import {
|
|||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
|
|
||||||
export class UpdateAccountDto {
|
export class UpdateAccountDto {
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
accountType?: AccountType;
|
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
balance: number;
|
balance: number;
|
||||||
|
|
||||||
|
@ -9,17 +9,21 @@ import {
|
|||||||
MAX_CHART_ITEMS,
|
MAX_CHART_ITEMS,
|
||||||
PROPERTY_BENCHMARKS
|
PROPERTY_BENCHMARKS
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import {
|
||||||
|
DATE_FORMAT,
|
||||||
|
calculateBenchmarkTrend
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
BenchmarkMarketDataDetails,
|
BenchmarkMarketDataDetails,
|
||||||
BenchmarkProperty,
|
BenchmarkProperty,
|
||||||
BenchmarkResponse,
|
BenchmarkResponse,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import { BenchmarkTrend } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { SymbolProfile } from '@prisma/client';
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { format } from 'date-fns';
|
import { format, subDays } from 'date-fns';
|
||||||
import { uniqBy } from 'lodash';
|
import { uniqBy } from 'lodash';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
|
|
||||||
@ -45,9 +49,34 @@ export class BenchmarkService {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBenchmarks({ useCache = true } = {}): Promise<
|
public async getBenchmarkTrends({ dataSource, symbol }: UniqueAsset) {
|
||||||
BenchmarkResponse['benchmarks']
|
const historicalData = await this.marketDataService.marketDataItems({
|
||||||
> {
|
orderBy: {
|
||||||
|
date: 'desc'
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
date: { gte: subDays(new Date(), 400) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fiftyDayAverage = calculateBenchmarkTrend({
|
||||||
|
historicalData,
|
||||||
|
days: 50
|
||||||
|
});
|
||||||
|
const twoHundredDayAverage = calculateBenchmarkTrend({
|
||||||
|
historicalData,
|
||||||
|
days: 200
|
||||||
|
});
|
||||||
|
|
||||||
|
return { trend50d: fiftyDayAverage, trend200d: twoHundredDayAverage };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getBenchmarks({
|
||||||
|
enableSharing = false,
|
||||||
|
useCache = true
|
||||||
|
} = {}): Promise<BenchmarkResponse['benchmarks']> {
|
||||||
let benchmarks: BenchmarkResponse['benchmarks'];
|
let benchmarks: BenchmarkResponse['benchmarks'];
|
||||||
|
|
||||||
if (useCache) {
|
if (useCache) {
|
||||||
@ -62,9 +91,16 @@ export class BenchmarkService {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles();
|
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles({
|
||||||
|
enableSharing
|
||||||
|
});
|
||||||
|
|
||||||
const promises: Promise<{ date: Date; marketPrice: number }>[] = [];
|
const promisesAllTimeHighs: Promise<{ date: Date; marketPrice: number }>[] =
|
||||||
|
[];
|
||||||
|
const promisesBenchmarkTrends: Promise<{
|
||||||
|
trend50d: BenchmarkTrend;
|
||||||
|
trend200d: BenchmarkTrend;
|
||||||
|
}>[] = [];
|
||||||
|
|
||||||
const quotes = await this.dataProviderService.getQuotes({
|
const quotes = await this.dataProviderService.getQuotes({
|
||||||
items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
|
items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
|
||||||
@ -73,10 +109,18 @@ export class BenchmarkService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const { dataSource, symbol } of benchmarkAssetProfiles) {
|
for (const { dataSource, symbol } of benchmarkAssetProfiles) {
|
||||||
promises.push(this.marketDataService.getMax({ dataSource, symbol }));
|
promisesAllTimeHighs.push(
|
||||||
|
this.marketDataService.getMax({ dataSource, symbol })
|
||||||
|
);
|
||||||
|
promisesBenchmarkTrends.push(
|
||||||
|
this.getBenchmarkTrends({ dataSource, symbol })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allTimeHighs = await Promise.all(promises);
|
const [allTimeHighs, benchmarkTrends] = await Promise.all([
|
||||||
|
Promise.all(promisesAllTimeHighs),
|
||||||
|
Promise.all(promisesBenchmarkTrends)
|
||||||
|
]);
|
||||||
let storeInCache = true;
|
let storeInCache = true;
|
||||||
|
|
||||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||||
@ -93,6 +137,7 @@ export class BenchmarkService {
|
|||||||
} else {
|
} else {
|
||||||
storeInCache = false;
|
storeInCache = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
marketCondition: this.getMarketCondition(
|
marketCondition: this.getMarketCondition(
|
||||||
performancePercentFromAllTimeHigh
|
performancePercentFromAllTimeHigh
|
||||||
@ -100,10 +145,12 @@ export class BenchmarkService {
|
|||||||
name: benchmarkAssetProfiles[index].name,
|
name: benchmarkAssetProfiles[index].name,
|
||||||
performances: {
|
performances: {
|
||||||
allTimeHigh: {
|
allTimeHigh: {
|
||||||
date: allTimeHigh.date,
|
date: allTimeHigh?.date,
|
||||||
performancePercent: performancePercentFromAllTimeHigh
|
performancePercent: performancePercentFromAllTimeHigh
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
trend50d: benchmarkTrends[index].trend50d,
|
||||||
|
trend200d: benchmarkTrends[index].trend200d
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -118,14 +165,24 @@ export class BenchmarkService {
|
|||||||
return benchmarks;
|
return benchmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
|
public async getBenchmarkAssetProfiles({
|
||||||
|
enableSharing = false
|
||||||
|
} = {}): Promise<Partial<SymbolProfile>[]> {
|
||||||
const symbolProfileIds: string[] = (
|
const symbolProfileIds: string[] = (
|
||||||
((await this.propertyService.getByKey(
|
((await this.propertyService.getByKey(
|
||||||
PROPERTY_BENCHMARKS
|
PROPERTY_BENCHMARKS
|
||||||
)) as BenchmarkProperty[]) ?? []
|
)) as BenchmarkProperty[]) ?? []
|
||||||
).map(({ symbolProfileId }) => {
|
)
|
||||||
return symbolProfileId;
|
.filter((benchmark) => {
|
||||||
});
|
if (enableSharing) {
|
||||||
|
return benchmark.enableSharing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(({ symbolProfileId }) => {
|
||||||
|
return symbolProfileId;
|
||||||
|
});
|
||||||
|
|
||||||
const assetProfiles =
|
const assetProfiles =
|
||||||
await this.symbolProfileService.getSymbolProfilesByIds(symbolProfileIds);
|
await this.symbolProfileService.getSymbolProfilesByIds(symbolProfileIds);
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
||||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
@ -33,6 +34,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
export class ImportService {
|
export class ImportService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly dataGatheringService: DataGatheringService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
@ -570,6 +572,12 @@ export class ImportService {
|
|||||||
index,
|
index,
|
||||||
{ currency, dataSource, symbol }
|
{ currency, dataSource, symbol }
|
||||||
] of uniqueActivitiesDto.entries()) {
|
] of uniqueActivitiesDto.entries()) {
|
||||||
|
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
|
||||||
|
throw new Error(
|
||||||
|
`activities.${index}.dataSource ("${dataSource}") is not valid`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (dataSource !== 'MANUAL') {
|
if (dataSource !== 'MANUAL') {
|
||||||
const assetProfile = (
|
const assetProfile = (
|
||||||
await this.dataProviderService.getAssetProfiles([
|
await this.dataProviderService.getAssetProfiles([
|
||||||
|
@ -15,7 +15,6 @@ import {
|
|||||||
PROPERTY_IS_READ_ONLY_MODE,
|
PROPERTY_IS_READ_ONLY_MODE,
|
||||||
PROPERTY_SLACK_COMMUNITY_USERS,
|
PROPERTY_SLACK_COMMUNITY_USERS,
|
||||||
PROPERTY_STRIPE_CONFIG,
|
PROPERTY_STRIPE_CONFIG,
|
||||||
PROPERTY_SYSTEM_MESSAGE,
|
|
||||||
ghostfolioFearAndGreedIndexDataSource
|
ghostfolioFearAndGreedIndexDataSource
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
@ -58,7 +57,6 @@ export class InfoService {
|
|||||||
const platforms = await this.platformService.getPlatforms({
|
const platforms = await this.platformService.getPlatforms({
|
||||||
orderBy: { name: 'asc' }
|
orderBy: { name: 'asc' }
|
||||||
});
|
});
|
||||||
let systemMessage: string;
|
|
||||||
|
|
||||||
const globalPermissions: string[] = [];
|
const globalPermissions: string[] = [];
|
||||||
|
|
||||||
@ -104,10 +102,6 @@ export class InfoService {
|
|||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SYSTEM_MESSAGE')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SYSTEM_MESSAGE')) {
|
||||||
globalPermissions.push(permissions.enableSystemMessage);
|
globalPermissions.push(permissions.enableSystemMessage);
|
||||||
|
|
||||||
systemMessage = (await this.propertyService.getByKey(
|
|
||||||
PROPERTY_SYSTEM_MESSAGE
|
|
||||||
)) as string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUserSignupEnabled =
|
const isUserSignupEnabled =
|
||||||
@ -135,7 +129,6 @@ export class InfoService {
|
|||||||
platforms,
|
platforms,
|
||||||
statistics,
|
statistics,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
systemMessage,
|
|
||||||
tags,
|
tags,
|
||||||
baseCurrency: DEFAULT_CURRENCY,
|
baseCurrency: DEFAULT_CURRENCY,
|
||||||
currencies: this.exchangeRateDataService.getCurrencies()
|
currencies: this.exchangeRateDataService.getCurrencies()
|
||||||
|
@ -61,6 +61,7 @@ export const CurrentRateServiceMock = {
|
|||||||
for (const dataGatheringItem of dataGatheringItems) {
|
for (const dataGatheringItem of dataGatheringItems) {
|
||||||
values.push({
|
values.push({
|
||||||
date,
|
date,
|
||||||
|
dataSource: dataGatheringItem.dataSource,
|
||||||
marketPriceInBaseCurrency: mockGetValue(
|
marketPriceInBaseCurrency: mockGetValue(
|
||||||
dataGatheringItem.symbol,
|
dataGatheringItem.symbol,
|
||||||
date
|
date
|
||||||
@ -74,6 +75,7 @@ export const CurrentRateServiceMock = {
|
|||||||
for (const dataGatheringItem of dataGatheringItems) {
|
for (const dataGatheringItem of dataGatheringItems) {
|
||||||
values.push({
|
values.push({
|
||||||
date,
|
date,
|
||||||
|
dataSource: dataGatheringItem.dataSource,
|
||||||
marketPriceInBaseCurrency: mockGetValue(
|
marketPriceInBaseCurrency: mockGetValue(
|
||||||
dataGatheringItem.symbol,
|
dataGatheringItem.symbol,
|
||||||
date
|
date
|
||||||
|
@ -2,6 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
@ -25,30 +26,30 @@ jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => {
|
|||||||
getRange: ({
|
getRange: ({
|
||||||
dateRangeEnd,
|
dateRangeEnd,
|
||||||
dateRangeStart,
|
dateRangeStart,
|
||||||
symbols
|
uniqueAssets
|
||||||
}: {
|
}: {
|
||||||
dateRangeEnd: Date;
|
dateRangeEnd: Date;
|
||||||
dateRangeStart: Date;
|
dateRangeStart: Date;
|
||||||
symbols: string[];
|
uniqueAssets: UniqueAsset[];
|
||||||
}) => {
|
}) => {
|
||||||
return Promise.resolve<MarketData[]>([
|
return Promise.resolve<MarketData[]>([
|
||||||
{
|
{
|
||||||
createdAt: dateRangeStart,
|
createdAt: dateRangeStart,
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: uniqueAssets[0].dataSource,
|
||||||
date: dateRangeStart,
|
date: dateRangeStart,
|
||||||
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
|
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
|
||||||
marketPrice: 1841.823902,
|
marketPrice: 1841.823902,
|
||||||
state: 'CLOSE',
|
state: 'CLOSE',
|
||||||
symbol: symbols[0]
|
symbol: uniqueAssets[0].symbol
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createdAt: dateRangeEnd,
|
createdAt: dateRangeEnd,
|
||||||
dataSource: DataSource.YAHOO,
|
dataSource: uniqueAssets[0].dataSource,
|
||||||
date: dateRangeEnd,
|
date: dateRangeEnd,
|
||||||
id: '082d6893-df27-4c91-8a5d-092e84315b56',
|
id: '082d6893-df27-4c91-8a5d-092e84315b56',
|
||||||
marketPrice: 1847.839966,
|
marketPrice: 1847.839966,
|
||||||
state: 'CLOSE',
|
state: 'CLOSE',
|
||||||
symbol: symbols[0]
|
symbol: uniqueAssets[0].symbol
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -134,6 +135,7 @@ describe('CurrentRateService', () => {
|
|||||||
errors: [],
|
errors: [],
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
|
dataSource: 'YAHOO',
|
||||||
date: undefined,
|
date: undefined,
|
||||||
marketPriceInBaseCurrency: 1841.823902,
|
marketPriceInBaseCurrency: 1841.823902,
|
||||||
symbol: 'AMZN'
|
symbol: 'AMZN'
|
||||||
|
@ -2,7 +2,11 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
import { DataProviderInfo, ResponseError } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
DataProviderInfo,
|
||||||
|
ResponseError,
|
||||||
|
UniqueAsset
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { isBefore, isToday } from 'date-fns';
|
import { isBefore, isToday } from 'date-fns';
|
||||||
import { flatten, isEmpty, uniqBy } from 'lodash';
|
import { flatten, isEmpty, uniqBy } from 'lodash';
|
||||||
@ -52,6 +56,7 @@ export class CurrentRateService {
|
|||||||
|
|
||||||
if (dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice) {
|
if (dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice) {
|
||||||
result.push({
|
result.push({
|
||||||
|
dataSource: dataGatheringItem.dataSource,
|
||||||
date: today,
|
date: today,
|
||||||
marketPriceInBaseCurrency:
|
marketPriceInBaseCurrency:
|
||||||
this.exchangeRateDataService.toCurrency(
|
this.exchangeRateDataService.toCurrency(
|
||||||
@ -75,27 +80,30 @@ export class CurrentRateService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const symbols = dataGatheringItems.map((dataGatheringItem) => {
|
const uniqueAssets: UniqueAsset[] = dataGatheringItems.map(
|
||||||
return dataGatheringItem.symbol;
|
({ dataSource, symbol }) => {
|
||||||
});
|
return { dataSource, symbol };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
promises.push(
|
promises.push(
|
||||||
this.marketDataService
|
this.marketDataService
|
||||||
.getRange({
|
.getRange({
|
||||||
dateQuery,
|
dateQuery,
|
||||||
symbols
|
uniqueAssets
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data.map((marketDataItem) => {
|
return data.map(({ dataSource, date, marketPrice, symbol }) => {
|
||||||
return {
|
return {
|
||||||
date: marketDataItem.date,
|
dataSource,
|
||||||
|
date,
|
||||||
|
symbol,
|
||||||
marketPriceInBaseCurrency:
|
marketPriceInBaseCurrency:
|
||||||
this.exchangeRateDataService.toCurrency(
|
this.exchangeRateDataService.toCurrency(
|
||||||
marketDataItem.marketPrice,
|
marketPrice,
|
||||||
currencies[marketDataItem.symbol],
|
currencies[symbol],
|
||||||
userCurrency
|
userCurrency
|
||||||
),
|
)
|
||||||
symbol: marketDataItem.symbol
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -112,7 +120,7 @@ export class CurrentRateService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!isEmpty(quoteErrors)) {
|
if (!isEmpty(quoteErrors)) {
|
||||||
for (const { symbol } of quoteErrors) {
|
for (const { dataSource, symbol } of quoteErrors) {
|
||||||
try {
|
try {
|
||||||
// If missing quote, fallback to the latest available historical market price
|
// If missing quote, fallback to the latest available historical market price
|
||||||
let value: GetValueObject = response.values.find((currentValue) => {
|
let value: GetValueObject = response.values.find((currentValue) => {
|
||||||
@ -121,6 +129,7 @@ export class CurrentRateService {
|
|||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
value = {
|
value = {
|
||||||
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
date: today,
|
date: today,
|
||||||
marketPriceInBaseCurrency: 0
|
marketPriceInBaseCurrency: 0
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export interface GetValueObject {
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
export interface GetValueObject extends UniqueAsset {
|
||||||
date: Date;
|
date: Date;
|
||||||
marketPriceInBaseCurrency: number;
|
marketPriceInBaseCurrency: number;
|
||||||
symbol: string;
|
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,12 @@ export class SymbolService {
|
|||||||
|
|
||||||
const marketData = await this.marketDataService.getRange({
|
const marketData = await this.marketDataService.getRange({
|
||||||
dateQuery: { gte: subDays(new Date(), days) },
|
dateQuery: { gte: subDays(new Date(), days) },
|
||||||
symbols: [dataGatheringItem.symbol]
|
uniqueAssets: [
|
||||||
|
{
|
||||||
|
dataSource: dataGatheringItem.dataSource,
|
||||||
|
symbol: dataGatheringItem.symbol
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
historicalData = marketData.map(({ date, marketPrice: value }) => {
|
historicalData = marketData.map(({ date, marketPrice: value }) => {
|
||||||
|
@ -7,9 +7,14 @@ import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
|||||||
import {
|
import {
|
||||||
DEFAULT_CURRENCY,
|
DEFAULT_CURRENCY,
|
||||||
PROPERTY_IS_READ_ONLY_MODE,
|
PROPERTY_IS_READ_ONLY_MODE,
|
||||||
|
PROPERTY_SYSTEM_MESSAGE,
|
||||||
locale
|
locale
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
User as IUser,
|
||||||
|
SystemMessage,
|
||||||
|
UserSettings
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import {
|
import {
|
||||||
getPermissions,
|
getPermissions,
|
||||||
hasRole,
|
hasRole,
|
||||||
@ -48,6 +53,17 @@ export class UserService {
|
|||||||
orderBy: { alias: 'asc' },
|
orderBy: { alias: 'asc' },
|
||||||
where: { GranteeUser: { id } }
|
where: { GranteeUser: { id } }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let systemMessage: SystemMessage;
|
||||||
|
|
||||||
|
const systemMessageProperty = (await this.propertyService.getByKey(
|
||||||
|
PROPERTY_SYSTEM_MESSAGE
|
||||||
|
)) as SystemMessage;
|
||||||
|
|
||||||
|
if (systemMessageProperty?.targetGroups?.includes(subscription.type)) {
|
||||||
|
systemMessage = systemMessageProperty;
|
||||||
|
}
|
||||||
|
|
||||||
let tags = await this.tagService.getByUser(id);
|
let tags = await this.tagService.getByUser(id);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -61,6 +77,7 @@ export class UserService {
|
|||||||
id,
|
id,
|
||||||
permissions,
|
permissions,
|
||||||
subscription,
|
subscription,
|
||||||
|
systemMessage,
|
||||||
tags,
|
tags,
|
||||||
access: access.map((accessItem) => {
|
access: access.map((accessItem) => {
|
||||||
return {
|
return {
|
||||||
@ -110,7 +127,9 @@ export class UserService {
|
|||||||
updatedAt
|
updatedAt
|
||||||
} = await this.prismaService.user.findUnique({
|
} = await this.prismaService.user.findUnique({
|
||||||
include: {
|
include: {
|
||||||
Account: true,
|
Account: {
|
||||||
|
include: { Platform: true }
|
||||||
|
},
|
||||||
Analytics: true,
|
Analytics: true,
|
||||||
Settings: true,
|
Settings: true,
|
||||||
Subscription: true
|
Subscription: true
|
||||||
@ -233,8 +252,8 @@ export class UserService {
|
|||||||
currentPermissions.push(permissions.impersonateAllUsers);
|
currentPermissions.push(permissions.impersonateAllUsers);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Account = sortBy(user.Account, (account) => {
|
user.Account = sortBy(user.Account, ({ name }) => {
|
||||||
return account.name;
|
return name.toLowerCase();
|
||||||
});
|
});
|
||||||
user.permissions = currentPermissions.sort();
|
user.permissions = currentPermissions.sort();
|
||||||
|
|
||||||
|
@ -58,10 +58,18 @@
|
|||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-8figures</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-8figures</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-allvue-systems</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-basil-finance</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-beanvest</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-beanvest</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -122,6 +130,10 @@
|
|||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-kubera</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-kubera</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-magnifi</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-markets.sh</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-markets.sh</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -130,6 +142,10 @@
|
|||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-maybe-finance</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-maybe-finance</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monarch-money</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monse</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monse</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -202,6 +218,10 @@
|
|||||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-yeekatee</loc>
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-yeekatee</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-ynab</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -348,10 +368,18 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-8figures</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-8figures</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-allvue-systems</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-basil-finance</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-beanvest</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-beanvest</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -412,6 +440,10 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-kubera</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-kubera</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-magnifi</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-markets.sh</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-markets.sh</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -420,6 +452,10 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-maybe-finance</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-maybe-finance</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monarch-money</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -492,6 +528,10 @@
|
|||||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc>
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-ynab</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/es</loc>
|
<loc>https://ghostfol.io/es</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -662,10 +702,18 @@
|
|||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-8figures</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-8figures</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-allvue-systems</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-basil-finance</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-beanvest</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-beanvest</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -726,6 +774,10 @@
|
|||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-kubera</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-kubera</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-magnifi</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-markets.sh</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-markets.sh</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -734,6 +786,10 @@
|
|||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-maybe-finance</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-maybe-finance</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monarch-money</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -806,6 +862,10 @@
|
|||||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc>
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-ynab</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl</loc>
|
<loc>https://ghostfol.io/nl</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -822,10 +882,18 @@
|
|||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-8figures</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-8figures</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-allvue-systems</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-basil-finance</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-beanvest</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-beanvest</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -886,6 +954,10 @@
|
|||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-kubera</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-kubera</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-magnifi</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-markets.sh</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-markets.sh</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -894,6 +966,10 @@
|
|||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-maybe-finance</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-maybe-finance</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monarch-money</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -966,6 +1042,10 @@
|
|||||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc>
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-ynab</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/nl/functionaliteiten</loc>
|
<loc>https://ghostfol.io/nl/functionaliteiten</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
@ -1012,6 +1092,10 @@
|
|||||||
<loc>https://ghostfol.io/nl/veelgestelde-vragen</loc>
|
<loc>https://ghostfol.io/nl/veelgestelde-vragen</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pl</loc>
|
||||||
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pt</loc>
|
<loc>https://ghostfol.io/pt</loc>
|
||||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
@ -106,8 +107,10 @@ export class AlphaVantageService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getQuotes({
|
public async getQuotes({
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
|
||||||
symbols
|
symbols
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
symbols: string[];
|
symbols: string[];
|
||||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
return {};
|
return {};
|
||||||
|
@ -135,8 +135,10 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getQuotes({
|
public async getQuotes({
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
|
||||||
symbols
|
symbols
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
symbols: string[];
|
symbols: string[];
|
||||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
@ -150,7 +152,7 @@ export class CoinGeckoService implements DataProviderInterface {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
}, DEFAULT_REQUEST_TIMEOUT);
|
}, requestTimeout);
|
||||||
|
|
||||||
const quotes = await got(
|
const quotes = await got(
|
||||||
`${this.URL}/simple/price?ids=${symbols.join(
|
`${this.URL}/simple/price?ids=${symbols.join(
|
||||||
|
@ -2,6 +2,7 @@ import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/in
|
|||||||
import { HttpException, Inject, Injectable } from '@nestjs/common';
|
import { HttpException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
import ms from 'ms';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataEnhancerService {
|
export class DataEnhancerService {
|
||||||
@ -24,6 +25,7 @@ export class DataEnhancerService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const assetProfile = await dataEnhancer.enhance({
|
const assetProfile = await dataEnhancer.enhance({
|
||||||
|
requestTimeout: ms('30 seconds'),
|
||||||
response: {
|
response: {
|
||||||
assetClass: 'EQUITY',
|
assetClass: 'EQUITY',
|
||||||
assetSubClass: 'ETF'
|
assetSubClass: 'ETF'
|
||||||
|
@ -15,9 +15,11 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async enhance({
|
public async enhance({
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
|
||||||
response,
|
response,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
response: Partial<SymbolProfile>;
|
response: Partial<SymbolProfile>;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}): Promise<Partial<SymbolProfile>> {
|
}): Promise<Partial<SymbolProfile>> {
|
||||||
@ -45,7 +47,7 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
}, DEFAULT_REQUEST_TIMEOUT);
|
}, requestTimeout);
|
||||||
|
|
||||||
const mappings = await got
|
const mappings = await got
|
||||||
.post(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, {
|
.post(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, {
|
||||||
|
@ -21,9 +21,11 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public async enhance({
|
public async enhance({
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
|
||||||
response,
|
response,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
response: Partial<SymbolProfile>;
|
response: Partial<SymbolProfile>;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}): Promise<Partial<SymbolProfile>> {
|
}): Promise<Partial<SymbolProfile>> {
|
||||||
@ -37,7 +39,7 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
}, DEFAULT_REQUEST_TIMEOUT);
|
}, requestTimeout);
|
||||||
|
|
||||||
const profile = await got(
|
const profile = await got(
|
||||||
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`,
|
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`,
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||||
import { DEFAULT_CURRENCY, UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import {
|
||||||
|
DEFAULT_CURRENCY,
|
||||||
|
DEFAULT_REQUEST_TIMEOUT,
|
||||||
|
UNKNOWN_KEY
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { isCurrency } from '@ghostfolio/common/helper';
|
import { isCurrency } from '@ghostfolio/common/helper';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
@ -72,9 +76,11 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async enhance({
|
public async enhance({
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
|
||||||
response,
|
response,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
response: Partial<SymbolProfile>;
|
response: Partial<SymbolProfile>;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}): Promise<Partial<SymbolProfile>> {
|
}): Promise<Partial<SymbolProfile>> {
|
||||||
|
@ -17,6 +17,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
|
|||||||
import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
|
import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
|
||||||
import { format, isValid } from 'date-fns';
|
import { format, isValid } from 'date-fns';
|
||||||
import { groupBy, isEmpty, isNumber } from 'lodash';
|
import { groupBy, isEmpty, isNumber } from 'lodash';
|
||||||
|
import ms from 'ms';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataProviderService {
|
export class DataProviderService {
|
||||||
@ -52,6 +53,7 @@ export class DataProviderService {
|
|||||||
symbol
|
symbol
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
requestTimeout: ms('30 seconds'),
|
||||||
useCache: false
|
useCache: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -236,9 +238,11 @@ export class DataProviderService {
|
|||||||
|
|
||||||
public async getQuotes({
|
public async getQuotes({
|
||||||
items,
|
items,
|
||||||
|
requestTimeout,
|
||||||
useCache = true
|
useCache = true
|
||||||
}: {
|
}: {
|
||||||
items: UniqueAsset[];
|
items: UniqueAsset[];
|
||||||
|
requestTimeout?: number;
|
||||||
useCache?: boolean;
|
useCache?: boolean;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
[symbol: string]: IDataProviderResponse;
|
[symbol: string]: IDataProviderResponse;
|
||||||
@ -312,7 +316,7 @@ export class DataProviderService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const promise = Promise.resolve(
|
const promise = Promise.resolve(
|
||||||
dataProvider.getQuotes({ symbols: symbolsChunk })
|
dataProvider.getQuotes({ requestTimeout, symbols: symbolsChunk })
|
||||||
);
|
);
|
||||||
|
|
||||||
promises.push(
|
promises.push(
|
||||||
|
@ -132,8 +132,10 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getQuotes({
|
public async getQuotes({
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
|
||||||
symbols
|
symbols
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
symbols: string[];
|
symbols: string[];
|
||||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
let response: { [symbol: string]: IDataProviderResponse } = {};
|
let response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
@ -151,7 +153,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
}, DEFAULT_REQUEST_TIMEOUT);
|
}, requestTimeout);
|
||||||
|
|
||||||
const realTimeResponse = await got(
|
const realTimeResponse = await got(
|
||||||
`${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?api_token=${
|
`${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?api_token=${
|
||||||
|
@ -114,8 +114,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getQuotes({
|
public async getQuotes({
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
|
||||||
symbols
|
symbols
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
symbols: string[];
|
symbols: string[];
|
||||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
@ -129,9 +131,9 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
}, DEFAULT_REQUEST_TIMEOUT);
|
}, requestTimeout);
|
||||||
|
|
||||||
const response = await got(
|
const quotes = await got(
|
||||||
`${this.URL}/quote/${symbols.join(',')}?apikey=${this.apiKey}`,
|
`${this.URL}/quote/${symbols.join(',')}?apikey=${this.apiKey}`,
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -139,7 +141,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
).json<any>();
|
).json<any>();
|
||||||
|
|
||||||
for (const { price, symbol } of response) {
|
for (const { price, symbol } of quotes) {
|
||||||
response[symbol] = {
|
response[symbol] = {
|
||||||
currency: DEFAULT_CURRENCY,
|
currency: DEFAULT_CURRENCY,
|
||||||
dataProviderInfo: this.getDataProviderInfo(),
|
dataProviderInfo: this.getDataProviderInfo(),
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
|
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
@ -100,8 +101,10 @@ export class GoogleSheetsService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getQuotes({
|
public async getQuotes({
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
|
||||||
symbols
|
symbols
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
symbols: string[];
|
symbols: string[];
|
||||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
|
@ -2,9 +2,11 @@ import { SymbolProfile } from '@prisma/client';
|
|||||||
|
|
||||||
export interface DataEnhancerInterface {
|
export interface DataEnhancerInterface {
|
||||||
enhance({
|
enhance({
|
||||||
|
requestTimeout,
|
||||||
response,
|
response,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
response: Partial<SymbolProfile>;
|
response: Partial<SymbolProfile>;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}): Promise<Partial<SymbolProfile>>;
|
}): Promise<Partial<SymbolProfile>>;
|
||||||
|
@ -37,8 +37,10 @@ export interface DataProviderInterface {
|
|||||||
getName(): DataSource;
|
getName(): DataSource;
|
||||||
|
|
||||||
getQuotes({
|
getQuotes({
|
||||||
|
requestTimeout,
|
||||||
symbols
|
symbols
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
symbols: string[];
|
symbols: string[];
|
||||||
}): Promise<{ [symbol: string]: IDataProviderResponse }>;
|
}): Promise<{ [symbol: string]: IDataProviderResponse }>;
|
||||||
|
|
||||||
|
@ -134,8 +134,10 @@ export class ManualService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getQuotes({
|
public async getQuotes({
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
|
||||||
symbols
|
symbols
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
symbols: string[];
|
symbols: string[];
|
||||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
|
@ -88,8 +88,10 @@ export class RapidApiService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getQuotes({
|
public async getQuotes({
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
|
||||||
symbols
|
symbols
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
symbols: string[];
|
symbols: string[];
|
||||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
if (symbols.length <= 0) {
|
if (symbols.length <= 0) {
|
||||||
|
@ -6,7 +6,10 @@ import {
|
|||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
|
import {
|
||||||
|
DEFAULT_CURRENCY,
|
||||||
|
DEFAULT_REQUEST_TIMEOUT
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
@ -157,8 +160,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getQuotes({
|
public async getQuotes({
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
|
||||||
symbols
|
symbols
|
||||||
}: {
|
}: {
|
||||||
|
requestTimeout?: number;
|
||||||
symbols: string[];
|
symbols: string[];
|
||||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
|
@ -59,10 +59,10 @@ export class MarketDataService {
|
|||||||
|
|
||||||
public async getRange({
|
public async getRange({
|
||||||
dateQuery,
|
dateQuery,
|
||||||
symbols
|
uniqueAssets
|
||||||
}: {
|
}: {
|
||||||
dateQuery: DateQuery;
|
dateQuery: DateQuery;
|
||||||
symbols: string[];
|
uniqueAssets: UniqueAsset[];
|
||||||
}): Promise<MarketData[]> {
|
}): Promise<MarketData[]> {
|
||||||
return await this.prismaService.marketData.findMany({
|
return await this.prismaService.marketData.findMany({
|
||||||
orderBy: [
|
orderBy: [
|
||||||
@ -74,24 +74,33 @@ export class MarketDataService {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
where: {
|
where: {
|
||||||
date: dateQuery,
|
OR: uniqueAssets.map(({ dataSource, symbol }) => {
|
||||||
symbol: {
|
return {
|
||||||
in: symbols
|
AND: [
|
||||||
}
|
{
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
date: dateQuery
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async marketDataItems(params: {
|
public async marketDataItems(params: {
|
||||||
|
select?: Prisma.MarketDataSelectScalar;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
take?: number;
|
take?: number;
|
||||||
cursor?: Prisma.MarketDataWhereUniqueInput;
|
cursor?: Prisma.MarketDataWhereUniqueInput;
|
||||||
where?: Prisma.MarketDataWhereInput;
|
where?: Prisma.MarketDataWhereInput;
|
||||||
orderBy?: Prisma.MarketDataOrderByWithRelationInput;
|
orderBy?: Prisma.MarketDataOrderByWithRelationInput;
|
||||||
}): Promise<MarketData[]> {
|
}): Promise<MarketData[]> {
|
||||||
const { skip, take, cursor, where, orderBy } = params;
|
const { select, skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
return this.prismaService.marketData.findMany({
|
return this.prismaService.marketData.findMany({
|
||||||
|
select,
|
||||||
cursor,
|
cursor,
|
||||||
orderBy,
|
orderBy,
|
||||||
skip,
|
skip,
|
||||||
|
@ -57,7 +57,7 @@ export class TwitterBotService {
|
|||||||
symbolItem.marketPrice
|
symbolItem.marketPrice
|
||||||
}/100)`;
|
}/100)`;
|
||||||
|
|
||||||
const benchmarkListing = await this.getBenchmarkListing(3);
|
const benchmarkListing = await this.getBenchmarkListing();
|
||||||
|
|
||||||
if (benchmarkListing?.length > 1) {
|
if (benchmarkListing?.length > 1) {
|
||||||
status += '\n\n';
|
status += '\n\n';
|
||||||
@ -78,29 +78,22 @@ export class TwitterBotService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBenchmarkListing(aMax: number) {
|
private async getBenchmarkListing() {
|
||||||
const benchmarks = await this.benchmarkService.getBenchmarks({
|
const benchmarks = await this.benchmarkService.getBenchmarks({
|
||||||
|
enableSharing: true,
|
||||||
useCache: false
|
useCache: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const benchmarkListing: string[] = [];
|
return benchmarks
|
||||||
|
.map(({ marketCondition, name, performances }) => {
|
||||||
for (const [index, benchmark] of benchmarks.entries()) {
|
return `${name} ${(
|
||||||
if (index > aMax - 1) {
|
performances.allTimeHigh.performancePercent * 100
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
benchmarkListing.push(
|
|
||||||
`${benchmark.name} ${(
|
|
||||||
benchmark.performances.allTimeHigh.performancePercent * 100
|
|
||||||
).toFixed(1)}%${
|
).toFixed(1)}%${
|
||||||
benchmark.marketCondition !== 'NEUTRAL_MARKET'
|
marketCondition !== 'NEUTRAL_MARKET'
|
||||||
? ' ' + resolveMarketCondition(benchmark.marketCondition).emoji
|
? ' ' + resolveMarketCondition(marketCondition).emoji
|
||||||
: ''
|
: ''
|
||||||
}`
|
}`;
|
||||||
);
|
})
|
||||||
}
|
.join('\n');
|
||||||
|
|
||||||
return benchmarkListing.join('\n');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,10 @@
|
|||||||
"baseHref": "/nl/",
|
"baseHref": "/nl/",
|
||||||
"localize": ["nl"]
|
"localize": ["nl"]
|
||||||
},
|
},
|
||||||
|
"development-pl": {
|
||||||
|
"baseHref": "/pl/",
|
||||||
|
"localize": ["pl"]
|
||||||
|
},
|
||||||
"development-pt": {
|
"development-pt": {
|
||||||
"baseHref": "/pt/",
|
"baseHref": "/pt/",
|
||||||
"localize": ["pt"]
|
"localize": ["pt"]
|
||||||
@ -170,6 +174,9 @@
|
|||||||
"development-nl": {
|
"development-nl": {
|
||||||
"browserTarget": "client:build:development-nl"
|
"browserTarget": "client:build:development-nl"
|
||||||
},
|
},
|
||||||
|
"development-pl": {
|
||||||
|
"browserTarget": "client:build:development-pl"
|
||||||
|
},
|
||||||
"development-pt": {
|
"development-pt": {
|
||||||
"browserTarget": "client:build:development-pt"
|
"browserTarget": "client:build:development-pt"
|
||||||
},
|
},
|
||||||
@ -193,6 +200,7 @@
|
|||||||
"messages.fr.xlf",
|
"messages.fr.xlf",
|
||||||
"messages.it.xlf",
|
"messages.it.xlf",
|
||||||
"messages.nl.xlf",
|
"messages.nl.xlf",
|
||||||
|
"messages.pl.xlf",
|
||||||
"messages.pt.xlf",
|
"messages.pt.xlf",
|
||||||
"messages.tr.xlf"
|
"messages.tr.xlf"
|
||||||
]
|
]
|
||||||
@ -235,6 +243,10 @@
|
|||||||
"baseHref": "/nl/",
|
"baseHref": "/nl/",
|
||||||
"translation": "apps/client/src/locales/messages.nl.xlf"
|
"translation": "apps/client/src/locales/messages.nl.xlf"
|
||||||
},
|
},
|
||||||
|
"pl": {
|
||||||
|
"baseHref": "/pl/",
|
||||||
|
"translation": "apps/client/src/locales/messages.pl.xlf"
|
||||||
|
},
|
||||||
"pt": {
|
"pt": {
|
||||||
"baseHref": "/pt/",
|
"baseHref": "/pt/",
|
||||||
"translation": "apps/client/src/locales/messages.pt.xlf"
|
"translation": "apps/client/src/locales/messages.pt.xlf"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<header>
|
<header>
|
||||||
<div
|
<div
|
||||||
*ngIf="canCreateAccount || (info?.systemMessage && user)"
|
*ngIf="canCreateAccount || user?.systemMessage"
|
||||||
class="info-message-container"
|
class="info-message-container"
|
||||||
>
|
>
|
||||||
<div class="info-message-inner-container position-fixed w-100">
|
<div class="info-message-inner-container position-fixed w-100">
|
||||||
@ -19,11 +19,11 @@
|
|||||||
</div></a
|
</div></a
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
*ngIf="!canCreateAccount && info?.systemMessage && user"
|
*ngIf="!canCreateAccount && user?.systemMessage"
|
||||||
class="cursor-pointer d-inline-block info-message text-truncate"
|
class="cursor-pointer d-inline-block info-message text-truncate"
|
||||||
(click)="onShowSystemMessage()"
|
(click)="onClickSystemMessage()"
|
||||||
>
|
>
|
||||||
{{ info.systemMessage }}
|
{{ user.systemMessage.message }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -127,8 +127,11 @@
|
|||||||
class="align-items-baseline d-flex"
|
class="align-items-baseline d-flex"
|
||||||
href="https://twitter.com/ghostfolio_"
|
href="https://twitter.com/ghostfolio_"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="Follow Ghostfolio on Twitter"
|
title="Follow Ghostfolio on X (formerly Twitter)"
|
||||||
>Twitter<ion-icon class="ml-1" name="open-outline"></ion-icon
|
>X (formerly Twitter)<ion-icon
|
||||||
|
class="ml-1"
|
||||||
|
name="open-outline"
|
||||||
|
></ion-icon
|
||||||
></a>
|
></a>
|
||||||
</li>
|
</li>
|
||||||
<li> </li>
|
<li> </li>
|
||||||
@ -150,6 +153,11 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
|
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
|
||||||
</li>
|
</li>
|
||||||
|
<!--
|
||||||
|
<li>
|
||||||
|
<a href="../pl" title="Ghostfolio in Polski">Polski</a>
|
||||||
|
</li>
|
||||||
|
-->
|
||||||
<li>
|
<li>
|
||||||
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -155,10 +155,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.hasInfoMessage =
|
this.hasInfoMessage =
|
||||||
hasPermission(
|
this.canCreateAccount || !!this.user?.systemMessage;
|
||||||
this.user?.permissions,
|
|
||||||
permissions.createUserAccount
|
|
||||||
) || !!this.info.systemMessage;
|
|
||||||
|
|
||||||
this.initializeTheme(this.user?.settings.colorScheme);
|
this.initializeTheme(this.user?.settings.colorScheme);
|
||||||
|
|
||||||
@ -166,12 +163,16 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCreateAccount() {
|
public onClickSystemMessage() {
|
||||||
this.tokenStorageService.signOut();
|
if (this.user.systemMessage.routerLink) {
|
||||||
|
this.router.navigate(this.user.systemMessage.routerLink);
|
||||||
|
} else {
|
||||||
|
alert(this.user.systemMessage.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onShowSystemMessage() {
|
public onCreateAccount() {
|
||||||
alert(this.info.systemMessage);
|
this.tokenStorageService.signOut();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSignOut() {
|
public onSignOut() {
|
||||||
|
@ -20,6 +20,7 @@ import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
|||||||
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { AssetSubClass, DataSource, Prisma } from '@prisma/client';
|
import { AssetSubClass, DataSource, Prisma } from '@prisma/client';
|
||||||
|
import { isUUID } from 'class-validator';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
|
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
|
||||||
@ -83,7 +84,7 @@ export class AdminMarketDataComponent
|
|||||||
public defaultDateFormat: string;
|
public defaultDateFormat: string;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public displayedColumns = [
|
public displayedColumns = [
|
||||||
'symbol',
|
'nameWithSymbol',
|
||||||
'dataSource',
|
'dataSource',
|
||||||
'assetClass',
|
'assetClass',
|
||||||
'assetSubClass',
|
'assetSubClass',
|
||||||
@ -97,6 +98,7 @@ export class AdminMarketDataComponent
|
|||||||
];
|
];
|
||||||
public filters$ = new Subject<Filter[]>();
|
public filters$ = new Subject<Filter[]>();
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
|
public isUUID = isUUID;
|
||||||
public placeholder = '';
|
public placeholder = '';
|
||||||
public pageSize = DEFAULT_PAGE_SIZE;
|
public pageSize = DEFAULT_PAGE_SIZE;
|
||||||
public totalItems = 0;
|
public totalItems = 0;
|
||||||
|
@ -28,6 +28,24 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="nameWithSymbol">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="px-1"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header="symbol"
|
||||||
|
>
|
||||||
|
<ng-container i18n>Name</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell>
|
||||||
|
<div class="text-truncate">{{ element.name }}</div>
|
||||||
|
<div *ngIf="!isUUID(element.symbol)">
|
||||||
|
<small class="text-muted">{{ element.symbol | gfSymbol }}</small>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="dataSource">
|
<ng-container matColumnDef="dataSource">
|
||||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
<ng-container i18n>Data Source</ng-container>
|
<ng-container i18n>Data Source</ng-container>
|
||||||
|
@ -6,6 +6,7 @@ import { MatPaginatorModule } from '@angular/material/paginator';
|
|||||||
import { MatSortModule } from '@angular/material/sort';
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/
|
|||||||
GfActivitiesFilterModule,
|
GfActivitiesFilterModule,
|
||||||
GfAssetProfileDialogModule,
|
GfAssetProfileDialogModule,
|
||||||
GfCreateAssetProfileDialogModule,
|
GfCreateAssetProfileDialogModule,
|
||||||
|
GfSymbolModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormBuilder, FormControl, Validators } from '@angular/forms';
|
import { FormBuilder, FormControl, Validators } from '@angular/forms';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
@ -25,8 +26,8 @@ import {
|
|||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { parse as csvToJson } from 'papaparse';
|
import { parse as csvToJson } from 'papaparse';
|
||||||
import { Subject } from 'rxjs';
|
import { EMPTY, Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { catchError, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { AssetProfileDialogParams } from './interfaces/interfaces';
|
import { AssetProfileDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@ -50,6 +51,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
assetClass: new FormControl<AssetClass>(undefined),
|
assetClass: new FormControl<AssetClass>(undefined),
|
||||||
assetSubClass: new FormControl<AssetSubClass>(undefined),
|
assetSubClass: new FormControl<AssetSubClass>(undefined),
|
||||||
comment: '',
|
comment: '',
|
||||||
|
historicalData: this.formBuilder.group({
|
||||||
|
csvString: ''
|
||||||
|
}),
|
||||||
name: ['', Validators.required],
|
name: ['', Validators.required],
|
||||||
scraperConfiguration: '',
|
scraperConfiguration: '',
|
||||||
symbolMapping: ''
|
symbolMapping: ''
|
||||||
@ -59,7 +63,6 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
public countries: {
|
public countries: {
|
||||||
[code: string]: { name: string; value: number };
|
[code: string]: { name: string; value: number };
|
||||||
};
|
};
|
||||||
public historicalDataAsCsvString: string;
|
|
||||||
public isBenchmark = false;
|
public isBenchmark = false;
|
||||||
public marketDataDetails: MarketData[] = [];
|
public marketDataDetails: MarketData[] = [];
|
||||||
public sectors: {
|
public sectors: {
|
||||||
@ -78,7 +81,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
|
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
public dialogRef: MatDialogRef<AssetProfileDialog>,
|
public dialogRef: MatDialogRef<AssetProfileDialog>,
|
||||||
private formBuilder: FormBuilder
|
private formBuilder: FormBuilder,
|
||||||
|
private snackBar: MatSnackBar
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
@ -88,9 +92,6 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public initialize() {
|
public initialize() {
|
||||||
this.historicalDataAsCsvString =
|
|
||||||
AssetProfileDialog.HISTORICAL_DATA_TEMPLATE;
|
|
||||||
|
|
||||||
this.adminService
|
this.adminService
|
||||||
.fetchAdminMarketDataBySymbol({
|
.fetchAdminMarketDataBySymbol({
|
||||||
dataSource: this.data.dataSource,
|
dataSource: this.data.dataSource,
|
||||||
@ -131,6 +132,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
assetClass: this.assetProfile.assetClass ?? null,
|
assetClass: this.assetProfile.assetClass ?? null,
|
||||||
assetSubClass: this.assetProfile.assetSubClass ?? null,
|
assetSubClass: this.assetProfile.assetSubClass ?? null,
|
||||||
comment: this.assetProfile?.comment ?? '',
|
comment: this.assetProfile?.comment ?? '',
|
||||||
|
historicalData: {
|
||||||
|
csvString: AssetProfileDialog.HISTORICAL_DATA_TEMPLATE
|
||||||
|
},
|
||||||
name: this.assetProfile.name ?? this.assetProfile.symbol,
|
name: this.assetProfile.name ?? this.assetProfile.symbol,
|
||||||
scraperConfiguration: JSON.stringify(
|
scraperConfiguration: JSON.stringify(
|
||||||
this.assetProfile?.scraperConfiguration ?? {}
|
this.assetProfile?.scraperConfiguration ?? {}
|
||||||
@ -163,26 +167,46 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onImportHistoricalData() {
|
public onImportHistoricalData() {
|
||||||
const marketData = csvToJson(this.historicalDataAsCsvString, {
|
try {
|
||||||
dynamicTyping: true,
|
const marketData = csvToJson(
|
||||||
header: true,
|
this.assetProfileForm.controls['historicalData'].controls['csvString']
|
||||||
skipEmptyLines: true
|
.value,
|
||||||
}).data;
|
{
|
||||||
|
dynamicTyping: true,
|
||||||
|
header: true,
|
||||||
|
skipEmptyLines: true
|
||||||
|
}
|
||||||
|
).data;
|
||||||
|
|
||||||
this.adminService
|
this.adminService
|
||||||
.postMarketData({
|
.postMarketData({
|
||||||
dataSource: this.data.dataSource,
|
dataSource: this.data.dataSource,
|
||||||
marketData: {
|
marketData: {
|
||||||
marketData: marketData.map(({ date, marketPrice }) => {
|
marketData: marketData.map(({ date, marketPrice }) => {
|
||||||
return { marketPrice, date: parseDate(date).toISOString() };
|
return { marketPrice, date: parseDate(date).toISOString() };
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
symbol: this.data.symbol
|
symbol: this.data.symbol
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(
|
||||||
.subscribe(() => {
|
catchError(({ error, message }) => {
|
||||||
this.initialize();
|
this.snackBar.open(`${error}: ${message[0]}`, undefined, {
|
||||||
});
|
duration: 3000
|
||||||
|
});
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
takeUntil(this.unsubscribeSubject)
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.initialize();
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
this.snackBar.open(
|
||||||
|
$localize`Oops! Could not parse historical data.`,
|
||||||
|
undefined,
|
||||||
|
{ duration: 3000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onMarketDataChanged(withRefresh: boolean = false) {
|
public onMarketDataChanged(withRefresh: boolean = false) {
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
(marketDataChanged)="onMarketDataChanged($event)"
|
(marketDataChanged)="onMarketDataChanged($event)"
|
||||||
></gf-admin-market-data-detail>
|
></gf-admin-market-data-detail>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3" formGroupName="historicalData">
|
||||||
<mat-form-field appearance="outline" class="w-100 without-hint">
|
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||||
<mat-label>
|
<mat-label>
|
||||||
<ng-container i18n>Historical Data</ng-container> (CSV)
|
<ng-container i18n>Historical Data</ng-container> (CSV)
|
||||||
@ -60,11 +60,9 @@
|
|||||||
<textarea
|
<textarea
|
||||||
cdkAutosizeMaxRows="5"
|
cdkAutosizeMaxRows="5"
|
||||||
cdkTextareaAutosize
|
cdkTextareaAutosize
|
||||||
|
formControlName="csvString"
|
||||||
matInput
|
matInput
|
||||||
placeholder="e.g. 20230601;1.61"
|
|
||||||
type="text"
|
type="text"
|
||||||
[ngModelOptions]="{standalone: true}"
|
|
||||||
[(ngModel)]="historicalDataAsCsvString"
|
|
||||||
(keyup.enter)="$event.stopPropagation()"
|
(keyup.enter)="$event.stopPropagation()"
|
||||||
></textarea>
|
></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@ -75,6 +73,7 @@
|
|||||||
color="accent"
|
color="accent"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
type="button"
|
type="button"
|
||||||
|
[disabled]="!assetProfileForm.controls['historicalData']?.controls['csvString'].touched || assetProfileForm.controls['historicalData']?.controls['csvString']?.value === ''"
|
||||||
(click)="onImportHistoricalData()"
|
(click)="onImportHistoricalData()"
|
||||||
>
|
>
|
||||||
<ng-container i18n>Import</ng-container>
|
<ng-container i18n>Import</ng-container>
|
||||||
@ -179,13 +178,13 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
|
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||||
<mat-label i18n>Name</mat-label>
|
<mat-label i18n>Name</mat-label>
|
||||||
<input formControlName="name" matInput type="text" />
|
<input formControlName="name" matInput type="text" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
|
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||||
<mat-label i18n>Asset Class</mat-label>
|
<mat-label i18n>Asset Class</mat-label>
|
||||||
<mat-select formControlName="assetClass">
|
<mat-select formControlName="assetClass">
|
||||||
<mat-option [value]="null"></mat-option>
|
<mat-option [value]="null"></mat-option>
|
||||||
@ -198,7 +197,7 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
|
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||||
<mat-label i18n>Asset Sub Class</mat-label>
|
<mat-label i18n>Asset Sub Class</mat-label>
|
||||||
<mat-select formControlName="assetSubClass">
|
<mat-select formControlName="assetSubClass">
|
||||||
<mat-option [value]="null"></mat-option>
|
<mat-option [value]="null"></mat-option>
|
||||||
|
@ -8,6 +8,7 @@ import { MatDialogModule } from '@angular/material/dialog';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
||||||
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
@ -28,6 +29,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
|
|||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
|
MatSnackBarModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
TextFieldModule
|
TextFieldModule
|
||||||
],
|
],
|
||||||
|
@ -12,7 +12,12 @@ import {
|
|||||||
PROPERTY_SYSTEM_MESSAGE,
|
PROPERTY_SYSTEM_MESSAGE,
|
||||||
ghostfolioPrefix
|
ghostfolioPrefix
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { Coupon, InfoItem, User } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
Coupon,
|
||||||
|
InfoItem,
|
||||||
|
SystemMessage,
|
||||||
|
User
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import {
|
import {
|
||||||
differenceInSeconds,
|
differenceInSeconds,
|
||||||
@ -39,6 +44,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
public hasPermissionToToggleReadOnlyMode: boolean;
|
public hasPermissionToToggleReadOnlyMode: boolean;
|
||||||
public info: InfoItem;
|
public info: InfoItem;
|
||||||
public permissions = permissions;
|
public permissions = permissions;
|
||||||
|
public systemMessage: SystemMessage;
|
||||||
public transactionCount: number;
|
public transactionCount: number;
|
||||||
public userCount: number;
|
public userCount: number;
|
||||||
public user: User;
|
public user: User;
|
||||||
@ -149,7 +155,13 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteSystemMessage() {
|
public onDeleteSystemMessage() {
|
||||||
this.putAdminSetting({ key: PROPERTY_SYSTEM_MESSAGE, value: undefined });
|
const confirmation = confirm(
|
||||||
|
$localize`Do you really want to delete this system message?`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmation === true) {
|
||||||
|
this.putAdminSetting({ key: PROPERTY_SYSTEM_MESSAGE, value: undefined });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onFlushCache() {
|
public onFlushCache() {
|
||||||
@ -184,12 +196,21 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onSetSystemMessage() {
|
public onSetSystemMessage() {
|
||||||
const systemMessage = prompt($localize`Please set your system message:`);
|
const systemMessage = prompt(
|
||||||
|
$localize`Please set your system message:`,
|
||||||
|
JSON.stringify(
|
||||||
|
this.systemMessage ??
|
||||||
|
<SystemMessage>{
|
||||||
|
message: '⚒️ Scheduled maintenance in progress...',
|
||||||
|
targetGroups: ['Basic', 'Premium']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (systemMessage) {
|
if (systemMessage) {
|
||||||
this.putAdminSetting({
|
this.putAdminSetting({
|
||||||
key: PROPERTY_SYSTEM_MESSAGE,
|
key: PROPERTY_SYSTEM_MESSAGE,
|
||||||
value: systemMessage
|
value: JSON.parse(systemMessage)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,6 +229,9 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? [];
|
this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? [];
|
||||||
this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[];
|
this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[];
|
||||||
this.exchangeRates = exchangeRates;
|
this.exchangeRates = exchangeRates;
|
||||||
|
this.systemMessage = settings[
|
||||||
|
PROPERTY_SYSTEM_MESSAGE
|
||||||
|
] as SystemMessage;
|
||||||
this.transactionCount = transactionCount;
|
this.transactionCount = transactionCount;
|
||||||
this.userCount = userCount;
|
this.userCount = userCount;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<table>
|
<table>
|
||||||
<tr *ngFor="let exchangeRate of exchangeRates">
|
<tr *ngFor="let exchangeRate of exchangeRates">
|
||||||
<td class="d-flex">
|
<td>
|
||||||
<gf-value
|
<gf-value
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[value]="1"
|
[value]="1"
|
||||||
@ -46,8 +46,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="pl-1">{{ exchangeRate.label1 }}</td>
|
<td class="pl-1">{{ exchangeRate.label1 }}</td>
|
||||||
<td class="px-1">=</td>
|
<td class="px-1">=</td>
|
||||||
<td class="d-flex justify-content-end">
|
<td align="right">
|
||||||
<gf-value
|
<gf-value
|
||||||
|
class="d-inline-block"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[precision]="4"
|
[precision]="4"
|
||||||
[value]="exchangeRate.value"
|
[value]="exchangeRate.value"
|
||||||
@ -55,26 +56,50 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="pl-1">{{ exchangeRate.label2 }}</td>
|
<td class="pl-1">{{ exchangeRate.label2 }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<button
|
||||||
class="h-100 mx-1 no-min-width px-2"
|
class="mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
[queryParams]="{
|
[matMenuTriggerFor]="exchangeRateActionsMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu
|
||||||
|
#exchangeRateActionsMenu="matMenu"
|
||||||
|
class="h-100 mx-1 no-min-width px-2"
|
||||||
|
xPosition="before"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
mat-menu-item
|
||||||
|
[queryParams]="{
|
||||||
assetProfileDialog: true,
|
assetProfileDialog: true,
|
||||||
dataSource: exchangeRate.dataSource,
|
dataSource: exchangeRate.dataSource,
|
||||||
symbol: exchangeRate.symbol
|
symbol: exchangeRate.symbol
|
||||||
}"
|
}"
|
||||||
[routerLink]="['/admin', 'market-data']"
|
[routerLink]="['/admin', 'market-data']"
|
||||||
>
|
>
|
||||||
<ion-icon name="create-outline"></ion-icon>
|
<span class="align-items-center d-flex">
|
||||||
</a>
|
<ion-icon
|
||||||
<button
|
class="mr-2"
|
||||||
*ngIf="customCurrencies.includes(exchangeRate.label2)"
|
name="create-outline"
|
||||||
class="h-100 mx-1 no-min-width px-2"
|
></ion-icon>
|
||||||
mat-button
|
<span i18n>Edit</span>
|
||||||
(click)="onDeleteCurrency(exchangeRate.label2)"
|
</span>
|
||||||
>
|
</a>
|
||||||
<ion-icon name="trash-outline"></ion-icon>
|
<button
|
||||||
</button>
|
*ngIf="customCurrencies.includes(exchangeRate.label2)"
|
||||||
|
mat-menu-item
|
||||||
|
(click)="onDeleteCurrency(exchangeRate.label2)"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-2"
|
||||||
|
name="trash-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span i18n>Delete</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@ -115,8 +140,8 @@
|
|||||||
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
|
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
|
||||||
<div class="w-50" i18n>System Message</div>
|
<div class="w-50" i18n>System Message</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<div *ngIf="info?.systemMessage">
|
<div *ngIf="systemMessage" class="align-items-center d-flex">
|
||||||
<span>{{ info.systemMessage }}</span>
|
<div class="text-truncate">{{ systemMessage | json }}</div>
|
||||||
<button
|
<button
|
||||||
class="h-100 mx-1 no-min-width px-2"
|
class="h-100 mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
@ -127,6 +152,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
*ngIf="!info?.systemMessage"
|
*ngIf="!info?.systemMessage"
|
||||||
|
class="mt-2"
|
||||||
color="accent"
|
color="accent"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
(click)="onSetSystemMessage()"
|
(click)="onSetSystemMessage()"
|
||||||
@ -148,17 +174,34 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr *ngFor="let coupon of coupons">
|
<tr *ngFor="let coupon of coupons">
|
||||||
<td class="text-monospace">{{ coupon.code }}</td>
|
<td class="text-monospace">{{ coupon.code }}</td>
|
||||||
<td class="d-flex justify-content-end pl-2">
|
<td class="pl-2 text-right">{{ coupon.duration }}</td>
|
||||||
{{ coupon.duration }}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
class="h-100 mx-1 no-min-width px-2"
|
class="mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
(click)="onDeleteCoupon(coupon.code)"
|
[matMenuTriggerFor]="couponActionsMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<ion-icon name="trash-outline"></ion-icon>
|
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
|
<mat-menu
|
||||||
|
#couponActionsMenu="matMenu"
|
||||||
|
class="h-100 mx-1 no-min-width px-2"
|
||||||
|
xPosition="before"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
(click)="onDeleteCoupon(coupon.code)"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-2"
|
||||||
|
name="trash-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span i18n>Delete</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
@ -20,6 +21,7 @@ import { AdminOverviewComponent } from './admin-overview.component';
|
|||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
MatMenuModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatSlideToggleModule,
|
MatSlideToggleModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
<gf-benchmark
|
<gf-benchmark
|
||||||
[benchmarks]="benchmarks"
|
[benchmarks]="benchmarks"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
|
[user]="user"
|
||||||
></gf-benchmark>
|
></gf-benchmark>
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
*ngIf="isLoading"
|
*ngIf="isLoading"
|
||||||
|
@ -33,6 +33,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
public isLoadingPerformance = true;
|
public isLoadingPerformance = true;
|
||||||
public performance: PortfolioPerformance;
|
public performance: PortfolioPerformance;
|
||||||
public showDetails = false;
|
public showDetails = false;
|
||||||
|
public unit: string;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -76,6 +77,8 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
!this.hasImpersonationId &&
|
!this.hasImpersonationId &&
|
||||||
!this.user.settings.isRestrictedView &&
|
!this.user.settings.isRestrictedView &&
|
||||||
this.user.settings.viewMode !== 'ZEN';
|
this.user.settings.viewMode !== 'ZEN';
|
||||||
|
|
||||||
|
this.unit = this.showDetails ? this.user.settings.baseCurrency : '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangeDateRange(dateRange: DateRange) {
|
public onChangeDateRange(dateRange: DateRange) {
|
||||||
|
@ -86,7 +86,6 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<gf-portfolio-performance
|
<gf-portfolio-performance
|
||||||
class="pb-4"
|
class="pb-4"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[errors]="errors"
|
[errors]="errors"
|
||||||
[isAllTimeHigh]="isAllTimeHigh"
|
[isAllTimeHigh]="isAllTimeHigh"
|
||||||
@ -95,6 +94,7 @@
|
|||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[performance]="performance"
|
[performance]="performance"
|
||||||
[showDetails]="showDetails"
|
[showDetails]="showDetails"
|
||||||
|
[unit]="unit"
|
||||||
></gf-portfolio-performance>
|
></gf-portfolio-performance>
|
||||||
<div *ngIf="showDetails" class="text-center">
|
<div *ngIf="showDetails" class="text-center">
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
|
@ -35,17 +35,7 @@
|
|||||||
<span #value id="value"></span>
|
<span #value id="value"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1 px-1">
|
<div class="flex-grow-1 px-1">
|
||||||
<ngx-skeleton-loader
|
{{ unit }}
|
||||||
*ngIf="isLoading"
|
|
||||||
animation="pulse"
|
|
||||||
[theme]="{
|
|
||||||
height: '1.3rem',
|
|
||||||
width: '2.5rem'
|
|
||||||
}"
|
|
||||||
></ngx-skeleton-loader>
|
|
||||||
<div *ngIf="!isLoading">
|
|
||||||
{{ unit }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="showDetails" class="row">
|
<div *ngIf="showDetails" class="row">
|
||||||
|
@ -25,7 +25,6 @@ import { isNumber } from 'lodash';
|
|||||||
styleUrls: ['./portfolio-performance.component.scss']
|
styleUrls: ['./portfolio-performance.component.scss']
|
||||||
})
|
})
|
||||||
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||||
@Input() baseCurrency: string;
|
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
@Input() errors: ResponseError['errors'];
|
@Input() errors: ResponseError['errors'];
|
||||||
@Input() isAllTimeHigh: boolean;
|
@Input() isAllTimeHigh: boolean;
|
||||||
@ -34,11 +33,10 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
|||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() performance: PortfolioPerformance;
|
@Input() performance: PortfolioPerformance;
|
||||||
@Input() showDetails: boolean;
|
@Input() showDetails: boolean;
|
||||||
|
@Input() unit: string;
|
||||||
|
|
||||||
@ViewChild('value') value: ElementRef;
|
@ViewChild('value') value: ElementRef;
|
||||||
|
|
||||||
public unit: string;
|
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
@ -50,8 +48,6 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isNumber(this.performance?.currentValue)) {
|
if (isNumber(this.performance?.currentValue)) {
|
||||||
this.unit = this.baseCurrency;
|
|
||||||
|
|
||||||
new CountUp('value', this.performance?.currentValue, {
|
new CountUp('value', this.performance?.currentValue, {
|
||||||
decimal: getNumberFormatDecimal(this.locale),
|
decimal: getNumberFormatDecimal(this.locale),
|
||||||
decimalPlaces:
|
decimalPlaces:
|
||||||
@ -63,8 +59,6 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
|||||||
separator: getNumberFormatGroup(this.locale)
|
separator: getNumberFormatGroup(this.locale)
|
||||||
}).start();
|
}).start();
|
||||||
} else if (this.performance?.currentValue === null) {
|
} else if (this.performance?.currentValue === null) {
|
||||||
this.unit = '%';
|
|
||||||
|
|
||||||
new CountUp(
|
new CountUp(
|
||||||
'value',
|
'value',
|
||||||
this.performance?.currentNetPerformancePercent * 100,
|
this.performance?.currentNetPerformancePercent * 100,
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
<div class="d-flex mr-2">
|
<div class="d-flex mr-2">
|
||||||
<gf-trend-indicator
|
<gf-trend-indicator
|
||||||
class="d-flex"
|
class="d-flex"
|
||||||
|
size="large"
|
||||||
[isLoading]="isLoading"
|
[isLoading]="isLoading"
|
||||||
[marketState]="position?.marketState"
|
[marketState]="position?.marketState"
|
||||||
[range]="range"
|
[range]="range"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
border-radius: 0.2rem;
|
border-radius: 0.2rem;
|
||||||
|
@ -44,6 +44,7 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
|||||||
'fr',
|
'fr',
|
||||||
'it',
|
'it',
|
||||||
'nl',
|
'nl',
|
||||||
|
'pl',
|
||||||
'pt',
|
'pt',
|
||||||
'tr'
|
'tr'
|
||||||
];
|
];
|
||||||
|
@ -74,6 +74,10 @@
|
|||||||
>Nederlands (<ng-container i18n>Community</ng-container
|
>Nederlands (<ng-container i18n>Community</ng-container
|
||||||
>)</mat-option
|
>)</mat-option
|
||||||
>
|
>
|
||||||
|
<mat-option value="pl"
|
||||||
|
>Polski (<ng-container i18n>Community</ng-container
|
||||||
|
>)</mat-option
|
||||||
|
>
|
||||||
<mat-option value="pt"
|
<mat-option value="pt"
|
||||||
>Português (<ng-container i18n>Community</ng-container
|
>Português (<ng-container i18n>Community</ng-container
|
||||||
>)</mat-option
|
>)</mat-option
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
community, tweet to
|
community, tweet to
|
||||||
<a
|
<a
|
||||||
href="https://twitter.com/ghostfolio_"
|
href="https://twitter.com/ghostfolio_"
|
||||||
title="Tweet to Ghostfolio on Twitter"
|
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||||
>@ghostfolio_</a
|
>@ghostfolio_</a
|
||||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||||
>, send an e-mail to
|
>, send an e-mail to
|
||||||
@ -70,14 +70,14 @@
|
|||||||
>GitHub</a
|
>GitHub</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-center">
|
<p class="align-items-center d-flex justify-content-center">
|
||||||
<a
|
<a
|
||||||
class="mx-2"
|
class="mx-2"
|
||||||
href="https://twitter.com/ghostfolio_"
|
href="https://twitter.com/ghostfolio_"
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
title="Follow Ghostfolio on Twitter"
|
title="Follow Ghostfolio on X (formerly Twitter)"
|
||||||
>
|
>
|
||||||
<ion-icon name="logo-twitter"></ion-icon>
|
<span class="line-height-1 text-center w-100">𝕏</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
*ngIf="user?.subscription?.type === 'Premium'"
|
*ngIf="user?.subscription?.type === 'Premium'"
|
||||||
|
@ -10,9 +10,17 @@
|
|||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>From</mat-label>
|
<mat-label i18n>From</mat-label>
|
||||||
<mat-select formControlName="fromAccount">
|
<mat-select formControlName="fromAccount">
|
||||||
<mat-option *ngFor="let account of accounts" [value]="account.id"
|
<mat-option *ngFor="let account of accounts" [value]="account.id">
|
||||||
>{{ account.name }}</mat-option
|
<div class="d-flex">
|
||||||
>
|
<gf-symbol-icon
|
||||||
|
*ngIf="account.Platform?.url"
|
||||||
|
class="mr-1"
|
||||||
|
[tooltip]="account.Platform?.name"
|
||||||
|
[url]="account.Platform?.url"
|
||||||
|
></gf-symbol-icon
|
||||||
|
><span>{{ account.name }}</span>
|
||||||
|
</div>
|
||||||
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@ -20,9 +28,17 @@
|
|||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>To</mat-label>
|
<mat-label i18n>To</mat-label>
|
||||||
<mat-select formControlName="toAccount">
|
<mat-select formControlName="toAccount">
|
||||||
<mat-option *ngFor="let account of accounts" [value]="account.id"
|
<mat-option *ngFor="let account of accounts" [value]="account.id">
|
||||||
>{{ account.name }}</mat-option
|
<div class="d-flex">
|
||||||
>
|
<gf-symbol-icon
|
||||||
|
*ngIf="account.Platform?.url"
|
||||||
|
class="mr-1"
|
||||||
|
[tooltip]="account.Platform?.name"
|
||||||
|
[url]="account.Platform?.url"
|
||||||
|
></gf-symbol-icon
|
||||||
|
><span>{{ account.name }}</span>
|
||||||
|
</div>
|
||||||
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,7 @@ import { MatDialogModule } from '@angular/material/dialog';
|
|||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||||
|
|
||||||
import { TransferBalanceDialog } from './transfer-balance-dialog.component';
|
import { TransferBalanceDialog } from './transfer-balance-dialog.component';
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ import { TransferBalanceDialog } from './transfer-balance-dialog.component';
|
|||||||
declarations: [TransferBalanceDialog],
|
declarations: [TransferBalanceDialog],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfSymbolIconModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h1 class="h3 mb-4 text-center">
|
<h1 class="h3 line-height-1 mb-4 text-center">
|
||||||
<span class="d-none d-sm-block" i18n>Blog</span>
|
<span class="d-none d-sm-block" i18n>Blog</span>
|
||||||
<small class="text-muted" i18n
|
<small class="text-muted" i18n
|
||||||
>Discover the latest Ghostfolio updates and insights on personal
|
>Discover the latest Ghostfolio updates and insights on personal
|
||||||
|
@ -233,7 +233,7 @@
|
|||||||
community,
|
community,
|
||||||
<a
|
<a
|
||||||
href="https://twitter.com/ghostfolio_"
|
href="https://twitter.com/ghostfolio_"
|
||||||
title="Tweet to Ghostfolio on Twitter"
|
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||||
>@ghostfolio_</a
|
>@ghostfolio_</a
|
||||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||||
>,
|
>,
|
||||||
@ -262,7 +262,7 @@
|
|||||||
>community, tweet to
|
>community, tweet to
|
||||||
<a
|
<a
|
||||||
href="https://twitter.com/ghostfolio_"
|
href="https://twitter.com/ghostfolio_"
|
||||||
title="Tweet to Ghostfolio on Twitter"
|
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||||
>@ghostfolio_</a
|
>@ghostfolio_</a
|
||||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||||
>, send an e-mail to
|
>, send an e-mail to
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h1 class="h3 mb-4 text-center">
|
<h1 class="h3 line-height-1 mb-4 text-center">
|
||||||
<span class="d-none d-sm-block" i18n>Features</span>
|
<span class="d-none d-sm-block" i18n>Features</span>
|
||||||
<small class="text-muted" i18n>
|
<small class="text-muted" i18n>
|
||||||
Check out the numerous features of Ghostfolio to manage your wealth
|
Check out the numerous features of Ghostfolio to manage your wealth
|
||||||
@ -245,7 +245,8 @@
|
|||||||
<h4 i18n>Multi-Language</h4>
|
<h4 i18n>Multi-Language</h4>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
Use Ghostfolio in multiple languages: English, Dutch, French,
|
Use Ghostfolio in multiple languages: English, Dutch, French,
|
||||||
German, Italian, Portuguese, Spanish and Turkish are currently
|
German, Italian,
|
||||||
|
<!-- Polish, -->Portuguese, Spanish and Turkish are currently
|
||||||
supported.
|
supported.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -327,7 +327,7 @@
|
|||||||
<div class="col-md-8 offset-md-2">
|
<div class="col-md-8 offset-md-2">
|
||||||
<gf-carousel [aria-label]="'Testimonials'">
|
<gf-carousel [aria-label]="'Testimonials'">
|
||||||
<div *ngFor="let testimonial of testimonials" gf-carousel-item>
|
<div *ngFor="let testimonial of testimonials" gf-carousel-item>
|
||||||
<div class="d-flex px-3">
|
<div class="d-flex px-4">
|
||||||
<gf-logo
|
<gf-logo
|
||||||
class="mr-3 mt-2 pt-1"
|
class="mr-3 mt-2 pt-1"
|
||||||
size="medium"
|
size="medium"
|
||||||
|
@ -72,9 +72,20 @@
|
|||||||
*ngIf="!activityForm.controls['accountId'].hasValidator(Validators.required)"
|
*ngIf="!activityForm.controls['accountId'].hasValidator(Validators.required)"
|
||||||
[value]="null"
|
[value]="null"
|
||||||
></mat-option>
|
></mat-option>
|
||||||
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
|
<mat-option
|
||||||
>{{ account.name }}</mat-option
|
*ngFor="let account of data.accounts"
|
||||||
|
[value]="account.id"
|
||||||
>
|
>
|
||||||
|
<div class="d-flex">
|
||||||
|
<gf-symbol-icon
|
||||||
|
*ngIf="account.Platform?.url"
|
||||||
|
class="mr-1"
|
||||||
|
[tooltip]="account.Platform?.name"
|
||||||
|
[url]="account.Platform?.url"
|
||||||
|
></gf-symbol-icon
|
||||||
|
><span>{{ account.name }}</span>
|
||||||
|
</div>
|
||||||
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,7 @@ import { MatDialogModule } from '@angular/material/dialog';
|
|||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||||
import { GfSymbolAutocompleteModule } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.module';
|
import { GfSymbolAutocompleteModule } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.module';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
GfSymbolAutocompleteModule,
|
GfSymbolAutocompleteModule,
|
||||||
|
GfSymbolIconModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Product } from '@ghostfolio/common/interfaces';
|
import { Product } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
import { AllvueSystemsPageComponent } from './products/allvue-systems-page.component';
|
||||||
import { AltooPageComponent } from './products/altoo-page.component';
|
import { AltooPageComponent } from './products/altoo-page.component';
|
||||||
|
import { BasilFinancePageComponent } from './products/basil-finance-page.component';
|
||||||
import { BeanvestPageComponent } from './products/beanvest-page.component';
|
import { BeanvestPageComponent } from './products/beanvest-page.component';
|
||||||
import { CapitallyPageComponent } from './products/capitally-page.component';
|
import { CapitallyPageComponent } from './products/capitally-page.component';
|
||||||
import { CapMonPageComponent } from './products/capmon-page.component';
|
import { CapMonPageComponent } from './products/capmon-page.component';
|
||||||
@ -17,8 +19,10 @@ import { GoSpatzPageComponent } from './products/gospatz-page.component';
|
|||||||
import { IntuitMintPageComponent } from './products/intuit-mint-page.component';
|
import { IntuitMintPageComponent } from './products/intuit-mint-page.component';
|
||||||
import { JustEtfPageComponent } from './products/justetf-page.component';
|
import { JustEtfPageComponent } from './products/justetf-page.component';
|
||||||
import { KuberaPageComponent } from './products/kubera-page.component';
|
import { KuberaPageComponent } from './products/kubera-page.component';
|
||||||
|
import { MagnifiPageComponent } from './products/magnifi-page.component';
|
||||||
import { MarketsShPageComponent } from './products/markets.sh-page.component';
|
import { MarketsShPageComponent } from './products/markets.sh-page.component';
|
||||||
import { MaybeFinancePageComponent } from './products/maybe-finance-page.component';
|
import { MaybeFinancePageComponent } from './products/maybe-finance-page.component';
|
||||||
|
import { MonarchMoneyPageComponent } from './products/monarch-money-page.component';
|
||||||
import { MonsePageComponent } from './products/monse-page.component';
|
import { MonsePageComponent } from './products/monse-page.component';
|
||||||
import { ParqetPageComponent } from './products/parqet-page.component';
|
import { ParqetPageComponent } from './products/parqet-page.component';
|
||||||
import { PlannixPageComponent } from './products/plannix-page.component';
|
import { PlannixPageComponent } from './products/plannix-page.component';
|
||||||
@ -37,6 +41,7 @@ import { UtlunaPageComponent } from './products/utluna-page.component';
|
|||||||
import { VyzerPageComponent } from './products/vyzer-page.component';
|
import { VyzerPageComponent } from './products/vyzer-page.component';
|
||||||
import { WealthicaPageComponent } from './products/wealthica-page.component';
|
import { WealthicaPageComponent } from './products/wealthica-page.component';
|
||||||
import { YeekateePageComponent } from './products/yeekatee-page.component';
|
import { YeekateePageComponent } from './products/yeekatee-page.component';
|
||||||
|
import { YnabPageComponent } from './products/ynab-page.component';
|
||||||
|
|
||||||
export const products: Product[] = [
|
export const products: Product[] = [
|
||||||
{
|
{
|
||||||
@ -62,6 +67,16 @@ export const products: Product[] = [
|
|||||||
slogan: 'Open Source Wealth Management',
|
slogan: 'Open Source Wealth Management',
|
||||||
useAnonymously: true
|
useAnonymously: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: AllvueSystemsPageComponent,
|
||||||
|
founded: 2019,
|
||||||
|
hasFreePlan: false,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
key: 'allvue-systems',
|
||||||
|
name: 'Allvue Systems',
|
||||||
|
origin: $localize`United States`,
|
||||||
|
slogan: 'Investment Software Suite'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: AltooPageComponent,
|
component: AltooPageComponent,
|
||||||
founded: 2017,
|
founded: 2017,
|
||||||
@ -71,6 +86,15 @@ export const products: Product[] = [
|
|||||||
origin: $localize`Switzerland`,
|
origin: $localize`Switzerland`,
|
||||||
slogan: 'Simplicity for Complex Wealth'
|
slogan: 'Simplicity for Complex Wealth'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: BasilFinancePageComponent,
|
||||||
|
founded: 2022,
|
||||||
|
hasFreePlan: true,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
key: 'basil-finance',
|
||||||
|
name: 'Basil Finance',
|
||||||
|
slogan: 'The ultimate solution for tracking and managing your investments'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: BeanvestPageComponent,
|
component: BeanvestPageComponent,
|
||||||
founded: 2020,
|
founded: 2020,
|
||||||
@ -239,6 +263,17 @@ export const products: Product[] = [
|
|||||||
pricingPerYear: '$150',
|
pricingPerYear: '$150',
|
||||||
slogan: 'The Time Machine for your Net Worth'
|
slogan: 'The Time Machine for your Net Worth'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: MagnifiPageComponent,
|
||||||
|
founded: 2018,
|
||||||
|
hasFreePlan: false,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
key: 'magnifi',
|
||||||
|
name: 'Magnifi',
|
||||||
|
origin: $localize`United States`,
|
||||||
|
pricingPerYear: '$132',
|
||||||
|
slogan: 'AI Investing Assistant'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: MarketsShPageComponent,
|
component: MarketsShPageComponent,
|
||||||
founded: 2022,
|
founded: 2022,
|
||||||
@ -265,6 +300,17 @@ export const products: Product[] = [
|
|||||||
region: $localize`United States`,
|
region: $localize`United States`,
|
||||||
slogan: 'Your financial future, in your control'
|
slogan: 'Your financial future, in your control'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: MonarchMoneyPageComponent,
|
||||||
|
founded: 2019,
|
||||||
|
hasFreePlan: false,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
key: 'monarch-money',
|
||||||
|
name: 'Monarch Money',
|
||||||
|
origin: $localize`United States`,
|
||||||
|
pricingPerYear: '$99.99',
|
||||||
|
slogan: 'The modern way to manage your money'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: MonsePageComponent,
|
component: MonsePageComponent,
|
||||||
hasFreePlan: false,
|
hasFreePlan: false,
|
||||||
@ -452,5 +498,16 @@ export const products: Product[] = [
|
|||||||
origin: $localize`Switzerland`,
|
origin: $localize`Switzerland`,
|
||||||
region: $localize`Switzerland`,
|
region: $localize`Switzerland`,
|
||||||
slogan: 'Connect. Share. Invest.'
|
slogan: 'Connect. Share. Invest.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: YnabPageComponent,
|
||||||
|
founded: 2004,
|
||||||
|
hasFreePlan: false,
|
||||||
|
hasSelfHostingAbility: false,
|
||||||
|
key: 'ynab',
|
||||||
|
name: 'YNAB (You Need a Budget)',
|
||||||
|
origin: $localize`United States`,
|
||||||
|
pricingPerYear: '$99',
|
||||||
|
slogan: 'Change Your Relationship With Money'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-allvue-systems-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class AllvueSystemsPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'allvue-systems';
|
||||||
|
});
|
||||||
|
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResourcesPersonalFinanceTools = [
|
||||||
|
'/' + $localize`resources`,
|
||||||
|
'personal-finance-tools'
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-basil-finance-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class BasilFinancePageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'basil-finance';
|
||||||
|
});
|
||||||
|
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResourcesPersonalFinanceTools = [
|
||||||
|
'/' + $localize`resources`,
|
||||||
|
'personal-finance-tools'
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-magnifi-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class MagnifiPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'magnifi';
|
||||||
|
});
|
||||||
|
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResourcesPersonalFinanceTools = [
|
||||||
|
'/' + $localize`resources`,
|
||||||
|
'personal-finance-tools'
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-monarch-money-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class MonarchMoneyPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'monarch-money';
|
||||||
|
});
|
||||||
|
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResourcesPersonalFinanceTools = [
|
||||||
|
'/' + $localize`resources`,
|
||||||
|
'personal-finance-tools'
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { products } from '../products';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'page' },
|
||||||
|
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||||
|
selector: 'gf-ynab-page',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['../product-page-template.scss'],
|
||||||
|
templateUrl: '../product-page-template.html'
|
||||||
|
})
|
||||||
|
export class YnabPageComponent {
|
||||||
|
public product1 = products.find(({ key }) => {
|
||||||
|
return key === 'ghostfolio';
|
||||||
|
});
|
||||||
|
|
||||||
|
public product2 = products.find(({ key }) => {
|
||||||
|
return key === 'ynab';
|
||||||
|
});
|
||||||
|
|
||||||
|
public routerLinkAbout = ['/' + $localize`about`];
|
||||||
|
public routerLinkFeatures = ['/' + $localize`features`];
|
||||||
|
public routerLinkResourcesPersonalFinanceTools = [
|
||||||
|
'/' + $localize`resources`,
|
||||||
|
'personal-finance-tools'
|
||||||
|
];
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
12844
apps/client/src/locales/messages.pl.xlf
Normal file
12844
apps/client/src/locales/messages.pl.xlf
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -101,6 +101,7 @@ export const SUPPORTED_LANGUAGE_CODES = [
|
|||||||
'fr',
|
'fr',
|
||||||
'it',
|
'it',
|
||||||
'nl',
|
'nl',
|
||||||
|
'pl',
|
||||||
'pt',
|
'pt',
|
||||||
'tr'
|
'tr'
|
||||||
];
|
];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as currencies from '@dinero.js/currencies';
|
import * as currencies from '@dinero.js/currencies';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
getDate,
|
getDate,
|
||||||
@ -10,11 +10,11 @@ import {
|
|||||||
parseISO,
|
parseISO,
|
||||||
subDays
|
subDays
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { de, es, fr, it, nl, pt, tr } from 'date-fns/locale';
|
import { de, es, fr, it, nl, pl, pt, tr } from 'date-fns/locale';
|
||||||
|
|
||||||
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
||||||
import { Benchmark, UniqueAsset } from './interfaces';
|
import { Benchmark, UniqueAsset } from './interfaces';
|
||||||
import { ColorScheme } from './types';
|
import { BenchmarkTrend, ColorScheme } from './types';
|
||||||
|
|
||||||
export const DATE_FORMAT = 'yyyy-MM-dd';
|
export const DATE_FORMAT = 'yyyy-MM-dd';
|
||||||
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
|
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
|
||||||
@ -22,6 +22,59 @@ export const DATE_FORMAT_YEARLY = 'yyyy';
|
|||||||
|
|
||||||
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
||||||
|
|
||||||
|
export function calculateBenchmarkTrend({
|
||||||
|
days,
|
||||||
|
historicalData
|
||||||
|
}: {
|
||||||
|
days: number;
|
||||||
|
historicalData: MarketData[];
|
||||||
|
}): BenchmarkTrend {
|
||||||
|
const hasEnoughData = historicalData.length >= 2 * days;
|
||||||
|
|
||||||
|
if (!hasEnoughData) {
|
||||||
|
return 'UNKNOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentPeriodAverage = calculateMovingAverage({
|
||||||
|
days,
|
||||||
|
prices: historicalData.slice(0, days).map(({ marketPrice }) => {
|
||||||
|
return new Big(marketPrice);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const pastPeriodAverage = calculateMovingAverage({
|
||||||
|
days,
|
||||||
|
prices: historicalData.slice(days, 2 * days).map(({ marketPrice }) => {
|
||||||
|
return new Big(marketPrice);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (recentPeriodAverage > pastPeriodAverage) {
|
||||||
|
return 'UP';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentPeriodAverage < pastPeriodAverage) {
|
||||||
|
return 'DOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'NEUTRAL';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateMovingAverage({
|
||||||
|
days,
|
||||||
|
prices
|
||||||
|
}: {
|
||||||
|
days: number;
|
||||||
|
prices: Big[];
|
||||||
|
}) {
|
||||||
|
return prices
|
||||||
|
.reduce((previous, current) => {
|
||||||
|
return previous.add(current);
|
||||||
|
}, new Big(0))
|
||||||
|
.div(days)
|
||||||
|
.toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
export function capitalize(aString: string) {
|
export function capitalize(aString: string) {
|
||||||
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
||||||
}
|
}
|
||||||
@ -106,6 +159,8 @@ export function getDateFnsLocale(aLanguageCode: string) {
|
|||||||
return it;
|
return it;
|
||||||
} else if (aLanguageCode === 'nl') {
|
} else if (aLanguageCode === 'nl') {
|
||||||
return nl;
|
return nl;
|
||||||
|
} else if (aLanguageCode === 'pl') {
|
||||||
|
return pl;
|
||||||
} else if (aLanguageCode === 'pt') {
|
} else if (aLanguageCode === 'pt') {
|
||||||
return pt;
|
return pt;
|
||||||
} else if (aLanguageCode === 'tr') {
|
} else if (aLanguageCode === 'tr') {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export interface BenchmarkProperty {
|
export interface BenchmarkProperty {
|
||||||
|
enableSharing?: boolean;
|
||||||
symbolProfileId: string;
|
symbolProfileId: string;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { BenchmarkTrend } from '@ghostfolio/common/types/';
|
||||||
|
|
||||||
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
||||||
|
|
||||||
export interface Benchmark {
|
export interface Benchmark {
|
||||||
@ -9,4 +11,6 @@ export interface Benchmark {
|
|||||||
performancePercent: number;
|
performancePercent: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
trend50d: BenchmarkTrend;
|
||||||
|
trend200d: BenchmarkTrend;
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@ import type { PortfolioPerformanceResponse } from './responses/portfolio-perform
|
|||||||
import type { ScraperConfiguration } from './scraper-configuration.interface';
|
import type { ScraperConfiguration } from './scraper-configuration.interface';
|
||||||
import type { Statistics } from './statistics.interface';
|
import type { Statistics } from './statistics.interface';
|
||||||
import type { Subscription } from './subscription.interface';
|
import type { Subscription } from './subscription.interface';
|
||||||
|
import { SystemMessage } from './system-message.interface';
|
||||||
import { TabConfiguration } from './tab-configuration.interface';
|
import { TabConfiguration } from './tab-configuration.interface';
|
||||||
import type { TimelinePosition } from './timeline-position.interface';
|
import type { TimelinePosition } from './timeline-position.interface';
|
||||||
import type { UniqueAsset } from './unique-asset.interface';
|
import type { UniqueAsset } from './unique-asset.interface';
|
||||||
@ -90,6 +91,7 @@ export {
|
|||||||
ResponseError,
|
ResponseError,
|
||||||
ScraperConfiguration,
|
ScraperConfiguration,
|
||||||
Statistics,
|
Statistics,
|
||||||
|
SystemMessage,
|
||||||
Subscription,
|
Subscription,
|
||||||
TabConfiguration,
|
TabConfiguration,
|
||||||
TimelinePosition,
|
TimelinePosition,
|
||||||
|
@ -17,6 +17,5 @@ export interface InfoItem {
|
|||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
stripePublicKey?: string;
|
stripePublicKey?: string;
|
||||||
subscriptions: { [offer in SubscriptionOffer]: Subscription };
|
subscriptions: { [offer in SubscriptionOffer]: Subscription };
|
||||||
systemMessage?: string;
|
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
|
||||||
|
|
||||||
|
export interface SystemMessage {
|
||||||
|
message: string;
|
||||||
|
routerLink?: string[];
|
||||||
|
targetGroups: SubscriptionType[];
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { SubscriptionOffer } from '@ghostfolio/common/types';
|
|||||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
|
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
|
||||||
import { Account, Tag } from '@prisma/client';
|
import { Account, Tag } from '@prisma/client';
|
||||||
|
|
||||||
|
import { SystemMessage } from './system-message.interface';
|
||||||
import { UserSettings } from './user-settings.interface';
|
import { UserSettings } from './user-settings.interface';
|
||||||
|
|
||||||
// TODO: Compare with UserWithSettings
|
// TODO: Compare with UserWithSettings
|
||||||
@ -14,6 +15,7 @@ export interface User {
|
|||||||
id: string;
|
id: string;
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
settings: UserSettings;
|
settings: UserSettings;
|
||||||
|
systemMessage?: SystemMessage;
|
||||||
subscription: {
|
subscription: {
|
||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
offer: SubscriptionOffer;
|
offer: SubscriptionOffer;
|
||||||
|
1
libs/common/src/lib/types/benchmark-trend.type.ts
Normal file
1
libs/common/src/lib/types/benchmark-trend.type.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type BenchmarkTrend = 'DOWN' | 'NEUTRAL' | 'UNKNOWN' | 'UP';
|
@ -1,6 +1,7 @@
|
|||||||
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
||||||
import type { AccountWithPlatform } from './account-with-platform.type';
|
import type { AccountWithPlatform } from './account-with-platform.type';
|
||||||
import type { AccountWithValue } from './account-with-value.type';
|
import type { AccountWithValue } from './account-with-value.type';
|
||||||
|
import type { BenchmarkTrend } from './benchmark-trend.type';
|
||||||
import type { ColorScheme } from './color-scheme.type';
|
import type { ColorScheme } from './color-scheme.type';
|
||||||
import type { DateRange } from './date-range.type';
|
import type { DateRange } from './date-range.type';
|
||||||
import type { Granularity } from './granularity.type';
|
import type { Granularity } from './granularity.type';
|
||||||
@ -20,6 +21,7 @@ export type {
|
|||||||
AccessWithGranteeUser,
|
AccessWithGranteeUser,
|
||||||
AccountWithPlatform,
|
AccountWithPlatform,
|
||||||
AccountWithValue,
|
AccountWithValue,
|
||||||
|
BenchmarkTrend,
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
DateRange,
|
DateRange,
|
||||||
Granularity,
|
Granularity,
|
||||||
|
@ -6,6 +6,54 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="trend50d">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell px-2 text-right"
|
||||||
|
mat-header-cell
|
||||||
|
>
|
||||||
|
<ng-container i18n>50-Day Trend</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-2" mat-cell>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-trend-indicator
|
||||||
|
*ngIf="element?.trend50d !== 'UNKNOWN'"
|
||||||
|
[value]="
|
||||||
|
element?.trend50d === 'UP'
|
||||||
|
? 0.001
|
||||||
|
: element?.trend50d === 'DOWN'
|
||||||
|
? -0.001
|
||||||
|
: 0
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="trend200d">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell px-2 text-right"
|
||||||
|
mat-header-cell
|
||||||
|
>
|
||||||
|
<ng-container i18n>200-Day Trend</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-2" mat-cell>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-trend-indicator
|
||||||
|
*ngIf="element?.trend200d !== 'UNKNOWN'"
|
||||||
|
[value]="
|
||||||
|
element?.trend200d === 'UP'
|
||||||
|
? 0.001
|
||||||
|
: element?.trend200d === 'DOWN'
|
||||||
|
? -0.001
|
||||||
|
: 0
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="date">
|
<ng-container matColumnDef="date">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
@ -20,7 +68,7 @@
|
|||||||
[isDate]="true"
|
[isDate]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="element?.performances?.allTimeHigh?.date"
|
[value]="element?.performances?.allTimeHigh?.date"
|
||||||
></gf-value>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -35,7 +83,6 @@
|
|||||||
<td *matCellDef="let element" class="px-2 text-right" mat-cell>
|
<td *matCellDef="let element" class="px-2 text-right" mat-cell>
|
||||||
<gf-value
|
<gf-value
|
||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
size="medium"
|
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
@ -45,7 +92,7 @@
|
|||||||
element?.performances?.allTimeHigh?.performancePercent > 0
|
element?.performances?.allTimeHigh?.performancePercent > 0
|
||||||
}"
|
}"
|
||||||
[value]="element?.performances?.allTimeHigh?.performancePercent"
|
[value]="element?.performances?.allTimeHigh?.performancePercent"
|
||||||
></gf-value>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -4,9 +4,8 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
OnChanges
|
OnChanges
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { locale } from '@ghostfolio/common/config';
|
|
||||||
import { resolveMarketCondition } from '@ghostfolio/common/helper';
|
import { resolveMarketCondition } from '@ghostfolio/common/helper';
|
||||||
import { Benchmark } from '@ghostfolio/common/interfaces';
|
import { Benchmark, User } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-benchmark',
|
selector: 'gf-benchmark',
|
||||||
@ -17,6 +16,7 @@ import { Benchmark } from '@ghostfolio/common/interfaces';
|
|||||||
export class BenchmarkComponent implements OnChanges {
|
export class BenchmarkComponent implements OnChanges {
|
||||||
@Input() benchmarks: Benchmark[];
|
@Input() benchmarks: Benchmark[];
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
|
@Input() user: User;
|
||||||
|
|
||||||
public displayedColumns = ['name', 'date', 'change', 'marketCondition'];
|
public displayedColumns = ['name', 'date', 'change', 'marketCondition'];
|
||||||
public resolveMarketCondition = resolveMarketCondition;
|
public resolveMarketCondition = resolveMarketCondition;
|
||||||
@ -24,8 +24,15 @@ export class BenchmarkComponent implements OnChanges {
|
|||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
if (!this.locale) {
|
if (this.user?.settings?.isExperimentalFeatures) {
|
||||||
this.locale = locale;
|
this.displayedColumns = [
|
||||||
|
'name',
|
||||||
|
'trend50d',
|
||||||
|
'trend200d',
|
||||||
|
'date',
|
||||||
|
'change',
|
||||||
|
'marketCondition'
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
import { GfTrendIndicatorModule } from '../trend-indicator';
|
||||||
import { GfValueModule } from '../value';
|
import { GfValueModule } from '../value';
|
||||||
import { BenchmarkComponent } from './benchmark.component';
|
import { BenchmarkComponent } from './benchmark.component';
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ import { BenchmarkComponent } from './benchmark.component';
|
|||||||
exports: [BenchmarkComponent],
|
exports: [BenchmarkComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfTrendIndicatorModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatTableModule,
|
MatTableModule,
|
||||||
NgxSkeletonLoaderModule
|
NgxSkeletonLoaderModule
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
*ngIf="this.showPrevArrow"
|
*ngIf="this.showPrevArrow"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
aria-label="previous"
|
aria-label="previous"
|
||||||
class="carousel-nav carousel-nav-prev no-min-width position-absolute"
|
class="carousel-nav carousel-nav-prev no-min-width position-absolute px-1"
|
||||||
mat-stroked-button
|
mat-button
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
(click)="previous()"
|
(click)="previous()"
|
||||||
>
|
>
|
||||||
@ -25,8 +25,8 @@
|
|||||||
*ngIf="this.showNextArrow"
|
*ngIf="this.showNextArrow"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
aria-label="next"
|
aria-label="next"
|
||||||
class="carousel-nav carousel-nav-next no-min-width position-absolute"
|
class="carousel-nav carousel-nav-next no-min-width position-absolute px-1"
|
||||||
mat-stroked-button
|
mat-button
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
(click)="next()"
|
(click)="next()"
|
||||||
>
|
>
|
||||||
|
@ -12,13 +12,14 @@
|
|||||||
button {
|
button {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
&.carousel-nav-prev {
|
&.carousel-nav-prev {
|
||||||
left: -50px;
|
left: -0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.carousel-nav-next {
|
&.carousel-nav-next {
|
||||||
right: -50px;
|
right: -0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
*ngIf="marketState === 'closed' && range === '1d'; else delayed"
|
*ngIf="marketState === 'closed' && range === '1d'; else delayed"
|
||||||
class="text-muted"
|
class="text-muted"
|
||||||
name="pause-circle-outline"
|
name="pause-circle-outline"
|
||||||
size="large"
|
[size]="size"
|
||||||
>
|
>
|
||||||
</ion-icon>
|
</ion-icon>
|
||||||
<ng-template #delayed>
|
<ng-template #delayed>
|
||||||
@ -21,7 +21,7 @@
|
|||||||
*ngIf="marketState === 'delayed' && range === '1d'; else trend"
|
*ngIf="marketState === 'delayed' && range === '1d'; else trend"
|
||||||
class="text-muted"
|
class="text-muted"
|
||||||
name="time-outline"
|
name="time-outline"
|
||||||
size="large"
|
[size]="size"
|
||||||
>
|
>
|
||||||
</ion-icon>
|
</ion-icon>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -31,21 +31,21 @@
|
|||||||
*ngIf="value <= -0.0005"
|
*ngIf="value <= -0.0005"
|
||||||
class="text-danger"
|
class="text-danger"
|
||||||
name="arrow-down-circle-outline"
|
name="arrow-down-circle-outline"
|
||||||
size="large"
|
|
||||||
[ngClass]="{ 'rotate-45-down': value > -0.01 }"
|
[ngClass]="{ 'rotate-45-down': value > -0.01 }"
|
||||||
|
[size]="size"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="value > -0.0005 && value < 0.0005"
|
*ngIf="value > -0.0005 && value < 0.0005"
|
||||||
class="text-muted"
|
class="text-muted"
|
||||||
name="arrow-forward-circle-outline"
|
name="arrow-forward-circle-outline"
|
||||||
size="large"
|
[size]="size"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="value >= 0.0005"
|
*ngIf="value >= 0.0005"
|
||||||
class="text-success"
|
class="text-success"
|
||||||
name="arrow-up-circle-outline"
|
name="arrow-up-circle-outline"
|
||||||
size="large"
|
|
||||||
[ngClass]="{ 'rotate-45-up': value < 0.01 }"
|
[ngClass]="{ 'rotate-45-up': value < 0.01 }"
|
||||||
|
[size]="size"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -11,6 +11,7 @@ export class TrendIndicatorComponent {
|
|||||||
@Input() isLoading = false;
|
@Input() isLoading = false;
|
||||||
@Input() marketState: MarketState = 'open';
|
@Input() marketState: MarketState = 'open';
|
||||||
@Input() range: DateRange = 'max';
|
@Input() range: DateRange = 'max';
|
||||||
|
@Input() size: 'large' | 'medium' | 'small' = 'small';
|
||||||
@Input() value = 0;
|
@Input() value = 0;
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "2.19.0",
|
"version": "2.23.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||||
@ -113,7 +113,7 @@
|
|||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"marked": "4.2.12",
|
"marked": "4.2.12",
|
||||||
"ms": "3.0.0-canary.1",
|
"ms": "3.0.0-canary.1",
|
||||||
"ng-extract-i18n-merge": "2.7.0",
|
"ng-extract-i18n-merge": "2.8.3",
|
||||||
"ngx-device-detector": "5.0.1",
|
"ngx-device-detector": "5.0.1",
|
||||||
"ngx-markdown": "15.1.0",
|
"ngx-markdown": "15.1.0",
|
||||||
"ngx-skeleton-loader": "7.0.0",
|
"ngx-skeleton-loader": "7.0.0",
|
||||||
@ -177,7 +177,7 @@
|
|||||||
"codelyzer": "6.0.1",
|
"codelyzer": "6.0.1",
|
||||||
"cypress": "6.2.1",
|
"cypress": "6.2.1",
|
||||||
"eslint": "8.33.0",
|
"eslint": "8.33.0",
|
||||||
"eslint-config-prettier": "8.6.0",
|
"eslint-config-prettier": "9.0.0",
|
||||||
"eslint-plugin-cypress": "2.14.0",
|
"eslint-plugin-cypress": "2.14.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"eslint-plugin-storybook": "0.6.12",
|
"eslint-plugin-storybook": "0.6.12",
|
||||||
@ -188,7 +188,7 @@
|
|||||||
"jest-environment-jsdom": "29.4.3",
|
"jest-environment-jsdom": "29.4.3",
|
||||||
"jest-preset-angular": "13.1.1",
|
"jest-preset-angular": "13.1.1",
|
||||||
"nx": "17.0.2",
|
"nx": "17.0.2",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.1.0",
|
||||||
"prettier-plugin-organize-attributes": "1.0.0",
|
"prettier-plugin-organize-attributes": "1.0.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Account" DROP COLUMN "accountType";
|
@ -21,7 +21,6 @@ model Access {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Account {
|
model Account {
|
||||||
accountType AccountType?
|
|
||||||
balance Float @default(0)
|
balance Float @default(0)
|
||||||
balances AccountBalance[]
|
balances AccountBalance[]
|
||||||
comment String?
|
comment String?
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
Date,Code,Currency,Price,Quantity,Action,Fee
|
Date,Code,Currency,Price,Quantity,Action,Fee
|
||||||
12/12/2021,BTC,<invalid>,44558.42,1,buy,0
|
12/12/2021,BTCUSD,<invalid>,44558.42,1,buy,0
|
||||||
|
|
159
yarn.lock
159
yarn.lock
@ -28,12 +28,12 @@
|
|||||||
"@angular-devkit/core" "16.2.9"
|
"@angular-devkit/core" "16.2.9"
|
||||||
rxjs "7.8.1"
|
rxjs "7.8.1"
|
||||||
|
|
||||||
"@angular-devkit/architect@^0.1600.0-next.6":
|
"@angular-devkit/architect@^0.1301.0 || ^0.1401.0 || ^0.1501.0 || ^0.1601.0 || ^0.1700.0":
|
||||||
version "0.1600.6"
|
version "0.1700.0"
|
||||||
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1600.6.tgz#216f4d89086b8b4ef562b2066e430a44f7a2cf57"
|
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1700.0.tgz#419d59be6f8bc0068f8d495d7e28f4f47cfdb2ce"
|
||||||
integrity sha512-Mk/pRujuer5qRMrgC7DPwLQ88wTAEKhbs0yJ/1prm4cx+VkxX9MMf6Y4AHKRmduKmFmd2LmX21/ACiU65acH8w==
|
integrity sha512-whi7HvOjv1J3He9f+H8xNJWKyjAmWuWNl8gxNW6EZP/XLcrOu+/5QT4bPtXQBRIL/avZuc++5sNQS+kReaNCig==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@angular-devkit/core" "16.0.6"
|
"@angular-devkit/core" "17.0.0"
|
||||||
rxjs "7.8.1"
|
rxjs "7.8.1"
|
||||||
|
|
||||||
"@angular-devkit/build-angular@16.2.9":
|
"@angular-devkit/build-angular@16.2.9":
|
||||||
@ -127,17 +127,6 @@
|
|||||||
rxjs "7.8.1"
|
rxjs "7.8.1"
|
||||||
source-map "0.7.4"
|
source-map "0.7.4"
|
||||||
|
|
||||||
"@angular-devkit/core@16.0.6":
|
|
||||||
version "16.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.0.6.tgz#6bedee38bb070e9203e60c9eeda38247ef39f57d"
|
|
||||||
integrity sha512-pHbDUwXDMTWTnX/vafkFnzvYDQD8lz+w8FvMQE23Q/vN6/Q0BRf0PWTAGla6Wt+E4HaqqrbQS5P0YBwS4te2Pw==
|
|
||||||
dependencies:
|
|
||||||
ajv "8.12.0"
|
|
||||||
ajv-formats "2.1.1"
|
|
||||||
jsonc-parser "3.2.0"
|
|
||||||
rxjs "7.8.1"
|
|
||||||
source-map "0.7.4"
|
|
||||||
|
|
||||||
"@angular-devkit/core@16.1.0":
|
"@angular-devkit/core@16.1.0":
|
||||||
version "16.1.0"
|
version "16.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.1.0.tgz#cb56b19e88fc936fb0b26c5ae62591f1e8906961"
|
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.1.0.tgz#cb56b19e88fc936fb0b26c5ae62591f1e8906961"
|
||||||
@ -160,18 +149,6 @@
|
|||||||
rxjs "7.8.1"
|
rxjs "7.8.1"
|
||||||
source-map "0.7.4"
|
source-map "0.7.4"
|
||||||
|
|
||||||
"@angular-devkit/core@16.2.8", "@angular-devkit/core@^16.0.0-next.6":
|
|
||||||
version "16.2.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.8.tgz#db74f3063e7fd573be7dafd022e8dc10e43140c0"
|
|
||||||
integrity sha512-PTGozYvh1Bin5lB15PwcXa26Ayd17bWGLS3H8Rs0s+04mUDvfNofmweaX1LgumWWy3nCUTDuwHxX10M3G0wE2g==
|
|
||||||
dependencies:
|
|
||||||
ajv "8.12.0"
|
|
||||||
ajv-formats "2.1.1"
|
|
||||||
jsonc-parser "3.2.0"
|
|
||||||
picomatch "2.3.1"
|
|
||||||
rxjs "7.8.1"
|
|
||||||
source-map "0.7.4"
|
|
||||||
|
|
||||||
"@angular-devkit/core@16.2.9":
|
"@angular-devkit/core@16.2.9":
|
||||||
version "16.2.9"
|
version "16.2.9"
|
||||||
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.9.tgz#81c5c95de8c423634bf93f616683045c6cdd4dd0"
|
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.9.tgz#81c5c95de8c423634bf93f616683045c6cdd4dd0"
|
||||||
@ -184,6 +161,18 @@
|
|||||||
rxjs "7.8.1"
|
rxjs "7.8.1"
|
||||||
source-map "0.7.4"
|
source-map "0.7.4"
|
||||||
|
|
||||||
|
"@angular-devkit/core@17.0.0", "@angular-devkit/core@^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0":
|
||||||
|
version "17.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.0.0.tgz#99cd048cca37cf4d0cb60a3b6871e19449a8006a"
|
||||||
|
integrity sha512-QUu3LnEi4A8t733v2+I0sLtyBJx3Q7zdTAhaauCbxbFhDid0cbYm8hYsyG/njor1irTPxSJbn6UoetVkwUQZxg==
|
||||||
|
dependencies:
|
||||||
|
ajv "8.12.0"
|
||||||
|
ajv-formats "2.1.1"
|
||||||
|
jsonc-parser "3.2.0"
|
||||||
|
picomatch "3.0.1"
|
||||||
|
rxjs "7.8.1"
|
||||||
|
source-map "0.7.4"
|
||||||
|
|
||||||
"@angular-devkit/schematics@16.0.1":
|
"@angular-devkit/schematics@16.0.1":
|
||||||
version "16.0.1"
|
version "16.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.0.1.tgz#d49387e9e41c9cce98b155da51b0e193333dd178"
|
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.0.1.tgz#d49387e9e41c9cce98b155da51b0e193333dd178"
|
||||||
@ -217,17 +206,6 @@
|
|||||||
ora "5.4.1"
|
ora "5.4.1"
|
||||||
rxjs "7.8.1"
|
rxjs "7.8.1"
|
||||||
|
|
||||||
"@angular-devkit/schematics@16.2.8", "@angular-devkit/schematics@^16.0.0-next.6":
|
|
||||||
version "16.2.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.8.tgz#cc11cf6d00cd9131adbede9a99f3a617aedd5bc4"
|
|
||||||
integrity sha512-MBiKZOlR9/YMdflALr7/7w/BGAfo/BGTrlkqsIB6rDWV1dYiCgxI+033HsiNssLS6RQyCFx/e7JA2aBBzu9zEg==
|
|
||||||
dependencies:
|
|
||||||
"@angular-devkit/core" "16.2.8"
|
|
||||||
jsonc-parser "3.2.0"
|
|
||||||
magic-string "0.30.1"
|
|
||||||
ora "5.4.1"
|
|
||||||
rxjs "7.8.1"
|
|
||||||
|
|
||||||
"@angular-devkit/schematics@16.2.9":
|
"@angular-devkit/schematics@16.2.9":
|
||||||
version "16.2.9"
|
version "16.2.9"
|
||||||
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.9.tgz#71eed819c1665068d717d75f912f5ea689c201f9"
|
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.9.tgz#71eed819c1665068d717d75f912f5ea689c201f9"
|
||||||
@ -239,6 +217,17 @@
|
|||||||
ora "5.4.1"
|
ora "5.4.1"
|
||||||
rxjs "7.8.1"
|
rxjs "7.8.1"
|
||||||
|
|
||||||
|
"@angular-devkit/schematics@17.0.0", "@angular-devkit/schematics@^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0":
|
||||||
|
version "17.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.0.0.tgz#bfcc09a1bd145ef978f92d660df89a11e69468d4"
|
||||||
|
integrity sha512-LD7fjDORuBf139/oJ/gSwbIzQPfsm6Y67s1FD+XLi0QXaRt6dw4r7BMD08l1r//oPQofNgbEH4coGVO4NdCL/A==
|
||||||
|
dependencies:
|
||||||
|
"@angular-devkit/core" "17.0.0"
|
||||||
|
jsonc-parser "3.2.0"
|
||||||
|
magic-string "0.30.5"
|
||||||
|
ora "5.4.1"
|
||||||
|
rxjs "7.8.1"
|
||||||
|
|
||||||
"@angular-eslint/bundled-angular-compiler@16.2.0":
|
"@angular-eslint/bundled-angular-compiler@16.2.0":
|
||||||
version "16.2.0"
|
version "16.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.2.0.tgz#09d0637d738850a2c6f0523f19632e992f790102"
|
resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.2.0.tgz#09d0637d738850a2c6f0523f19632e992f790102"
|
||||||
@ -4688,13 +4677,13 @@
|
|||||||
"@angular-devkit/schematics" "16.2.9"
|
"@angular-devkit/schematics" "16.2.9"
|
||||||
jsonc-parser "3.2.0"
|
jsonc-parser "3.2.0"
|
||||||
|
|
||||||
"@schematics/angular@^16.0.0-next.6":
|
"@schematics/angular@^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0":
|
||||||
version "16.2.8"
|
version "17.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.8.tgz#d4c236767e89c536c2c15951394cac20f07bfc1f"
|
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-17.0.0.tgz#63ddf8bfbb3b117fe7a355bd22b43d2c9ff7f0ee"
|
||||||
integrity sha512-yxfxJ2IMRIt+dQcqyJR30qd/osb5NwRsi9US3gFIHP1jfjOAs1Nk8ENNd5ycYV+yykCa78KWhmbOw4G1zpR56Q==
|
integrity sha512-9jKU5x/WzaBsfSkUowK1X74FqtMXa6+A60XgW4ACO8i6fwKfPeS+tIrAieeYOX80/njBh7I5CvcpHmWA2SbcXQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@angular-devkit/core" "16.2.8"
|
"@angular-devkit/core" "17.0.0"
|
||||||
"@angular-devkit/schematics" "16.2.8"
|
"@angular-devkit/schematics" "17.0.0"
|
||||||
jsonc-parser "3.2.0"
|
jsonc-parser "3.2.0"
|
||||||
|
|
||||||
"@sigstore/bundle@^1.1.0":
|
"@sigstore/bundle@^1.1.0":
|
||||||
@ -8507,16 +8496,11 @@ commander@^6.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
||||||
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
|
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
|
||||||
|
|
||||||
commander@^8.3.0, commander@~8.3.0:
|
commander@^8.3.0:
|
||||||
version "8.3.0"
|
version "8.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
|
||||||
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
|
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
|
||||||
|
|
||||||
commander@~7.1.0:
|
|
||||||
version "7.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff"
|
|
||||||
integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg==
|
|
||||||
|
|
||||||
comment-json@4.2.3:
|
comment-json@4.2.3:
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365"
|
resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365"
|
||||||
@ -10230,10 +10214,10 @@ escodegen@^2.0.0:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
source-map "~0.6.1"
|
source-map "~0.6.1"
|
||||||
|
|
||||||
eslint-config-prettier@8.6.0:
|
eslint-config-prettier@9.0.0:
|
||||||
version "8.6.0"
|
version "9.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz#dec1d29ab728f4fa63061774e1672ac4e363d207"
|
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz#eb25485946dd0c66cd216a46232dc05451518d1f"
|
||||||
integrity sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==
|
integrity sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==
|
||||||
|
|
||||||
eslint-import-resolver-node@^0.3.7:
|
eslint-import-resolver-node@^0.3.7:
|
||||||
version "0.3.9"
|
version "0.3.9"
|
||||||
@ -13271,11 +13255,6 @@ jiti@^1.18.2:
|
|||||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42"
|
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42"
|
||||||
integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==
|
integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==
|
||||||
|
|
||||||
js-levenshtein@~1.1.6:
|
|
||||||
version "1.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
|
|
||||||
integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
|
|
||||||
|
|
||||||
js-sdsl@^4.1.4:
|
js-sdsl@^4.1.4:
|
||||||
version "4.4.2"
|
version "4.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847"
|
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847"
|
||||||
@ -14005,7 +13984,7 @@ magic-string@0.30.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.15"
|
"@jridgewell/sourcemap-codec" "^1.4.15"
|
||||||
|
|
||||||
magic-string@~0.30.2:
|
magic-string@0.30.5, magic-string@~0.30.2:
|
||||||
version "0.30.5"
|
version "0.30.5"
|
||||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
|
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
|
||||||
integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==
|
integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==
|
||||||
@ -14554,18 +14533,16 @@ neo-async@^2.5.0, neo-async@^2.6.2:
|
|||||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||||
|
|
||||||
ng-extract-i18n-merge@2.7.0:
|
ng-extract-i18n-merge@2.8.3:
|
||||||
version "2.7.0"
|
version "2.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/ng-extract-i18n-merge/-/ng-extract-i18n-merge-2.7.0.tgz#18e2acd1a7598100300c42887917e16c4782589d"
|
resolved "https://registry.yarnpkg.com/ng-extract-i18n-merge/-/ng-extract-i18n-merge-2.8.3.tgz#a092f7758df7c566df7a1d710dbc709c6a8f56d1"
|
||||||
integrity sha512-HG0Gjg4J8GqkROQSdHeCS1jtqz3ExzswH2zA8nbJNZU5ctA25O8dpfSXVl63PWxNhYtJOnP4rEPXNiyvlHaHwA==
|
integrity sha512-w6LdzpfjRBLpT7lnMEqduivjn6kg2oKDZBL6P9W5GKRZ4bgmFthAmwN1lvWrzkwcPHPARJR+qC4DBRVsv4vmkg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@angular-devkit/architect" "^0.1600.0-next.6"
|
"@angular-devkit/architect" "^0.1301.0 || ^0.1401.0 || ^0.1501.0 || ^0.1601.0 || ^0.1700.0"
|
||||||
"@angular-devkit/core" "^16.0.0-next.6"
|
"@angular-devkit/core" "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||||
"@angular-devkit/schematics" "^16.0.0-next.6"
|
"@angular-devkit/schematics" "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||||
"@schematics/angular" "^16.0.0-next.6"
|
"@schematics/angular" "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||||
xliff-simple-merge "~1.0.1"
|
xmldoc "^1.1.2"
|
||||||
xml_normalize "^1.0.0"
|
|
||||||
xmldoc "~1.1.2"
|
|
||||||
|
|
||||||
ngx-device-detector@5.0.1:
|
ngx-device-detector@5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
@ -15546,6 +15523,11 @@ picomatch@2.3.1, picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch
|
|||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||||
|
|
||||||
|
picomatch@3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-3.0.1.tgz#817033161def55ec9638567a2f3bbc876b3e7516"
|
||||||
|
integrity sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==
|
||||||
|
|
||||||
pify@^2.2.0, pify@^2.3.0:
|
pify@^2.2.0, pify@^2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||||
@ -15919,10 +15901,10 @@ prettier-plugin-organize-attributes@1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-attributes/-/prettier-plugin-organize-attributes-1.0.0.tgz#037870ee3111b3c1d6371f677b64888de353cc63"
|
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-attributes/-/prettier-plugin-organize-attributes-1.0.0.tgz#037870ee3111b3c1d6371f677b64888de353cc63"
|
||||||
integrity sha512-+NmameaLxbCcylEXsKPmawtzla5EE6ECqvGkpfQz4KM847fXDifB1gFnPQEpoADAq6IXg+cMI8Z0ISJEXa6fhg==
|
integrity sha512-+NmameaLxbCcylEXsKPmawtzla5EE6ECqvGkpfQz4KM847fXDifB1gFnPQEpoADAq6IXg+cMI8Z0ISJEXa6fhg==
|
||||||
|
|
||||||
prettier@3.0.3:
|
prettier@3.1.0:
|
||||||
version "3.0.3"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.0.tgz#c6d16474a5f764ea1a4a373c593b779697744d5e"
|
||||||
integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
|
integrity sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==
|
||||||
|
|
||||||
prettier@^2.8.0:
|
prettier@^2.8.0:
|
||||||
version "2.8.8"
|
version "2.8.8"
|
||||||
@ -19091,15 +19073,6 @@ ws@^8.11.0, ws@^8.13.0, ws@^8.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f"
|
||||||
integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
|
integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
|
||||||
|
|
||||||
xliff-simple-merge@~1.0.1:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/xliff-simple-merge/-/xliff-simple-merge-1.0.2.tgz#55f88a84630de625db2b3ddfc3d0d741ac940bfd"
|
|
||||||
integrity sha512-9Dtw/l91o0DeLkNFJrlh5nxJSS8OD+IHeq5rjA6hkVtv6SWf7rJyr4YNSQc/6opDssRI8JgAWcQlj2ZfcvW11Q==
|
|
||||||
dependencies:
|
|
||||||
commander "~8.3.0"
|
|
||||||
js-levenshtein "~1.1.6"
|
|
||||||
xmldoc "~1.1.2"
|
|
||||||
|
|
||||||
xml-name-validator@^3.0.0:
|
xml-name-validator@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
||||||
@ -19110,23 +19083,15 @@ xml-name-validator@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
|
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
|
||||||
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
|
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
|
||||||
|
|
||||||
xml_normalize@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/xml_normalize/-/xml_normalize-1.0.0.tgz#e844d8abae27b64fcb4eb0d567ecff278e0b166c"
|
|
||||||
integrity sha512-VzDbw9DW849WoLor6CP1eIPiVWwbq8CV3dlSrfVfsMqBqvp3VVkmLxA8J55WyLf6CnAf2sV29TQO77BKM/cxBw==
|
|
||||||
dependencies:
|
|
||||||
commander "~7.1.0"
|
|
||||||
xmldoc "~1.1.2"
|
|
||||||
|
|
||||||
xmlchars@^2.2.0:
|
xmlchars@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||||
|
|
||||||
xmldoc@~1.1.2:
|
xmldoc@^1.1.2:
|
||||||
version "1.1.4"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.1.4.tgz#ea4e26dca76b1d218a2f777018bce404ba374a86"
|
resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.3.0.tgz#7823225b096c74036347c9ec5924d06b6a3cebab"
|
||||||
integrity sha512-rQshsBGR5s7pUNENTEncpI2LTCuzicri0DyE4SCV5XmS0q81JS8j1iPijP0Q5c4WLGbKh3W92hlOwY6N9ssW1w==
|
integrity sha512-y7IRWW6PvEnYQZNZFMRLNJw+p3pezM4nKYPfr15g4OOW9i8VpeydycFuipE2297OvZnh3jSb2pxOt9QpkZUVng==
|
||||||
dependencies:
|
dependencies:
|
||||||
sax "^1.2.4"
|
sax "^1.2.4"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user