Browse Source

Merge branch 'main' into fix/GF-2782

pull/3852/head
Thomas Kaul 11 months ago
committed by GitHub
parent
commit
704a8a982b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      CHANGELOG.md
  2. 42
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  3. 30
      apps/api/src/app/portfolio/portfolio.service.ts
  4. 1
      apps/api/src/app/user/update-user-setting.dto.ts
  5. 8
      apps/api/src/models/rules/account-cluster-risk/current-investment.ts
  6. 4
      apps/api/src/models/rules/account-cluster-risk/single-account.ts
  7. 6
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  8. 8
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  9. 10
      apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts
  10. 8
      apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts
  11. 2
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  12. 3
      apps/client/src/app/components/home-holdings/home-holdings.html
  13. 2
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts
  14. 32
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
  15. 2
      apps/client/src/app/components/rule/rule.component.html
  16. 17
      apps/client/src/app/components/rule/rule.component.ts
  17. 12
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  18. 1
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  19. 2
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  20. 8
      apps/client/src/locales/messages.ca.xlf
  21. 8
      apps/client/src/locales/messages.de.xlf
  22. 8
      apps/client/src/locales/messages.es.xlf
  23. 8
      apps/client/src/locales/messages.fr.xlf
  24. 8
      apps/client/src/locales/messages.it.xlf
  25. 8
      apps/client/src/locales/messages.nl.xlf
  26. 8
      apps/client/src/locales/messages.pl.xlf
  27. 8
      apps/client/src/locales/messages.pt.xlf
  28. 8
      apps/client/src/locales/messages.tr.xlf
  29. 7
      apps/client/src/locales/messages.xlf
  30. 8
      apps/client/src/locales/messages.zh.xlf
  31. 4
      libs/common/src/lib/types/x-ray-rules-settings.type.ts
  32. 18
      libs/ui/src/lib/activities-table/activities-table.component.ts
  33. 14
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  34. 6
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html
  35. 6
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

7
CHANGELOG.md

@ -7,12 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added a message to the search asset component if no results have been found in the create or update activity dialog
- Added support to customize the rule thresholds in the _X-ray_ section (experimental)
### Changed ### Changed
- Optimized the portfolio calculations with smarter date interval selection
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)
### Fixed ### Fixed
- Fixed an issue in the calculation of allocations by market (_Unknown_)
- Fixed the `eslint` configuration - Fixed the `eslint` configuration
## 2.111.0 - 2024-09-28 ## 2.111.0 - 2024-09-28

42
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

