Browse Source

Merge branch 'main' into pr/3393

pull/3393/head
Thomas Kaul 1 year ago
parent
commit
49d292904a
  1. 10
      CHANGELOG.md
  2. 49
      DEVELOPMENT.md
  3. 47
      README.md
  4. 17
      apps/api/src/app/portfolio/rules.service.ts
  5. 6
      apps/api/src/app/user/update-user-setting.dto.ts
  6. 11
      apps/api/src/app/user/user.controller.ts
  7. 12
      apps/api/src/app/user/user.service.ts
  8. 2
      apps/api/src/models/rules/account-cluster-risk/current-investment.ts
  9. 2
      apps/api/src/models/rules/account-cluster-risk/single-account.ts
  10. 2
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  11. 2
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  12. 2
      apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts
  13. 2
      apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts
  14. 30
      apps/client/src/app/components/rule/rule.component.html
  15. 18
      apps/client/src/app/components/rule/rule.component.ts
  16. 9
      apps/client/src/app/components/rule/rule.module.ts
  17. 21
      apps/client/src/app/components/rules/rules.component.html
  18. 18
      apps/client/src/app/components/rules/rules.component.ts
  19. 9
      apps/client/src/app/components/rules/rules.module.ts
  20. 98
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  21. 49
      apps/client/src/app/pages/portfolio/fire/fire-page.html
  22. 326
      apps/client/src/locales/messages.ca.xlf
  23. 326
      apps/client/src/locales/messages.de.xlf
  24. 326
      apps/client/src/locales/messages.es.xlf
  25. 326
      apps/client/src/locales/messages.fr.xlf
  26. 326
      apps/client/src/locales/messages.it.xlf
  27. 326
      apps/client/src/locales/messages.nl.xlf
  28. 326
      apps/client/src/locales/messages.pl.xlf
  29. 326
      apps/client/src/locales/messages.pt.xlf
  30. 326
      apps/client/src/locales/messages.tr.xlf
  31. 319
      apps/client/src/locales/messages.xlf
  32. 326
      apps/client/src/locales/messages.zh.xlf
  33. 6
      libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts
  34. 4
      libs/common/src/lib/interfaces/user-settings.interface.ts
  35. 21
      libs/common/src/lib/personal-finance-tools.ts
  36. 4
      libs/common/src/lib/types/index.ts
  37. 12
      libs/common/src/lib/types/x-ray-rules-settings.type.ts
  38. 2
      package.json

10
CHANGELOG.md

@ -5,7 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## 2.105.0 - 2024-08-21
### Added
- Added support to deactivate rules in the _X-ray_ section (experimental)
### Changed
- Improved the language localization for German (`de`)
### Fixed ### Fixed

49
DEVELOPMENT.md

@ -1,5 +1,54 @@
# Ghostfolio Development Guide # Ghostfolio Development Guide
## Development Environment
### Prerequisites
- [Docker](https://www.docker.com/products/docker-desktop)
- [Node.js](https://nodejs.org/en/download) (version 20+)
- Create a local copy of this Git repository (clone)
- Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`)
### Setup
1. Run `npm install`
1. Run `docker compose --env-file ./.env -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
1. Run `npm run database:setup` to initialize the database schema
1. Run `git config core.hooksPath ./git-hooks/` to setup git hooks
1. Start the server and the client (see [_Development_](#Development))
1. Open https://localhost:4200/en in your browser
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
### Start Server
#### Debug
Run `npm run watch:server` and click _Debug API_ in [Visual Studio Code](https://code.visualstudio.com)
#### Serve
Run `npm run start:server`
### Start Client
Run `npm run start:client` and open https://localhost:4200/en in your browser
### Start _Storybook_
Run `npm run start:storybook`
### Migrate Database
With the following command you can keep your database schema in sync:
```bash
npm run database:push
```
## Testing
Run `npm test`
## Experimental Features ## Experimental Features
New functionality can be enabled using a feature flag switch from the user settings. New functionality can be enabled using a feature flag switch from the user settings.

47
README.md

@ -146,52 +146,7 @@ Ghostfolio is available for various home server systems, including [CasaOS](http
## Development ## Development
### Prerequisites For detailed information on the environment setup and development process, please refer to [DEVELOPMENT.md](./DEVELOPMENT.md).
- [Docker](https://www.docker.com/products/docker-desktop)
- [Node.js](https://nodejs.org/en/download) (version 20+)
- Create a local copy of this Git repository (clone)
- Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`)
### Setup
1. Run `npm install`
1. Run `docker compose --env-file ./.env -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
1. Run `npm run database:setup` to initialize the database schema
1. Run `git config core.hooksPath ./git-hooks/` to setup git hooks
1. Start the server and the client (see [_Development_](#Development))
1. Open https://localhost:4200/en in your browser
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
### Start Server
#### Debug
Run `npm run watch:server` and click _Debug API_ in [Visual Studio Code](https://code.visualstudio.com)
#### Serve
Run `npm run start:server`
### Start Client
Run `npm run start:client` and open https://localhost:4200/en in your browser
### Start _Storybook_
Run `npm run start:storybook`
### Migrate Database
With the following command you can keep your database schema in sync:
```bash
npm run database:push
```
## Testing
Run `npm test`
## Public API ## Public API

17
apps/api/src/app/portfolio/rules.service.ts

@ -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()
};
}
});
} }
} }

6
apps/api/src/app/user/update-user-setting.dto.ts

@ -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;
} }

11
apps/api/src/app/user/user.controller.ts

@ -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';
@ -149,10 +149,11 @@ export class UserController {
'filters.assetClasses' in data || 'filters.assetClasses' in data ||
'filters.tags' in data; 'filters.tags' in data;
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) {

12
apps/api/src/app/user/user.service.ts

@ -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) {

2
apps/api/src/models/rules/account-cluster-risk/current-investment.ts

@ -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
}; };
} }

