Browse Source

Merge remote-tracking branch 'origin/main' into bugfix/improve-portfolio-calc

pull/5130/head
KenTandrian 2 months ago
parent
commit
fdee70f8d8
  1. 12
      CHANGELOG.md
  2. 22
      apps/api/src/app/export/export.service.ts
  3. 14
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  4. 16
      apps/client/src/locales/messages.es.xlf
  5. 6
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts
  6. 4
      libs/ui/src/lib/i18n.ts
  7. 42
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
  8. 2
      prisma/migrations/20250708090630_added_alternative_investment_to_asset_class/migration.sql
  9. 2
      prisma/migrations/20250708090631_added_collectible_to_asset_sub_class/migration.sql
  10. 2
      prisma/schema.prisma

12
CHANGELOG.md

@ -7,9 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added alternative investment as an asset class
- Added collectible as an asset sub class
### Changed ### Changed
- Respected the filter by account for accounts when exporting activities on the portfolio activities page
- Improved the label for asset profiles with `MANUAL` data source in the chart of the holdings tab on the home page
- Improved the language localization for Catalan (`ca`) - Improved the language localization for Catalan (`ca`)
- Improved the language localization for Spanish (`es`)
### Fixed
- Fixed the export functionality for accounts without activities
## 2.179.0 - 2025-07-07 ## 2.179.0 - 2025-07-07

22
apps/api/src/app/export/export.service.ts