@ -16,13 +16,14 @@ import {
addDays, addDays,
addMilliseconds, addMilliseconds,
differenceInDays, differenceInDays,
eachDayOfInterval,
format, format,
isBefore isBefore
} from 'date-fns'; } from 'date-fns';
import { cloneDeep, first, last, sortBy } from 'lodash'; import { cloneDeep, first, last, sortBy } from 'lodash';
export class TWRPortfolioCalculator extends PortfolioCalculator { export class TWRPortfolioCalculator extends PortfolioCalculator {
private chartDatesDescending: string[];
protected calculateOverallPerformance( protected calculateOverallPerformance(
positions: TimelinePosition[] positions: TimelinePosition[]
): PortfolioSnapshot { ): PortfolioSnapshot {
@ -820,31 +821,35 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
startDate = start; startDate = start;
} }
const endDateString = format(endDate, DATE_FORMAT);
const startDateString = format(startDate, DATE_FORMAT);
const currentValuesAtDateRangeStartWithCurrencyEffect = const currentValuesAtDateRangeStartWithCurrencyEffect =
currentValuesWithCurrencyEffect[format(startDate, DATE_FORMAT)] ?? currentValuesWithCurrencyEffect[startDateString] ?? new Big(0);
new Big(0);
const investmentValuesAccumulatedAtStartDateWithCurrencyEffect = const investmentValuesAccumulatedAtStartDateWithCurrencyEffect =
investmentValuesAccumulatedWithCurrencyEffect[ investmentValuesAccumulatedWithCurrencyEffect[startDateString] ??
format(startDate, DATE_FORMAT) new Big(0);
] ?? new Big(0);
const grossPerformanceAtDateRangeStartWithCurrencyEffect = const grossPerformanceAtDateRangeStartWithCurrencyEffect =
currentValuesAtDateRangeStartWithCurrencyEffect.minus( currentValuesAtDateRangeStartWithCurrencyEffect.minus(
investmentValuesAccumulatedAtStartDateWithCurrencyEffect investmentValuesAccumulatedAtStartDateWithCurrencyEffect
); );
const dates = eachDayOfInterval({
end: endDate,
start: startDate
}).map((date) => {
return format(date, DATE_FORMAT);
});
let average = new Big(0); let average = new Big(0);
let dayCount = 0; let dayCount = 0;
for (const date of dates) { if (!this.chartDatesDescending) {
this.chartDatesDescending = Object.keys(chartDateMap).sort().reverse();
}
for (const date of this.chartDatesDescending) {
if (date > endDateString) {
continue;
} else if (date < startDateString) {
break;
}
if ( if (
investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big &&
investmentValuesAccumulatedWithCurrencyEffect[date].gt(0) investmentValuesAccumulatedWithCurrencyEffect[date].gt(0)
@ -864,17 +869,14 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
} }
netPerformanceWithCurrencyEffectMap[dateRange] = netPerformanceWithCurrencyEffectMap[dateRange] =
netPerformanceValuesWithCurrencyEffect[ netPerformanceValuesWithCurrencyEffect[endDateString]?.minus(
format(endDate, DATE_FORMAT)
]?.minus(
// If the date range is 'max', take 0 as a start value. Otherwise, // If the date range is 'max', take 0 as a start value. Otherwise,
// the value of the end of the day of the start date is taken which // the value of the end of the day of the start date is taken which
// differs from the buying price. // differs from the buying price.
dateRange === 'max' dateRange === 'max'
? new Big(0) ? new Big(0)
: (netPerformanceValuesWithCurrencyEffect[ : (netPerformanceValuesWithCurrencyEffect[startDateString] ??
format(startDate, DATE_FORMAT) new Big(0))
] ?? new Big(0))
) ?? new Big(0); ) ?? new Big(0);
netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0) netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0)

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

@ -470,8 +470,7 @@ export class PortfolioService {
if (withMarkets) { if (withMarkets) {
({ markets, marketsAdvanced } = this.getMarkets({ ({ markets, marketsAdvanced } = this.getMarkets({
assetProfile, assetProfile
valueInBaseCurrency
})); }));
} }
@ -1433,11 +1432,9 @@ export class PortfolioService {
} }
private getMarkets({ private getMarkets({
assetProfile, assetProfile
valueInBaseCurrency
}: { }: {
assetProfile: EnhancedSymbolProfile; assetProfile: EnhancedSymbolProfile;
valueInBaseCurrency: Big;
}) { }) {
const markets = { const markets = {
[UNKNOWN_KEY]: 0, [UNKNOWN_KEY]: 0,
@ -1499,16 +1496,23 @@ export class PortfolioService {
.toNumber(); .toNumber();
} }
} }
} else {
markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY])
.plus(valueInBaseCurrency)
.toNumber();
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY])
.plus(valueInBaseCurrency)
.toNumber();
} }
markets[UNKNOWN_KEY] = new Big(1)
.minus(markets.developedMarkets)
.minus(markets.emergingMarkets)
.minus(markets.otherMarkets)
.toNumber();
marketsAdvanced[UNKNOWN_KEY] = new Big(1)
.minus(marketsAdvanced.asiaPacific)
.minus(marketsAdvanced.emergingMarkets)
.minus(marketsAdvanced.europe)
.minus(marketsAdvanced.japan)
.minus(marketsAdvanced.northAmerica)
.minus(marketsAdvanced.otherMarkets)
.toNumber();
return { markets, marketsAdvanced }; return { markets, marketsAdvanced };
} }

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

