Feature/add support to deactivate x-ray rules (#3537)
* Add support to deactivate X-ray rules * Update changelog
This commit is contained in:
parent
ac5aec9262
commit
2b212078b8
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support to deactivate rules in the _X-ray_ section (experimental)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed the currency conversion for fees and values in the dividend import by applying the correct rate based on the activity date
|
- Fixed the currency conversion for fees and values in the dividend import by applying the correct rate based on the activity date
|
||||||
|
@ -12,11 +12,8 @@ export class RulesService {
|
|||||||
aRules: Rule<T>[],
|
aRules: Rule<T>[],
|
||||||
aUserSettings: UserSettings
|
aUserSettings: UserSettings
|
||||||
) {
|
) {
|
||||||
return aRules
|
return aRules.map((rule) => {
|
||||||
.filter((rule) => {
|
if (rule.getSettings(aUserSettings)?.isActive) {
|
||||||
return rule.getSettings(aUserSettings)?.isActive;
|
|
||||||
})
|
|
||||||
.map((rule) => {
|
|
||||||
const { evaluation, value } = rule.evaluate(
|
const { evaluation, value } = rule.evaluate(
|
||||||
rule.getSettings(aUserSettings)
|
rule.getSettings(aUserSettings)
|
||||||
);
|
);
|
||||||
@ -24,9 +21,17 @@ export class RulesService {
|
|||||||
return {
|
return {
|
||||||
evaluation,
|
evaluation,
|
||||||
value,
|
value,
|
||||||
|
isActive: true,
|
||||||
key: rule.getKey(),
|
key: rule.getKey(),
|
||||||
name: rule.getName()
|
name: rule.getName()
|
||||||
};
|
};
|
||||||
});
|
} else {
|
||||||
|
return {
|
||||||
|
isActive: false,
|
||||||
|
key: rule.getKey(),
|
||||||
|
name: rule.getName()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ import type {
|
|||||||
ColorScheme,
|
ColorScheme,
|
||||||
DateRange,
|
DateRange,
|
||||||
HoldingsViewMode,
|
HoldingsViewMode,
|
||||||
ViewMode
|
ViewMode,
|
||||||
|
XRayRulesSettings
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -102,4 +103,7 @@ export class UpdateUserSettingDto {
|
|||||||
@IsIn(<ViewMode[]>['DEFAULT', 'ZEN'])
|
@IsIn(<ViewMode[]>['DEFAULT', 'ZEN'])
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
viewMode?: ViewMode;
|
viewMode?: ViewMode;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
xRayRules?: XRayRulesSettings;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ import { JwtService } from '@nestjs/jwt';
|
|||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { User as UserModel } from '@prisma/client';
|
import { User as UserModel } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
import { size } from 'lodash';
|
import { merge, size } from 'lodash';
|
||||||
|
|
||||||
import { DeleteOwnUserDto } from './delete-own-user.dto';
|
import { DeleteOwnUserDto } from './delete-own-user.dto';
|
||||||
import { UserItem } from './interfaces/user-item.interface';
|
import { UserItem } from './interfaces/user-item.interface';
|
||||||
@ -144,10 +144,11 @@ export class UserController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userSettings: UserSettings = {
|
const userSettings: UserSettings = merge(
|
||||||
...(<UserSettings>this.request.user.Settings.settings),
|
{},
|
||||||
...data
|
<UserSettings>this.request.user.Settings.settings,
|
||||||
};
|
data
|
||||||
|
);
|
||||||
|
|
||||||
for (const key in userSettings) {
|
for (const key in userSettings) {
|
||||||
if (userSettings[key] === false || userSettings[key] === null) {
|
if (userSettings[key] === false || userSettings[key] === null) {
|
||||||
|
@ -197,6 +197,18 @@ export class UserService {
|
|||||||
(user.Settings.settings as UserSettings).viewMode = 'DEFAULT';
|
(user.Settings.settings as UserSettings).viewMode = 'DEFAULT';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set default values for X-ray rules
|
||||||
|
if (!(user.Settings.settings as UserSettings).xRayRules) {
|
||||||
|
(user.Settings.settings as UserSettings).xRayRules = {
|
||||||
|
AccountClusterRiskCurrentInvestment: { isActive: true },
|
||||||
|
AccountClusterRiskSingleAccount: { isActive: true },
|
||||||
|
CurrencyClusterRiskBaseCurrencyCurrentInvestment: { isActive: true },
|
||||||
|
CurrencyClusterRiskCurrentInvestment: { isActive: true },
|
||||||
|
EmergencyFundSetup: { isActive: true },
|
||||||
|
FeeRatioInitialInvestment: { isActive: true }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let currentPermissions = getPermissions(user.role);
|
let currentPermissions = getPermissions(user.role);
|
||||||
|
|
||||||
if (!(user.Settings.settings as UserSettings).isExperimentalFeatures) {
|
if (!(user.Settings.settings as UserSettings).isExperimentalFeatures) {
|
||||||
|
@ -79,7 +79,7 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
|
|||||||
public getSettings(aUserSettings: UserSettings): Settings {
|
public getSettings(aUserSettings: UserSettings): Settings {
|
||||||
return {
|
return {
|
||||||
baseCurrency: aUserSettings.baseCurrency,
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
isActive: true,
|
isActive: aUserSettings.xRayRules[this.getKey()].isActive,
|
||||||
thresholdMax: 0.5
|
thresholdMax: 0.5
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
|
|||||||
|
|
||||||
public getSettings(aUserSettings: UserSettings): RuleSettings {
|
public getSettings(aUserSettings: UserSettings): RuleSettings {
|
||||||
return {
|
return {
|
||||||
isActive: true
|
isActive: aUserSettings.xRayRules[this.getKey()].isActive
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
|
|||||||
public getSettings(aUserSettings: UserSettings): Settings {
|
public getSettings(aUserSettings: UserSettings): Settings {
|
||||||
return {
|
return {
|
||||||
baseCurrency: aUserSettings.baseCurrency,
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
isActive: true
|
isActive: aUserSettings.xRayRules[this.getKey()].isActive
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
|||||||
public getSettings(aUserSettings: UserSettings): Settings {
|
public getSettings(aUserSettings: UserSettings): Settings {
|
||||||
return {
|
return {
|
||||||
baseCurrency: aUserSettings.baseCurrency,
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
isActive: true,
|
isActive: aUserSettings.xRayRules[this.getKey()].isActive,
|
||||||
thresholdMax: 0.5
|
thresholdMax: 0.5
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ export class EmergencyFundSetup extends Rule<Settings> {
|
|||||||
public getSettings(aUserSettings: UserSettings): Settings {
|
public getSettings(aUserSettings: UserSettings): Settings {
|
||||||
return {
|
return {
|
||||||
baseCurrency: aUserSettings.baseCurrency,
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
isActive: true,
|
isActive: aUserSettings.xRayRules[this.getKey()].isActive,
|
||||||
thresholdMin: 0
|
thresholdMin: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
|
|||||||
public getSettings(aUserSettings: UserSettings): Settings {
|
public getSettings(aUserSettings: UserSettings): Settings {
|
||||||
return {
|
return {
|
||||||
baseCurrency: aUserSettings.baseCurrency,
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
isActive: true,
|
isActive: aUserSettings.xRayRules[this.getKey()].isActive,
|
||||||
thresholdMax: 0.01
|
thresholdMax: 0.01
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,17 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<div
|
<div
|
||||||
class="align-items-center d-flex icon-container mr-2 px-2"
|
class="align-items-center d-flex icon-container mr-2 px-2"
|
||||||
[ngClass]="{ okay: rule?.value === true, warn: rule?.value === false }"
|
[ngClass]="{
|
||||||
|
okay: rule?.value === true,
|
||||||
|
warn: rule?.value === false
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
@if (rule?.value === true) {
|
@if (rule?.value === true) {
|
||||||
<ion-icon name="checkmark-circle-outline" />
|
<ion-icon name="checkmark-circle-outline" />
|
||||||
} @else {
|
} @else if (rule?.isActive === true) {
|
||||||
<ion-icon name="warning-outline" />
|
<ion-icon name="warning-outline" />
|
||||||
|
} @else {
|
||||||
|
<ion-icon class="text-muted" name="remove-circle-outline" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -46,6 +51,27 @@
|
|||||||
<div class="h6 my-1">{{ rule?.name }}</div>
|
<div class="h6 my-1">{{ rule?.name }}</div>
|
||||||
<div class="evaluation">{{ rule?.evaluation }}</div>
|
<div class="evaluation">{{ rule?.evaluation }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
@if (hasPermissionToUpdateUserSettings) {
|
||||||
|
<button
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[matMenuTriggerFor]="rulesMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-horizontal" />
|
||||||
|
</button>
|
||||||
|
<mat-menu #rulesMenu="matMenu" xPosition="before">
|
||||||
|
<button mat-menu-item (click)="onUpdateRule(rule)">
|
||||||
|
@if (rule?.isActive) {
|
||||||
|
<ng-container i18n>Deactivate</ng-container>
|
||||||
|
} @else {
|
||||||
|
<ng-container i18n>Activate</ng-container>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
||||||
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnInit
|
OnInit,
|
||||||
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -14,10 +17,23 @@ import {
|
|||||||
styleUrls: ['./rule.component.scss']
|
styleUrls: ['./rule.component.scss']
|
||||||
})
|
})
|
||||||
export class RuleComponent implements OnInit {
|
export class RuleComponent implements OnInit {
|
||||||
|
@Input() hasPermissionToUpdateUserSettings: boolean;
|
||||||
@Input() isLoading: boolean;
|
@Input() isLoading: boolean;
|
||||||
@Input() rule: PortfolioReportRule;
|
@Input() rule: PortfolioReportRule;
|
||||||
|
|
||||||
|
@Output() ruleUpdated = new EventEmitter<UpdateUserSettingDto>();
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
|
public onUpdateRule(rule: PortfolioReportRule) {
|
||||||
|
const settings: UpdateUserSettingDto = {
|
||||||
|
xRayRules: {
|
||||||
|
[rule.key]: { isActive: !rule.isActive }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ruleUpdated.emit(settings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { RuleComponent } from './rule.component';
|
import { RuleComponent } from './rule.component';
|
||||||
@ -7,7 +9,12 @@ import { RuleComponent } from './rule.component';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [RuleComponent],
|
declarations: [RuleComponent],
|
||||||
exports: [RuleComponent],
|
exports: [RuleComponent],
|
||||||
imports: [CommonModule, NgxSkeletonLoaderModule],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatMenuModule,
|
||||||
|
NgxSkeletonLoaderModule
|
||||||
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfRuleModule {}
|
export class GfRuleModule {}
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
<div class="container p-0">
|
<div class="container p-0">
|
||||||
<div class="row no-gutters">
|
<div class="row no-gutters">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@if (hasPermissionToCreateOrder && rules === null) {
|
@if (isLoading) {
|
||||||
<mat-card appearance="outlined" class="my-2 text-center">
|
|
||||||
<mat-card-content>
|
|
||||||
<gf-no-transactions-info-indicator [hasBorder]="false" />
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (rules?.length === 0) {
|
|
||||||
<gf-rule [isLoading]="true" />
|
<gf-rule [isLoading]="true" />
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (rules !== null && rules !== undefined) {
|
@if (rules !== null && rules !== undefined) {
|
||||||
@for (rule of rules; track rule) {
|
@for (rule of rules; track rule.key) {
|
||||||
<gf-rule [rule]="rule" />
|
<gf-rule
|
||||||
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
hasPermissionToUpdateUserSettings
|
||||||
|
"
|
||||||
|
[rule]="rule"
|
||||||
|
(ruleUpdated)="onRuleUpdated($event)"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
|
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
||||||
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-rules',
|
selector: 'gf-rules',
|
||||||
@ -9,8 +16,15 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
|||||||
styleUrls: ['./rules.component.scss']
|
styleUrls: ['./rules.component.scss']
|
||||||
})
|
})
|
||||||
export class RulesComponent {
|
export class RulesComponent {
|
||||||
@Input() hasPermissionToCreateOrder: boolean;
|
@Input() hasPermissionToUpdateUserSettings: boolean;
|
||||||
|
@Input() isLoading: boolean;
|
||||||
@Input() rules: PortfolioReportRule[];
|
@Input() rules: PortfolioReportRule[];
|
||||||
|
|
||||||
|
@Output() rulesUpdated = new EventEmitter<UpdateUserSettingDto>();
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
|
public onRuleUpdated(event: UpdateUserSettingDto) {
|
||||||
|
this.rulesUpdated.emit(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
|
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
|
||||||
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
|
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -11,13 +10,7 @@ import { RulesComponent } from './rules.component';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [RulesComponent],
|
declarations: [RulesComponent],
|
||||||
exports: [RulesComponent],
|
exports: [RulesComponent],
|
||||||
imports: [
|
imports: [CommonModule, GfRuleModule, MatButtonModule, MatCardModule],
|
||||||
CommonModule,
|
|
||||||
GfNoTransactionsInfoComponent,
|
|
||||||
GfRuleModule,
|
|
||||||
MatButtonModule,
|
|
||||||
MatCardModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfRulesModule {}
|
export class GfRulesModule {}
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { PortfolioReportRule, User } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
PortfolioReport,
|
||||||
|
PortfolioReportRule,
|
||||||
|
User
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
@ -23,9 +28,10 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
public feeRules: PortfolioReportRule[];
|
public feeRules: PortfolioReportRule[];
|
||||||
public fireWealth: Big;
|
public fireWealth: Big;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
|
public inactiveRules: PortfolioReportRule[];
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
|
public isLoadingPortfolioReport = false;
|
||||||
public user: User;
|
public user: User;
|
||||||
public withdrawalRatePerMonth: Big;
|
public withdrawalRatePerMonth: Big;
|
||||||
public withdrawalRatePerYear: Big;
|
public withdrawalRatePerYear: Big;
|
||||||
@ -64,21 +70,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dataService
|
|
||||||
.fetchPortfolioReport()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((portfolioReport) => {
|
|
||||||
this.accountClusterRiskRules =
|
|
||||||
portfolioReport.rules['accountClusterRisk'] || null;
|
|
||||||
this.currencyClusterRiskRules =
|
|
||||||
portfolioReport.rules['currencyClusterRisk'] || null;
|
|
||||||
this.emergencyFundRules =
|
|
||||||
portfolioReport.rules['emergencyFund'] || null;
|
|
||||||
this.feeRules = portfolioReport.rules['fees'] || null;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
.onChangeHasImpersonation()
|
.onChangeHasImpersonation()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
@ -92,11 +83,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
this.hasPermissionToCreateOrder = hasPermission(
|
|
||||||
this.user.permissions,
|
|
||||||
permissions.createOrder
|
|
||||||
);
|
|
||||||
|
|
||||||
this.hasPermissionToUpdateUserSettings =
|
this.hasPermissionToUpdateUserSettings =
|
||||||
this.user.subscription?.type === 'Basic'
|
this.user.subscription?.type === 'Basic'
|
||||||
? false
|
? false
|
||||||
@ -108,6 +94,8 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.initializePortfolioReport();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onAnnualInterestRateChange(annualInterestRate: number) {
|
public onAnnualInterestRateChange(annualInterestRate: number) {
|
||||||
@ -149,6 +137,17 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onRulesUpdated(event: UpdateUserSettingDto) {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.putUserSetting(event)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.initializePortfolioReport();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onSavingsRateChange(savingsRate: number) {
|
public onSavingsRateChange(savingsRate: number) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.putUserSetting({ savingsRate })
|
.putUserSetting({ savingsRate })
|
||||||
@ -192,4 +191,59 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initializePortfolioReport() {
|
||||||
|
this.isLoadingPortfolioReport = true;
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchPortfolioReport()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((portfolioReport) => {
|
||||||
|
this.inactiveRules = this.mergeInactiveRules(portfolioReport);
|
||||||
|
|
||||||
|
this.accountClusterRiskRules =
|
||||||
|
portfolioReport.rules['accountClusterRisk']?.filter(
|
||||||
|
({ isActive }) => {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
this.currencyClusterRiskRules =
|
||||||
|
portfolioReport.rules['currencyClusterRisk']?.filter(
|
||||||
|
({ isActive }) => {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
this.emergencyFundRules =
|
||||||
|
portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => {
|
||||||
|
return isActive;
|
||||||
|
}) ?? null;
|
||||||
|
|
||||||
|
this.feeRules =
|
||||||
|
portfolioReport.rules['fees']?.filter(({ isActive }) => {
|
||||||
|
return isActive;
|
||||||
|
}) ?? null;
|
||||||
|
|
||||||
|
this.isLoadingPortfolioReport = false;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] {
|
||||||
|
let inactiveRules: PortfolioReportRule[] = [];
|
||||||
|
|
||||||
|
for (const category in report.rules) {
|
||||||
|
const rulesArray = report.rules[category];
|
||||||
|
|
||||||
|
inactiveRules = inactiveRules.concat(
|
||||||
|
rulesArray.filter(({ isActive }) => {
|
||||||
|
return !isActive;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inactiveRules;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,8 +125,14 @@
|
|||||||
}
|
}
|
||||||
</h4>
|
</h4>
|
||||||
<gf-rules
|
<gf-rules
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
!hasImpersonationId &&
|
||||||
|
hasPermissionToUpdateUserSettings &&
|
||||||
|
user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
|
[isLoading]="isLoadingPortfolioReport"
|
||||||
[rules]="emergencyFundRules"
|
[rules]="emergencyFundRules"
|
||||||
|
(rulesUpdated)="onRulesUpdated($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@ -137,8 +143,14 @@
|
|||||||
}
|
}
|
||||||
</h4>
|
</h4>
|
||||||
<gf-rules
|
<gf-rules
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
!hasImpersonationId &&
|
||||||
|
hasPermissionToUpdateUserSettings &&
|
||||||
|
user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
|
[isLoading]="isLoadingPortfolioReport"
|
||||||
[rules]="currencyClusterRiskRules"
|
[rules]="currencyClusterRiskRules"
|
||||||
|
(rulesUpdated)="onRulesUpdated($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@ -149,11 +161,17 @@
|
|||||||
}
|
}
|
||||||
</h4>
|
</h4>
|
||||||
<gf-rules
|
<gf-rules
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
!hasImpersonationId &&
|
||||||
|
hasPermissionToUpdateUserSettings &&
|
||||||
|
user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
|
[isLoading]="isLoadingPortfolioReport"
|
||||||
[rules]="accountClusterRiskRules"
|
[rules]="accountClusterRiskRules"
|
||||||
|
(rulesUpdated)="onRulesUpdated($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="mb-4">
|
||||||
<h4 class="align-items-center d-flex m-0">
|
<h4 class="align-items-center d-flex m-0">
|
||||||
<span i18n>Fees</span>
|
<span i18n>Fees</span>
|
||||||
@if (user?.subscription?.type === 'Basic') {
|
@if (user?.subscription?.type === 'Basic') {
|
||||||
@ -161,10 +179,31 @@
|
|||||||
}
|
}
|
||||||
</h4>
|
</h4>
|
||||||
<gf-rules
|
<gf-rules
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
!hasImpersonationId &&
|
||||||
|
hasPermissionToUpdateUserSettings &&
|
||||||
|
user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
|
[isLoading]="isLoadingPortfolioReport"
|
||||||
[rules]="feeRules"
|
[rules]="feeRules"
|
||||||
|
(rulesUpdated)="onRulesUpdated($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@if (inactiveRules?.length > 0) {
|
||||||
|
<div>
|
||||||
|
<h4 class="m-0" i18n>Inactive</h4>
|
||||||
|
<gf-rules
|
||||||
|
[hasPermissionToUpdateUserSettings]="
|
||||||
|
!hasImpersonationId &&
|
||||||
|
hasPermissionToUpdateUserSettings &&
|
||||||
|
user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
|
[isLoading]="isLoadingPortfolioReport"
|
||||||
|
[rules]="inactiveRules"
|
||||||
|
(rulesUpdated)="onRulesUpdated($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
export interface PortfolioReportRule {
|
export interface PortfolioReportRule {
|
||||||
evaluation: string;
|
evaluation?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
value: boolean;
|
value?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,8 @@ import {
|
|||||||
ColorScheme,
|
ColorScheme,
|
||||||
DateRange,
|
DateRange,
|
||||||
HoldingsViewMode,
|
HoldingsViewMode,
|
||||||
ViewMode
|
ViewMode,
|
||||||
|
XRayRulesSettings
|
||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
|
|
||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
@ -23,4 +24,5 @@ export interface UserSettings {
|
|||||||
retirementDate?: string;
|
retirementDate?: string;
|
||||||
savingsRate?: number;
|
savingsRate?: number;
|
||||||
viewMode?: ViewMode;
|
viewMode?: ViewMode;
|
||||||
|
xRayRules?: XRayRulesSettings;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import type { SubscriptionOffer } from './subscription-offer.type';
|
|||||||
import type { ToggleOption } from './toggle-option.type';
|
import type { ToggleOption } from './toggle-option.type';
|
||||||
import type { UserWithSettings } from './user-with-settings.type';
|
import type { UserWithSettings } from './user-with-settings.type';
|
||||||
import type { ViewMode } from './view-mode.type';
|
import type { ViewMode } from './view-mode.type';
|
||||||
|
import type { XRayRulesSettings } from './x-ray-rules-settings.type';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
AccessType,
|
AccessType,
|
||||||
@ -41,5 +42,6 @@ export type {
|
|||||||
SubscriptionOffer,
|
SubscriptionOffer,
|
||||||
ToggleOption,
|
ToggleOption,
|
||||||
UserWithSettings,
|
UserWithSettings,
|
||||||
ViewMode
|
ViewMode,
|
||||||
|
XRayRulesSettings
|
||||||
};
|
};
|
||||||
|
12
libs/common/src/lib/types/x-ray-rules-settings.type.ts
Normal file
12
libs/common/src/lib/types/x-ray-rules-settings.type.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export type XRayRulesSettings = {
|
||||||
|
AccountClusterRiskCurrentInvestment?: RuleSettings;
|
||||||
|
AccountClusterRiskSingleAccount?: RuleSettings;
|
||||||
|
CurrencyClusterRiskBaseCurrencyCurrentInvestment?: RuleSettings;
|
||||||
|
CurrencyClusterRiskCurrentInvestment?: RuleSettings;
|
||||||
|
EmergencyFundSetup?: RuleSettings;
|
||||||
|
FeeRatioInitialInvestment?: RuleSettings;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RuleSettings {
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user