@ -5,7 +5,8 @@ import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { Filter, Export } from '@ghostfolio/common/interfaces'; import { Filter, Export } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Platform } from '@prisma/client'; import { Platform, Prisma } from '@prisma/client';
import { groupBy } from 'lodash';
@Injectable() @Injectable()
export class ExportService { export class ExportService {
@ -26,6 +27,9 @@ export class ExportService {
userCurrency: string; userCurrency: string;
userId: string; userId: string;
}): Promise<Export> { }): Promise<Export> {
const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => {
return type;
});
const platformsMap: { [platformId: string]: Platform } = {}; const platformsMap: { [platformId: string]: Platform } = {};
let { activities } = await this.orderService.getOrders({ let { activities } = await this.orderService.getOrders({
@ -44,20 +48,30 @@ export class ExportService {
}); });
} }
const where: Prisma.AccountWhereInput = { userId };
if (filtersByAccount?.length > 0) {
where.id = {
in: filtersByAccount.map(({ id }) => {
return id;
})
};
}
const accounts = ( const accounts = (
await this.accountService.accounts({ await this.accountService.accounts({
where,
include: { include: {
balances: true, balances: true,
platform: true platform: true
}, },
orderBy: { orderBy: {
name: 'asc' name: 'asc'
}, }
where: { userId }
}) })
) )
.filter(({ id }) => { .filter(({ id }) => {
return activities.length > 0 return activityIds?.length > 0
? activities.some(({ accountId }) => { ? activities.some(({ accountId }) => {
return accountId === id; return accountId === id;
}) })

14
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -68,16 +68,10 @@ export class AdminMarketDataComponent
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
public activeFilters: Filter[] = []; public activeFilters: Filter[] = [];
public allFilters: Filter[] = [ public allFilters: Filter[] = Object.keys(AssetSubClass)
AssetSubClass.BOND, .filter((assetSubClass) => {
AssetSubClass.COMMODITY, return assetSubClass !== 'CASH';
AssetSubClass.CRYPTOCURRENCY, })
AssetSubClass.ETF,
AssetSubClass.MUTUALFUND,
AssetSubClass.PRECIOUS_METAL,
AssetSubClass.PRIVATE_EQUITY,
AssetSubClass.STOCK
]
.map((assetSubClass) => { .map((assetSubClass) => {
return { return {
id: assetSubClass.toString(), id: assetSubClass.toString(),

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

@ -6527,7 +6527,7 @@
</trans-unit> </trans-unit>
<trans-unit id="2427223107800831324" datatype="html"> <trans-unit id="2427223107800831324" datatype="html">
<source>Italy</source> <source>Italy</source>
<target state="new">Italy</target> <target state="translated">Italia</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">85</context> <context context-type="linenumber">85</context>
@ -6535,7 +6535,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3783587393795767345" datatype="html"> <trans-unit id="3783587393795767345" datatype="html">
<source>Netherlands</source> <source>Netherlands</source>
<target state="new">Netherlands</target> <target state="translated">Países Bajos</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">87</context> <context context-type="linenumber">87</context>
@ -6543,7 +6543,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3591085113786124083" datatype="html"> <trans-unit id="3591085113786124083" datatype="html">
<source>New Zealand</source> <source>New Zealand</source>
<target state="new">New Zealand</target> <target state="translated">Nueva Zelanda</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">88</context> <context context-type="linenumber">88</context>
@ -6551,7 +6551,7 @@
</trans-unit> </trans-unit>
<trans-unit id="2356316679035829946" datatype="html"> <trans-unit id="2356316679035829946" datatype="html">
<source>Poland</source> <source>Poland</source>
<target state="new">Poland</target> <target state="translated">Polonia</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">89</context>
@ -6559,7 +6559,7 @@
</trans-unit> </trans-unit>
<trans-unit id="545992063382313902" datatype="html"> <trans-unit id="545992063382313902" datatype="html">
<source>Romania</source> <source>Romania</source>
<target state="new">Romania</target> <target state="translated">Rumanía</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">90</context> <context context-type="linenumber">90</context>
@ -6567,7 +6567,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8039968326438721789" datatype="html"> <trans-unit id="8039968326438721789" datatype="html">
<source>South Africa</source> <source>South Africa</source>
<target state="new">South Africa</target> <target state="translated">Sudáfrica</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">92</context> <context context-type="linenumber">92</context>
@ -6575,7 +6575,7 @@
</trans-unit> </trans-unit>
<trans-unit id="2040543873210054611" datatype="html"> <trans-unit id="2040543873210054611" datatype="html">
<source>Thailand</source> <source>Thailand</source>
<target state="new">Thailand</target> <target state="translated">Tailandia</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">94</context> <context context-type="linenumber">94</context>
@ -6583,7 +6583,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6133495983093212227" datatype="html"> <trans-unit id="6133495983093212227" datatype="html">
<source>United States</source> <source>United States</source>
<target state="new">United States</target> <target state="translated">Estados Unidos</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">97</context> <context context-type="linenumber">97</context>

6
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts

@ -1,4 +1,5 @@
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { SearchMode } from '@ghostfolio/ui/assistant/enums/search-mode'; import { SearchMode } from '@ghostfolio/ui/assistant/enums/search-mode';
import { import {
IAssetSearchResultItem, IAssetSearchResultItem,
@ -54,13 +55,16 @@ export class GfAssistantListItemComponent
dataSource: this.item?.dataSource, dataSource: this.item?.dataSource,
symbol: this.item?.symbol symbol: this.item?.symbol
}; };
this.routerLink = ['/admin', 'market-data'];
this.routerLink =
internalRoutes.adminControl.subRoutes.marketData.routerLink;
} else if (this.item?.mode === SearchMode.HOLDING) { } else if (this.item?.mode === SearchMode.HOLDING) {
this.queryParams = { this.queryParams = {
dataSource: this.item?.dataSource, dataSource: this.item?.dataSource,
holdingDetailDialog: true, holdingDetailDialog: true,
symbol: this.item?.symbol symbol: this.item?.symbol
}; };
this.routerLink = []; this.routerLink = [];
} else if (this.item?.mode === SearchMode.QUICK_LINK) { } else if (this.item?.mode === SearchMode.QUICK_LINK) {
this.queryParams = {}; this.queryParams = {};

4
libs/ui/src/lib/i18n.ts

@ -41,7 +41,7 @@ const locales = {
SELL: $localize`Sell`, SELL: $localize`Sell`,
// AssetClass (enum) // AssetClass (enum)
CASH: $localize`Cash`, ALTERNATIVE_INVESTMENT: $localize`Alternative Investment`,
COMMODITY: $localize`Commodity`, COMMODITY: $localize`Commodity`,
EQUITY: $localize`Equity`, EQUITY: $localize`Equity`,
FIXED_INCOME: $localize`Fixed Income`, FIXED_INCOME: $localize`Fixed Income`,
@ -50,6 +50,8 @@ const locales = {
// AssetSubClass (enum) // AssetSubClass (enum)
BOND: $localize`Bond`, BOND: $localize`Bond`,
CASH: $localize`Cash`,
COLLECTIBLE: $localize`Collectible`,
CRYPTOCURRENCY: $localize`Cryptocurrency`, CRYPTOCURRENCY: $localize`Cryptocurrency`,
ETF: $localize`ETF`, ETF: $localize`ETF`,
MUTUALFUND: $localize`Mutual Fund`, MUTUALFUND: $localize`Mutual Fund`,

42
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -29,6 +29,7 @@ import { ChartConfiguration } from 'chart.js';
import { LinearScale } from 'chart.js'; import { LinearScale } from 'chart.js';
import { Chart, Tooltip } from 'chart.js'; import { Chart, Tooltip } from 'chart.js';
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
import { isUUID } from 'class-validator';
import { differenceInDays, max } from 'date-fns'; import { differenceInDays, max } from 'date-fns';
import { orderBy } from 'lodash'; import { orderBy } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -199,18 +200,18 @@ export class GfTreemapChartComponent
const data: ChartConfiguration<'treemap'>['data'] = { const data: ChartConfiguration<'treemap'>['data'] = {
datasets: [ datasets: [
{ {
backgroundColor: (ctx) => { backgroundColor: (context) => {
let annualizedNetPerformancePercent = let annualizedNetPerformancePercent =
getAnnualizedPerformancePercent({ getAnnualizedPerformancePercent({
daysInMarket: differenceInDays( daysInMarket: differenceInDays(
endDate, endDate,
max([ max([
ctx.raw._data.dateOfFirstActivity ?? new Date(0), context.raw._data.dateOfFirstActivity ?? new Date(0),
startDate startDate
]) ])
), ),
netPerformancePercentage: new Big( netPerformancePercentage: new Big(
ctx.raw._data.netPerformancePercentWithCurrencyEffect context.raw._data.netPerformancePercentWithCurrencyEffect
) )
}).toNumber(); }).toNumber();
@ -230,18 +231,18 @@ export class GfTreemapChartComponent
key: 'allocationInPercentage', key: 'allocationInPercentage',
labels: { labels: {
align: 'left', align: 'left',
color: (ctx) => { color: (context) => {
let annualizedNetPerformancePercent = let annualizedNetPerformancePercent =
getAnnualizedPerformancePercent({ getAnnualizedPerformancePercent({
daysInMarket: differenceInDays( daysInMarket: differenceInDays(
endDate, endDate,
max([ max([
ctx.raw._data.dateOfFirstActivity ?? new Date(0), context.raw._data.dateOfFirstActivity ?? new Date(0),
startDate startDate
]) ])
), ),
netPerformancePercentage: new Big( netPerformancePercentage: new Big(
ctx.raw._data.netPerformancePercentWithCurrencyEffect context.raw._data.netPerformancePercentWithCurrencyEffect
) )
}).toNumber(); }).toNumber();
@ -259,11 +260,11 @@ export class GfTreemapChartComponent
}, },
display: true, display: true,
font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }], font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }],
formatter: (ctx) => { formatter: ({ raw }) => {
// Round to 4 decimal places // Round to 4 decimal places
let netPerformancePercentWithCurrencyEffect = let netPerformancePercentWithCurrencyEffect =
Math.round( Math.round(
ctx.raw._data.netPerformancePercentWithCurrencyEffect * 10000 raw._data.netPerformancePercentWithCurrencyEffect * 10000
) / 10000; ) / 10000;
if (Math.abs(netPerformancePercentWithCurrencyEffect) === 0) { if (Math.abs(netPerformancePercentWithCurrencyEffect) === 0) {
@ -272,8 +273,11 @@ export class GfTreemapChartComponent
); );
} }
const name = raw._data.name;
const symbol = raw._data.symbol;
return [ return [
ctx.raw._data.symbol, isUUID(symbol) ? (name ?? symbol) : symbol,
`${netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''}${(netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%` `${netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''}${(netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`
]; ];
}, },
@ -341,19 +345,17 @@ export class GfTreemapChartComponent
locale: this.locale locale: this.locale
}), }),
callbacks: { callbacks: {
label: (context) => { label: ({ raw }) => {
const allocationInPercentage = `${((context.raw._data.allocationInPercentage as number) * 100).toFixed(2)}%`; const allocationInPercentage = `${((raw._data.allocationInPercentage as number) * 100).toFixed(2)}%`;
const name = context.raw._data.name; const name = raw._data.name;
const sign = const sign =
context.raw._data.netPerformancePercentWithCurrencyEffect > 0 raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : '';
? '+' const symbol = raw._data.symbol;
: '';
const symbol = context.raw._data.symbol;
const netPerformanceInPercentageWithSign = `${sign}${(context.raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`; const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`;
if (context.raw._data.valueInBaseCurrency !== null) { if (raw._data.valueInBaseCurrency !== null) {
const value = context.raw._data.valueInBaseCurrency as number; const value = raw._data.valueInBaseCurrency as number;
return [ return [
`${name ?? symbol} (${allocationInPercentage})`, `${name ?? symbol} (${allocationInPercentage})`,
@ -363,7 +365,7 @@ export class GfTreemapChartComponent
})} ${this.baseCurrency}`, })} ${this.baseCurrency}`,
'', '',
$localize`Change` + ' (' + $localize`Performance` + ')', $localize`Change` + ' (' + $localize`Performance` + ')',
`${sign}${context.raw._data.netPerformanceWithCurrencyEffect.toLocaleString( `${sign}${raw._data.netPerformanceWithCurrencyEffect.toLocaleString(
this.locale, this.locale,
{ {
maximumFractionDigits: 2, maximumFractionDigits: 2,

2
prisma/migrations/20250708090630_added_alternative_investment_to_asset_class/migration.sql

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "AssetClass" ADD VALUE 'ALTERNATIVE_INVESTMENT';

2
prisma/migrations/20250708090631_added_collectible_to_asset_sub_class/migration.sql

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "AssetSubClass" ADD VALUE 'COLLECTIBLE';

2
prisma/schema.prisma

@ -278,6 +278,7 @@ enum AccessPermission {
} }
enum AssetClass { enum AssetClass {
ALTERNATIVE_INVESTMENT
COMMODITY COMMODITY
EQUITY EQUITY
FIXED_INCOME FIXED_INCOME
@ -288,6 +289,7 @@ enum AssetClass {
enum AssetSubClass { enum AssetSubClass {
BOND BOND
CASH CASH
COLLECTIBLE
COMMODITY COMMODITY
CRYPTOCURRENCY CRYPTOCURRENCY
ETF ETF

Loading…
Cancel
Save