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/),
|
||||
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
|
||||
|
||||
### 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
|
||||
- Increased the timeout in the health check endpoint for data enhancers
|
||||
- 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 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
|
||||
|
||||
### Changed
|
||||
@ -2399,7 +2450,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- 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
|
||||
|
||||
|
@ -9,17 +9,21 @@ import {
|
||||
MAX_CHART_ITEMS,
|
||||
PROPERTY_BENCHMARKS
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
calculateBenchmarkTrend
|
||||
} from '@ghostfolio/common/helper';
|
||||
import {
|
||||
BenchmarkMarketDataDetails,
|
||||
BenchmarkProperty,
|
||||
BenchmarkResponse,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { BenchmarkTrend } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SymbolProfile } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import { format } from 'date-fns';
|
||||
import { format, subDays } from 'date-fns';
|
||||
import { uniqBy } from 'lodash';
|
||||
import ms from 'ms';
|
||||
|
||||
@ -45,9 +49,34 @@ export class BenchmarkService {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async getBenchmarks({ useCache = true } = {}): Promise<
|
||||
BenchmarkResponse['benchmarks']
|
||||
> {
|
||||
public async getBenchmarkTrends({ dataSource, symbol }: UniqueAsset) {
|
||||
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'];
|
||||
|
||||
if (useCache) {
|
||||
@ -62,9 +91,16 @@ export class BenchmarkService {
|
||||
} 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({
|
||||
items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
|
||||
@ -73,10 +109,18 @@ export class BenchmarkService {
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||
@ -93,6 +137,7 @@ export class BenchmarkService {
|
||||
} else {
|
||||
storeInCache = false;
|
||||
}
|
||||
|
||||
return {
|
||||
marketCondition: this.getMarketCondition(
|
||||
performancePercentFromAllTimeHigh
|
||||
@ -100,10 +145,12 @@ export class BenchmarkService {
|
||||
name: benchmarkAssetProfiles[index].name,
|
||||
performances: {
|
||||
allTimeHigh: {
|
||||
date: allTimeHigh.date,
|
||||
date: allTimeHigh?.date,
|
||||
performancePercent: performancePercentFromAllTimeHigh
|
||||
}
|
||||
}
|
||||
},
|
||||
trend50d: benchmarkTrends[index].trend50d,
|
||||
trend200d: benchmarkTrends[index].trend200d
|
||||
};
|
||||
});
|
||||
|
||||
@ -118,12 +165,22 @@ export class BenchmarkService {
|
||||
return benchmarks;
|
||||
}
|
||||
|
||||
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
|
||||
public async getBenchmarkAssetProfiles({
|
||||
enableSharing = false
|
||||
} = {}): Promise<Partial<SymbolProfile>[]> {
|
||||
const symbolProfileIds: string[] = (
|
||||
((await this.propertyService.getByKey(
|
||||
PROPERTY_BENCHMARKS
|
||||
)) as BenchmarkProperty[]) ?? []
|
||||
).map(({ symbolProfileId }) => {
|
||||
)
|
||||
.filter((benchmark) => {
|
||||
if (enableSharing) {
|
||||
return benchmark.enableSharing;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map(({ symbolProfileId }) => {
|
||||
return symbolProfileId;
|
||||
});
|
||||
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import { PlatformService } from '@ghostfolio/api/app/platform/platform.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 { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.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 {
|
||||
public constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly dataGatheringService: DataGatheringService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
@ -570,6 +572,12 @@ export class ImportService {
|
||||
index,
|
||||
{ currency, dataSource, symbol }
|
||||
] of uniqueActivitiesDto.entries()) {
|
||||
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
|
||||
throw new Error(
|
||||
`activities.${index}.dataSource ("${dataSource}") is not valid`
|
||||
);
|
||||
}
|
||||
|
||||
if (dataSource !== 'MANUAL') {
|
||||
const assetProfile = (
|
||||
await this.dataProviderService.getAssetProfiles([
|
||||
|
@ -15,7 +15,6 @@ import {
|
||||
PROPERTY_IS_READ_ONLY_MODE,
|
||||
PROPERTY_SLACK_COMMUNITY_USERS,
|
||||
PROPERTY_STRIPE_CONFIG,
|
||||
PROPERTY_SYSTEM_MESSAGE,
|
||||
ghostfolioFearAndGreedIndexDataSource
|
||||
} from '@ghostfolio/common/config';
|
||||
import {
|
||||
@ -58,7 +57,6 @@ export class InfoService {
|
||||
const platforms = await this.platformService.getPlatforms({
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
let systemMessage: string;
|
||||
|
||||
const globalPermissions: string[] = [];
|
||||
|
||||
@ -104,10 +102,6 @@ export class InfoService {
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SYSTEM_MESSAGE')) {
|
||||
globalPermissions.push(permissions.enableSystemMessage);
|
||||
|
||||
systemMessage = (await this.propertyService.getByKey(
|
||||
PROPERTY_SYSTEM_MESSAGE
|
||||
)) as string;
|
||||
}
|
||||
|
||||
const isUserSignupEnabled =
|
||||
@ -135,7 +129,6 @@ export class InfoService {
|
||||
platforms,
|
||||
statistics,
|
||||
subscriptions,
|
||||
systemMessage,
|
||||
tags,
|
||||
baseCurrency: DEFAULT_CURRENCY,
|
||||
currencies: this.exchangeRateDataService.getCurrencies()
|
||||
|
@ -61,6 +61,7 @@ export const CurrentRateServiceMock = {
|
||||
for (const dataGatheringItem of dataGatheringItems) {
|
||||
values.push({
|
||||
date,
|
||||
dataSource: dataGatheringItem.dataSource,
|
||||
marketPriceInBaseCurrency: mockGetValue(
|
||||
dataGatheringItem.symbol,
|
||||
date
|
||||
@ -74,6 +75,7 @@ export const CurrentRateServiceMock = {
|
||||
for (const dataGatheringItem of dataGatheringItems) {
|
||||
values.push({
|
||||
date,
|
||||
dataSource: dataGatheringItem.dataSource,
|
||||
marketPriceInBaseCurrency: mockGetValue(
|
||||
dataGatheringItem.symbol,
|
||||
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 { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { DataSource, MarketData } from '@prisma/client';
|
||||
|
||||
import { CurrentRateService } from './current-rate.service';
|
||||
@ -25,30 +26,30 @@ jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => {
|
||||
getRange: ({
|
||||
dateRangeEnd,
|
||||
dateRangeStart,
|
||||
symbols
|
||||
uniqueAssets
|
||||
}: {
|
||||
dateRangeEnd: Date;
|
||||
dateRangeStart: Date;
|
||||
symbols: string[];
|
||||
uniqueAssets: UniqueAsset[];
|
||||
}) => {
|
||||
return Promise.resolve<MarketData[]>([
|
||||
{
|
||||
createdAt: dateRangeStart,
|
||||
dataSource: DataSource.YAHOO,
|
||||
dataSource: uniqueAssets[0].dataSource,
|
||||
date: dateRangeStart,
|
||||
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
|
||||
marketPrice: 1841.823902,
|
||||
state: 'CLOSE',
|
||||
symbol: symbols[0]
|
||||
symbol: uniqueAssets[0].symbol
|
||||
},
|
||||
{
|
||||
createdAt: dateRangeEnd,
|
||||
dataSource: DataSource.YAHOO,
|
||||
dataSource: uniqueAssets[0].dataSource,
|
||||
date: dateRangeEnd,
|
||||
id: '082d6893-df27-4c91-8a5d-092e84315b56',
|
||||
marketPrice: 1847.839966,
|
||||
state: 'CLOSE',
|
||||
symbol: symbols[0]
|
||||
symbol: uniqueAssets[0].symbol
|
||||
}
|
||||
]);
|
||||
}
|
||||
@ -134,6 +135,7 @@ describe('CurrentRateService', () => {
|
||||
errors: [],
|
||||
values: [
|
||||
{
|
||||
dataSource: 'YAHOO',
|
||||
date: undefined,
|
||||
marketPriceInBaseCurrency: 1841.823902,
|
||||
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 { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||
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 { isBefore, isToday } from 'date-fns';
|
||||
import { flatten, isEmpty, uniqBy } from 'lodash';
|
||||
@ -52,6 +56,7 @@ export class CurrentRateService {
|
||||
|
||||
if (dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice) {
|
||||
result.push({
|
||||
dataSource: dataGatheringItem.dataSource,
|
||||
date: today,
|
||||
marketPriceInBaseCurrency:
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
@ -75,27 +80,30 @@ export class CurrentRateService {
|
||||
);
|
||||
}
|
||||
|
||||
const symbols = dataGatheringItems.map((dataGatheringItem) => {
|
||||
return dataGatheringItem.symbol;
|
||||
});
|
||||
const uniqueAssets: UniqueAsset[] = dataGatheringItems.map(
|
||||
({ dataSource, symbol }) => {
|
||||
return { dataSource, symbol };
|
||||
}
|
||||
);
|
||||
|
||||
promises.push(
|
||||
this.marketDataService
|
||||
.getRange({
|
||||
dateQuery,
|
||||
symbols
|
||||
uniqueAssets
|
||||
})
|
||||
.then((data) => {
|
||||
return data.map((marketDataItem) => {
|
||||
return data.map(({ dataSource, date, marketPrice, symbol }) => {
|
||||
return {
|
||||
date: marketDataItem.date,
|
||||
dataSource,
|
||||
date,
|
||||
symbol,
|
||||
marketPriceInBaseCurrency:
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
marketDataItem.marketPrice,
|
||||
currencies[marketDataItem.symbol],
|
||||
marketPrice,
|
||||
currencies[symbol],
|
||||
userCurrency
|
||||
),
|
||||
symbol: marketDataItem.symbol
|
||||
)
|
||||
};
|
||||
});
|
||||
})
|
||||
@ -112,7 +120,7 @@ export class CurrentRateService {
|
||||
};
|
||||
|
||||
if (!isEmpty(quoteErrors)) {
|
||||
for (const { symbol } of quoteErrors) {
|
||||
for (const { dataSource, symbol } of quoteErrors) {
|
||||
try {
|
||||
// If missing quote, fallback to the latest available historical market price
|
||||
let value: GetValueObject = response.values.find((currentValue) => {
|
||||
@ -121,6 +129,7 @@ export class CurrentRateService {
|
||||
|
||||
if (!value) {
|
||||
value = {
|
||||
dataSource,
|
||||
symbol,
|
||||
date: today,
|
||||
marketPriceInBaseCurrency: 0
|
||||
|
@ -1,5 +1,6 @@
|
||||
export interface GetValueObject {
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface GetValueObject extends UniqueAsset {
|
||||
date: Date;
|
||||
marketPriceInBaseCurrency: number;
|
||||
symbol: string;
|
||||
}
|
||||
|
@ -40,7 +40,12 @@ export class SymbolService {
|
||||
|
||||
const marketData = await this.marketDataService.getRange({
|
||||
dateQuery: { gte: subDays(new Date(), days) },
|
||||
symbols: [dataGatheringItem.symbol]
|
||||
uniqueAssets: [
|
||||
{
|
||||
dataSource: dataGatheringItem.dataSource,
|
||||
symbol: dataGatheringItem.symbol
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
historicalData = marketData.map(({ date, marketPrice: value }) => {
|
||||
|
@ -7,9 +7,14 @@ import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
PROPERTY_IS_READ_ONLY_MODE,
|
||||
PROPERTY_SYSTEM_MESSAGE,
|
||||
locale
|
||||
} from '@ghostfolio/common/config';
|
||||
import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
User as IUser,
|
||||
SystemMessage,
|
||||
UserSettings
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
getPermissions,
|
||||
hasRole,
|
||||
@ -48,6 +53,17 @@ export class UserService {
|
||||
orderBy: { alias: 'asc' },
|
||||
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);
|
||||
|
||||
if (
|
||||
@ -61,6 +77,7 @@ export class UserService {
|
||||
id,
|
||||
permissions,
|
||||
subscription,
|
||||
systemMessage,
|
||||
tags,
|
||||
access: access.map((accessItem) => {
|
||||
return {
|
||||
@ -110,7 +127,9 @@ export class UserService {
|
||||
updatedAt
|
||||
} = await this.prismaService.user.findUnique({
|
||||
include: {
|
||||
Account: true,
|
||||
Account: {
|
||||
include: { Platform: true }
|
||||
},
|
||||
Analytics: true,
|
||||
Settings: true,
|
||||
Subscription: true
|
||||
@ -233,8 +252,8 @@ export class UserService {
|
||||
currentPermissions.push(permissions.impersonateAllUsers);
|
||||
}
|
||||
|
||||
user.Account = sortBy(user.Account, (account) => {
|
||||
return account.name;
|
||||
user.Account = sortBy(user.Account, ({ name }) => {
|
||||
return name.toLowerCase();
|
||||
});
|
||||
user.permissions = currentPermissions.sort();
|
||||
|
||||
|
@ -66,6 +66,10 @@
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</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>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-beanvest</loc>
|
||||
<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>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</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>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-markets.sh</loc>
|
||||
<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>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</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>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-beanvest</loc>
|
||||
<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>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</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>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-markets.sh</loc>
|
||||
<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>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</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>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-beanvest</loc>
|
||||
<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>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</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>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-markets.sh</loc>
|
||||
<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>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</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>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-beanvest</loc>
|
||||
<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>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</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>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-markets.sh</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -1060,6 +1092,10 @@
|
||||
<loc>https://ghostfol.io/nl/veelgestelde-vragen</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/pl</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/pt</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
|
@ -133,7 +133,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
abortController.abort();
|
||||
}, requestTimeout);
|
||||
|
||||
const response = await got(
|
||||
const quotes = await got(
|
||||
`${this.URL}/quote/${symbols.join(',')}?apikey=${this.apiKey}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
@ -141,7 +141,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
for (const { price, symbol } of response) {
|
||||
for (const { price, symbol } of quotes) {
|
||||
response[symbol] = {
|
||||
currency: DEFAULT_CURRENCY,
|
||||
dataProviderInfo: this.getDataProviderInfo(),
|
||||
|
@ -59,12 +59,12 @@ export class MarketDataService {
|
||||
|
||||
public async getRange({
|
||||
dateQuery,
|
||||
symbols
|
||||
uniqueAssets
|
||||
}: {
|
||||
dateQuery: DateQuery;
|
||||
symbols: string[];
|
||||
uniqueAssets: UniqueAsset[];
|
||||
}): Promise<MarketData[]> {
|
||||
return await this.prismaService.marketData.findMany({
|
||||
return this.prismaService.marketData.findMany({
|
||||
orderBy: [
|
||||
{
|
||||
date: 'asc'
|
||||
@ -74,24 +74,33 @@ export class MarketDataService {
|
||||
}
|
||||
],
|
||||
where: {
|
||||
dataSource: {
|
||||
in: uniqueAssets.map(({ dataSource }) => {
|
||||
return dataSource;
|
||||
})
|
||||
},
|
||||
date: dateQuery,
|
||||
symbol: {
|
||||
in: symbols
|
||||
in: uniqueAssets.map(({ symbol }) => {
|
||||
return symbol;
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async marketDataItems(params: {
|
||||
select?: Prisma.MarketDataSelectScalar;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
cursor?: Prisma.MarketDataWhereUniqueInput;
|
||||
where?: Prisma.MarketDataWhereInput;
|
||||
orderBy?: Prisma.MarketDataOrderByWithRelationInput;
|
||||
}): Promise<MarketData[]> {
|
||||
const { skip, take, cursor, where, orderBy } = params;
|
||||
const { select, skip, take, cursor, where, orderBy } = params;
|
||||
|
||||
return this.prismaService.marketData.findMany({
|
||||
select,
|
||||
cursor,
|
||||
orderBy,
|
||||
skip,
|
||||
|
@ -57,7 +57,7 @@ export class TwitterBotService {
|
||||
symbolItem.marketPrice
|
||||
}/100)`;
|
||||
|
||||
const benchmarkListing = await this.getBenchmarkListing(3);
|
||||
const benchmarkListing = await this.getBenchmarkListing();
|
||||
|
||||
if (benchmarkListing?.length > 1) {
|
||||
status += '\n\n';
|
||||
@ -78,29 +78,22 @@ export class TwitterBotService {
|
||||
}
|
||||
}
|
||||
|
||||
private async getBenchmarkListing(aMax: number) {
|
||||
private async getBenchmarkListing() {
|
||||
const benchmarks = await this.benchmarkService.getBenchmarks({
|
||||
enableSharing: true,
|
||||
useCache: false
|
||||
});
|
||||
|
||||
const benchmarkListing: string[] = [];
|
||||
|
||||
for (const [index, benchmark] of benchmarks.entries()) {
|
||||
if (index > aMax - 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
benchmarkListing.push(
|
||||
`${benchmark.name} ${(
|
||||
benchmark.performances.allTimeHigh.performancePercent * 100
|
||||
return benchmarks
|
||||
.map(({ marketCondition, name, performances }) => {
|
||||
return `${name} ${(
|
||||
performances.allTimeHigh.performancePercent * 100
|
||||
).toFixed(1)}%${
|
||||
benchmark.marketCondition !== 'NEUTRAL_MARKET'
|
||||
? ' ' + resolveMarketCondition(benchmark.marketCondition).emoji
|
||||
marketCondition !== 'NEUTRAL_MARKET'
|
||||
? ' ' + resolveMarketCondition(marketCondition).emoji
|
||||
: ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
return benchmarkListing.join('\n');
|
||||
}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,10 @@
|
||||
"baseHref": "/nl/",
|
||||
"localize": ["nl"]
|
||||
},
|
||||
"development-pl": {
|
||||
"baseHref": "/pl/",
|
||||
"localize": ["pl"]
|
||||
},
|
||||
"development-pt": {
|
||||
"baseHref": "/pt/",
|
||||
"localize": ["pt"]
|
||||
@ -170,6 +174,9 @@
|
||||
"development-nl": {
|
||||
"browserTarget": "client:build:development-nl"
|
||||
},
|
||||
"development-pl": {
|
||||
"browserTarget": "client:build:development-pl"
|
||||
},
|
||||
"development-pt": {
|
||||
"browserTarget": "client:build:development-pt"
|
||||
},
|
||||
@ -193,6 +200,7 @@
|
||||
"messages.fr.xlf",
|
||||
"messages.it.xlf",
|
||||
"messages.nl.xlf",
|
||||
"messages.pl.xlf",
|
||||
"messages.pt.xlf",
|
||||
"messages.tr.xlf"
|
||||
]
|
||||
@ -235,6 +243,10 @@
|
||||
"baseHref": "/nl/",
|
||||
"translation": "apps/client/src/locales/messages.nl.xlf"
|
||||
},
|
||||
"pl": {
|
||||
"baseHref": "/pl/",
|
||||
"translation": "apps/client/src/locales/messages.pl.xlf"
|
||||
},
|
||||
"pt": {
|
||||
"baseHref": "/pt/",
|
||||
"translation": "apps/client/src/locales/messages.pt.xlf"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<header>
|
||||
<div
|
||||
*ngIf="canCreateAccount || (info?.systemMessage && user)"
|
||||
*ngIf="canCreateAccount || user?.systemMessage"
|
||||
class="info-message-container"
|
||||
>
|
||||
<div class="info-message-inner-container position-fixed w-100">
|
||||
@ -19,11 +19,11 @@
|
||||
</div></a
|
||||
>
|
||||
<div
|
||||
*ngIf="!canCreateAccount && info?.systemMessage && user"
|
||||
*ngIf="!canCreateAccount && user?.systemMessage"
|
||||
class="cursor-pointer d-inline-block info-message text-truncate"
|
||||
(click)="onShowSystemMessage()"
|
||||
(click)="onClickSystemMessage()"
|
||||
>
|
||||
{{ info.systemMessage }}
|
||||
{{ user.systemMessage.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -127,8 +127,11 @@
|
||||
class="align-items-baseline d-flex"
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
target="_blank"
|
||||
title="Follow Ghostfolio on Twitter"
|
||||
>Twitter<ion-icon class="ml-1" name="open-outline"></ion-icon
|
||||
title="Follow Ghostfolio on X (formerly Twitter)"
|
||||
>X (formerly Twitter)<ion-icon
|
||||
class="ml-1"
|
||||
name="open-outline"
|
||||
></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li> </li>
|
||||
@ -150,6 +153,11 @@
|
||||
<li>
|
||||
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
|
||||
</li>
|
||||
<!--
|
||||
<li>
|
||||
<a href="../pl" title="Ghostfolio in Polski">Polski</a>
|
||||
</li>
|
||||
-->
|
||||
<li>
|
||||
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
||||
</li>
|
||||
|
@ -155,10 +155,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
);
|
||||
|
||||
this.hasInfoMessage =
|
||||
hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
) || !!this.info.systemMessage;
|
||||
this.canCreateAccount || !!this.user?.systemMessage;
|
||||
|
||||
this.initializeTheme(this.user?.settings.colorScheme);
|
||||
|
||||
@ -166,12 +163,16 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public onCreateAccount() {
|
||||
this.tokenStorageService.signOut();
|
||||
public onClickSystemMessage() {
|
||||
if (this.user.systemMessage.routerLink) {
|
||||
this.router.navigate(this.user.systemMessage.routerLink);
|
||||
} else {
|
||||
alert(this.user.systemMessage.message);
|
||||
}
|
||||
}
|
||||
|
||||
public onShowSystemMessage() {
|
||||
alert(this.info.systemMessage);
|
||||
public onCreateAccount() {
|
||||
this.tokenStorageService.signOut();
|
||||
}
|
||||
|
||||
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 { translate } from '@ghostfolio/ui/i18n';
|
||||
import { AssetSubClass, DataSource, Prisma } from '@prisma/client';
|
||||
import { isUUID } from 'class-validator';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
|
||||
@ -83,7 +84,7 @@ export class AdminMarketDataComponent
|
||||
public defaultDateFormat: string;
|
||||
public deviceType: string;
|
||||
public displayedColumns = [
|
||||
'symbol',
|
||||
'nameWithSymbol',
|
||||
'dataSource',
|
||||
'assetClass',
|
||||
'assetSubClass',
|
||||
@ -97,6 +98,7 @@ export class AdminMarketDataComponent
|
||||
];
|
||||
public filters$ = new Subject<Filter[]>();
|
||||
public isLoading = false;
|
||||
public isUUID = isUUID;
|
||||
public placeholder = '';
|
||||
public pageSize = DEFAULT_PAGE_SIZE;
|
||||
public totalItems = 0;
|
||||
|
@ -28,6 +28,24 @@
|
||||
</td>
|
||||
</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">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Data Source</ng-container>
|
||||
|
@ -6,6 +6,7 @@ import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
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 { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
@ -20,6 +21,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/
|
||||
GfActivitiesFilterModule,
|
||||
GfAssetProfileDialogModule,
|
||||
GfCreateAssetProfileDialogModule,
|
||||
GfSymbolModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
|
@ -12,7 +12,12 @@ import {
|
||||
PROPERTY_SYSTEM_MESSAGE,
|
||||
ghostfolioPrefix
|
||||
} 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 {
|
||||
differenceInSeconds,
|
||||
@ -39,6 +44,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
public hasPermissionToToggleReadOnlyMode: boolean;
|
||||
public info: InfoItem;
|
||||
public permissions = permissions;
|
||||
public systemMessage: SystemMessage;
|
||||
public transactionCount: number;
|
||||
public userCount: number;
|
||||
public user: User;
|
||||
@ -149,8 +155,14 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
public onFlushCache() {
|
||||
const confirmation = confirm(
|
||||
@ -184,12 +196,21 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
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) {
|
||||
this.putAdminSetting({
|
||||
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.customCurrencies = settings[PROPERTY_CURRENCIES] as string[];
|
||||
this.exchangeRates = exchangeRates;
|
||||
this.systemMessage = settings[
|
||||
PROPERTY_SYSTEM_MESSAGE
|
||||
] as SystemMessage;
|
||||
this.transactionCount = transactionCount;
|
||||
this.userCount = userCount;
|
||||
this.version = version;
|
||||
|
@ -38,7 +38,7 @@
|
||||
<div class="w-50">
|
||||
<table>
|
||||
<tr *ngFor="let exchangeRate of exchangeRates">
|
||||
<td class="d-flex">
|
||||
<td>
|
||||
<gf-value
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="1"
|
||||
@ -46,8 +46,9 @@
|
||||
</td>
|
||||
<td class="pl-1">{{ exchangeRate.label1 }}</td>
|
||||
<td class="px-1">=</td>
|
||||
<td class="d-flex justify-content-end">
|
||||
<td align="right">
|
||||
<gf-value
|
||||
class="d-inline-block"
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="4"
|
||||
[value]="exchangeRate.value"
|
||||
@ -55,9 +56,21 @@
|
||||
</td>
|
||||
<td class="pl-1">{{ exchangeRate.label2 }}</td>
|
||||
<td>
|
||||
<a
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
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]="{
|
||||
assetProfileDialog: true,
|
||||
dataSource: exchangeRate.dataSource,
|
||||
@ -65,16 +78,28 @@
|
||||
}"
|
||||
[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>
|
||||
<button
|
||||
*ngIf="customCurrencies.includes(exchangeRate.label2)"
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
mat-menu-item
|
||||
(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>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -115,8 +140,8 @@
|
||||
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
|
||||
<div class="w-50" i18n>System Message</div>
|
||||
<div class="w-50">
|
||||
<div *ngIf="info?.systemMessage">
|
||||
<span>{{ info.systemMessage }}</span>
|
||||
<div *ngIf="systemMessage" class="align-items-center d-flex">
|
||||
<div class="text-truncate">{{ systemMessage | json }}</div>
|
||||
<button
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
@ -127,6 +152,7 @@
|
||||
</div>
|
||||
<button
|
||||
*ngIf="!info?.systemMessage"
|
||||
class="mt-2"
|
||||
color="accent"
|
||||
mat-flat-button
|
||||
(click)="onSetSystemMessage()"
|
||||
@ -148,17 +174,34 @@
|
||||
<table>
|
||||
<tr *ngFor="let coupon of coupons">
|
||||
<td class="text-monospace">{{ coupon.code }}</td>
|
||||
<td class="d-flex justify-content-end pl-2">
|
||||
{{ coupon.duration }}
|
||||
</td>
|
||||
<td class="pl-2 text-right">{{ coupon.duration }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
class="mx-1 no-min-width px-2"
|
||||
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)"
|
||||
>
|
||||
<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>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { RouterModule } from '@angular/router';
|
||||
@ -20,6 +21,7 @@ import { AdminOverviewComponent } from './admin-overview.component';
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatSelectModule,
|
||||
MatSlideToggleModule,
|
||||
ReactiveFormsModule,
|
||||
|
@ -31,6 +31,7 @@
|
||||
<gf-benchmark
|
||||
[benchmarks]="benchmarks"
|
||||
[locale]="user?.settings?.locale"
|
||||
[user]="user"
|
||||
></gf-benchmark>
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
|
@ -70,10 +70,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
|
||||
this.unit = this.hasImpersonationId
|
||||
? '%'
|
||||
: this.user?.settings?.baseCurrency;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
@ -81,6 +77,8 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
!this.hasImpersonationId &&
|
||||
!this.user.settings.isRestrictedView &&
|
||||
this.user.settings.viewMode !== 'ZEN';
|
||||
|
||||
this.unit = this.showDetails ? this.user.settings.baseCurrency : '%';
|
||||
}
|
||||
|
||||
public onChangeDateRange(dateRange: DateRange) {
|
||||
|
@ -13,6 +13,7 @@
|
||||
<div class="d-flex mr-2">
|
||||
<gf-trend-indicator
|
||||
class="d-flex"
|
||||
size="large"
|
||||
[isLoading]="isLoading"
|
||||
[marketState]="position?.marketState"
|
||||
[range]="range"
|
||||
|
@ -1,5 +1,6 @@
|
||||
:host {
|
||||
display: block;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
border-radius: 0.2rem;
|
||||
|
@ -44,6 +44,7 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
||||
'fr',
|
||||
'it',
|
||||
'nl',
|
||||
'pl',
|
||||
'pt',
|
||||
'tr'
|
||||
];
|
||||
|
@ -74,6 +74,10 @@
|
||||
>Nederlands (<ng-container i18n>Community</ng-container
|
||||
>)</mat-option
|
||||
>
|
||||
<mat-option value="pl"
|
||||
>Polski (<ng-container i18n>Community</ng-container
|
||||
>)</mat-option
|
||||
>
|
||||
<mat-option value="pt"
|
||||
>Português (<ng-container i18n>Community</ng-container
|
||||
>)</mat-option
|
||||
|
@ -52,10 +52,10 @@
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack</a
|
||||
>
|
||||
community, tweet to
|
||||
community, post to
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Tweet to Ghostfolio on Twitter"
|
||||
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>, send an e-mail to
|
||||
@ -70,14 +70,14 @@
|
||||
>GitHub</a
|
||||
>.
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<p class="align-items-center d-flex justify-content-center">
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
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
|
||||
*ngIf="user?.subscription?.type === 'Premium'"
|
||||
|
@ -10,9 +10,17 @@
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>From</mat-label>
|
||||
<mat-select formControlName="fromAccount">
|
||||
<mat-option *ngFor="let account of accounts" [value]="account.id"
|
||||
>{{ account.name }}</mat-option
|
||||
>
|
||||
<mat-option *ngFor="let account of 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-form-field>
|
||||
</div>
|
||||
@ -20,9 +28,17 @@
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>To</mat-label>
|
||||
<mat-select formControlName="toAccount">
|
||||
<mat-option *ngFor="let account of accounts" [value]="account.id"
|
||||
>{{ account.name }}</mat-option
|
||||
>
|
||||
<mat-option *ngFor="let account of 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-form-field>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||
|
||||
import { TransferBalanceDialog } from './transfer-balance-dialog.component';
|
||||
|
||||
@ -13,6 +14,7 @@ import { TransferBalanceDialog } from './transfer-balance-dialog.component';
|
||||
declarations: [TransferBalanceDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfSymbolIconModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<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>
|
||||
<small class="text-muted" i18n
|
||||
>Discover the latest Ghostfolio updates and insights on personal
|
||||
|
@ -233,7 +233,7 @@
|
||||
community,
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Tweet to Ghostfolio on Twitter"
|
||||
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>,
|
||||
@ -259,10 +259,10 @@
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack </a
|
||||
>community, tweet to
|
||||
>community, post to
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Tweet to Ghostfolio on Twitter"
|
||||
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>, send an e-mail to
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<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>
|
||||
<small class="text-muted" i18n>
|
||||
Check out the numerous features of Ghostfolio to manage your wealth
|
||||
@ -245,7 +245,8 @@
|
||||
<h4 i18n>Multi-Language</h4>
|
||||
<p class="m-0">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
@ -327,7 +327,7 @@
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<gf-carousel [aria-label]="'Testimonials'">
|
||||
<div *ngFor="let testimonial of testimonials" gf-carousel-item>
|
||||
<div class="d-flex px-3">
|
||||
<div class="d-flex px-4">
|
||||
<gf-logo
|
||||
class="mr-3 mt-2 pt-1"
|
||||
size="medium"
|
||||
|
@ -72,9 +72,20 @@
|
||||
*ngIf="!activityForm.controls['accountId'].hasValidator(Validators.required)"
|
||||
[value]="null"
|
||||
></mat-option>
|
||||
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
|
||||
>{{ account.name }}</mat-option
|
||||
<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-form-field>
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@ import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
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 { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
@ -21,6 +22,7 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfSymbolAutocompleteModule,
|
||||
GfSymbolIconModule,
|
||||
GfValueModule,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
|
@ -2,6 +2,7 @@ import { Product } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { AllvueSystemsPageComponent } from './products/allvue-systems-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 { CapitallyPageComponent } from './products/capitally-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 { JustEtfPageComponent } from './products/justetf-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 { MaybeFinancePageComponent } from './products/maybe-finance-page.component';
|
||||
import { MonarchMoneyPageComponent } from './products/monarch-money-page.component';
|
||||
@ -84,6 +86,15 @@ export const products: Product[] = [
|
||||
origin: $localize`Switzerland`,
|
||||
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,
|
||||
founded: 2020,
|
||||
@ -252,6 +263,17 @@ export const products: Product[] = [
|
||||
pricingPerYear: '$150',
|
||||
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,
|
||||
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',
|
||||
'it',
|
||||
'nl',
|
||||
'pl',
|
||||
'pt',
|
||||
'tr'
|
||||
];
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as currencies from '@dinero.js/currencies';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { DataSource, MarketData } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import {
|
||||
getDate,
|
||||
@ -10,11 +10,11 @@ import {
|
||||
parseISO,
|
||||
subDays
|
||||
} 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 { Benchmark, UniqueAsset } from './interfaces';
|
||||
import { ColorScheme } from './types';
|
||||
import { BenchmarkTrend, ColorScheme } from './types';
|
||||
|
||||
export const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
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;
|
||||
|
||||
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) {
|
||||
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
||||
}
|
||||
@ -106,6 +159,8 @@ export function getDateFnsLocale(aLanguageCode: string) {
|
||||
return it;
|
||||
} else if (aLanguageCode === 'nl') {
|
||||
return nl;
|
||||
} else if (aLanguageCode === 'pl') {
|
||||
return pl;
|
||||
} else if (aLanguageCode === 'pt') {
|
||||
return pt;
|
||||
} else if (aLanguageCode === 'tr') {
|
||||
|
@ -1,3 +1,4 @@
|
||||
export interface BenchmarkProperty {
|
||||
enableSharing?: boolean;
|
||||
symbolProfileId: string;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { BenchmarkTrend } from '@ghostfolio/common/types/';
|
||||
|
||||
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
||||
|
||||
export interface Benchmark {
|
||||
@ -9,4 +11,6 @@ export interface Benchmark {
|
||||
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 { Statistics } from './statistics.interface';
|
||||
import type { Subscription } from './subscription.interface';
|
||||
import { SystemMessage } from './system-message.interface';
|
||||
import { TabConfiguration } from './tab-configuration.interface';
|
||||
import type { TimelinePosition } from './timeline-position.interface';
|
||||
import type { UniqueAsset } from './unique-asset.interface';
|
||||
@ -90,6 +91,7 @@ export {
|
||||
ResponseError,
|
||||
ScraperConfiguration,
|
||||
Statistics,
|
||||
SystemMessage,
|
||||
Subscription,
|
||||
TabConfiguration,
|
||||
TimelinePosition,
|
||||
|
@ -17,6 +17,5 @@ export interface InfoItem {
|
||||
statistics: Statistics;
|
||||
stripePublicKey?: string;
|
||||
subscriptions: { [offer in SubscriptionOffer]: Subscription };
|
||||
systemMessage?: string;
|
||||
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 { Account, Tag } from '@prisma/client';
|
||||
|
||||
import { SystemMessage } from './system-message.interface';
|
||||
import { UserSettings } from './user-settings.interface';
|
||||
|
||||
// TODO: Compare with UserWithSettings
|
||||
@ -14,6 +15,7 @@ export interface User {
|
||||
id: string;
|
||||
permissions: string[];
|
||||
settings: UserSettings;
|
||||
systemMessage?: SystemMessage;
|
||||
subscription: {
|
||||
expiresAt?: Date;
|
||||
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 { AccountWithPlatform } from './account-with-platform.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 { DateRange } from './date-range.type';
|
||||
import type { Granularity } from './granularity.type';
|
||||
@ -20,6 +21,7 @@ export type {
|
||||
AccessWithGranteeUser,
|
||||
AccountWithPlatform,
|
||||
AccountWithValue,
|
||||
BenchmarkTrend,
|
||||
ColorScheme,
|
||||
DateRange,
|
||||
Granularity,
|
||||
|
@ -6,6 +6,54 @@
|
||||
</td>
|
||||
</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">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
@ -20,7 +68,7 @@
|
||||
[isDate]="true"
|
||||
[locale]="locale"
|
||||
[value]="element?.performances?.allTimeHigh?.date"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -35,7 +83,6 @@
|
||||
<td *matCellDef="let element" class="px-2 text-right" mat-cell>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
size="medium"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[ngClass]="{
|
||||
@ -45,7 +92,7 @@
|
||||
element?.performances?.allTimeHigh?.performancePercent > 0
|
||||
}"
|
||||
[value]="element?.performances?.allTimeHigh?.performancePercent"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
@ -4,9 +4,8 @@ import {
|
||||
Input,
|
||||
OnChanges
|
||||
} from '@angular/core';
|
||||
import { locale } from '@ghostfolio/common/config';
|
||||
import { resolveMarketCondition } from '@ghostfolio/common/helper';
|
||||
import { Benchmark } from '@ghostfolio/common/interfaces';
|
||||
import { Benchmark, User } from '@ghostfolio/common/interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-benchmark',
|
||||
@ -17,6 +16,7 @@ import { Benchmark } from '@ghostfolio/common/interfaces';
|
||||
export class BenchmarkComponent implements OnChanges {
|
||||
@Input() benchmarks: Benchmark[];
|
||||
@Input() locale: string;
|
||||
@Input() user: User;
|
||||
|
||||
public displayedColumns = ['name', 'date', 'change', 'marketCondition'];
|
||||
public resolveMarketCondition = resolveMarketCondition;
|
||||
@ -24,8 +24,15 @@ export class BenchmarkComponent implements OnChanges {
|
||||
public constructor() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
if (!this.locale) {
|
||||
this.locale = locale;
|
||||
if (this.user?.settings?.isExperimentalFeatures) {
|
||||
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 { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { GfTrendIndicatorModule } from '../trend-indicator';
|
||||
import { GfValueModule } from '../value';
|
||||
import { BenchmarkComponent } from './benchmark.component';
|
||||
|
||||
@ -11,6 +12,7 @@ import { BenchmarkComponent } from './benchmark.component';
|
||||
exports: [BenchmarkComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfTrendIndicatorModule,
|
||||
GfValueModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule
|
||||
|
@ -2,8 +2,8 @@
|
||||
*ngIf="this.showPrevArrow"
|
||||
aria-hidden="true"
|
||||
aria-label="previous"
|
||||
class="carousel-nav carousel-nav-prev no-min-width position-absolute"
|
||||
mat-stroked-button
|
||||
class="carousel-nav carousel-nav-prev no-min-width position-absolute px-1"
|
||||
mat-button
|
||||
tabindex="-1"
|
||||
(click)="previous()"
|
||||
>
|
||||
@ -25,8 +25,8 @@
|
||||
*ngIf="this.showNextArrow"
|
||||
aria-hidden="true"
|
||||
aria-label="next"
|
||||
class="carousel-nav carousel-nav-next no-min-width position-absolute"
|
||||
mat-stroked-button
|
||||
class="carousel-nav carousel-nav-next no-min-width position-absolute px-1"
|
||||
mat-button
|
||||
tabindex="-1"
|
||||
(click)="next()"
|
||||
>
|
||||
|
@ -12,13 +12,14 @@
|
||||
button {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
|
||||
&.carousel-nav-prev {
|
||||
left: -50px;
|
||||
left: -0.5rem;
|
||||
}
|
||||
|
||||
&.carousel-nav-next {
|
||||
right: -50px;
|
||||
right: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
*ngIf="marketState === 'closed' && range === '1d'; else delayed"
|
||||
class="text-muted"
|
||||
name="pause-circle-outline"
|
||||
size="large"
|
||||
[size]="size"
|
||||
>
|
||||
</ion-icon>
|
||||
<ng-template #delayed>
|
||||
@ -21,7 +21,7 @@
|
||||
*ngIf="marketState === 'delayed' && range === '1d'; else trend"
|
||||
class="text-muted"
|
||||
name="time-outline"
|
||||
size="large"
|
||||
[size]="size"
|
||||
>
|
||||
</ion-icon>
|
||||
</ng-template>
|
||||
@ -31,21 +31,21 @@
|
||||
*ngIf="value <= -0.0005"
|
||||
class="text-danger"
|
||||
name="arrow-down-circle-outline"
|
||||
size="large"
|
||||
[ngClass]="{ 'rotate-45-down': value > -0.01 }"
|
||||
[size]="size"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="value > -0.0005 && value < 0.0005"
|
||||
class="text-muted"
|
||||
name="arrow-forward-circle-outline"
|
||||
size="large"
|
||||
[size]="size"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="value >= 0.0005"
|
||||
class="text-success"
|
||||
name="arrow-up-circle-outline"
|
||||
size="large"
|
||||
[ngClass]="{ 'rotate-45-up': value < 0.01 }"
|
||||
[size]="size"
|
||||
></ion-icon>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
@ -11,6 +11,7 @@ export class TrendIndicatorComponent {
|
||||
@Input() isLoading = false;
|
||||
@Input() marketState: MarketState = 'open';
|
||||
@Input() range: DateRange = 'max';
|
||||
@Input() size: 'large' | 'medium' | 'small' = 'small';
|
||||
@Input() value = 0;
|
||||
|
||||
public constructor() {}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.20.0",
|
||||
"version": "2.24.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||
@ -113,7 +113,7 @@
|
||||
"lodash": "4.17.21",
|
||||
"marked": "4.2.12",
|
||||
"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-markdown": "15.1.0",
|
||||
"ngx-skeleton-loader": "7.0.0",
|
||||
@ -177,7 +177,7 @@
|
||||
"codelyzer": "6.0.1",
|
||||
"cypress": "6.2.1",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-prettier": "8.6.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-plugin-cypress": "2.14.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-storybook": "0.6.12",
|
||||
@ -188,7 +188,7 @@
|
||||
"jest-environment-jsdom": "29.4.3",
|
||||
"jest-preset-angular": "13.1.1",
|
||||
"nx": "17.0.2",
|
||||
"prettier": "3.0.3",
|
||||
"prettier": "3.1.0",
|
||||
"prettier-plugin-organize-attributes": "1.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
|
@ -1,2 +1,2 @@
|
||||
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"
|
||||
rxjs "7.8.1"
|
||||
|
||||
"@angular-devkit/architect@^0.1600.0-next.6":
|
||||
version "0.1600.6"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1600.6.tgz#216f4d89086b8b4ef562b2066e430a44f7a2cf57"
|
||||
integrity sha512-Mk/pRujuer5qRMrgC7DPwLQ88wTAEKhbs0yJ/1prm4cx+VkxX9MMf6Y4AHKRmduKmFmd2LmX21/ACiU65acH8w==
|
||||
"@angular-devkit/architect@^0.1301.0 || ^0.1401.0 || ^0.1501.0 || ^0.1601.0 || ^0.1700.0":
|
||||
version "0.1700.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1700.0.tgz#419d59be6f8bc0068f8d495d7e28f4f47cfdb2ce"
|
||||
integrity sha512-whi7HvOjv1J3He9f+H8xNJWKyjAmWuWNl8gxNW6EZP/XLcrOu+/5QT4bPtXQBRIL/avZuc++5sNQS+kReaNCig==
|
||||
dependencies:
|
||||
"@angular-devkit/core" "16.0.6"
|
||||
"@angular-devkit/core" "17.0.0"
|
||||
rxjs "7.8.1"
|
||||
|
||||
"@angular-devkit/build-angular@16.2.9":
|
||||
@ -127,17 +127,6 @@
|
||||
rxjs "7.8.1"
|
||||
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":
|
||||
version "16.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.1.0.tgz#cb56b19e88fc936fb0b26c5ae62591f1e8906961"
|
||||
@ -160,18 +149,6 @@
|
||||
rxjs "7.8.1"
|
||||
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":
|
||||
version "16.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.9.tgz#81c5c95de8c423634bf93f616683045c6cdd4dd0"
|
||||
@ -184,6 +161,18 @@
|
||||
rxjs "7.8.1"
|
||||
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":
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.0.1.tgz#d49387e9e41c9cce98b155da51b0e193333dd178"
|
||||
@ -217,17 +206,6 @@
|
||||
ora "5.4.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":
|
||||
version "16.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.9.tgz#71eed819c1665068d717d75f912f5ea689c201f9"
|
||||
@ -239,6 +217,17 @@
|
||||
ora "5.4.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":
|
||||
version "16.2.0"
|
||||
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"
|
||||
jsonc-parser "3.2.0"
|
||||
|
||||
"@schematics/angular@^16.0.0-next.6":
|
||||
version "16.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.8.tgz#d4c236767e89c536c2c15951394cac20f07bfc1f"
|
||||
integrity sha512-yxfxJ2IMRIt+dQcqyJR30qd/osb5NwRsi9US3gFIHP1jfjOAs1Nk8ENNd5ycYV+yykCa78KWhmbOw4G1zpR56Q==
|
||||
"@schematics/angular@^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0":
|
||||
version "17.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-17.0.0.tgz#63ddf8bfbb3b117fe7a355bd22b43d2c9ff7f0ee"
|
||||
integrity sha512-9jKU5x/WzaBsfSkUowK1X74FqtMXa6+A60XgW4ACO8i6fwKfPeS+tIrAieeYOX80/njBh7I5CvcpHmWA2SbcXQ==
|
||||
dependencies:
|
||||
"@angular-devkit/core" "16.2.8"
|
||||
"@angular-devkit/schematics" "16.2.8"
|
||||
"@angular-devkit/core" "17.0.0"
|
||||
"@angular-devkit/schematics" "17.0.0"
|
||||
jsonc-parser "3.2.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"
|
||||
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
|
||||
|
||||
commander@^8.3.0, commander@~8.3.0:
|
||||
commander@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
|
||||
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:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365"
|
||||
@ -10230,10 +10214,10 @@ escodegen@^2.0.0:
|
||||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
eslint-config-prettier@8.6.0:
|
||||
version "8.6.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz#dec1d29ab728f4fa63061774e1672ac4e363d207"
|
||||
integrity sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==
|
||||
eslint-config-prettier@9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz#eb25485946dd0c66cd216a46232dc05451518d1f"
|
||||
integrity sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==
|
||||
|
||||
eslint-import-resolver-node@^0.3.7:
|
||||
version "0.3.9"
|
||||
@ -13271,11 +13255,6 @@ jiti@^1.18.2:
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42"
|
||||
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:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847"
|
||||
@ -14005,7 +13984,7 @@ magic-string@0.30.1:
|
||||
dependencies:
|
||||
"@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"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
|
||||
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"
|
||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||
|
||||
ng-extract-i18n-merge@2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/ng-extract-i18n-merge/-/ng-extract-i18n-merge-2.7.0.tgz#18e2acd1a7598100300c42887917e16c4782589d"
|
||||
integrity sha512-HG0Gjg4J8GqkROQSdHeCS1jtqz3ExzswH2zA8nbJNZU5ctA25O8dpfSXVl63PWxNhYtJOnP4rEPXNiyvlHaHwA==
|
||||
ng-extract-i18n-merge@2.8.3:
|
||||
version "2.8.3"
|
||||
resolved "https://registry.yarnpkg.com/ng-extract-i18n-merge/-/ng-extract-i18n-merge-2.8.3.tgz#a092f7758df7c566df7a1d710dbc709c6a8f56d1"
|
||||
integrity sha512-w6LdzpfjRBLpT7lnMEqduivjn6kg2oKDZBL6P9W5GKRZ4bgmFthAmwN1lvWrzkwcPHPARJR+qC4DBRVsv4vmkg==
|
||||
dependencies:
|
||||
"@angular-devkit/architect" "^0.1600.0-next.6"
|
||||
"@angular-devkit/core" "^16.0.0-next.6"
|
||||
"@angular-devkit/schematics" "^16.0.0-next.6"
|
||||
"@schematics/angular" "^16.0.0-next.6"
|
||||
xliff-simple-merge "~1.0.1"
|
||||
xml_normalize "^1.0.0"
|
||||
xmldoc "~1.1.2"
|
||||
"@angular-devkit/architect" "^0.1301.0 || ^0.1401.0 || ^0.1501.0 || ^0.1601.0 || ^0.1700.0"
|
||||
"@angular-devkit/core" "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
"@angular-devkit/schematics" "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
"@schematics/angular" "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
xmldoc "^1.1.2"
|
||||
|
||||
ngx-device-detector@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"
|
||||
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:
|
||||
version "2.3.0"
|
||||
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"
|
||||
integrity sha512-+NmameaLxbCcylEXsKPmawtzla5EE6ECqvGkpfQz4KM847fXDifB1gFnPQEpoADAq6IXg+cMI8Z0ISJEXa6fhg==
|
||||
|
||||
prettier@3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
|
||||
integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
|
||||
prettier@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.0.tgz#c6d16474a5f764ea1a4a373c593b779697744d5e"
|
||||
integrity sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==
|
||||
|
||||
prettier@^2.8.0:
|
||||
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"
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||
|
||||
xmldoc@~1.1.2:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.1.4.tgz#ea4e26dca76b1d218a2f777018bce404ba374a86"
|
||||
integrity sha512-rQshsBGR5s7pUNENTEncpI2LTCuzicri0DyE4SCV5XmS0q81JS8j1iPijP0Q5c4WLGbKh3W92hlOwY6N9ssW1w==
|
||||
xmldoc@^1.1.2:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.3.0.tgz#7823225b096c74036347c9ec5924d06b6a3cebab"
|
||||
integrity sha512-y7IRWW6PvEnYQZNZFMRLNJw+p3pezM4nKYPfr15g4OOW9i8VpeydycFuipE2297OvZnh3jSb2pxOt9QpkZUVng==
|
||||
dependencies:
|
||||
sax "^1.2.4"
|
||||
|
||||
|
Reference in New Issue
Block a user