Browse Source

implements dynamic creation of each category's rules

pull/5454/head
tobikugel 2 months ago
parent
commit
ba5b00bcda
  1. 45
      apps/api/src/app/portfolio/portfolio.service.ts
  2. 1
      apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts
  3. 2
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
  4. 2
      apps/client/src/app/components/rule/rule.component.ts
  5. 1
      apps/client/src/app/components/rules/rules.component.html
  6. 1
      apps/client/src/app/components/rules/rules.component.ts
  7. 199
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
  8. 54
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts

45
apps/api/src/app/portfolio/portfolio.service.ts

@ -33,6 +33,7 @@ import {
} from '@ghostfolio/common/calculation-helper';
import {
DEFAULT_CURRENCY,
DEFAULT_LANGUAGE_CODE,
TAG_ID_EMERGENCY_FUND,
TAG_ID_EXCLUDE_FROM_ANALYSIS,
UNKNOWN_KEY
@ -1235,7 +1236,10 @@ export class PortfolioService {
const categories: PortfolioReportResponse['xRay']['categories'] = [
{
key: 'accountClusterRisk',
name: 'accountClusterRisk',
name: this.i18nService.getTranslation({
id: 'rule.accountClusterRisk.category',
languageCode: userSettings.language || DEFAULT_LANGUAGE_CODE
}),
rules:
summary.activityCount > 0
? await this.rulesService.evaluate(
@ -1259,7 +1263,10 @@ export class PortfolioService {
},
{
key: 'assetClassClusterRisk',
name: 'assetClassClusterRisk',
name: this.i18nService.getTranslation({
id: 'rule.assetClassClusterRisk.category',
languageCode: userSettings.language || DEFAULT_LANGUAGE_CODE
}),
rules:
summary.activityCount > 0
? await this.rulesService.evaluate(
@ -1283,7 +1290,10 @@ export class PortfolioService {
},
{
key: 'currencyClusterRisk',
name: 'currencyClusterRisk',
name: this.i18nService.getTranslation({
id: 'rule.currencyClusterRisk.category',
languageCode: userSettings.language || DEFAULT_LANGUAGE_CODE
}),
rules:
summary.activityCount > 0
? await this.rulesService.evaluate(
@ -1307,7 +1317,10 @@ export class PortfolioService {
},
{
key: 'economicMarketClusterRisk',
name: 'economicMarketClusterRisk',
name: this.i18nService.getTranslation({
id: 'rule.economicMarketClusterRisk.category',
languageCode: userSettings.language || DEFAULT_LANGUAGE_CODE
}),
rules:
summary.activityCount > 0
? await this.rulesService.evaluate(
@ -1333,7 +1346,10 @@ export class PortfolioService {
},
{
key: 'emergencyFund',
name: 'emergencyFund',
name: this.i18nService.getTranslation({
id: 'rule.emergencyFund.category',
languageCode: userSettings.language || DEFAULT_LANGUAGE_CODE
}),
rules: await this.rulesService.evaluate(
[
new EmergencyFundSetup(
@ -1352,7 +1368,10 @@ export class PortfolioService {
},
{
key: 'fees',
name: 'fees',
name: this.i18nService.getTranslation({
id: 'rule.fees.category',
languageCode: userSettings.language || DEFAULT_LANGUAGE_CODE
}),
rules: await this.rulesService.evaluate(
[
new FeeRatioInitialInvestment(
@ -1368,7 +1387,10 @@ export class PortfolioService {
},
{
key: 'liquidity',
name: 'liquidity',
name: this.i18nService.getTranslation({
id: 'rule.liquidity.category',
languageCode: userSettings.language || DEFAULT_LANGUAGE_CODE
}),
rules: await this.rulesService.evaluate(
[
new BuyingPower(
@ -1383,7 +1405,10 @@ export class PortfolioService {
},
{
key: 'regionalMarketClusterRisk',
name: 'regionalMarketClusterRisk',
name: this.i18nService.getTranslation({
id: 'rule.regionalMarketClusterRisk.category',
languageCode: userSettings.language || DEFAULT_LANGUAGE_CODE
}),
rules:
summary.activityCount > 0
? await this.rulesService.evaluate(
@ -1434,7 +1459,9 @@ export class PortfolioService {
xRay: {
categories,
statistics: this.getReportStatistics(
categories.flatMap(({ rules }) => rules ?? [])
categories.flatMap(({ rules }) => {
return rules ?? [];
})
)
}
};

1
apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts

@ -4,6 +4,7 @@ import {
} from '@ghostfolio/common/interfaces';
export interface IRuleSettingsDialogParams {
categoryName: string;
rule: PortfolioReportRule;
settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment'];
}

2
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html

@ -1,4 +1,4 @@
<div mat-dialog-title>{{ data.rule.name }}</div>
<div mat-dialog-title>{{ data.categoryName }} › {{ data.rule.name }}</div>
<div class="py-3" mat-dialog-content>
@if (

2
apps/client/src/app/components/rule/rule.component.ts

@ -37,6 +37,7 @@ import { GfRuleSettingsDialogComponent } from './rule-settings-dialog/rule-setti
standalone: false
})
export class RuleComponent implements OnInit {
@Input() categoryName: string;
@Input() hasPermissionToUpdateUserSettings: boolean;
@Input() isLoading: boolean;
@Input() rule: PortfolioReportRule;
@ -68,6 +69,7 @@ export class RuleComponent implements OnInit {
public onCustomizeRule(rule: PortfolioReportRule) {
const dialogRef = this.dialog.open(GfRuleSettingsDialogComponent, {
data: {
categoryName: this.categoryName,
rule,
settings: this.settings
} as IRuleSettingsDialogParams,

1
apps/client/src/app/components/rules/rules.component.html

@ -8,6 +8,7 @@
@if (rules !== null && rules !== undefined) {
@for (rule of rules; track rule.key) {
<gf-rule
[categoryName]="categoryName"
[hasPermissionToUpdateUserSettings]="
hasPermissionToUpdateUserSettings
"

1
apps/client/src/app/components/rules/rules.component.ts

@ -20,6 +20,7 @@ import {
standalone: false
})
export class RulesComponent {
@Input() categoryName: string;
@Input() hasPermissionToUpdateUserSettings: boolean;
@Input() isLoading: boolean;
@Input() rules: PortfolioReportRule[];

199
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html

@ -59,182 +59,29 @@
</div>
}
</div>
<div
class="mb-4"
[ngClass]="{
'd-none': liquidityRules?.length === 0
}"
>
<h4 class="align-items-center d-flex m-0">
<span i18n>Liquidity</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[isLoading]="isLoading"
[rules]="liquidityRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div
class="mb-4"
[ngClass]="{
'd-none': emergencyFundRules?.length === 0
}"
>
<h4 class="align-items-center d-flex m-0">
<span i18n>Emergency Fund</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[isLoading]="isLoading"
[rules]="emergencyFundRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div
class="mb-4"
[ngClass]="{
'd-none': currencyClusterRiskRules?.length === 0
}"
>
<h4 class="align-items-center d-flex m-0">
<span i18n>Currency Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[isLoading]="isLoading"
[rules]="currencyClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div
class="mb-4"
[ngClass]="{
'd-none': assetClassClusterRiskRules?.length === 0
}"
>
<h4 class="align-items-center d-flex m-0">
<span i18n>Asset Class Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[isLoading]="isLoading"
[rules]="assetClassClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div
class="mb-4"
[ngClass]="{
'd-none': accountClusterRiskRules?.length === 0
}"
>
<h4 class="align-items-center d-flex m-0">
<span i18n>Account Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[isLoading]="isLoading"
[rules]="accountClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div
class="mb-4"
[ngClass]="{
'd-none': economicMarketClusterRiskRules?.length === 0
}"
>
<h4 class="align-items-center d-flex m-0">
<span i18n>Economic Market Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[isLoading]="isLoading"
[rules]="economicMarketClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div
class="mb-4"
[ngClass]="{
'd-none': regionalMarketClusterRiskRules?.length === 0
}"
>
<h4 class="align-items-center d-flex m-0">
<span i18n>Regional Market Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[isLoading]="isLoading"
[rules]="regionalMarketClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div
class="mb-4"
[ngClass]="{
'd-none': feeRules?.length === 0
}"
>
<h4 class="align-items-center d-flex m-0">
<span i18n>Fees</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[isLoading]="isLoading"
[rules]="feeRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
@for (category of categories; track category.key) {
<div
class="mb-4"
[ngClass]="{ 'd-none': category.rules?.length === 0 }"
>
<h4 class="align-items-center d-flex m-0">
<span>{{ category.name }}</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[categoryName]="category.name"
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[isLoading]="isLoading"
[rules]="category.rules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
}
@if (inactiveRules?.length > 0) {
<div>
<h4 class="m-0" i18n>Inactive</h4>

54
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts

@ -36,18 +36,15 @@ import { Subject, takeUntil } from 'rxjs';
templateUrl: './x-ray-page.component.html'
})
export class GfXRayPageComponent {
public accountClusterRiskRules: PortfolioReportRule[];
public assetClassClusterRiskRules: PortfolioReportRule[];
public currencyClusterRiskRules: PortfolioReportRule[];
public economicMarketClusterRiskRules: PortfolioReportRule[];
public emergencyFundRules: PortfolioReportRule[];
public feeRules: PortfolioReportRule[];
public categories: {
key: string;
name: string;
rules: PortfolioReportRule[];
}[];
public hasImpersonationId: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public inactiveRules: PortfolioReportRule[];
public isLoading = false;
public liquidityRules: PortfolioReportRule[];
public regionalMarketClusterRiskRules: PortfolioReportRule[];
public statistics: PortfolioReportResponse['xRay']['statistics'];
public user: User;
@ -119,46 +116,7 @@ export class GfXRayPageComponent {
.subscribe(({ xRay: { categories, statistics } }) => {
this.inactiveRules = this.mergeInactiveRules(categories);
this.statistics = statistics;
this.accountClusterRiskRules =
categories
.find(({ key }) => key === 'accountClusterRisk')
?.rules?.filter(({ isActive }) => isActive) ?? null;
this.assetClassClusterRiskRules =
categories
.find(({ key }) => key === 'assetClassClusterRisk')
?.rules?.filter(({ isActive }) => isActive) ?? null;
this.currencyClusterRiskRules =
categories
.find(({ key }) => key === 'currencyClusterRisk')
?.rules?.filter(({ isActive }) => isActive) ?? null;
this.economicMarketClusterRiskRules =
categories
.find(({ key }) => key === 'economicMarketClusterRisk')
?.rules?.filter(({ isActive }) => isActive) ?? null;
this.emergencyFundRules =
categories
.find(({ key }) => key === 'emergencyFund')
?.rules?.filter(({ isActive }) => isActive) ?? null;
this.feeRules =
categories
.find(({ key }) => key === 'fees')
?.rules?.filter(({ isActive }) => isActive) ?? null;
this.liquidityRules =
categories
.find(({ key }) => key === 'liquidity')
?.rules?.filter(({ isActive }) => isActive) ?? null;
this.regionalMarketClusterRiskRules =
categories
.find(({ key }) => key === 'regionalMarketClusterRisk')
?.rules?.filter(({ isActive }) => isActive) ?? null;
this.categories = categories;
this.isLoading = false;

Loading…
Cancel
Save