Browse Source

Merge branch 'main' into translate-zh-fee-ratio

pull/6348/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
fc2d1b6f00
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 38
      CHANGELOG.md
  2. 4
      apps/api/src/app/admin/admin.service.ts
  3. 134
      apps/api/src/app/import/import.service.ts
  4. 25
      apps/api/src/app/order/order.controller.ts
  5. 43
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  6. 122
      apps/api/src/services/data-provider/data-provider.service.ts
  7. 7
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  8. 7
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  9. 12
      apps/client/src/app/pages/about/changelog/changelog-page.component.ts
  10. 12
      apps/client/src/app/pages/about/oss-friends/oss-friends-page.component.ts
  11. 12
      apps/client/src/app/pages/admin/admin-page.component.ts
  12. 15
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  13. 8
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  14. 8
      apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts
  15. 20
      apps/client/src/locales/messages.ca.xlf
  16. 20
      apps/client/src/locales/messages.de.xlf
  17. 28
      apps/client/src/locales/messages.es.xlf
  18. 20
      apps/client/src/locales/messages.fr.xlf
  19. 20
      apps/client/src/locales/messages.it.xlf
  20. 20
      apps/client/src/locales/messages.ko.xlf
  21. 20
      apps/client/src/locales/messages.nl.xlf
  22. 20
      apps/client/src/locales/messages.pl.xlf
  23. 20
      apps/client/src/locales/messages.pt.xlf
  24. 20
      apps/client/src/locales/messages.tr.xlf
  25. 20
      apps/client/src/locales/messages.uk.xlf
  26. 19
      apps/client/src/locales/messages.xlf
  27. 20
      apps/client/src/locales/messages.zh.xlf
  28. 5
      libs/common/src/lib/config.ts
  29. 3
      libs/common/src/lib/types/market-data-preset.type.ts
  30. 10
      libs/ui/src/lib/account-balances/account-balances.component.html
  31. 68
      libs/ui/src/lib/account-balances/account-balances.component.ts
  32. 2
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  33. 8
      libs/ui/src/lib/activities-filter/activities-filter.component.html
  34. 83
      libs/ui/src/lib/activities-filter/activities-filter.component.ts
  35. 47
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts
  36. 3
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html
  37. 2
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts
  38. 24
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html
  39. 60
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.spec.ts
  40. 133
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts
  41. 12
      package-lock.json
  42. 7
      package.json

38
CHANGELOG.md

@ -7,14 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
- Improved the language localization for Chinese (`zh`)
## 2.242.0 - 2026-02-22
### Changed
- Changed the account field to optional in the create or update activity dialog
### Fixed
- Fixed a validation issue for valuables used in the create and import activity logic
- Fixed the page size for presets in the historical market data table of the admin control panel
## 2.241.0 - 2026-02-21
### Changed
- Improved the usability of the portfolio summary tab on the home page in the _Presenter View_
- Refreshed the cryptocurrencies list
- Improved the language localization for German (`de`)
- Improved the language localization for Spanish (`es`)
### Fixed
- Fixed an issue with `balanceInBaseCurrency` of the accounts in the value redaction interceptor for the impersonation mode
- Fixed an issue with `comment` of the accounts in the value redaction interceptor for the impersonation mode
- Fixed an issue with `dividendInBaseCurrency` of the accounts in the value redaction interceptor for the impersonation mode
- Fixed an issue with `interestInBaseCurrency` of the accounts in the value redaction interceptor for the impersonation mode
- Fixed an issue with `value` of the accounts in the value redaction interceptor for the impersonation mode
## 2.240.0 - 2026-02-18
### Added
- Added a _No Activities_ preset to the historical market data table of the admin control panel
- Added support for custom cryptocurrencies defined in the database
- Added support for the cryptocurrency _Sky_
### Changed
- Improved the language localization for Chinese (`zh`)
- Harmonized the validation for the create activity endpoint with the existing import activity logic
- Upgraded `marked` from version `17.0.1` to `17.0.2`
- Upgraded `ngx-markdown` from version `21.0.1` to `21.1.0`
## 2.239.0 - 2026-02-15

4
apps/api/src/app/admin/admin.service.ts