@ -1,4 +1,5 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code'; import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
import type { import type {
ColorScheme, ColorScheme,
DateRange, DateRange,

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

@ -76,11 +76,11 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
}; };
} }
public getSettings(aUserSettings: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency: aUserSettings.baseCurrency, baseCurrency,
isActive: aUserSettings.xRayRules[this.getKey()].isActive, isActive: xRayRules[this.getKey()].isActive,
thresholdMax: 0.5 thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.5
}; };
} }
} }

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

@ -34,9 +34,9 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
}; };
} }
public getSettings(aUserSettings: UserSettings): RuleSettings { public getSettings({ xRayRules }: UserSettings): RuleSettings {
return { return {
isActive: aUserSettings.xRayRules[this.getKey()].isActive isActive: xRayRules[this.getKey()].isActive
}; };
} }
} }

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

@ -62,10 +62,10 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
}; };
} }
public getSettings(aUserSettings: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency: aUserSettings.baseCurrency, baseCurrency,
isActive: aUserSettings.xRayRules[this.getKey()].isActive isActive: xRayRules[this.getKey()].isActive
}; };
} }
} }

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

@ -62,11 +62,11 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
}; };
} }
public getSettings(aUserSettings: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency: aUserSettings.baseCurrency, baseCurrency,
isActive: aUserSettings.xRayRules[this.getKey()].isActive, isActive: xRayRules[this.getKey()].isActive,
thresholdMax: 0.5 thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.5
}; };
} }
} }

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

@ -19,7 +19,7 @@ export class EmergencyFundSetup extends Rule<Settings> {
} }
public evaluate(ruleSettings: Settings) { public evaluate(ruleSettings: Settings) {
if (this.emergencyFund < ruleSettings.thresholdMin) { if (!this.emergencyFund) {
return { return {
evaluation: 'No emergency fund has been set up', evaluation: 'No emergency fund has been set up',
value: false value: false
@ -32,16 +32,14 @@ export class EmergencyFundSetup extends Rule<Settings> {
}; };
} }
public getSettings(aUserSettings: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency: aUserSettings.baseCurrency, baseCurrency,
isActive: aUserSettings.xRayRules[this.getKey()].isActive, isActive: xRayRules[this.getKey()].isActive
thresholdMin: 0
}; };
} }
} }
interface Settings extends RuleSettings { interface Settings extends RuleSettings {
baseCurrency: string; baseCurrency: string;
thresholdMin: number;
} }

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

@ -43,11 +43,11 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
}; };
} }
public getSettings(aUserSettings: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency: aUserSettings.baseCurrency, baseCurrency,
isActive: aUserSettings.xRayRules[this.getKey()].isActive, isActive: xRayRules[this.getKey()].isActive,
thresholdMax: 0.01 thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.01
}; };
} }
} }

2
apps/client/src/app/components/home-holdings/home-holdings.component.ts

