From ee0cbbd8d035049631876866d033156cae57bacb Mon Sep 17 00:00:00 2001 From: Quan Huynh Van Date: Sat, 12 Jul 2025 12:25:33 +0700 Subject: [PATCH] feat: Show category in rule settings dialog of x-ray --- .../app/portfolio/rule-category-mapping.ts | 53 +++++++++++++++++++ apps/api/src/app/portfolio/rules.service.ts | 6 +++ .../rule-settings-dialog.html | 8 ++- .../portfolio-report-rule.interface.ts | 1 + 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/app/portfolio/rule-category-mapping.ts diff --git a/apps/api/src/app/portfolio/rule-category-mapping.ts b/apps/api/src/app/portfolio/rule-category-mapping.ts new file mode 100644 index 000000000..08811dfe1 --- /dev/null +++ b/apps/api/src/app/portfolio/rule-category-mapping.ts @@ -0,0 +1,53 @@ +import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; +import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; +import { AssetClassClusterRiskEquity } from '@ghostfolio/api/models/rules/asset-class-cluster-risk/equity'; +import { AssetClassClusterRiskFixedIncome } from '@ghostfolio/api/models/rules/asset-class-cluster-risk/fixed-income'; +import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment'; +import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; +import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/developed-markets'; +import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets'; +import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; +import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; +import { RegionalMarketClusterRiskAsiaPacific } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/asia-pacific'; +import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/emerging-markets'; +import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe'; +import { RegionalMarketClusterRiskJapan } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/japan'; +import { RegionalMarketClusterRiskNorthAmerica } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/north-america'; + +export const RULE_CATEGORY_MAPPING = { + // Account Cluster Risk Rules + [AccountClusterRiskCurrentInvestment.name]: 'Account Cluster Risk', + [AccountClusterRiskSingleAccount.name]: 'Account Cluster Risk', + + // Asset Class Cluster Risk Rules + [AssetClassClusterRiskEquity.name]: 'Asset Class Cluster Risk', + [AssetClassClusterRiskFixedIncome.name]: 'Asset Class Cluster Risk', + + // Currency Cluster Risk Rules + [CurrencyClusterRiskBaseCurrencyCurrentInvestment.name]: + 'Currency Cluster Risk', + [CurrencyClusterRiskCurrentInvestment.name]: 'Currency Cluster Risk', + + // Economic Market Cluster Risk Rules + [EconomicMarketClusterRiskDevelopedMarkets.name]: + 'Economic Market Cluster Risk', + [EconomicMarketClusterRiskEmergingMarkets.name]: + 'Economic Market Cluster Risk', + + // Emergency Fund Rules + [EmergencyFundSetup.name]: 'Emergency Fund', + + // Fee Rules + [FeeRatioInitialInvestment.name]: 'Fees', + + // Regional Market Cluster Risk Rules + [RegionalMarketClusterRiskAsiaPacific.name]: 'Regional Market Cluster Risk', + [RegionalMarketClusterRiskEmergingMarkets.name]: + 'Regional Market Cluster Risk', + [RegionalMarketClusterRiskEurope.name]: 'Regional Market Cluster Risk', + [RegionalMarketClusterRiskJapan.name]: 'Regional Market Cluster Risk', + [RegionalMarketClusterRiskNorthAmerica.name]: 'Regional Market Cluster Risk' +} as const; + +export type RuleCategoryName = + (typeof RULE_CATEGORY_MAPPING)[keyof typeof RULE_CATEGORY_MAPPING]; diff --git a/apps/api/src/app/portfolio/rules.service.ts b/apps/api/src/app/portfolio/rules.service.ts index 48d1658aa..f9763d5fc 100644 --- a/apps/api/src/app/portfolio/rules.service.ts +++ b/apps/api/src/app/portfolio/rules.service.ts @@ -7,6 +7,8 @@ import { import { Injectable } from '@nestjs/common'; +import { RULE_CATEGORY_MAPPING } from './rule-category-mapping'; + @Injectable() export class RulesService { public async evaluate( @@ -15,6 +17,8 @@ export class RulesService { ): Promise { return aRules.map((rule) => { const settings = rule.getSettings(aUserSettings); + const category = + RULE_CATEGORY_MAPPING[rule.constructor.name] || 'Unknown'; if (settings?.isActive) { const { evaluation, value } = rule.evaluate(settings); @@ -22,6 +26,7 @@ export class RulesService { return { evaluation, value, + category, configuration: rule.getConfiguration(), isActive: true, key: rule.getKey(), @@ -29,6 +34,7 @@ export class RulesService { }; } else { return { + category, isActive: false, key: rule.getKey(), name: rule.getName() diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html index 97854ad7d..4f9b2aa0f 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html @@ -1,4 +1,10 @@ -
{{ data.rule.name }}
+
+ @if (data.rule.category) { + {{ data.rule.category }} › {{ data.rule.name }} + } @else { + {{ data.rule.name }} + } +
@if ( diff --git a/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts b/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts index 0296606b8..f6bccbb25 100644 --- a/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts @@ -1,4 +1,5 @@ export interface PortfolioReportRule { + category?: string; configuration?: { threshold?: { max: number;