2
apps/api/src/models/rules/account-cluster-risk/single-account.ts

@ -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
}; };
} }
} }

2
apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts

@ -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
}; };
} }
} }

2
apps/api/src/models/rules/currency-cluster-risk/current-investment.ts

@ -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
}; };
} }

2
apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts

@ -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
}; };
} }

2
apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts

@ -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
}; };
} }

30
apps/client/src/app/components/rule/rule.component.html

@ -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>

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

@ -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);
}
} }

9
apps/client/src/app/components/rule/rule.module.ts

@ -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 {}

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

@ -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>

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

@ -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);
}
} }

9
apps/client/src/app/components/rules/rules.module.ts

@ -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 {}

98
apps/client/src/app/pages/portfolio/fire/fire-page.component.ts

@ -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;
}
} }

49
apps/client/src/app/pages/portfolio/fire/fire-page.html

@ -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>

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

6
libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts

@ -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;
} }

4
libs/common/src/lib/interfaces/user-settings.interface.ts

@ -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;
} }

21
libs/common/src/lib/personal-finance-tools.ts

@ -62,6 +62,17 @@ export const personalFinanceTools: Product[] = [
pricingPerYear: '$100', pricingPerYear: '$100',
slogan: 'Stock Portfolio Tracker for Smart Investors' slogan: 'Stock Portfolio Tracker for Smart Investors'
}, },
{
founded: 2013,
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'cointracking',
languages: ['Deutsch', 'English'],
name: 'CoinTracking',
origin: 'Germany',
pricingPerYear: '$120',
slogan: 'The leading Crypto Portfolio Tracker & Tax Calculator'
},
{ {
founded: 2022, founded: 2022,
hasFreePlan: true, hasFreePlan: true,
@ -657,5 +668,15 @@ export const personalFinanceTools: Product[] = [
origin: 'United States', origin: 'United States',
pricingPerYear: '$99', pricingPerYear: '$99',
slogan: 'Change Your Relationship With Money' slogan: 'Change Your Relationship With Money'
},
{
founded: 2019,
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'ziggma',
name: 'Ziggma',
origin: 'United States',
pricingPerYear: '$90',
slogan: 'Your solution for investing success'
} }
]; ];

4
libs/common/src/lib/types/index.ts

@ -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

@ -0,0 +1,12 @@
export type XRayRulesSettings = {
AccountClusterRiskCurrentInvestment?: RuleSettings;
AccountClusterRiskSingleAccount?: RuleSettings;
CurrencyClusterRiskBaseCurrencyCurrentInvestment?: RuleSettings;
CurrencyClusterRiskCurrentInvestment?: RuleSettings;
EmergencyFundSetup?: RuleSettings;
FeeRatioInitialInvestment?: RuleSettings;
};
interface RuleSettings {
isActive: boolean;
}

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.104.1", "version": "2.105.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",

Loading…
Cancel
Save