diff --git a/CHANGELOG.md b/CHANGELOG.md index f08ab4b7a..199404c8a 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 2cc8bbfb8..d77fde346 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/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 }) => { diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index a787927b5..497b8a7e9 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/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[]; - 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; - } = {}; - 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 = { 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; - } } diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 73c295f1b..c7021809e 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/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 { + 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; diff --git a/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json b/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json index e456f6f1d..d00ded6ef 100644 --- a/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json +++ b/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 @@ "币安人生": "币安人生", "恶俗企鹅": "恶俗企鹅", "我踏马来了": "我踏马来了", + "狗屎": "狗屎", "老子": "老子", "雪球": "雪球", "黑马": "黑马" diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index 5a088c0e4..8ee8761c0 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/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, + '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; + } = {}; + + 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 = { 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, diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index 8f956b782..0beca6f3c 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/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[]; @@ -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) { diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 99e0c9a53..0e26a49a8 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/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' ) { (); - public onLoad() { this.isLoading = false; } - - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } } diff --git a/apps/client/src/app/pages/about/oss-friends/oss-friends-page.component.ts b/apps/client/src/app/pages/about/oss-friends/oss-friends-page.component.ts index bdbbdf9a7..c2e500a52 100644 --- a/apps/client/src/app/pages/about/oss-friends/oss-friends-page.component.ts +++ b/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(); - public constructor() { addIcons({ arrowForwardOutline }); } - - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } } diff --git a/apps/client/src/app/pages/admin/admin-page.component.ts b/apps/client/src/app/pages/admin/admin-page.component.ts index b9243dcb9..284d3c41d 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/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(); - 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(); - } } diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index 8695f04ed..fc42a504d 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/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); diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index 42fbd0ebf..278ddcbbd 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -84,12 +84,8 @@ > Account - @if ( - !activityForm.get('accountId').hasValidator(Validators.required) || - (!activityForm.get('accountId').value && mode === 'update') - ) { - - } + + @for (account of data.accounts; track account) {
diff --git a/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts b/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts index a7707ad3b..4cc0f52f8 100644 --- a/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts +++ b/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(); + } } diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf index 6400807bd..585a3d779 100644 --- a/apps/client/src/locales/messages.ca.xlf +++ b/apps/client/src/locales/messages.ca.xlf @@ -895,7 +895,7 @@ Filtra per... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -935,7 +935,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -1003,7 +1003,7 @@ Oooh! No s’han pogut recopilar les dades históriques. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -1035,7 +1035,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -5924,6 +5924,14 @@ 27 + + No Activities + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision Provisió de jubilació @@ -6757,7 +6765,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -7335,7 +7343,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index 2be7713eb..2ca3a95bf 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -506,7 +506,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -2630,7 +2630,7 @@ Filtern nach... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -3234,7 +3234,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -3357,6 +3357,14 @@ 22 + + No Activities + Keine Aktivitäten + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision Altersvorsorge @@ -5716,7 +5724,7 @@ Ups! Die historischen Daten konnten nicht geparsed werden. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -6781,7 +6789,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -7359,7 +7367,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index fb4773203..ceb7d5b06 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -507,7 +507,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -2615,7 +2615,7 @@ Filtrar por... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -3219,7 +3219,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -3342,6 +3342,14 @@ 22 + + No Activities + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision Provisión de jubilación @@ -5693,7 +5701,7 @@ ¡Ups! No se pudieron analizar los datos históricos. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -6758,7 +6766,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -7336,7 +7344,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 @@ -7883,7 +7891,7 @@ Fee Ratio (legacy) - Relación de tarifas + Relación de tarifas (heredado) apps/client/src/app/pages/i18n/i18n-page.html 152 @@ -7907,7 +7915,7 @@ Fee Ratio - Fee Ratio + Relación de tarifas apps/client/src/app/pages/i18n/i18n-page.html 161 @@ -7915,7 +7923,7 @@ The fees do exceed ${thresholdMax}% of your total investment volume (${feeRatio}%) - The fees do exceed ${thresholdMax}% of your total investment volume (${feeRatio}%) + Las tarifas superan el ${thresholdMax}% de su volumen total de inversión (${feeRatio}%) apps/client/src/app/pages/i18n/i18n-page.html 163 @@ -7923,7 +7931,7 @@ The fees do not exceed ${thresholdMax}% of your total investment volume (${feeRatio}%) - The fees do not exceed ${thresholdMax}% of your total investment volume (${feeRatio}%) + Las tarifas no superan el ${thresholdMax}% de su volumen total de inversión (${feeRatio}%) apps/client/src/app/pages/i18n/i18n-page.html 167 diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index 8cb2b2f48..f3a8d4942 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -530,7 +530,7 @@ Filtrer par... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -570,7 +570,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -2222,7 +2222,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -3341,6 +3341,14 @@ 22 + + No Activities + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision Réserve pour retraite @@ -5692,7 +5700,7 @@ Oops! Echec du parsing des données historiques. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -6757,7 +6765,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -7335,7 +7343,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index 61b08b673..f021dae49 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -507,7 +507,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -2615,7 +2615,7 @@ Filtra per... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -3219,7 +3219,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -3342,6 +3342,14 @@ 22 + + No Activities + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision Fondo pensione @@ -5693,7 +5701,7 @@ Ops! Impossibile elaborare i dati storici. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -6758,7 +6766,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -7336,7 +7344,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 diff --git a/apps/client/src/locales/messages.ko.xlf b/apps/client/src/locales/messages.ko.xlf index 22c06b596..f188d4924 100644 --- a/apps/client/src/locales/messages.ko.xlf +++ b/apps/client/src/locales/messages.ko.xlf @@ -792,7 +792,7 @@ 다음 기준으로 필터... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -832,7 +832,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -884,7 +884,7 @@ 이런! 과거 데이터를 파싱할 수 없습니다. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -916,7 +916,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -5376,6 +5376,14 @@ 27 + + No Activities + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision 퇴직금 @@ -6742,7 +6750,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -7360,7 +7368,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index c79ecc1d3..76a74e6fd 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -506,7 +506,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -2614,7 +2614,7 @@ Filter op... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -3218,7 +3218,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -3341,6 +3341,14 @@ 22 + + No Activities + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision Pensioen @@ -5692,7 +5700,7 @@ Oeps! Ophalen van historische data is mislukt. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -6757,7 +6765,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -7335,7 +7343,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index a7c72f3ca..fdc528254 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -783,7 +783,7 @@ Filtruj według... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -823,7 +823,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -859,7 +859,7 @@ Ups! Nie udało się sparsować danych historycznych. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -883,7 +883,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -5307,6 +5307,14 @@ 27 + + No Activities + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision Świadczenia Emerytalne @@ -6757,7 +6765,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -7335,7 +7343,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index 55d622ed9..194922435 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -530,7 +530,7 @@ Filtrar por... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -570,7 +570,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -3174,7 +3174,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -3341,6 +3341,14 @@ 22 + + No Activities + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision Provisão de Reforma @@ -5692,7 +5700,7 @@ Ops! Não foi possível analisar os dados históricos. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -6757,7 +6765,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -7335,7 +7343,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index 841da4721..a7b86f624 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -739,7 +739,7 @@ Filtrele... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -779,7 +779,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -3371,7 +3371,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -5003,6 +5003,14 @@ 27 + + No Activities + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision Yaşlılık Provizyonu @@ -5692,7 +5700,7 @@ Hay Allah! Geçmiş veriler ayrıştırılamadı. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -6757,7 +6765,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -7335,7 +7343,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 diff --git a/apps/client/src/locales/messages.uk.xlf b/apps/client/src/locales/messages.uk.xlf index 27c55d792..61cebd47a 100644 --- a/apps/client/src/locales/messages.uk.xlf +++ b/apps/client/src/locales/messages.uk.xlf @@ -875,7 +875,7 @@ Фільтрувати за... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -931,7 +931,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -2323,7 +2323,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 @@ -4576,7 +4576,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -6419,7 +6419,7 @@ Упс! Не вдалося отримати історичні дані. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -6595,7 +6595,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -6774,6 +6774,14 @@ 27 + + No Activities + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision Пенсійне накопичення diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index d15b93002..03644820a 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -740,7 +740,7 @@ Filter by... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -777,7 +777,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -823,7 +823,7 @@ Oops! Could not parse historical data. libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -852,7 +852,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -4907,6 +4907,13 @@ 27 + + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision @@ -6126,7 +6133,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -6685,7 +6692,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index 6527ceb60..67007cdef 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -792,7 +792,7 @@ 过滤... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 385 + 390 @@ -832,7 +832,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 44 + 40 @@ -868,7 +868,7 @@ 哎呀!无法解析历史数据。 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts - 262 + 284 @@ -892,7 +892,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 71 + 67 @@ -5360,6 +5360,14 @@ 27 + + No Activities + No Activities + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 146 + + Retirement Provision 退休金 @@ -6758,7 +6766,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 46 + 47 libs/ui/src/lib/i18n.ts @@ -7336,7 +7344,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html - 48 + 49 diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index eae1b73cd..5da0e0122 100644 --- a/libs/common/src/lib/config.ts +++ b/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', diff --git a/libs/common/src/lib/types/market-data-preset.type.ts b/libs/common/src/lib/types/market-data-preset.type.ts index 0dbf914fa..2622feac3 100644 --- a/libs/common/src/lib/types/market-data-preset.type.ts +++ b/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'; diff --git a/libs/ui/src/lib/account-balances/account-balances.component.html b/libs/ui/src/lib/account-balances/account-balances.component.html index caef922ed..29037a985 100644 --- a/libs/ui/src/lib/account-balances/account-balances.component.html +++ b/libs/ui/src/lib/account-balances/account-balances.component.html @@ -12,7 +12,7 @@ Date - + @@ -37,7 +37,7 @@
@@ -48,7 +48,7 @@
- {{ accountCurrency }} + {{ accountCurrency() }}
@@ -58,7 +58,7 @@ - @if (showActions) { + @if (showActions()) { @@ -23,7 +23,7 @@ [matAutocomplete]="autocomplete" [matChipInputFor]="chipList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes" - [placeholder]="placeholder" + [placeholder]="placeholder()" (matChipInputTokenEnd)="onAddFilter($event)" /> @@ -35,7 +35,7 @@ @for (filter of filterGroup.filters; track filter) { - {{ filter.label | gfSymbol }} + {{ filter.label ?? '' | gfSymbol }} } @@ -46,7 +46,7 @@ disabled mat-icon-button matSuffix - [ngClass]="{ 'd-none': !isLoading }" + [ngClass]="{ 'd-none': !isLoading() }" > diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.ts b/libs/ui/src/lib/activities-filter/activities-filter.component.ts index 34f883c67..25fad683d 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.ts +++ b/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(); + @ViewChild('autocomplete') protected matAutocomplete: MatAutocomplete; + @ViewChild('searchInput') protected searchInput: ElementRef; - @ViewChild('autocomplete') matAutocomplete: MatAutocomplete; - @ViewChild('searchInput') searchInput: ElementRef; + public readonly isLoading = input.required(); + public readonly placeholder = input.required(); + public readonly valueChanged = output(); - public filterGroups$: Subject = new BehaviorSubject([]); - public filters$: Subject = new BehaviorSubject([]); - public filters: Observable = this.filters$.asObservable(); - public searchControl = new FormControl(undefined); - public selectedFilters: Filter[] = []; - public separatorKeysCodes: number[] = [ENTER, COMMA]; - - private unsubscribeSubject = new Subject(); + protected readonly filterGroups$ = new BehaviorSubject([]); + protected readonly searchControl = new FormControl( + 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; } ); diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts index 7e7094dd3..3e5e3b2e9 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts +++ b/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(); +export class GfHistoricalMarketDataEditorDialogComponent implements OnInit { + public readonly data = + inject(MAT_DIALOG_DATA); + + protected readonly marketPrice = signal(this.data.marketPrice); + + private readonly destroyRef = inject(DestroyRef); + private readonly locale = + this.data.user.settings.locale ?? inject(MAT_DATE_LOCALE); public constructor( private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, - @Inject(MAT_DIALOG_DATA) - public data: HistoricalMarketDataEditorDialogParams, private dataService: DataService, - private dateAdapter: DateAdapter, - public dialogRef: MatDialogRef, - @Inject(MAT_DATE_LOCALE) private locale: string + private dateAdapter: DateAdapter, + public dialogRef: MatDialogRef ) { 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(); - } } diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html index 8e7e30649..7bb5827ef 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html +++ b/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)" /> {{ data.currency }}
diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts index 4248b3fdb..edb9a852f 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts +++ b/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; } diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html index 3c2807146..91e3dd8d7 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -3,28 +3,24 @@
{{ itemByMonth.key }}
- @for (dayItem of days; track dayItem; let i = $index) { + @for (day of days; track day) {
diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.spec.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.spec.ts new file mode 100644 index 000000000..7cb9636f0 --- /dev/null +++ b/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; + + 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'); + }); + }); +}); diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts index 098f4e295..cde180dd9 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts +++ b/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(); - 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 & { 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(); + public readonly locale = input(getLocale()); + public readonly marketData = input.required(); + + 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(() => + 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[] = []; + 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( + 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: { diff --git a/package-lock.json b/package-lock.json index 13d3a99ca..7a1ebfa67 100644 --- a/package-lock.json +++ b/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" diff --git a/package.json b/package.json index 89d6e9f23..2bf5eebe0 100644 --- a/package.json +++ b/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",