Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
6f11627006 | |||
215098e418 | |||
781496383b | |||
f0f304c012 | |||
4bf97c104b | |||
0b35a3c7a7 | |||
1586cd3a59 | |||
ae763cbb87 | |||
aa72287d54 | |||
d155ab6f28 | |||
913ca71aa5 | |||
1ffde2a27e | |||
fcf0cea982 | |||
ae1968aadf | |||
3e6333ef95 | |||
c69686651e | |||
93b6011ddc | |||
f567e25f27 | |||
5dc538bafb | |||
b4de06fcf0 | |||
27da0eb26e | |||
8ff80c10e5 | |||
5db5d5e79a | |||
12aac101bd | |||
3a66ccdebe | |||
6a722d1bb7 |
57
CHANGELOG.md
57
CHANGELOG.md
@ -5,11 +5,62 @@ 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.24.0 - 2023-11-16
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the "too many bind variables in prepared statement" issue of the data range functionality (`getRange()`) in the market data service
|
||||||
|
|
||||||
|
## 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 the 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
|
## 2.20.0 - 2023-11-08
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Removed the loading indicator of the unit on the overview tab of the home page
|
- 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
|
- 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 enhancers
|
||||||
- Increased the timeout in the health check endpoint for data providers
|
- Increased the timeout in the health check endpoint for data providers
|
||||||
@ -159,7 +210,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
|
||||||
@ -2399,7 +2450,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
|
||||||
|
|
||||||
|
@ -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,12 +165,22 @@ 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 }) => {
|
)
|
||||||
|
.filter((benchmark) => {
|
||||||
|
if (enableSharing) {
|
||||||
|
return benchmark.enableSharing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(({ symbolProfileId }) => {
|
||||||
return symbolProfileId;
|
return symbolProfileId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -66,6 +66,10 @@
|
|||||||
<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>
|
||||||
@ -126,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>
|
||||||
@ -368,6 +376,10 @@
|
|||||||
<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>
|
||||||
@ -428,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>
|
||||||
@ -694,6 +710,10 @@
|
|||||||
<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>
|
||||||
@ -754,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>
|
||||||
@ -866,6 +890,10 @@
|
|||||||
<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>
|
||||||
@ -926,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>
|
||||||
@ -1060,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>
|
||||||
|
@ -133,7 +133,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
|||||||
abortController.abort();
|
abortController.abort();
|
||||||
}, requestTimeout);
|
}, 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
|
||||||
@ -141,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(),
|
||||||
|
@ -59,12 +59,12 @@ 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 this.prismaService.marketData.findMany({
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{
|
{
|
||||||
date: 'asc'
|
date: 'asc'
|
||||||
@ -74,24 +74,33 @@ export class MarketDataService {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
where: {
|
where: {
|
||||||
|
dataSource: {
|
||||||
|
in: uniqueAssets.map(({ dataSource }) => {
|
||||||
|
return dataSource;
|
||||||
|
})
|
||||||
|
},
|
||||||
date: dateQuery,
|
date: dateQuery,
|
||||||
symbol: {
|
symbol: {
|
||||||
in: symbols
|
in: uniqueAssets.map(({ symbol }) => {
|
||||||
|
return symbol;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
@ -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,8 +155,14 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteSystemMessage() {
|
public onDeleteSystemMessage() {
|
||||||
|
const confirmation = confirm(
|
||||||
|
$localize`Do you really want to delete this system message?`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmation === true) {
|
||||||
this.putAdminSetting({ key: PROPERTY_SYSTEM_MESSAGE, value: undefined });
|
this.putAdminSetting({ key: PROPERTY_SYSTEM_MESSAGE, value: undefined });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onFlushCache() {
|
public onFlushCache() {
|
||||||
const confirmation = confirm(
|
const confirmation = confirm(
|
||||||
@ -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,9 +56,21 @@
|
|||||||
</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
|
||||||
|
[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]="{
|
[queryParams]="{
|
||||||
assetProfileDialog: true,
|
assetProfileDialog: true,
|
||||||
dataSource: exchangeRate.dataSource,
|
dataSource: exchangeRate.dataSource,
|
||||||
@ -65,16 +78,28 @@
|
|||||||
}"
|
}"
|
||||||
[routerLink]="['/admin', 'market-data']"
|
[routerLink]="['/admin', 'market-data']"
|
||||||
>
|
>
|
||||||
<ion-icon name="create-outline"></ion-icon>
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-2"
|
||||||
|
name="create-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span i18n>Edit</span>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
*ngIf="customCurrencies.includes(exchangeRate.label2)"
|
*ngIf="customCurrencies.includes(exchangeRate.label2)"
|
||||||
class="h-100 mx-1 no-min-width px-2"
|
mat-menu-item
|
||||||
mat-button
|
|
||||||
(click)="onDeleteCurrency(exchangeRate.label2)"
|
(click)="onDeleteCurrency(exchangeRate.label2)"
|
||||||
>
|
>
|
||||||
<ion-icon name="trash-outline"></ion-icon>
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-2"
|
||||||
|
name="trash-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span i18n>Delete</span>
|
||||||
|
</span>
|
||||||
</button>
|
</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
|
||||||
|
[matMenuTriggerFor]="couponActionsMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||||
|
</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)"
|
(click)="onDeleteCoupon(coupon.code)"
|
||||||
>
|
>
|
||||||
<ion-icon name="trash-outline"></ion-icon>
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-2"
|
||||||
|
name="trash-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span i18n>Delete</span>
|
||||||
|
</span>
|
||||||
</button>
|
</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"
|
||||||
|
@ -70,10 +70,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
.subscribe((impersonationId) => {
|
.subscribe((impersonationId) => {
|
||||||
this.hasImpersonationId = !!impersonationId;
|
this.hasImpersonationId = !!impersonationId;
|
||||||
|
|
||||||
this.unit = this.hasImpersonationId
|
|
||||||
? '%'
|
|
||||||
: this.user?.settings?.baseCurrency;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -81,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) {
|
||||||
|
@ -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
|
||||||
|
@ -52,10 +52,10 @@
|
|||||||
title="Join the Ghostfolio Slack community"
|
title="Join the Ghostfolio Slack community"
|
||||||
>Slack</a
|
>Slack</a
|
||||||
>
|
>
|
||||||
community, tweet to
|
community, post 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'"
|
||||||
>,
|
>,
|
||||||
@ -259,10 +259,10 @@
|
|||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
title="Join the Ghostfolio Slack community"
|
title="Join the Ghostfolio Slack community"
|
||||||
>Slack </a
|
>Slack </a
|
||||||
>community, tweet to
|
>community, post 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,
|
||||||
|
@ -2,6 +2,7 @@ import { Product } from '@ghostfolio/common/interfaces';
|
|||||||
|
|
||||||
import { AllvueSystemsPageComponent } from './products/allvue-systems-page.component';
|
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';
|
||||||
@ -18,6 +19,7 @@ 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 { MonarchMoneyPageComponent } from './products/monarch-money-page.component';
|
||||||
@ -84,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,
|
||||||
@ -252,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,
|
||||||
|
@ -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'
|
||||||
|
];
|
||||||
|
}
|
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
12860
apps/client/src/locales/messages.pl.xlf
Normal file
12860
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.20.0",
|
"version": "2.24.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",
|
||||||
|
@ -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