Browse Source

Merge branch 'main' into task/extract-non-investment-activity-types-constant

pull/7535/head
Thomas Kaul 4 weeks ago
parent
commit
b82c1a5a0b
  1. 6
      CHANGELOG.md
  2. 30
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 8
      apps/api/src/app/user/user.service.ts
  4. 13
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts
  5. 3
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  6. 2
      libs/common/src/lib/interfaces/portfolio-position.interface.ts
  7. 5
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.util.ts

6
CHANGELOG.md

@ -9,9 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Extended the support of the _Exclude from Analysis_ tag from accounts to activities
- Optimized the performance of the search in the assistant by reusing the cached portfolio snapshot
- Improved the validation of the import functionality when referencing an asset profile with the data source `MANUAL` - Improved the validation of the import functionality when referencing an asset profile with the data source `MANUAL`
- Improved the validation of the endpoint to add a custom asset profile in the admin control panel - Improved the validation of the endpoint to add a custom asset profile in the admin control panel
### Fixed
- Fixed the fuzzy search for the holdings in the assistant
## 3.41.0 - 2026-08-03 ## 3.41.0 - 2026-08-03
### Added ### Added

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

@ -165,6 +165,10 @@ export class PortfolioService {
}; };
} }
const filtersWithoutSearchQueryFilter = filters?.filter(({ type }) => {
return type !== 'SEARCH_QUERY';
});
const [accounts, details, user] = await Promise.all([ const [accounts, details, user] = await Promise.all([
this.accountService.accounts({ this.accountService.accounts({
where, where,
@ -176,8 +180,8 @@ export class PortfolioService {
orderBy: { name: 'asc' } orderBy: { name: 'asc' }
}), }),
this.getDetails({ this.getDetails({
filters,
withExcludedAccounts, withExcludedAccounts,
filters: filtersWithoutSearchQueryFilter,
impersonationId: userId, impersonationId: userId,
userId: this.request.user.id userId: this.request.user.id
}), }),
@ -369,14 +373,6 @@ export class PortfolioService {
userId: string; userId: string;
}) { }) {
userId = await this.getUserId(impersonationId, userId); userId = await this.getUserId(impersonationId, userId);
const { holdings: holdingsMap } = await this.getDetails({
dateRange,
filters,
impersonationId,
userId
});
let holdings = Object.values(holdingsMap);
const { SEARCH_QUERY: [filterBySearchQuery] = [] } = groupBy( const { SEARCH_QUERY: [filterBySearchQuery] = [] } = groupBy(
filters, filters,
@ -385,9 +381,22 @@ export class PortfolioService {
} }
); );
const filtersWithoutSearchQueryFilter = filters?.filter(({ type }) => {
return type !== 'SEARCH_QUERY';
});
const { holdings: holdingsMap } = await this.getDetails({
dateRange,
impersonationId,
userId,
filters: filtersWithoutSearchQueryFilter
});
let holdings = Object.values(holdingsMap);
if (filterBySearchQuery) { if (filterBySearchQuery) {
const fuse = new Fuse(holdings, { const fuse = new Fuse(holdings, {
keys: ['isin', 'name', 'symbol'], keys: ['assetProfile.isin', 'assetProfile.name', 'assetProfile.symbol'],
threshold: 0.3 threshold: 0.3
}); });
@ -651,6 +660,7 @@ export class PortfolioService {
}; };
} }
), ),
isin: assetProfile.isin,
name: assetProfile.name, name: assetProfile.name,
sectors: assetProfile.sectors, sectors: assetProfile.sectors,
symbol: assetProfile.symbol, symbol: assetProfile.symbol,

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

@ -186,15 +186,15 @@ export class UserService {
systemMessage = systemMessageProperty; systemMessage = systemMessageProperty;
} }
let tags = tagsForUser.filter((tag) => { let tags = tagsForUser;
return tag.id !== TAG_ID_EXCLUDE_FROM_ANALYSIS;
});
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
subscription.type === SubscriptionType.Basic subscription.type === SubscriptionType.Basic
) { ) {
tags = []; tags = tags.filter(({ id }) => {
return id === TAG_ID_EXCLUDE_FROM_ANALYSIS;
});
} }
return { return {

13
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts

@ -1,5 +1,4 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config';
import { CreateAccountDto, UpdateAccountDto } from '@ghostfolio/common/dtos'; import { CreateAccountDto, UpdateAccountDto } from '@ghostfolio/common/dtos';
import { getStringOrNull } from '@ghostfolio/common/helper'; import { getStringOrNull } from '@ghostfolio/common/helper';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -86,19 +85,13 @@ export class GfCreateOrUpdateAccountDialogComponent {
permissions.createOwnTag permissions.createOwnTag
); );
this.tagsAvailable = [ this.tagsAvailable =
...(this.data.user?.tags ?? []), this.data.user?.tags?.map((tag) => {
{
id: TAG_ID_EXCLUDE_FROM_ANALYSIS,
name: 'EXCLUDE_FROM_ANALYSIS',
userId: null
}
].map((tag) => {
return { return {
...tag, ...tag,
name: translate(tag.name) name: translate(tag.name)
}; };
}); }) ?? [];
this.accountForm = this.formBuilder.group({ this.accountForm = this.formBuilder.group({
accountId: [{ disabled: true, value: this.data.account.id }], accountId: [{ disabled: true, value: this.data.account.id }],

3
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -95,7 +95,7 @@ export class GfAllocationsPageComponent implements OnInit {
| 'assetSubClassLabel' | 'assetSubClassLabel'
| 'currency' | 'currency'
| 'name' | 'name'
> & { etfProvider: string; exchange?: string; value: number }; > & { etfProvider: string; value: number };
}; };
protected isLoading = false; protected isLoading = false;
protected markets: PortfolioDetails['markets']; protected markets: PortfolioDetails['markets'];
@ -381,7 +381,6 @@ export class GfAllocationsPageComponent implements OnInit {
assetSubClass: position.assetProfile.assetSubClass, assetSubClass: position.assetProfile.assetSubClass,
name: position.assetProfile.name name: position.assetProfile.name
}), }),
exchange: position.exchange,
name: position.assetProfile.name, name: position.assetProfile.name,
value: this.showValuesInPercentage() value: this.showValuesInPercentage()
? position.allocationInPercentage ? position.allocationInPercentage

2
libs/common/src/lib/interfaces/portfolio-position.interface.ts

@ -15,6 +15,7 @@ export interface PortfolioPosition {
| 'currency' | 'currency'
| 'dataSource' | 'dataSource'
| 'holdings' | 'holdings'
| 'isin'
| 'name' | 'name'
| 'sectors' | 'sectors'
| 'symbol' | 'symbol'
@ -25,7 +26,6 @@ export interface PortfolioPosition {
}; };
dateOfFirstActivity: Date; dateOfFirstActivity: Date;
dividend: number; dividend: number;
exchange?: string;
grossPerformance: number; grossPerformance: number;
grossPerformancePercent: number; grossPerformancePercent: number;
grossPerformancePercentWithCurrencyEffect: number; grossPerformancePercentWithCurrencyEffect: number;

5
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.util.ts

@ -1,3 +1,4 @@
import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
@ -105,8 +106,8 @@ export function getTagFilters(
): Filter[] { ): Filter[] {
return ( return (
tags tags
?.filter(({ isUsed }) => { ?.filter(({ id, isUsed }) => {
return isUsed; return id !== TAG_ID_EXCLUDE_FROM_ANALYSIS && isUsed;
}) })
?.map(({ id, name }) => { ?.map(({ id, name }) => {
return { return {

Loading…
Cancel
Save