Compare commits

...

26 Commits

Author SHA1 Message Date
6f11627006 Release 2.24.0 (#2661) 2023-11-16 20:29:49 +01:00
215098e418 Feature/improve language localization for german 20231116 (#2660)
* Update locales

* Update changelog
2023-11-16 20:28:09 +01:00
781496383b Bugfix/improve get range query in market data service (#2659)
* Attempt to fix "too many bind variables in prepared statement, expected maximum of 32767"

* Update changelog
2023-11-16 20:22:56 +01:00
f0f304c012 Change tweet to post (#2658) 2023-11-16 20:22:18 +01:00
4bf97c104b Update changelog (#2656) 2023-11-15 21:45:38 +01:00
0b35a3c7a7 Release 2.23.0 (#2655) 2023-11-15 21:22:20 +01:00
1586cd3a59 Feature/change twitter to x (#2654)
* Change Twitter to X

* Update changelog
2023-11-15 21:20:51 +01:00
ae763cbb87 Improve style of sub title (#2652) 2023-11-15 21:11:10 +01:00
aa72287d54 Extend benchmarks in the markets overview by 50-Day and 200-Day trends (#2575)
* Extend benchmarks in the markets overview by 50-Day and 200-Day trends

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-11-15 20:25:16 +01:00
d155ab6f28 Feature/improve data source validation in activities import (#2645)
* Improve data source validation

* Update changelog
2023-11-14 19:15:57 +01:00
913ca71aa5 Feature/upgrade prettier to version 3.1.0 (#2649)
* Upgrade prettier to version 3.1.0

* Update changelog
2023-11-13 20:40:25 +01:00
1ffde2a27e Feature/setup polish (#2650)
* Set up polski

* Update changelog
2023-11-13 20:35:15 +01:00
fcf0cea982 Change BTC to BTCUSD (#2646) 2023-11-13 20:22:47 +01:00
ae1968aadf Feature/extract locales 20231112 (#2643)
* Update locales

* Update changelog
2023-11-12 19:03:11 +01:00
3e6333ef95 Feature/upgrade ng extract i18n merge to version 2.8.3 (#2642)
* Upgrade ng-extract-i18n-merge to version 2.8.3

* Update changelog
2023-11-12 18:28:26 +01:00
c69686651e Release 2.22.0 (#2638) 2023-11-11 18:59:43 +01:00
93b6011ddc Feature/refactor get range in market data service (#2631)
* Refactor to unique asset in getRange()

* Update changelog
2023-11-11 18:57:41 +01:00
f567e25f27 Feature/introduce action menus in overview of action control panel (#2637)
* Introduce action menus

* Exchange rates management
* Coupons management

* Update changelog
2023-11-11 18:48:23 +01:00
5dc538bafb Feature/optimize testimonial carousel style on mobile (#2634)
* Optimize style on mobile

* Update changelog
2023-11-11 17:29:59 +01:00
b4de06fcf0 Feature/add platform icons to account selectors (#2633)
* Add platform icons to account selectors

* Update changelog
2023-11-11 17:27:29 +01:00
27da0eb26e Feature/harmonize name column of historical market data table (#2632)
* Harmonize name column

* Update changelog
2023-11-11 09:02:12 +01:00
8ff80c10e5 Feature/extend personal finance tools pages 20231110 (#2630)
* Add Magnifi

* Add Basil Finance
2023-11-11 09:01:58 +01:00
5db5d5e79a Release 2.21.0 (#2629) 2023-11-09 19:25:14 +01:00
12aac101bd Feature/extend system message (#2628)
* Extend system message

* Update changelog
2023-11-09 19:23:36 +01:00
3a66ccdebe Bugfix/fix unit in overview of home page (#2626)
* Fix unit

* Update changelog
2023-11-09 19:22:15 +01:00
6a722d1bb7 Bugfix/fix get quotes in financial modeling prep service (#2627)
* Fix get quotes

* Update changelog
2023-11-09 18:03:09 +01:00
70 changed files with 23313 additions and 6996 deletions

View File

@ -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

View File

@ -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,14 +165,24 @@ 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 }) => {
return symbolProfileId;
});
)
.filter((benchmark) => {
if (enableSharing) {
return benchmark.enableSharing;
}
return true;
})
.map(({ symbolProfileId }) => {
return symbolProfileId;
});
const assetProfiles =
await this.symbolProfileService.getSymbolProfilesByIds(symbolProfileIds);

View File

@ -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([

View File

@ -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()

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -1,5 +1,6 @@
export interface GetValueObject {
import { UniqueAsset } from '@ghostfolio/common/interfaces';
export interface GetValueObject extends UniqueAsset {
date: Date;
marketPriceInBaseCurrency: number;
symbol: string;
}

View File

@ -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 }) => {

View File

@ -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();

View File

@ -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>

View File

@ -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(),

View File

@ -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,

View File

@ -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');
}
}

View File

@ -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"

View File

@ -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>&nbsp;</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>

View File

@ -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() {

View File

@ -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;

View File

@ -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>

View File

@ -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,

View File

@ -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,7 +155,13 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
}
public onDeleteSystemMessage() {
this.putAdminSetting({ key: PROPERTY_SYSTEM_MESSAGE, value: undefined });
const confirmation = confirm(
$localize`Do you really want to delete this system message?`
);
if (confirmation === true) {
this.putAdminSetting({ key: PROPERTY_SYSTEM_MESSAGE, value: undefined });
}
}
public onFlushCache() {
@ -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;

View File

@ -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,26 +56,50 @@
</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
[queryParams]="{
[matMenuTriggerFor]="exchangeRateActionsMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal"></ion-icon>
</button>
<mat-menu
#exchangeRateActionsMenu="matMenu"
class="h-100 mx-1 no-min-width px-2"
xPosition="before"
>
<a
mat-menu-item
[queryParams]="{
assetProfileDialog: true,
dataSource: exchangeRate.dataSource,
symbol: exchangeRate.symbol
}"
[routerLink]="['/admin', 'market-data']"
>
<ion-icon name="create-outline"></ion-icon>
</a>
<button
*ngIf="customCurrencies.includes(exchangeRate.label2)"
class="h-100 mx-1 no-min-width px-2"
mat-button
(click)="onDeleteCurrency(exchangeRate.label2)"
>
<ion-icon name="trash-outline"></ion-icon>
</button>
[routerLink]="['/admin', 'market-data']"
>
<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)"
mat-menu-item
(click)="onDeleteCurrency(exchangeRate.label2)"
>
<span class="align-items-center d-flex">
<ion-icon
class="mr-2"
name="trash-outline"
></ion-icon>
<span i18n>Delete</span>
</span>
</button>
</mat-menu>
</td>
</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
(click)="onDeleteCoupon(coupon.code)"
[matMenuTriggerFor]="couponActionsMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="trash-outline"></ion-icon>
<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)"
>
<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>

View File

@ -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,

View File

@ -31,6 +31,7 @@
<gf-benchmark
[benchmarks]="benchmarks"
[locale]="user?.settings?.locale"
[user]="user"
></gf-benchmark>
<ngx-skeleton-loader
*ngIf="isLoading"

View File

@ -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) {

View File

@ -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"

View File

@ -1,5 +1,6 @@
:host {
display: block;
align-items: center;
display: flex;
img {
border-radius: 0.2rem;

View File

@ -44,6 +44,7 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
'fr',
'it',
'nl',
'pl',
'pt',
'tr'
];

View File

@ -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

View File

@ -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'"

View File

@ -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>

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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'
];
}

View File

@ -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

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

View File

@ -101,6 +101,7 @@ export const SUPPORTED_LANGUAGE_CODES = [
'fr',
'it',
'nl',
'pl',
'pt',
'tr'
];

View File

@ -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') {

View File

@ -1,3 +1,4 @@
export interface BenchmarkProperty {
enableSharing?: boolean;
symbolProfileId: string;
}

View File

@ -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;
}

View File

@ -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,

View File

@ -17,6 +17,5 @@ export interface InfoItem {
statistics: Statistics;
stripePublicKey?: string;
subscriptions: { [offer in SubscriptionOffer]: Subscription };
systemMessage?: string;
tags: Tag[];
}

View File

@ -0,0 +1,7 @@
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
export interface SystemMessage {
message: string;
routerLink?: string[];
targetGroups: SubscriptionType[];
}

View File

@ -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;

View File

@ -0,0 +1 @@
export type BenchmarkTrend = 'DOWN' | 'NEUTRAL' | 'UNKNOWN' | 'UP';

View File

@ -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,

View File

@ -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>

View File

@ -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'
];
}
}
}

View File

@ -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

View File

@ -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()"
>

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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() {}

View File

@ -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",

View File

@ -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

1 Date Code Currency Price Quantity Action Fee
2 12/12/2021 BTC BTCUSD <invalid> 44558.42 1 buy 0

159
yarn.lock
View File

@ -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"