@ -225,6 +225,10 @@ export class AdminService {
presetId === 'ETF_WITHOUT_SECTORS'
) {
filters = [{ id: 'ETF', type: 'ASSET_SUB_CLASS' }];
} else if (presetId === 'NO_ACTIVITIES') {
where.activities = {
none: {}
};
}
const searchQuery = filters.find(({ type }) => {

134
apps/api/src/app/import/import.service.ts

@ -3,7 +3,6 @@ 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 { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
@ -33,7 +32,7 @@ import {
} from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
import { DataSource, Prisma } from '@prisma/client';
import { Big } from 'big.js';
import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns';
import { omit, uniqBy } from 'lodash';
@ -46,7 +45,6 @@ export class ImportService {
public constructor(
private readonly accountService: AccountService,
private readonly apiService: ApiService,
private readonly configurationService: ConfigurationService,
private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
@ -395,7 +393,7 @@ export class ImportService {
}
}
const assetProfiles = await this.validateActivities({
const assetProfiles = await this.dataProviderService.validateActivities({
activitiesDto,
assetProfilesWithMarketDataDto,
maxActivitiesToImport,
@ -729,132 +727,4 @@ export class ImportService {
return uniqueAccountIds.size === 1;
}
private async validateActivities({
activitiesDto,
assetProfilesWithMarketDataDto,
maxActivitiesToImport,
user
}: {
activitiesDto: Partial<CreateOrderDto>[];
assetProfilesWithMarketDataDto: ImportDataDto['assetProfiles'];
maxActivitiesToImport: number;
user: UserWithSettings;
}) {
if (activitiesDto?.length > maxActivitiesToImport) {
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`);
}
const assetProfiles: {
[assetProfileIdentifier: string]: Partial<SymbolProfile>;
} = {};
const dataSources = await this.dataProviderService.getDataSources();
for (const [
index,
{ currency, dataSource, symbol, type }
] of activitiesDto.entries()) {
if (!dataSources.includes(dataSource)) {
throw new Error(
`activities.${index}.dataSource ("${dataSource}") is not valid`
);
}
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user.subscription.type === 'Basic'
) {
const dataProvider = this.dataProviderService.getDataProvider(
DataSource[dataSource]
);
if (dataProvider.getDataProviderInfo().isPremium) {
throw new Error(
`activities.${index}.dataSource ("${dataSource}") is not valid`
);
}
}
if (!assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })]) {
if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) {
// Skip asset profile validation for FEE, INTEREST, and LIABILITY
// as these activity types don't require asset profiles
const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
(profile) => {
return (
profile.dataSource === dataSource && profile.symbol === symbol
);
}
);
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] = {
currency,
dataSource,
symbol,
name: assetProfileInImport?.name
};
continue;
}
let assetProfile: Partial<SymbolProfile> = { currency };
try {
assetProfile = (
await this.dataProviderService.getAssetProfiles([
{ dataSource, symbol }
])
)?.[symbol];
} catch {}
if (!assetProfile?.name) {
const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
(profile) => {
return (
profile.dataSource === dataSource && profile.symbol === symbol
);
}
);
if (assetProfileInImport) {
// Merge all fields of custom asset profiles into the validation object
Object.assign(assetProfile, {
assetClass: assetProfileInImport.assetClass,
assetSubClass: assetProfileInImport.assetSubClass,
comment: assetProfileInImport.comment,
countries: assetProfileInImport.countries,
currency: assetProfileInImport.currency,
cusip: assetProfileInImport.cusip,
dataSource: assetProfileInImport.dataSource,
figi: assetProfileInImport.figi,
figiComposite: assetProfileInImport.figiComposite,
figiShareClass: assetProfileInImport.figiShareClass,
holdings: assetProfileInImport.holdings,
isActive: assetProfileInImport.isActive,
isin: assetProfileInImport.isin,
name: assetProfileInImport.name,
scraperConfiguration: assetProfileInImport.scraperConfiguration,
sectors: assetProfileInImport.sectors,
symbol: assetProfileInImport.symbol,
symbolMapping: assetProfileInImport.symbolMapping,
url: assetProfileInImport.url
});
}
}
if (!['FEE', 'INTEREST', 'LIABILITY'].includes(type)) {
if (!assetProfile?.name) {
throw new Error(
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
);
}
}
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =
assetProfile;
}
}
return assetProfiles;
}
}

25
apps/api/src/app/order/order.controller.ts

@ -4,6 +4,7 @@ import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
@ -46,6 +47,7 @@ import { OrderService } from './order.service';
export class OrderController {
public constructor(
private readonly apiService: ApiService,
private readonly dataProviderService: DataProviderService,
private readonly dataGatheringService: DataGatheringService,
private readonly impersonationService: ImpersonationService,
private readonly orderService: OrderService,
@ -190,6 +192,29 @@ export class OrderController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async createOrder(@Body() data: CreateOrderDto): Promise<OrderModel> {
try {
await this.dataProviderService.validateActivities({
activitiesDto: [
{
currency: data.currency,
dataSource: data.dataSource,
symbol: data.symbol,
type: data.type
}
],
maxActivitiesToImport: 1,
user: this.request.user
});
} catch (error) {
throw new HttpException(
{
error: getReasonPhrase(StatusCodes.BAD_REQUEST),
message: [error.message]
},
StatusCodes.BAD_REQUEST
);
}
const currency = data.currency;
const customCurrency = data.customCurrency;
const dataSource = data.dataSource;

43
apps/api/src/assets/cryptocurrencies/cryptocurrencies.json

@ -421,6 +421,7 @@
"AGS": "Aegis",
"AGT": "Alaya Governance Token",
"AGURI": "Aguri-Chan",
"AGUSTO": "Agusto",
"AGV": "Astra Guild Ventures",
"AGVC": "AgaveCoin",
"AGVE": "Agave",
@ -662,6 +663,7 @@
"ALN": "Aluna",
"ALNV1": "Aluna v1",
"ALOHA": "Aloha",
"ALOKA": "ALOKA",
"ALON": "Alon",
"ALOR": "The Algorix",
"ALOT": "Dexalot",
@ -708,6 +710,7 @@
"AMADEUS": "AMADEUS",
"AMAL": "AMAL",
"AMAPT": "Amnis Finance",
"AMARA": "AMARA",
"AMATEN": "Amaten",
"AMATO": "AMATO",
"AMAZINGTEAM": "AmazingTeamDAO",
@ -1344,6 +1347,7 @@
"AZIT": "Azit",
"AZNX": "AstraZeneca xStock",
"AZR": "Azure",
"AZTEC": "AZTEC",
"AZU": "Azultec",
"AZUKI": "Azuki",
"AZUKI2": "AZUKI 2.0",
@ -1373,6 +1377,7 @@
"BABI": "Babylons",
"BABL": "Babylon Finance",
"BABY": "Babylon",
"BABY4": "Baby 4",
"BABYANDY": "Baby Andy",
"BABYASTER": "Baby Aster",
"BABYB": "Baby Bali",
@ -2342,6 +2347,7 @@
"BNPL": "BNPL Pay",
"BNR": "BiNeuro",
"BNRTX": "BnrtxCoin",
"BNRY": "Binary Coin",
"BNS": "BNS token",
"BNSAI": "bonsAI Network",
"BNSD": "BNSD Finance",
@ -2526,9 +2532,10 @@
"BOSSCOQ": "THE COQFATHER",
"BOST": "BoostCoin",
"BOSU": "Bosu Inu",
"BOT": "Bot Planet",
"BOT": "HyperBot",
"BOTC": "BotChain",
"BOTIFY": "BOTIFY",
"BOTPLANET": "Bot Planet",
"BOTS": "ArkDAO",
"BOTTO": "Botto",
"BOTX": "BOTXCOIN",
@ -3201,6 +3208,7 @@
"CATCO": "CatCoin",
"CATCOIN": "CatCoin",
"CATCOINETH": "Catcoin",
"CATCOINIO": "Catcoin",
"CATCOINOFSOL": "Cat Coin",
"CATCOINV2": "CatCoin Cash",
"CATDOG": "Cat-Dog",
@ -3583,6 +3591,7 @@
"CIC": "Crazy Internet Coin",
"CICHAIN": "CIChain",
"CIF": "Crypto Improvement Fund",
"CIFRON": "Cipher Mining (Ondo Tokenized)",
"CIG": "cig",
"CIM": "COINCOME",
"CIN": "CinderCoin",
@ -3718,6 +3727,7 @@
"CMPT": "Spatial Computing",
"CMPV2": "Caduceus Protocol",
"CMQ": "Communique",
"CMR": "U.S Critical Mineral Reserve",
"CMS": "COMSA",
"CMSN": "The Commission",
"CMT": "CyberMiles",
@ -4630,6 +4640,7 @@
"DEFIL": "DeFIL",
"DEFILAB": "Defi",
"DEFISCALE": "DeFiScale",
"DEFISSI": "DEFI.ssi",
"DEFIT": "Digital Fitness",
"DEFLA": "Defla",
"DEFLCT": "Deflect",
@ -6323,7 +6334,7 @@
"FIFTY": "FIFTYONEFIFTY",
"FIG": "FlowCom",
"FIGH": "FIGHT FIGHT FIGHT",
"FIGHT": "Fight to MAGA",
"FIGHT2MAGA": "Fight to MAGA",
"FIGHTMAGA": "FIGHT MAGA",
"FIGHTPEPE": "FIGHT PEPE",
"FIGHTRUMP": "FIGHT TRUMP",
@ -8039,6 +8050,7 @@
"HONOR": "HonorLand",
"HONX": "Honeywell xStock",
"HOODOG": "Hoodog",
"HOODON": "Robinhood Markets (Ondo Tokenized)",
"HOODRAT": "Hoodrat Coin",
"HOODX": "Robinhood xStock",
"HOOF": "Metaderby Hoof",
@ -8395,6 +8407,7 @@
"IMS": "Independent Money System",
"IMST": "Imsmart",
"IMT": "Immortal Token",
"IMU": "Immunefi",
"IMUSIFY": "imusify",
"IMVR": "ImmVRse",
"IMX": "Immutable X",
@ -8750,6 +8763,7 @@
"JFIVE": "Jonny Five",
"JFOX": "JuniperFox AI",
"JFP": "JUSTICE FOR PEANUT",
"JGGL": "JGGL Token",
"JGLP": "Jones GLP",
"JGN": "Juggernaut",
"JHH": "Jen-Hsun Huang",
@ -9891,7 +9905,7 @@
"LRN": "Loopring [NEO]",
"LRT": "LandRocker",
"LSC": "LS Coin",
"LSD": "Pontem Liquidswap",
"LSD": "LSD",
"LSDOGE": "LSDoge",
"LSETH": "Liquid Staked ETH",
"LSHARE": "LSHARE",
@ -10167,8 +10181,7 @@
"MANUSAI": "Manus AI Agent",
"MANYU": "Manyu",
"MANYUDOG": "MANYU",
"MAO": "MAO",
"MAOMEME": "Mao",
"MAO": "Mao",
"MAOW": "MAOW",
"MAP": "MAP Protocol",
"MAPC": "MapCoin",
@ -10631,6 +10644,7 @@
"MICRO": "Micro GPT",
"MICRODOGE": "MicroDoge",
"MICROMINES": "Micromines",
"MICROVISION": "MicroVisionChain",
"MIDAI": "Midway AI",
"MIDAS": "Midas",
"MIDASDOLLAR": "Midas Dollar Share",
@ -13146,6 +13160,7 @@
"PONKE": "Ponke",
"PONKEBNB": "Ponke BNB",
"PONKEI": "Chinese Ponkei the Original",
"PONTEM": "Pontem Liquidswap",
"PONYO": "Ponyo Impact",
"PONZI": "Ponzi",
"PONZIO": "Ponzio The Cat",
@ -13573,6 +13588,7 @@
"QNX": "QueenDex Coin",
"QOBI": "Qobit",
"QOM": "Shiba Predator",
"QONE": "QONE",
"QOOB": "QOOBER",
"QORA": "QoraCoin",
"QORPO": "QORPO WORLD",
@ -15153,6 +15169,7 @@
"SNAP": "SnapEx",
"SNAPCAT": "Snapcat",
"SNAPKERO": "SNAP",
"SNAPON": "Snap (Ondo Tokenized)",
"SNB": "SynchroBitcoin",
"SNC": "SunContract",
"SNCT": "SnakeCity",
@ -15380,7 +15397,7 @@
"SP8DE": "Sp8de",
"SPA": "Sperax",
"SPAC": "SPACE DOGE",
"SPACE": "MicroVisionChain",
"SPACE": "Spacecoin",
"SPACECOIN": "SpaceCoin",
"SPACED": "SPACE DRAGON",
"SPACEHAMSTER": "Space Hamster",
@ -15868,6 +15885,7 @@
"SUPERCYCLE": "Crypto SuperCycle",
"SUPERDAPP": "SuperDapp",
"SUPERF": "SUPER FLOKI",
"SUPERFL": "Superfluid",
"SUPERGROK": "SuperGrok",
"SUPEROETHB": "Super OETH",
"SUPERT": "Super Trump",
@ -16790,6 +16808,7 @@
"TSLAON": "Tesla (Ondo Tokenized)",
"TSLAX": "Tesla xStock",
"TSLT": "Tamkin",
"TSMON": "Taiwan Semiconductor Manufacturing (Ondo Tokenized)",
"TSN": "Tsunami Exchange Token",
"TSO": "Thesirion",
"TSOTCHKE": "tsotchke",
@ -17181,8 +17200,10 @@
"USDL": "Lift Dollar",
"USDM": "USDM",
"USDMA": "USD mars",
"USDN": "Neutral AI",
"USDN": "Ultimate Synthetic Delta Neutral",
"USDNEUTRAL": "Neutral AI",
"USDO": "USD Open Dollar",
"USDON": "U.S. Dollar Tokenized Currency (Ondo)",
"USDP": "Pax Dollar",
"USDPLUS": "Overnight.fi USD+",
"USDQ": "Quantoz USDQ",
@ -17456,6 +17477,7 @@
"VIDZ": "PureVidz",
"VIEW": "Viewly",
"VIG": "TheVig",
"VIGI": "Vigi",
"VIK": "VIKTAMA",
"VIKITA": "VIKITA",
"VIKKY": "VikkyToken",
@ -17513,6 +17535,7 @@
"VLC": "Volcano Uni",
"VLDY": "Validity",
"VLK": "Vulkania",
"VLR": "Velora",
"VLS": "Veles",
"VLT": "Veltor",
"VLTC": "Venus LTC",
@ -17733,6 +17756,7 @@
"WANUSDT": "wanUSDT",
"WAP": "Wet Ass Pussy",
"WAR": "WAR",
"WARD": "Warden",
"WARP": "WarpCoin",
"WARPED": "Warped Games",
"WARPIE": "Warpie",
@ -18494,6 +18518,7 @@
"XP": "Xphere",
"XPA": "XPA",
"XPARTY": "X Party",
"XPASS": "XPASS Token",
"XPAT": "Bitnation Pangea",
"XPAY": "Wallet Pay",
"XPB": "Pebble Coin",
@ -18869,8 +18894,7 @@
"ZEBU": "ZEBU",
"ZEC": "ZCash",
"ZECD": "ZCashDarkCoin",
"ZED": "ZED Token",
"ZEDCOIN": "ZedCoin",
"ZED": "ZedCoins",
"ZEDD": "ZedDex",
"ZEDTOKEN": "Zed Token",
"ZEDX": "ZEDX Сoin",
@ -19108,6 +19132,7 @@
"币安人生": "币安人生",
"恶俗企鹅": "恶俗企鹅",
"我踏马来了": "我踏马来了",
"狗屎": "狗屎",
"老子": "老子",
"雪球": "雪球",
"黑马": "黑马"

122
apps/api/src/services/data-provider/data-provider.service.ts

@ -1,3 +1,4 @@
import { ImportDataDto } from '@ghostfolio/api/app/import/import-data.dto';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
@ -10,8 +11,10 @@ import {
PROPERTY_API_KEY_GHOSTFOLIO,
PROPERTY_DATA_SOURCE_MAPPING
} from '@ghostfolio/common/config';
import { CreateOrderDto } from '@ghostfolio/common/dtos';
import {
DATE_FORMAT,
getAssetProfileIdentifier,
getCurrencyFromSymbol,
getStartOfUtcDate,
isCurrency,
@ -185,6 +188,125 @@ export class DataProviderService implements OnModuleInit {
return dataSources.sort();
}
public async validateActivities({
activitiesDto,
assetProfilesWithMarketDataDto,
maxActivitiesToImport,
user
}: {
activitiesDto: Pick<
Partial<CreateOrderDto>,
'currency' | 'dataSource' | 'symbol' | 'type'
>[];
assetProfilesWithMarketDataDto?: ImportDataDto['assetProfiles'];
maxActivitiesToImport: number;
user: UserWithSettings;
}) {
if (activitiesDto?.length > maxActivitiesToImport) {
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`);
}
const assetProfiles: {
[assetProfileIdentifier: string]: Partial<SymbolProfile>;
} = {};
const dataSources = await this.getDataSources();
for (const [
index,
{ currency, dataSource, symbol, type }
] of activitiesDto.entries()) {
const activityPath =
maxActivitiesToImport === 1 ? 'activity' : `activities.${index}`;
if (!dataSources.includes(dataSource)) {
throw new Error(
`${activityPath}.dataSource ("${dataSource}") is not valid`
);
}
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user.subscription.type === 'Basic'
) {
const dataProvider = this.getDataProvider(DataSource[dataSource]);
if (dataProvider.getDataProviderInfo().isPremium) {
throw new Error(
`${activityPath}.dataSource ("${dataSource}") is not valid`
);
}
}
const assetProfileIdentifier = getAssetProfileIdentifier({
dataSource,
symbol
});
if (!assetProfiles[assetProfileIdentifier]) {
if (
(dataSource === DataSource.MANUAL && type === 'BUY') ||
['FEE', 'INTEREST', 'LIABILITY'].includes(type)
) {
const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
(assetProfile) => {
return (
assetProfile.dataSource === dataSource &&
assetProfile.symbol === symbol
);
}
);
assetProfiles[assetProfileIdentifier] = {
currency,
dataSource,
symbol,
name: assetProfileInImport?.name ?? symbol
};
continue;
}
let assetProfile: Partial<SymbolProfile> = { currency };
try {
assetProfile = (
await this.getAssetProfiles([
{
dataSource,
symbol
}
])
)?.[symbol];
} catch {}
if (!assetProfile?.name) {
const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
(profile) => {
return (
profile.dataSource === dataSource && profile.symbol === symbol
);
}
);
if (assetProfileInImport) {
Object.assign(assetProfile, assetProfileInImport);
}
}
if (!assetProfile?.name) {
throw new Error(
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
);
}
assetProfiles[assetProfileIdentifier] = assetProfile;
}
}
return assetProfiles;
}
public async getDividends({
dataSource,
from,

7
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -140,6 +140,11 @@ export class GfAdminMarketDataComponent
id: 'ETF_WITHOUT_SECTORS',
label: $localize`ETFs without Sectors`,
type: 'PRESET_ID' as Filter['type']
},
{
id: 'NO_ACTIVITIES',
label: $localize`No Activities`,
type: 'PRESET_ID' as Filter['type']
}
];
public benchmarks: Partial<SymbolProfile>[];
@ -374,7 +379,7 @@ export class GfAdminMarketDataComponent
this.pageSize =
this.activeFilters.length === 1 &&
this.activeFilters[0].type === 'PRESET_ID'
? undefined
? Number.MAX_SAFE_INTEGER
: DEFAULT_PAGE_SIZE;
if (pageIndex === 0 && this.paginator) {

7
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -184,18 +184,21 @@
[ngClass]="{
'cursor-pointer':
hasPermissionToUpdateUserSettings &&
!user?.settings?.isRestrictedView &&
user?.subscription?.type !== 'Basic'
}"
(click)="
hasPermissionToUpdateUserSettings &&
!user?.settings?.isRestrictedView &&
user?.subscription?.type !== 'Basic' &&
onEditEmergencyFund()
"
>
@if (
hasPermissionToUpdateUserSettings &&
user?.subscription?.type !== 'Basic' &&
!isLoading
!isLoading &&
!user?.settings?.isRestrictedView &&
user?.subscription?.type !== 'Basic'
) {
<ion-icon
class="mr-1 text-muted"

12
apps/client/src/app/pages/about/changelog/changelog-page.component.ts

@ -1,7 +1,6 @@
import { Component, OnDestroy } from '@angular/core';
import { Component } from '@angular/core';
import { MarkdownModule } from 'ngx-markdown';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
@Component({
imports: [MarkdownModule, NgxSkeletonLoaderModule],
@ -9,17 +8,10 @@ import { Subject } from 'rxjs';
styleUrls: ['./changelog-page.scss'],
templateUrl: './changelog-page.html'
})
export class GfChangelogPageComponent implements OnDestroy {
export class GfChangelogPageComponent {
public isLoading = true;
private unsubscribeSubject = new Subject<void>();
public onLoad() {
this.isLoading = false;
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

12
apps/client/src/app/pages/about/oss-friends/oss-friends-page.component.ts

@ -1,10 +1,9 @@
import { Component, OnDestroy } from '@angular/core';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { arrowForwardOutline } from 'ionicons/icons';
import { Subject } from 'rxjs';
const ossFriends = require('../../../../assets/oss-friends.json');
@ -14,17 +13,10 @@ const ossFriends = require('../../../../assets/oss-friends.json');
styleUrls: ['./oss-friends-page.scss'],
templateUrl: './oss-friends-page.html'
})
export class GfOpenSourceSoftwareFriendsPageComponent implements OnDestroy {
export class GfOpenSourceSoftwareFriendsPageComponent {
public ossFriends = ossFriends.data;
private unsubscribeSubject = new Subject<void>();
public constructor() {
addIcons({ arrowForwardOutline });
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

12
apps/client/src/app/pages/admin/admin-page.component.ts

@ -1,7 +1,7 @@
import { TabConfiguration } from '@ghostfolio/common/interfaces';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
@ -14,7 +14,6 @@ import {
settingsOutline
} from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page has-tabs' },
@ -23,12 +22,10 @@ import { Subject } from 'rxjs';
styleUrls: ['./admin-page.scss'],
templateUrl: './admin-page.html'
})
export class AdminPageComponent implements OnDestroy, OnInit {
export class AdminPageComponent implements OnInit {
public deviceType: string;
public tabs: TabConfiguration[] = [];
private unsubscribeSubject = new Subject<void>();
public constructor(private deviceService: DeviceDetectorService) {
addIcons({
flashOutline,
@ -74,9 +71,4 @@ export class AdminPageComponent implements OnDestroy, OnInit {
}
];
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

15
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -188,8 +188,7 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
!this.data.activity?.accountId &&
this.mode === 'create'
? this.data.accounts[0].id
: this.data.activity?.accountId,
Validators.required
: this.data.activity?.accountId
],
assetClass: [this.data.activity?.SymbolProfile?.assetClass],
assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass],
@ -365,11 +364,6 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
(this.activityForm.get('dataSource').value === 'MANUAL' &&
type === 'BUY')
) {
this.activityForm
.get('accountId')
.removeValidators(Validators.required);
this.activityForm.get('accountId').updateValueAndValidity();
const currency =
this.data.accounts.find(({ id }) => {
return id === this.activityForm.get('accountId').value;
@ -397,11 +391,6 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
this.activityForm.get('updateAccountBalance').disable();
this.activityForm.get('updateAccountBalance').setValue(false);
} else if (['FEE', 'INTEREST', 'LIABILITY'].includes(type)) {
this.activityForm
.get('accountId')
.removeValidators(Validators.required);
this.activityForm.get('accountId').updateValueAndValidity();
const currency =
this.data.accounts.find(({ id }) => {
return id === this.activityForm.get('accountId').value;
@ -447,8 +436,6 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
this.activityForm.get('updateAccountBalance').setValue(false);
}
} else {
this.activityForm.get('accountId').setValidators(Validators.required);
this.activityForm.get('accountId').updateValueAndValidity();
this.activityForm
.get('dataSource')
.setValidators(Validators.required);

8
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -84,12 +84,8 @@
>
<mat-label i18n>Account</mat-label>
<mat-select formControlName="accountId">
@if (
!activityForm.get('accountId').hasValidator(Validators.required) ||
(!activityForm.get('accountId').value && mode === 'update')
) {
<mat-option [value]="null" />
}
<mat-option [value]="null" />
@for (account of data.accounts; track account) {
<mat-option [value]="account.id">
<div class="d-flex">

8
apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts

@ -9,6 +9,7 @@ import {
Component,
CUSTOM_ELEMENTS_SCHEMA,
Inject,
OnDestroy,
ViewChild
} from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@ -52,7 +53,7 @@ import { UserAccountRegistrationDialogParams } from './interfaces/interfaces';
styleUrls: ['./user-account-registration-dialog.scss'],
templateUrl: 'user-account-registration-dialog.html'
})
export class GfUserAccountRegistrationDialogComponent {
export class GfUserAccountRegistrationDialogComponent implements OnDestroy {
@ViewChild(MatStepper) stepper!: MatStepper;
public accessToken: string;
@ -95,4 +96,9 @@ export class GfUserAccountRegistrationDialogComponent {
public onChangeDislaimerChecked() {
this.isDisclaimerChecked = !this.isDisclaimerChecked;
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

20
apps/client/src/locales/messages.ca.xlf

@ -895,7 +895,7 @@
<target state="translated">Filtra per...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="6182733719813772142" datatype="html">
@ -935,7 +935,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="6130372166370766747" datatype="html">
@ -1003,7 +1003,7 @@
<target state="translated">Oooh! No s’han pogut recopilar les dades históriques.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="4405333887341433096" datatype="html">
@ -1035,7 +1035,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="5299488188278756127" datatype="html">
@ -5924,6 +5924,14 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="new">No Activities</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">Provisió de jubilació</target>
@ -6757,7 +6765,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -7335,7 +7343,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="7156797854368699223" datatype="html">

20
apps/client/src/locales/messages.de.xlf

@ -506,7 +506,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="8122024350760043460" datatype="html">
@ -2630,7 +2630,7 @@
<target state="translated">Filtern nach...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="1965206604774400" datatype="html">
@ -3234,7 +3234,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="4798457301875181136" datatype="html">
@ -3357,6 +3357,14 @@
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="translated">Keine Aktivitäten</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">Altersvorsorge</target>
@ -5716,7 +5724,7 @@
<target state="translated">Ups! Die historischen Daten konnten nicht geparsed werden.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="297546430113071258" datatype="html">
@ -6781,7 +6789,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -7359,7 +7367,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="7156797854368699223" datatype="html">

28
apps/client/src/locales/messages.es.xlf

@ -507,7 +507,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="8122024350760043460" datatype="html">
@ -2615,7 +2615,7 @@
<target state="translated">Filtrar por...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="5342721262799645301" datatype="html">
@ -3219,7 +3219,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="4798457301875181136" datatype="html">
@ -3342,6 +3342,14 @@
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="new">No Activities</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">Provisión de jubilación</target>
@ -5693,7 +5701,7 @@
<target state="translated">¡Ups! No se pudieron analizar los datos históricos.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="297546430113071258" datatype="html">
@ -6758,7 +6766,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -7336,7 +7344,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="7156797854368699223" datatype="html">
@ -7883,7 +7891,7 @@
</trans-unit>
<trans-unit id="rule.feeRatioInitialInvestment" datatype="html">
<source>Fee Ratio (legacy)</source>
<target state="new">Relación de tarifas</target>
<target state="translated">Relación de tarifas (heredado)</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">152</context>
@ -7907,7 +7915,7 @@
</trans-unit>
<trans-unit id="rule.feeRatioTotalInvestmentVolume" datatype="html">
<source>Fee Ratio</source>
<target state="new">Fee Ratio</target>
<target state="translated">Relación de tarifas</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">161</context>
@ -7915,7 +7923,7 @@
</trans-unit>
<trans-unit id="rule.feeRatioTotalInvestmentVolume.false" datatype="html">
<source>The fees do exceed ${thresholdMax}% of your total investment volume (${feeRatio}%)</source>
<target state="new">The fees do exceed ${thresholdMax}% of your total investment volume (${feeRatio}%)</target>
<target state="translated">Las tarifas superan el ${thresholdMax}% de su volumen total de inversión (${feeRatio}%)</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">163</context>
@ -7923,7 +7931,7 @@
</trans-unit>
<trans-unit id="rule.feeRatioTotalInvestmentVolume.true" datatype="html">
<source>The fees do not exceed ${thresholdMax}% of your total investment volume (${feeRatio}%)</source>
<target state="new">The fees do not exceed ${thresholdMax}% of your total investment volume (${feeRatio}%)</target>
<target state="translated">Las tarifas no superan el ${thresholdMax}% de su volumen total de inversión (${feeRatio}%)</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">167</context>

20
apps/client/src/locales/messages.fr.xlf

@ -530,7 +530,7 @@
<target state="translated">Filtrer par...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="6182733719813772142" datatype="html">
@ -570,7 +570,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="6130372166370766747" datatype="html">
@ -2222,7 +2222,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="2666668717343771434" datatype="html">
@ -3341,6 +3341,14 @@
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="new">No Activities</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">Réserve pour retraite</target>
@ -5692,7 +5700,7 @@
<target state="translated">Oops! Echec du parsing des données historiques.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="297546430113071258" datatype="html">
@ -6757,7 +6765,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -7335,7 +7343,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="7156797854368699223" datatype="html">

20
apps/client/src/locales/messages.it.xlf

@ -507,7 +507,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="8122024350760043460" datatype="html">
@ -2615,7 +2615,7 @@
<target state="translated">Filtra per...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="5342721262799645301" datatype="html">
@ -3219,7 +3219,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="4798457301875181136" datatype="html">
@ -3342,6 +3342,14 @@
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="new">No Activities</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">Fondo pensione</target>
@ -5693,7 +5701,7 @@
<target state="translated">Ops! Impossibile elaborare i dati storici.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="297546430113071258" datatype="html">
@ -6758,7 +6766,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -7336,7 +7344,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="7156797854368699223" datatype="html">

20
apps/client/src/locales/messages.ko.xlf

@ -792,7 +792,7 @@
<target state="translated">다음 기준으로 필터...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="6182733719813772142" datatype="html">
@ -832,7 +832,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="6130372166370766747" datatype="html">
@ -884,7 +884,7 @@
<target state="translated">이런! 과거 데이터를 파싱할 수 없습니다.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="1102717806459547726" datatype="html">
@ -916,7 +916,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="5299488188278756127" datatype="html">
@ -5376,6 +5376,14 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="new">No Activities</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">퇴직금</target>
@ -6742,7 +6750,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -7360,7 +7368,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">

20
apps/client/src/locales/messages.nl.xlf

@ -506,7 +506,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="8122024350760043460" datatype="html">
@ -2614,7 +2614,7 @@
<target state="translated">Filter op...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="5342721262799645301" datatype="html">
@ -3218,7 +3218,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="4798457301875181136" datatype="html">
@ -3341,6 +3341,14 @@
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="new">No Activities</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">Pensioen</target>
@ -5692,7 +5700,7 @@
<target state="translated">Oeps! Ophalen van historische data is mislukt.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="297546430113071258" datatype="html">
@ -6757,7 +6765,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -7335,7 +7343,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="7156797854368699223" datatype="html">

20
apps/client/src/locales/messages.pl.xlf

@ -783,7 +783,7 @@
<target state="translated">Filtruj według...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="6182733719813772142" datatype="html">
@ -823,7 +823,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="6130372166370766747" datatype="html">
@ -859,7 +859,7 @@
<target state="translated">Ups! Nie udało się sparsować danych historycznych.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="1102717806459547726" datatype="html">
@ -883,7 +883,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="5299488188278756127" datatype="html">
@ -5307,6 +5307,14 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="new">No Activities</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">Świadczenia Emerytalne</target>
@ -6757,7 +6765,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -7335,7 +7343,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="7156797854368699223" datatype="html">

20
apps/client/src/locales/messages.pt.xlf

@ -530,7 +530,7 @@
<target state="translated">Filtrar por...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="6182733719813772142" datatype="html">
@ -570,7 +570,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="3720539089813177542" datatype="html">
@ -3174,7 +3174,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="7763941937414903315" datatype="html">
@ -3341,6 +3341,14 @@
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="new">No Activities</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">Provisão de Reforma</target>
@ -5692,7 +5700,7 @@
<target state="translated">Ops! Não foi possível analisar os dados históricos.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="297546430113071258" datatype="html">
@ -6757,7 +6765,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -7335,7 +7343,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="7156797854368699223" datatype="html">

20
apps/client/src/locales/messages.tr.xlf

@ -739,7 +739,7 @@
<target state="translated">Filtrele...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="6182733719813772142" datatype="html">
@ -779,7 +779,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="6130372166370766747" datatype="html">
@ -3371,7 +3371,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="2666668717343771434" datatype="html">
@ -5003,6 +5003,14 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="new">No Activities</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">Yaşlılık Provizyonu</target>
@ -5692,7 +5700,7 @@
<target state="translated">Hay Allah! Geçmiş veriler ayrıştırılamadı.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="297546430113071258" datatype="html">
@ -6757,7 +6765,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -7335,7 +7343,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="7156797854368699223" datatype="html">

20
apps/client/src/locales/messages.uk.xlf

@ -875,7 +875,7 @@
<target state="translated">Фільтрувати за...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="8411428959611082933" datatype="html">
@ -931,7 +931,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="6130372166370766747" datatype="html">
@ -2323,7 +2323,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="5403336912114537863" datatype="html">
@ -4576,7 +4576,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="2666668717343771434" datatype="html">
@ -6419,7 +6419,7 @@
<target state="translated">Упс! Не вдалося отримати історичні дані.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="8927080808898221200" datatype="html">
@ -6595,7 +6595,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -6774,6 +6774,14 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="new">No Activities</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">Пенсійне накопичення</target>

19
apps/client/src/locales/messages.xlf

@ -740,7 +740,7 @@
<source>Filter by...</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="6182733719813772142" datatype="html">
@ -777,7 +777,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="6130372166370766747" datatype="html">
@ -823,7 +823,7 @@
<source>Oops! Could not parse historical data.</source>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="1102717806459547726" datatype="html">
@ -852,7 +852,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="5299488188278756127" datatype="html">
@ -4907,6 +4907,13 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<context-group purpose="location">
@ -6126,7 +6133,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -6685,7 +6692,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="1769610706135259386" datatype="html">

20
apps/client/src/locales/messages.zh.xlf

@ -792,7 +792,7 @@
<target state="translated">过滤...</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="6182733719813772142" datatype="html">
@ -832,7 +832,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">44</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="6130372166370766747" datatype="html">
@ -868,7 +868,7 @@
<target state="translated">哎呀!无法解析历史数据。</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">284</context>
</context-group>
</trans-unit>
<trans-unit id="1102717806459547726" datatype="html">
@ -892,7 +892,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="5299488188278756127" datatype="html">
@ -5360,6 +5360,14 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="9218541487912911620" datatype="html">
<source>No Activities</source>
<target state="new">No Activities</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/admin-market-data.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9219851060664514927" datatype="html">
<source>Retirement Provision</source>
<target state="translated">退休金</target>
@ -6758,7 +6766,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
@ -7336,7 +7344,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="7156797854368699223" datatype="html">

5
libs/common/src/lib/config.ts

@ -80,6 +80,11 @@ export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000;
export const DEFAULT_REDACTED_PATHS = [
'accounts[*].balance',
'accounts[*].balanceInBaseCurrency',
'accounts[*].comment',
'accounts[*].dividendInBaseCurrency',
'accounts[*].interestInBaseCurrency',
'accounts[*].value',
'accounts[*].valueInBaseCurrency',
'activities[*].account.balance',
'activities[*].account.comment',

3
libs/common/src/lib/types/market-data-preset.type.ts

@ -2,4 +2,5 @@ export type MarketDataPreset =
| 'BENCHMARKS'
| 'CURRENCIES'
| 'ETF_WITHOUT_COUNTRIES'
| 'ETF_WITHOUT_SECTORS';
| 'ETF_WITHOUT_SECTORS'
| 'NO_ACTIVITIES';

10
libs/ui/src/lib/account-balances/account-balances.component.html

@ -12,7 +12,7 @@
<ng-container i18n>Date</ng-container>
</th>
<td *matCellDef="let element" class="px-2" mat-cell>
<gf-value [isDate]="true" [locale]="locale" [value]="element?.date" />
<gf-value [isDate]="true" [locale]="locale()" [value]="element?.date" />
</td>
<td *matFooterCellDef class="px-2" mat-footer-cell>
<mat-form-field appearance="outline" class="py-1 without-hint">
@ -37,7 +37,7 @@
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[locale]="locale()"
[unit]="element?.account?.currency"
[value]="element?.value"
/>
@ -48,7 +48,7 @@
<mat-form-field appearance="outline" class="without-hint">
<input formControlName="balance" matInput type="number" />
<div class="ml-2" matTextSuffix>
{{ accountCurrency }}
{{ accountCurrency() }}
</div>
</mat-form-field>
</div>
@ -58,7 +58,7 @@
<ng-container matColumnDef="actions" stickyEnd>
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
@if (showActions) {
@if (showActions()) {
<button
class="mx-1 no-min-width px-2"
mat-button
@ -100,7 +100,7 @@
<tr
*matFooterRowDef="displayedColumns"
mat-footer-row
[hidden]="!showActions"
[hidden]="!showActions()"
></tr>
</table>
</form>

68
libs/ui/src/lib/account-balances/account-balances.component.ts

@ -10,12 +10,12 @@ import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
ViewChild
inject,
input,
viewChild
} from '@angular/core';
import {
FormGroup,
@ -39,8 +39,7 @@ import {
ellipsisHorizontal,
trashOutline
} from 'ionicons/icons';
import { get } from 'lodash';
import { Subject } from 'rxjs';
import { get, isNil } from 'lodash';
import { GfValueComponent } from '../value';
@ -63,50 +62,44 @@ import { GfValueComponent } from '../value';
styleUrls: ['./account-balances.component.scss'],
templateUrl: './account-balances.component.html'
})
export class GfAccountBalancesComponent
implements OnChanges, OnDestroy, OnInit
{
@Input() accountBalances: AccountBalancesResponse['balances'];
@Input() accountCurrency: string;
@Input() accountId: string;
@Input() locale = getLocale();
@Input() showActions = true;
export class GfAccountBalancesComponent implements OnChanges, OnInit {
@Output() accountBalanceCreated = new EventEmitter<CreateAccountBalanceDto>();
@Output() accountBalanceDeleted = new EventEmitter<string>();
@ViewChild(MatSort) sort: MatSort;
public readonly accountBalances =
input.required<AccountBalancesResponse['balances']>();
public readonly accountCurrency = input.required<string>();
public readonly accountId = input.required<string>();
public readonly displayedColumns: string[] = ['date', 'value', 'actions'];
public readonly locale = input(getLocale());
public readonly showActions = input(true);
public readonly sort = viewChild(MatSort);
public accountBalanceForm = new FormGroup({
balance: new FormControl(0, Validators.required),
date: new FormControl(new Date(), Validators.required)
balance: new FormControl(0, (control) => Validators.required(control)),
date: new FormControl(new Date(), (control) => Validators.required(control))
});
public dataSource = new MatTableDataSource<
AccountBalancesResponse['balances'][0]
>();
public displayedColumns: string[] = ['date', 'value', 'actions'];
public Validators = Validators;
private unsubscribeSubject = new Subject<void>();
private dateAdapter = inject<DateAdapter<Date, string>>(DateAdapter);
private notificationService = inject(NotificationService);
public constructor(
private dateAdapter: DateAdapter<any>,
private notificationService: NotificationService
) {
public constructor() {
addIcons({ calendarClearOutline, ellipsisHorizontal, trashOutline });
}
public ngOnInit() {
this.dateAdapter.setLocale(this.locale);
this.dateAdapter.setLocale(this.locale());
}
public ngOnChanges() {
if (this.accountBalances) {
this.dataSource = new MatTableDataSource(this.accountBalances);
if (this.accountBalances()) {
this.dataSource = new MatTableDataSource(this.accountBalances());
this.dataSource.sort = this.sort;
this.dataSource.sort = this.sort();
this.dataSource.sortingDataAccessor = get;
}
}
@ -122,10 +115,16 @@ export class GfAccountBalancesComponent
}
public async onSubmitAccountBalance() {
const { balance, date } = this.accountBalanceForm.value;
if (isNil(balance) || !date) {
return;
}
const accountBalance: CreateAccountBalanceDto = {
accountId: this.accountId,
balance: this.accountBalanceForm.get('balance').value,
date: format(this.accountBalanceForm.get('date').value, DATE_FORMAT)
balance,
accountId: this.accountId(),
date: format(date, DATE_FORMAT)
};
try {
@ -141,9 +140,4 @@ export class GfAccountBalancesComponent
this.accountBalanceCreated.emit(accountBalance);
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

2
libs/ui/src/lib/accounts-table/accounts-table.component.ts

@ -53,7 +53,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
templateUrl: './accounts-table.component.html'
})
export class GfAccountsTableComponent {
public readonly accounts = input.required<Account[] | undefined>();
public readonly accounts = input.required<Account[]>();
public readonly activitiesCount = input<number>();
public readonly baseCurrency = input<string>();
public readonly hasPermissionToOpenDetails = input(true);

8
libs/ui/src/lib/activities-filter/activities-filter.component.html

@ -10,7 +10,7 @@
[removable]="true"
(removed)="onRemoveFilter(filter)"
>
{{ filter.label | gfSymbol }}
{{ filter.label ?? '' | gfSymbol }}
<button matChipRemove>
<ion-icon name="close-outline" />
</button>
@ -23,7 +23,7 @@
[matAutocomplete]="autocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[placeholder]="placeholder"
[placeholder]="placeholder()"
(matChipInputTokenEnd)="onAddFilter($event)"
/>
</mat-chip-grid>
@ -35,7 +35,7 @@
<mat-optgroup [label]="filterGroup.name">
@for (filter of filterGroup.filters; track filter) {
<mat-option [value]="filter.id">
{{ filter.label | gfSymbol }}
{{ filter.label ?? '' | gfSymbol }}
</mat-option>
}
</mat-optgroup>
@ -46,7 +46,7 @@
disabled
mat-icon-button
matSuffix
[ngClass]="{ 'd-none': !isLoading }"
[ngClass]="{ 'd-none': !isLoading() }"
>
<mat-spinner matSuffix [diameter]="20" />
</button>

83
libs/ui/src/lib/activities-filter/activities-filter.component.ts

@ -8,14 +8,14 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
Output,
SimpleChanges,
ViewChild
ViewChild,
input,
output
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import {
MatAutocomplete,
@ -30,8 +30,7 @@ import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { closeOutline, searchOutline } from 'ionicons/icons';
import { groupBy } from 'lodash';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { BehaviorSubject } from 'rxjs';
import { translate } from '../i18n';
@ -53,28 +52,26 @@ import { translate } from '../i18n';
styleUrls: ['./activities-filter.component.scss'],
templateUrl: './activities-filter.component.html'
})
export class GfActivitiesFilterComponent implements OnChanges, OnDestroy {
export class GfActivitiesFilterComponent implements OnChanges {
@Input() allFilters: Filter[];
@Input() isLoading: boolean;
@Input() placeholder: string;
@Output() valueChanged = new EventEmitter<Filter[]>();
@ViewChild('autocomplete') protected matAutocomplete: MatAutocomplete;
@ViewChild('searchInput') protected searchInput: ElementRef<HTMLInputElement>;
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
public readonly isLoading = input.required<boolean>();
public readonly placeholder = input.required<string>();
public readonly valueChanged = output<Filter[]>();
public filterGroups$: Subject<FilterGroup[]> = new BehaviorSubject([]);
public filters$: Subject<Filter[]> = new BehaviorSubject([]);
public filters: Observable<Filter[]> = this.filters$.asObservable();
public searchControl = new FormControl<Filter | string>(undefined);
public selectedFilters: Filter[] = [];
public separatorKeysCodes: number[] = [ENTER, COMMA];
private unsubscribeSubject = new Subject<void>();
protected readonly filterGroups$ = new BehaviorSubject<FilterGroup[]>([]);
protected readonly searchControl = new FormControl<Filter | string | null>(
null
);
protected selectedFilters: Filter[] = [];
protected readonly separatorKeysCodes: number[] = [ENTER, COMMA];
public constructor() {
this.searchControl.valueChanges
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed())
.subscribe((filterOrSearchTerm) => {
if (filterOrSearchTerm) {
const searchTerm =
@ -97,41 +94,39 @@ export class GfActivitiesFilterComponent implements OnChanges, OnDestroy {
}
}
public onAddFilter({ input, value }: MatChipInputEvent) {
public onAddFilter({ chipInput, value }: MatChipInputEvent) {
if (value?.trim()) {
this.updateFilters();
}
// Reset the input value
if (input) {
input.value = '';
if (chipInput.inputElement) {
chipInput.inputElement.value = '';
}
this.searchControl.setValue(undefined);
this.searchControl.setValue(null);
}
public onRemoveFilter(aFilter: Filter) {
this.selectedFilters = this.selectedFilters.filter((filter) => {
return filter.id !== aFilter.id;
this.selectedFilters = this.selectedFilters.filter(({ id }) => {
return id !== aFilter.id;
});
this.updateFilters();
}
public onSelectFilter(event: MatAutocompleteSelectedEvent) {
this.selectedFilters.push(
this.allFilters.find((filter) => {
return filter.id === event.option.value;
})
);
const filter = this.allFilters.find(({ id }) => {
return id === event.option.value;
});
if (filter) {
this.selectedFilters.push(filter);
}
this.updateFilters();
this.searchInput.nativeElement.value = '';
this.searchControl.setValue(undefined);
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
this.searchControl.setValue(null);
}
private getGroupedFilters(searchTerm?: string) {
@ -139,23 +134,23 @@ export class GfActivitiesFilterComponent implements OnChanges, OnDestroy {
this.allFilters
.filter((filter) => {
// Filter selected filters
return !this.selectedFilters.some((selectedFilter) => {
return selectedFilter.id === filter.id;
return !this.selectedFilters.some(({ id }) => {
return id === filter.id;
});
})
.filter((filter) => {
if (searchTerm) {
// Filter by search term
return filter.label
.toLowerCase()
?.toLowerCase()
.includes(searchTerm.toLowerCase());
}
return filter;
})
.sort((a, b) => a.label?.localeCompare(b.label)),
(filter) => {
return filter.type;
.sort((a, b) => (a.label ?? '').localeCompare(b.label ?? '')),
({ type }) => {
return type;
}
);

47
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts

@ -5,10 +5,12 @@ import {
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
Inject,
OnDestroy,
OnInit
DestroyRef,
OnInit,
inject,
signal
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
@ -23,7 +25,6 @@ import { MatInputModule } from '@angular/material/input';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { calendarClearOutline, refreshOutline } from 'ionicons/icons';
import { Subject, takeUntil } from 'rxjs';
import { HistoricalMarketDataEditorDialogParams } from './interfaces/interfaces';
@ -45,26 +46,27 @@ import { HistoricalMarketDataEditorDialogParams } from './interfaces/interfaces'
styleUrls: ['./historical-market-data-editor-dialog.scss'],
templateUrl: 'historical-market-data-editor-dialog.html'
})
export class GfHistoricalMarketDataEditorDialogComponent
implements OnDestroy, OnInit
{
private unsubscribeSubject = new Subject<void>();
export class GfHistoricalMarketDataEditorDialogComponent implements OnInit {
public readonly data =
inject<HistoricalMarketDataEditorDialogParams>(MAT_DIALOG_DATA);
protected readonly marketPrice = signal(this.data.marketPrice);
private readonly destroyRef = inject(DestroyRef);
private readonly locale =
this.data.user.settings.locale ?? inject<string>(MAT_DATE_LOCALE);
public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA)
public data: HistoricalMarketDataEditorDialogParams,
private dataService: DataService,
private dateAdapter: DateAdapter<any>,
public dialogRef: MatDialogRef<GfHistoricalMarketDataEditorDialogComponent>,
@Inject(MAT_DATE_LOCALE) private locale: string
private dateAdapter: DateAdapter<Date, string>,
public dialogRef: MatDialogRef<GfHistoricalMarketDataEditorDialogComponent>
) {
addIcons({ calendarClearOutline, refreshOutline });
}
public ngOnInit() {
this.locale = this.data.user?.settings?.locale;
this.dateAdapter.setLocale(this.locale);
}
@ -79,15 +81,19 @@ export class GfHistoricalMarketDataEditorDialogComponent
dateString: this.data.dateString,
symbol: this.data.symbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ marketPrice }) => {
this.data.marketPrice = marketPrice;
this.marketPrice.set(marketPrice);
this.changeDetectorRef.markForCheck();
});
}
public onUpdate() {
if (this.marketPrice() === undefined) {
return;
}
this.dataService
.postMarketData({
dataSource: this.data.dataSource,
@ -95,20 +101,15 @@ export class GfHistoricalMarketDataEditorDialogComponent
marketData: [
{
date: this.data.dateString,
marketPrice: this.data.marketPrice
marketPrice: this.marketPrice()
}
]
},
symbol: this.data.symbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.dialogRef.close({ withRefresh: true });
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

3
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html

@ -28,7 +28,8 @@
matInput
name="marketPrice"
type="number"
[(ngModel)]="data.marketPrice"
[ngModel]="marketPrice()"
(ngModelChange)="marketPrice.set($event)"
/>
<span class="ml-2" matTextSuffix>{{ data.currency }}</span>
</mat-form-field>

2
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts

@ -6,7 +6,7 @@ export interface HistoricalMarketDataEditorDialogParams {
currency: string;
dataSource: DataSource;
dateString: string;
marketPrice: number;
marketPrice?: number;
symbol: string;
user: User;
}

24
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html

@ -3,28 +3,24 @@
<div class="d-flex">
<div class="date mr-1 text-nowrap">{{ itemByMonth.key }}</div>
<div class="align-items-center d-flex flex-grow-1 px-1">
@for (dayItem of days; track dayItem; let i = $index) {
@for (day of days; track day) {
<div
class="day"
[ngClass]="{
'cursor-pointer valid': isDateOfInterest(
`${itemByMonth.key}-${i + 1 < 10 ? `0${i + 1}` : i + 1}`
`${itemByMonth.key}-${formatDay(day)}`
),
available:
marketDataByMonth[itemByMonth.key][
i + 1 < 10 ? `0${i + 1}` : i + 1
]?.marketPrice,
today: isToday(
`${itemByMonth.key}-${i + 1 < 10 ? `0${i + 1}` : i + 1}`
)
marketDataByMonth[itemByMonth.key][formatDay(day)]?.marketPrice,
today: isToday(`${itemByMonth.key}-${formatDay(day)}`)
}"
[title]="
(`${itemByMonth.key}-${i + 1 < 10 ? `0${i + 1}` : i + 1}`
| date: defaultDateFormat) ?? ''
(`${itemByMonth.key}-${formatDay(day)}`
| date: defaultDateFormat()) ?? ''
"
(click)="
onOpenMarketDataDetail({
day: i + 1 < 10 ? `0${i + 1}` : i + 1,
day: formatDay(day),
yearMonth: itemByMonth.key
})
"
@ -61,10 +57,10 @@
mat-flat-button
type="button"
[disabled]="
!historicalDataForm.controls['historicalData']?.controls['csvString']
!historicalDataForm.controls.historicalData.controls.csvString
.touched ||
historicalDataForm.controls['historicalData']?.controls['csvString']
?.value === ''
historicalDataForm.controls.historicalData.controls.csvString
.value === ''
"
(click)="onImportHistoricalData()"
>

60
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.spec.ts

@ -0,0 +1,60 @@
import { DataService } from '@ghostfolio/ui/services';
import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { DeviceDetectorService } from 'ngx-device-detector';
import { GfHistoricalMarketDataEditorComponent } from './historical-market-data-editor.component';
jest.mock(
'./historical-market-data-editor-dialog/historical-market-data-editor-dialog.component',
() => ({
GfHistoricalMarketDataEditorDialogComponent: class {}
})
);
describe('GfHistoricalMarketDataEditorComponent', () => {
let component: GfHistoricalMarketDataEditorComponent;
let fixture: ComponentFixture<GfHistoricalMarketDataEditorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GfHistoricalMarketDataEditorComponent],
providers: [
FormBuilder,
{ provide: DataService, useValue: {} },
{
provide: DeviceDetectorService,
useValue: {
deviceInfo: signal({ deviceType: 'desktop' })
}
},
{ provide: MatDialog, useValue: {} },
{ provide: MatSnackBar, useValue: {} }
]
}).compileComponents();
fixture = TestBed.createComponent(GfHistoricalMarketDataEditorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('formatDay', () => {
it('should pad single digit days with zero', () => {
expect(component.formatDay(1)).toBe('01');
expect(component.formatDay(9)).toBe('09');
});
it('should not pad double digit days', () => {
expect(component.formatDay(10)).toBe('10');
expect(component.formatDay(31)).toBe('31');
});
});
});

133
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts

@ -8,16 +8,21 @@ import { LineChartItem, User } from '@ghostfolio/common/interfaces';
import { DataService } from '@ghostfolio/ui/services';
import { CommonModule } from '@angular/common';
import type { HttpErrorResponse } from '@angular/common/http';
import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
EventEmitter,
inject,
input,
Input,
OnChanges,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
@ -40,7 +45,7 @@ import { first, last } from 'lodash';
import ms from 'ms';
import { DeviceDetectorService } from 'ngx-device-detector';
import { parse as csvToJson } from 'papaparse';
import { EMPTY, Subject, takeUntil } from 'rxjs';
import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { GfHistoricalMarketDataEditorDialogComponent } from './historical-market-data-editor-dialog/historical-market-data-editor-dialog.component';
@ -54,74 +59,80 @@ import { HistoricalMarketDataEditorDialogParams } from './historical-market-data
templateUrl: './historical-market-data-editor.component.html'
})
export class GfHistoricalMarketDataEditorComponent
implements OnChanges, OnDestroy, OnInit
implements OnChanges, OnInit
{
private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format(
new Date(),
DATE_FORMAT
)};123.45`;
@Input() currency: string;
@Input() dataSource: DataSource;
@Input() dateOfFirstActivity: string;
@Input() locale = getLocale();
@Input() marketData: MarketData[];
@Input() symbol: string;
@Input() user: User;
@Output() marketDataChanged = new EventEmitter<boolean>();
public days = Array(31);
public defaultDateFormat: string;
public deviceType: string;
public historicalDataForm = this.formBuilder.group({
historicalData: this.formBuilder.group({
csvString: ''
})
});
public historicalDataItems: LineChartItem[];
public marketDataByMonth: {
[yearMonth: string]: {
[day: string]: Pick<MarketData, 'date' | 'marketPrice'> & { day: number };
[day: string]: {
date: Date;
day: number;
marketPrice?: number;
};
};
} = {};
private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format(
new Date(),
DATE_FORMAT
)};123.45`;
private unsubscribeSubject = new Subject<void>();
public readonly locale = input(getLocale());
public readonly marketData = input.required<MarketData[]>();
protected readonly days = Array.from({ length: 31 }, (_, i) => i + 1);
protected readonly defaultDateFormat = computed(() =>
getDateFormatString(this.locale())
);
private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
);
private readonly historicalDataItems = computed<LineChartItem[]>(() =>
this.marketData().map(({ date, marketPrice }) => {
return {
date: format(date, DATE_FORMAT),
value: marketPrice
};
})
);
public constructor(
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private formBuilder: FormBuilder,
private snackBar: MatSnackBar
) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
}
) {}
public ngOnInit() {
this.initializeHistoricalDataForm();
}
public ngOnChanges() {
this.defaultDateFormat = getDateFormatString(this.locale);
this.historicalDataItems = this.marketData.map(({ date, marketPrice }) => {
return {
date: format(date, DATE_FORMAT),
value: marketPrice
};
});
if (this.dateOfFirstActivity) {
let date = parseISO(this.dateOfFirstActivity);
const missingMarketData: Partial<MarketData>[] = [];
const missingMarketData: { date: Date; marketPrice?: number }[] = [];
if (this.historicalDataItems?.[0]?.date) {
if (this.historicalDataItems()?.[0]?.date) {
while (
isBefore(
date,
parse(this.historicalDataItems[0].date, DATE_FORMAT, new Date())
parse(this.historicalDataItems()[0].date, DATE_FORMAT, new Date())
)
) {
missingMarketData.push({
@ -133,9 +144,10 @@ export class GfHistoricalMarketDataEditorComponent
}
}
const marketDataItems = [...missingMarketData, ...this.marketData];
const marketDataItems = [...missingMarketData, ...this.marketData()];
if (!isToday(last(marketDataItems)?.date)) {
const lastDate = last(marketDataItems)?.date;
if (!lastDate || !isToday(lastDate)) {
marketDataItems.push({ date: new Date() });
}
@ -160,25 +172,34 @@ export class GfHistoricalMarketDataEditorComponent
// Fill up missing months
const dates = Object.keys(this.marketDataByMonth).sort();
const startDateString = first(dates);
const startDate = min([
parseISO(this.dateOfFirstActivity),
parseISO(first(dates))
...(startDateString ? [parseISO(startDateString)] : [])
]);
const endDate = parseISO(last(dates));
const endDateString = last(dates);
let currentDate = startDate;
if (endDateString) {
const endDate = parseISO(endDateString);
while (isBefore(currentDate, endDate)) {
const key = format(currentDate, 'yyyy-MM');
if (!this.marketDataByMonth[key]) {
this.marketDataByMonth[key] = {};
}
let currentDate = startDate;
currentDate = addMonths(currentDate, 1);
while (isBefore(currentDate, endDate)) {
const key = format(currentDate, 'yyyy-MM');
if (!this.marketDataByMonth[key]) {
this.marketDataByMonth[key] = {};
}
currentDate = addMonths(currentDate, 1);
}
}
}
}
public formatDay(day: number): string {
return day < 10 ? `0${day}` : `${day}`;
}
public isDateOfInterest(aDateString: string) {
// Date is valid and in the past
const date = parse(aDateString, DATE_FORMAT, new Date());
@ -201,7 +222,8 @@ export class GfHistoricalMarketDataEditorComponent
const dialogRef = this.dialog.open<
GfHistoricalMarketDataEditorDialogComponent,
HistoricalMarketDataEditorDialogParams
HistoricalMarketDataEditorDialogParams,
{ withRefresh: boolean }
>(GfHistoricalMarketDataEditorDialogComponent, {
data: {
marketPrice,
@ -211,13 +233,13 @@ export class GfHistoricalMarketDataEditorComponent
symbol: this.symbol,
user: this.user
},
height: this.deviceType === 'mobile' ? '98vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ withRefresh } = { withRefresh: false }) => {
this.marketDataChanged.emit(withRefresh);
});
@ -225,15 +247,15 @@ export class GfHistoricalMarketDataEditorComponent
public onImportHistoricalData() {
try {
const marketData = csvToJson(
this.historicalDataForm.controls['historicalData'].controls['csvString']
.value,
const marketData = csvToJson<UpdateMarketDataDto>(
this.historicalDataForm.controls.historicalData.controls.csvString
.value ?? '',
{
dynamicTyping: true,
header: true,
skipEmptyLines: true
}
).data as UpdateMarketDataDto[];
).data;
this.dataService
.postMarketData({
@ -244,13 +266,13 @@ export class GfHistoricalMarketDataEditorComponent
symbol: this.symbol
})
.pipe(
catchError(({ error, message }) => {
catchError(({ error, message }: HttpErrorResponse) => {
this.snackBar.open(`${error}: ${message[0]}`, undefined, {
duration: ms('3 seconds')
});
return EMPTY;
}),
takeUntil(this.unsubscribeSubject)
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
this.initializeHistoricalDataForm();
@ -268,11 +290,6 @@ export class GfHistoricalMarketDataEditorComponent
}
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private initializeHistoricalDataForm() {
this.historicalDataForm.setValue({
historicalData: {

12
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "2.239.0",
"version": "2.242.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "2.239.0",
"version": "2.242.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -70,7 +70,7 @@
"ionicons": "8.0.13",
"jsonpath": "1.1.1",
"lodash": "4.17.23",
"marked": "17.0.1",
"marked": "17.0.2",
"ms": "3.0.0-canary.1",
"ng-extract-i18n-merge": "3.2.1",
"ngx-device-detector": "11.0.0",
@ -25625,9 +25625,9 @@
}
},
"node_modules/marked": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz",
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.2.tgz",
"integrity": "sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"

7
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.239.0",
"version": "2.242.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -43,10 +43,11 @@
"start:production": "npm run database:migrate && npm run database:seed && node main",
"start:server": "nx run api:copy-assets && nx run api:serve --watch",
"start:storybook": "nx run ui:storybook",
"test": "npm run test:api && npm run test:common",
"test": "npx dotenv-cli -e .env.example -- npx nx run-many --target=test --all --parallel=4",
"test:api": "npx dotenv-cli -e .env.example -- nx test api",
"test:common": "npx dotenv-cli -e .env.example -- nx test common",
"test:single": "nx run api:test --test-file object.helper.spec.ts",
"test:ui": "npx dotenv-cli -e .env.example -- nx test ui",
"ts-node": "ts-node",
"update": "nx migrate latest",
"watch:server": "nx run api:copy-assets && nx run api:build --watch",
@ -114,7 +115,7 @@
"ionicons": "8.0.13",
"jsonpath": "1.1.1",
"lodash": "4.17.23",
"marked": "17.0.1",
"marked": "17.0.2",
"ms": "3.0.0-canary.1",
"ng-extract-i18n-merge": "3.2.1",
"ngx-device-detector": "11.0.0",

Loading…
Cancel
Save