diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d3af3346e..959f6f09ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the label of the cash positions in the holdings charts and table - Upgraded `@openrouter/ai-sdk-provider` from version `3.0.0` to `3.1.0` - Upgraded `ai` from version `7.0.37` to `7.0.114` - Upgraded `dotenv` from version `17.4.2` to `18.0.3` diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index cbdacaa3f1..915c6833ab 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -11,6 +11,7 @@ import { convertValuesToPercentagesOfTotal, getAssetProfileIdentifier, getCountryName, + getHoldingName, isCashPosition } from '@ghostfolio/common/helper'; import { @@ -533,7 +534,7 @@ export class GfAllocationsPageComponent implements OnInit { value, dataSource: position.assetProfile.dataSource, isClickable: canOpenHoldingDetail(position), - name: position.assetProfile.name ?? '' + name: getHoldingName(position.assetProfile) }; } } diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index 46606753e4..8fa8abde18 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -3,6 +3,7 @@ import { convertValuesToPercentagesOfTotal, getAssetProfileIdentifier, getCountryName, + getHoldingName, isCashPosition } from '@ghostfolio/common/helper'; import { @@ -260,7 +261,7 @@ export class GfPublicPageComponent implements OnInit { this.symbols[symbol] = { symbol, value, - name: position.assetProfile.name ?? symbol + name: getHoldingName(position.assetProfile) }; } } diff --git a/libs/common/src/lib/helper.spec.ts b/libs/common/src/lib/helper.spec.ts index b7e7635670..71b934628c 100644 --- a/libs/common/src/lib/helper.spec.ts +++ b/libs/common/src/lib/helper.spec.ts @@ -7,6 +7,7 @@ import { extractNumberFromString, getCountryCodeFromCurrency, getEmojiFlag, + getHoldingName, getNumberFormatGroup, getStringOrNull, getStringOrUndefined, @@ -143,6 +144,52 @@ describe('Helper', () => { }); }); + describe('Get holding name', () => { + it('Cash position', () => { + expect( + getHoldingName({ + assetSubClass: 'CASH', + assetSubClassLabel: 'Cash', + name: 'CHF', + symbol: 'CHF' + }) + ).toEqual('Cash (CHF)'); + }); + + it('Cash position without asset sub class label', () => { + expect( + getHoldingName({ + assetSubClass: 'CASH', + assetSubClassLabel: undefined, + name: 'CHF', + symbol: 'CHF' + }) + ).toEqual('CHF'); + }); + + it('Holding with name', () => { + expect( + getHoldingName({ + assetSubClass: 'STOCK', + assetSubClassLabel: 'Stock', + name: 'Apple Inc.', + symbol: 'AAPL' + }) + ).toEqual('Apple Inc.'); + }); + + it('Holding without name', () => { + expect( + getHoldingName({ + assetSubClass: 'STOCK', + assetSubClassLabel: 'Stock', + name: undefined, + symbol: 'AAPL' + }) + ).toEqual('AAPL'); + }); + }); + describe('Get number format group', () => { let languageGetter: jest.SpyInstance; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 2a76261dae..7e4e13de7e 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -453,6 +453,22 @@ export function getEmojiFlag(aCountryCode: string) { }); } +export function getHoldingName({ + assetSubClass, + assetSubClassLabel, + name, + symbol +}: Pick< + PortfolioPosition['assetProfile'], + 'assetSubClass' | 'assetSubClassLabel' | 'name' | 'symbol' +>) { + if (isCashPosition({ assetSubClass }) && assetSubClassLabel) { + return `${assetSubClassLabel} (${symbol})`; + } + + return name ?? symbol; +} + export function getLocale() { return navigator.language ?? DEFAULT_LOCALE; } diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.html b/libs/ui/src/lib/holdings-table/holdings-table.component.html index 2001a1b990..e1c012ab15 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.html +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -20,7 +20,7 @@ [countryCode]="getCountryCodeForCashPosition(element)" [dataSource]="element.assetProfile.dataSource" [symbol]="element.assetProfile.symbol" - [tooltip]="element.assetProfile.name" + [tooltip]="getHoldingName(element.assetProfile)" /> @@ -40,9 +40,14 @@ mat-cell [class.w-100]="mode() === 'simple'" > -
- {{ element.assetProfile.name }} - @if (element.assetProfile.name === element.assetProfile.symbol) { + @let holdingName = getHoldingName(element.assetProfile); + +
+ {{ holdingName }} + @if ( + !isCashPosition(element.assetProfile) && + element.assetProfile.name === element.assetProfile.symbol + ) { ({{ element.assetProfile.assetSubClassLabel }}) }
diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.ts b/libs/ui/src/lib/holdings-table/holdings-table.component.ts index eac1c9d918..1894260b78 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.ts +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.ts @@ -1,6 +1,7 @@ import { canOpenHoldingDetail, getCountryCodeFromCurrency, + getHoldingName, getLocale, getLowercase, isCashPosition @@ -89,6 +90,9 @@ export class GfHoldingsTableComponent { return columns; }); + protected readonly getHoldingName = getHoldingName; + protected readonly isCashPosition = isCashPosition; + protected readonly isLoading = computed(() => { return !this.holdings(); }); @@ -104,7 +108,13 @@ export class GfHoldingsTableComponent { }); public constructor() { - this.dataSource.sortingDataAccessor = getLowercase; + this.dataSource.sortingDataAccessor = (holding, path) => { + if (path === 'assetProfile.name') { + return getHoldingName(holding.assetProfile).toLocaleLowerCase(); + } + + return getLowercase(holding, path) as number | string; + }; // Reactive data update effect(() => { diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 83b9916c6e..01037f77f4 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -3,7 +3,11 @@ import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; -import { canOpenHoldingDetail, getLocale } from '@ghostfolio/common/helper'; +import { + canOpenHoldingDetail, + getHoldingName, + getLocale +} from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, PortfolioPosition @@ -399,20 +403,18 @@ export class GfTreemapChartComponent const raw = context.raw as GfTreemapDataPoint; const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`; - const name = raw._data.assetProfile.name; + const name = getHoldingName(raw._data.assetProfile); const sign = raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''; - const symbol = raw._data.assetProfile.symbol; - const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`; if (raw._data.valueInBaseCurrency !== null) { const value = raw._data.valueInBaseCurrency; return [ - `${name ?? symbol} (${allocationInPercentage})`, + `${name} (${allocationInPercentage})`, `${value?.toLocaleString(this.locale(), { maximumFractionDigits: 2, minimumFractionDigits: 2 @@ -429,7 +431,7 @@ export class GfTreemapChartComponent ]; } else { return [ - `${name ?? symbol} (${allocationInPercentage})`, + `${name} (${allocationInPercentage})`, '', $localize`Performance`, netPerformanceInPercentageWithSign