@ -111,7 +111,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
this.initialize(); this.initialize();
} }
public onSymbolClicked({ dataSource, symbol }: AssetProfileIdentifier) { public onHoldingClicked({ dataSource, symbol }: AssetProfileIdentifier) {
if (dataSource && symbol) { if (dataSource && symbol) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true } queryParams: { dataSource, symbol, holdingDetailDialog: true }

3
apps/client/src/app/components/home-holdings/home-holdings.html

@ -40,7 +40,7 @@
cursor="pointer" cursor="pointer"
[dateRange]="user?.settings?.dateRange" [dateRange]="user?.settings?.dateRange"
[holdings]="holdings" [holdings]="holdings"
(treemapChartClicked)="onSymbolClicked($event)" (treemapChartClicked)="onHoldingClicked($event)"
/> />
} }
<div [ngClass]="{ 'd-none': viewModeFormControl.value !== 'TABLE' }"> <div [ngClass]="{ 'd-none': viewModeFormControl.value !== 'TABLE' }">
@ -50,6 +50,7 @@
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder" [hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
[holdings]="holdings" [holdings]="holdings"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
(holdingClicked)="onHoldingClicked($event)"
/> />
@if (hasPermissionToCreateOrder && holdings?.length > 0) { @if (hasPermissionToCreateOrder && holdings?.length > 0) {
<div class="text-center"> <div class="text-center">

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

@ -2,6 +2,7 @@ import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { import {
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
@ -16,6 +17,7 @@ import { IRuleSettingsDialogParams } from './interfaces/interfaces';
@Component({ @Component({
imports: [ imports: [
CommonModule, CommonModule,
FormsModule,
MatButtonModule, MatButtonModule,
MatDialogModule, MatDialogModule,
MatFormFieldModule, MatFormFieldModule,

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

@ -1,23 +1,37 @@
<div mat-dialog-title>{{ data.rule.name }}</div> <div mat-dialog-title>{{ data.rule.name }}</div>
<div class="py-3" mat-dialog-content> <div class="py-3" mat-dialog-content>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field
appearance="outline"
class="w-100"
[ngClass]="{ 'd-none': settings.thresholdMin === undefined }"
>
<mat-label i18n>Threshold Min</mat-label> <mat-label i18n>Threshold Min</mat-label>
<input matInput name="thresholdMin" type="number" /> <input
matInput
name="thresholdMin"
type="number"
[(ngModel)]="settings.thresholdMin"
/>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field
appearance="outline"
class="w-100"
[ngClass]="{ 'd-none': settings.thresholdMax === undefined }"
>
<mat-label i18n>Threshold Max</mat-label> <mat-label i18n>Threshold Max</mat-label>
<input matInput name="thresholdMax" type="number" /> <input
matInput
name="thresholdMax"
type="number"
[(ngModel)]="settings.thresholdMax"
/>
</mat-form-field> </mat-form-field>
</div> </div>
<div align="end" mat-dialog-actions> <div align="end" mat-dialog-actions>
<button i18n mat-button (click)="dialogRef.close()">Close</button> <button i18n mat-button (click)="dialogRef.close()">Close</button>
<button <button color="primary" mat-flat-button (click)="dialogRef.close(settings)">
color="primary"
mat-flat-button
(click)="dialogRef.close({ settings })"
>
<ng-container i18n>Save</ng-container> <ng-container i18n>Save</ng-container>
</button> </button>
</div> </div>

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

@ -62,7 +62,7 @@
<ion-icon name="ellipsis-horizontal" /> <ion-icon name="ellipsis-horizontal" />
</button> </button>
<mat-menu #rulesMenu="matMenu" xPosition="before"> <mat-menu #rulesMenu="matMenu" xPosition="before">
@if (rule?.isActive && !isEmpty(rule.settings) && false) { @if (rule?.isActive && !isEmpty(rule.settings)) {
<button mat-menu-item (click)="onCustomizeRule(rule)"> <button mat-menu-item (click)="onCustomizeRule(rule)">
<ng-container i18n>Customize</ng-container>... <ng-container i18n>Customize</ng-container>...
</button> </button>

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

@ -55,16 +55,15 @@ export class RuleComponent implements OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe( .subscribe((settings: PortfolioReportRule['settings']) => {
({ settings }: { settings: PortfolioReportRule['settings'] }) => { if (settings) {
if (settings) { this.ruleUpdated.emit({
console.log(settings); xRayRules: {
[rule.key]: settings
// TODO }
// this.ruleUpdated.emit(settings); });
}
} }
); });
} }
public onUpdateRule(rule: PortfolioReportRule) { public onUpdateRule(rule: PortfolioReportRule) {

12
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -7,7 +7,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { downloadAsFile } from '@ghostfolio/common/helper'; import { downloadAsFile } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier, 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';
@ -138,6 +138,16 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
this.fetchActivities(); this.fetchActivities();
} }
public onClickActivity({ dataSource, symbol }: AssetProfileIdentifier) {
this.router.navigate([], {
queryParams: {
dataSource,
symbol,
holdingDetailDialog: true
}
});
}
public onCloneActivity(aActivity: Activity) { public onCloneActivity(aActivity: Activity) {
this.openCreateActivityDialog(aActivity); this.openCreateActivityDialog(aActivity);
} }

1
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -21,6 +21,7 @@
[sortDirection]="sortDirection" [sortDirection]="sortDirection"
[totalItems]="totalItems" [totalItems]="totalItems"
(activitiesDeleted)="onDeleteActivities()" (activitiesDeleted)="onDeleteActivities()"
(activityClicked)="onClickActivity($event)"
(activityDeleted)="onDeleteActivity($event)" (activityDeleted)="onDeleteActivity($event)"
(activityToClone)="onCloneActivity($event)" (activityToClone)="onCloneActivity($event)"
(activityToUpdate)="onUpdateActivity($event)" (activityToUpdate)="onUpdateActivity($event)"

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

@ -134,8 +134,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
} }
public onRulesUpdated(event: UpdateUserSettingDto) { public onRulesUpdated(event: UpdateUserSettingDto) {
this.isLoading = true;
this.dataService this.dataService
.putUserSetting(event) .putUserSetting(event)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

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

@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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

@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="translated">Ups! Es konnten leider keine Assets gefunden werden.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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

@ -7259,6 +7259,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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

@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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

@ -7259,6 +7259,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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

@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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

@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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

@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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

@ -7258,6 +7258,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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

@ -6565,6 +6565,13 @@
<context context-type="linenumber">49</context> <context context-type="linenumber">49</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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

@ -7259,6 +7259,14 @@
<context context-type="linenumber">280</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="b225488f8b209e9704760dc9f5d99845a5d07bf6" datatype="html">
<source>Oops! Could not find any assets.</source>
<target state="new">Oops! Could not find any assets.</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

4
libs/common/src/lib/types/x-ray-rules-settings.type.ts

@ -1,3 +1,5 @@
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
export type XRayRulesSettings = { export type XRayRulesSettings = {
AccountClusterRiskCurrentInvestment?: RuleSettings; AccountClusterRiskCurrentInvestment?: RuleSettings;
AccountClusterRiskSingleAccount?: RuleSettings; AccountClusterRiskSingleAccount?: RuleSettings;
@ -7,6 +9,6 @@ export type XRayRulesSettings = {
FeeRatioInitialInvestment?: RuleSettings; FeeRatioInitialInvestment?: RuleSettings;
}; };
interface RuleSettings { interface RuleSettings extends Pick<PortfolioReportRule, 'settings'> {
isActive: boolean; isActive: boolean;
} }

18
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -42,7 +42,6 @@ import {
} from '@angular/material/sort'; } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { Router, RouterModule } from '@angular/router';
import { isUUID } from 'class-validator'; import { isUUID } from 'class-validator';
import { endOfToday, isAfter } from 'date-fns'; import { endOfToday, isAfter } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -64,8 +63,7 @@ import { Subject, Subscription, takeUntil } from 'rxjs';
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,
MatTooltipModule, MatTooltipModule,
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule
RouterModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-activities-table', selector: 'gf-activities-table',
@ -95,6 +93,7 @@ export class GfActivitiesTableComponent
@Input() totalItems = Number.MAX_SAFE_INTEGER; @Input() totalItems = Number.MAX_SAFE_INTEGER;
@Output() activitiesDeleted = new EventEmitter<void>(); @Output() activitiesDeleted = new EventEmitter<void>();
@Output() activityClicked = new EventEmitter<AssetProfileIdentifier>();
@Output() activityDeleted = new EventEmitter<string>(); @Output() activityDeleted = new EventEmitter<string>();
@Output() activityToClone = new EventEmitter<OrderWithAccount>(); @Output() activityToClone = new EventEmitter<OrderWithAccount>();
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>(); @Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
@ -122,10 +121,7 @@ export class GfActivitiesTableComponent
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(private notificationService: NotificationService) {}
private notificationService: NotificationService,
private router: Router
) {}
public ngOnInit() { public ngOnInit() {
if (this.showCheckbox) { if (this.showCheckbox) {
@ -203,7 +199,7 @@ export class GfActivitiesTableComponent
activity.isDraft === false && activity.isDraft === false &&
['BUY', 'DIVIDEND', 'SELL'].includes(activity.type) ['BUY', 'DIVIDEND', 'SELL'].includes(activity.type)
) { ) {
this.onOpenPositionDialog({ this.activityClicked.emit({
dataSource: activity.SymbolProfile.dataSource, dataSource: activity.SymbolProfile.dataSource,
symbol: activity.SymbolProfile.symbol symbol: activity.SymbolProfile.symbol
}); });
@ -268,12 +264,6 @@ export class GfActivitiesTableComponent
}); });
} }
public onOpenPositionDialog({ dataSource, symbol }: AssetProfileIdentifier) {
this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true }
});
}
public onUpdateActivity(aActivity: OrderWithAccount) { public onUpdateActivity(aActivity: OrderWithAccount) {
this.activityToUpdate.emit(aActivity); this.activityToUpdate.emit(aActivity);
} }

14
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -14,10 +14,12 @@ import {
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
EventEmitter,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@ -25,7 +27,6 @@ import { MatDialogModule } from '@angular/material/dialog';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { Router, RouterModule } from '@angular/router';
import { AssetSubClass } from '@prisma/client'; import { AssetSubClass } from '@prisma/client';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
@ -44,8 +45,7 @@ import { Subject, Subscription } from 'rxjs';
MatPaginatorModule, MatPaginatorModule,
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule
RouterModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-holdings-table', selector: 'gf-holdings-table',
@ -63,6 +63,8 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy {
@Input() locale = getLocale(); @Input() locale = getLocale();
@Input() pageSize = Number.MAX_SAFE_INTEGER; @Input() pageSize = Number.MAX_SAFE_INTEGER;
@Output() holdingClicked = new EventEmitter<AssetProfileIdentifier>();
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ -75,7 +77,7 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor(private router: Router) {} public constructor() {}
public ngOnChanges() { public ngOnChanges() {
this.displayedColumns = ['icon', 'nameWithSymbol', 'dateOfFirstActivity']; this.displayedColumns = ['icon', 'nameWithSymbol', 'dateOfFirstActivity'];
@ -105,9 +107,7 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy {
public onOpenHoldingDialog({ dataSource, symbol }: AssetProfileIdentifier) { public onOpenHoldingDialog({ dataSource, symbol }: AssetProfileIdentifier) {
if (this.hasPermissionToOpenDetails) { if (this.hasPermissionToOpenDetails) {
this.router.navigate([], { this.holdingClicked.emit({ dataSource, symbol });
queryParams: { dataSource, symbol, holdingDetailDialog: true }
});
} }
} }

6
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html

@ -31,6 +31,12 @@
} }
</small> </small>
</mat-option> </mat-option>
} @empty {
@if (control.value?.length > 1) {
<mat-option class="line-height-1" disabled="true" i18n
>Oops! Could not find any assets.
</mat-option>
}
} }
} }
</mat-autocomplete> </mat-autocomplete>

6
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

@ -117,17 +117,17 @@ export class GfSymbolAutocompleteComponent
this.control.valueChanges this.control.valueChanges
.pipe( .pipe(
debounceTime(400),
distinctUntilChanged(),
filter((query) => { filter((query) => {
return isString(query) && query.length > 1; return isString(query) && query.length > 1;
}), }),
takeUntil(this.unsubscribeSubject),
tap(() => { tap(() => {
this.isLoading = true; this.isLoading = true;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}), }),
debounceTime(400),
distinctUntilChanged(),
takeUntil(this.unsubscribeSubject),
switchMap((query: string) => { switchMap((query: string) => {
return this.dataService.fetchSymbols({ return this.dataService.fetchSymbols({
query, query,

Loading…
Cancel
Save