diff --git a/.gitignore b/.gitignore index 58520c230b..5699a5929d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # compiled output /out-tsc +/prisma/generated /tmp # dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index e5036ab1ab..db21112dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,53 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 3.76.0 - 2026-09-30 + +### Added + +- Added a loading indicator to the platform management of the admin control panel +- Added a loading indicator to the tag management of the admin control panel +- Added a _Refresh_ action to the user detail dialog menu in the admin control panel +- Added the total column to the table of the tag management in the admin control panel + +### Changed + +- Improved the charts to update their colors if the appearance is set to auto and the operating system switches between light and dark mode +- Extended the emergency fund dialog in the portfolio summary to include the currency suffix +- Extended the user detail dialog in the users section of the admin control panel to include the last request +- Migrated the client to the code generated by the new `prisma-client` generator of _Prisma_ +- Improved the language localization for German (`de`) +- Migrated from `lodash` to `lodash-es` + +### Fixed + +- Fixed the colors of the investment chart component (dark mode) +- Fixed the colors of the charts if the appearance is set to light and the operating system uses dark mode +- Fixed the allocation column of the top holdings of ETFs on the allocations page after a change of the user settings +- Fixed the allocation column of the top holdings of funds other than ETFs on the allocations page +- Fixed the value column of the top holdings of ETFs on the allocations page in the _Presenter View_ +- Fixed the projected sustainable retirement income on the _FIRE_ page for a projected total amount of zero (experimental) +- Fixed the sustainable retirement income on the _FIRE_ page for redacted values +- Fixed the emergency fund in the portfolio summary for redacted values +- Fixed the redaction of `valueInBaseCurrency` in the holdings of ETFs in a restricted view +- Fixed the redaction of `comment`, `scraperConfiguration` and `symbolMapping` in the asset profile relation of the activities in a restricted view +- Resolved the data source transformation in the asset profile relation of the activities + +## 3.75.0 - 2026-09-28 ### Changed +- Extended the net performance on the analysis page to include the dividends (experimental) +- Extended the net performance in the portfolio summary to include the dividends (experimental) - Improved the get quotes functionality of the _Manual_ service +- Migrated the historical market data editor dialog from `ngModel` to form control - Migrated the _ESLint_ configuration to the flat config format without `FlatCompat` - Upgraded `chartjs-chart-treemap` from version `4.2.0` to `4.2.2` ### Fixed - Fixed an issue where holdings without a quote have been valued at the unit price of the latest activity instead of the latest market price +- Fixed the discovery of the _OpenID Connect_ (`OIDC`) configuration for issuer URLs with a trailing slash (experimental) ## 3.74.0 - 2026-09-27 diff --git a/apps/api/eslint.config.cjs b/apps/api/eslint.config.cjs index 043802a0df..07054e44b9 100644 --- a/apps/api/eslint.config.cjs +++ b/apps/api/eslint.config.cjs @@ -21,7 +21,10 @@ module.exports = [ { files: ['**/*.ts', '**/*.tsx'], // Override or add rules here - rules: {} + rules: { + // TODO: Remove once the API uses the code generated by the new prisma-client generator + '@typescript-eslint/no-restricted-imports': 'off' + } }, { files: ['**/*.js', '**/*.jsx'], diff --git a/apps/api/src/app/account-balance/account-balance.service.ts b/apps/api/src/app/account-balance/account-balance.service.ts index 3823498df3..ad990c66bd 100644 --- a/apps/api/src/app/account-balance/account-balance.service.ts +++ b/apps/api/src/app/account-balance/account-balance.service.ts @@ -24,7 +24,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { AccountBalance, Prisma } from '@prisma/client'; import { Big } from 'big.js'; import { endOfToday, format, min, parseISO } from 'date-fns'; -import { groupBy } from 'lodash'; +import { groupBy } from 'lodash-es'; @Injectable() export class AccountBalanceService { diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 59315602a8..d1a895a5a3 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -28,7 +28,7 @@ import { } from '@prisma/client'; import { Big } from 'big.js'; import { format } from 'date-fns'; -import { groupBy, isNil } from 'lodash'; +import { groupBy, isNil } from 'lodash-es'; import { CashDetails } from './interfaces/cash-details.interface'; diff --git a/apps/api/src/app/activities/activities-filter.dto.ts b/apps/api/src/app/activities/activities-filter.dto.ts index 0c56aacc85..17385148cc 100644 --- a/apps/api/src/app/activities/activities-filter.dto.ts +++ b/apps/api/src/app/activities/activities-filter.dto.ts @@ -5,7 +5,7 @@ import { DateRange } from '@ghostfolio/common/types'; import { Type as ActivityType } from '@prisma/client'; import { Transform, TransformFnParams } from 'class-transformer'; import { IsEnum, IsOptional, Matches } from 'class-validator'; -import { isString } from 'lodash'; +import { isString } from 'lodash-es'; export class ActivitiesFilterDto extends FilterDto { @IsEnum(ActivityType, { each: true }) diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index ecc2296a90..59d1ba9c33 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -60,7 +60,7 @@ import { Type as ActivityType } from '@prisma/client'; import { Big } from 'big.js'; -import { groupBy, uniqBy } from 'lodash'; +import { groupBy, uniqBy } from 'lodash-es'; import { randomUUID } from 'node:crypto'; @Injectable() diff --git a/apps/api/src/app/asset/asset.controller.ts b/apps/api/src/app/asset/asset.controller.ts index 49afb43d71..352d3e18b1 100644 --- a/apps/api/src/app/asset/asset.controller.ts +++ b/apps/api/src/app/asset/asset.controller.ts @@ -5,7 +5,7 @@ import type { AssetResponse } from '@ghostfolio/common/interfaces'; import { Controller, Get, Param, UseInterceptors } from '@nestjs/common'; import { DataSource } from '@prisma/client'; -import { pick } from 'lodash'; +import { pick } from 'lodash-es'; @Controller('asset') export class AssetController { diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts index e2a71ca843..879463cc13 100644 --- a/apps/api/src/app/auth/auth.module.ts +++ b/apps/api/src/app/auth/auth.module.ts @@ -22,6 +22,7 @@ import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { GoogleStrategy } from './google.strategy'; import { JwtStrategy } from './jwt.strategy'; +import { getOidcDiscoveryUrl } from './oidc.helper'; import { OidcStrategy } from './oidc.strategy'; @Module({ @@ -94,9 +95,15 @@ import { OidcStrategy } from './oidc.strategy'; // Fetch OIDC configuration from discovery endpoint try { const response = await fetchService.fetch( - `${issuer}/.well-known/openid-configuration` + getOidcDiscoveryUrl(issuer) ); + if (!response.ok) { + throw new Error( + `OIDC discovery request failed with status ${response.status}` + ); + } + const config = (await response.json()) as { authorization_endpoint: string; token_endpoint: string; diff --git a/apps/api/src/app/auth/oidc.helper.spec.ts b/apps/api/src/app/auth/oidc.helper.spec.ts new file mode 100644 index 0000000000..9234f12c95 --- /dev/null +++ b/apps/api/src/app/auth/oidc.helper.spec.ts @@ -0,0 +1,20 @@ +import { getOidcDiscoveryUrl } from './oidc.helper'; + +describe('getOidcDiscoveryUrl', () => { + it.each([ + [ + 'https://auth.example.com', + 'https://auth.example.com/.well-known/openid-configuration' + ], + [ + 'https://auth.example.com/', + 'https://auth.example.com/.well-known/openid-configuration' + ], + [ + 'https://auth.example.com/application/o/ghostfolio/', + 'https://auth.example.com/application/o/ghostfolio/.well-known/openid-configuration' + ] + ])('creates the discovery URL for %s', (issuer, expected) => { + expect(getOidcDiscoveryUrl(issuer)).toBe(expected); + }); +}); diff --git a/apps/api/src/app/auth/oidc.helper.ts b/apps/api/src/app/auth/oidc.helper.ts new file mode 100644 index 0000000000..4e8de829fc --- /dev/null +++ b/apps/api/src/app/auth/oidc.helper.ts @@ -0,0 +1,4 @@ +export function getOidcDiscoveryUrl(issuer: string) { + // Remove trailing slashes + return `${issuer.replace(/\/+$/, '')}/.well-known/openid-configuration`; +} diff --git a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts index e9aad6911f..4a2507e697 100644 --- a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts +++ b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts @@ -29,7 +29,7 @@ import { MarketDataPreset } from '@ghostfolio/common/types'; import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { AssetClass, AssetSubClass, DataSource, Prisma } from '@prisma/client'; -import { groupBy } from 'lodash'; +import { groupBy } from 'lodash-es'; @Injectable() export class AssetProfilesService { diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts index beff7ffa60..908db59b7f 100644 --- a/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts +++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts @@ -14,7 +14,7 @@ import { DateRange } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; import { format, isSameDay } from 'date-fns'; -import { isNumber } from 'lodash'; +import { isNumber } from 'lodash-es'; @Injectable() export class BenchmarksService { diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts index ec0eeeb868..4042241449 100644 --- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts @@ -40,7 +40,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { DataSource, SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; import { addMilliseconds, isBefore } from 'date-fns'; -import { isEmpty } from 'lodash'; +import { isEmpty } from 'lodash-es'; @Injectable() export class GhostfolioService { diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 35db209935..db08f62052 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -11,7 +11,7 @@ import { import { Injectable } from '@nestjs/common'; import { Platform, Prisma, Type as ActivityType } from '@prisma/client'; -import { groupBy, uniqBy } from 'lodash'; +import { groupBy, uniqBy } from 'lodash-es'; @Injectable() export class ExportService { diff --git a/apps/api/src/app/export/get-export.dto.ts b/apps/api/src/app/export/get-export.dto.ts index 5fc3c81baa..524c357112 100644 --- a/apps/api/src/app/export/get-export.dto.ts +++ b/apps/api/src/app/export/get-export.dto.ts @@ -2,7 +2,7 @@ import { ActivitiesFilterDto } from '@ghostfolio/api/app/activities/activities-f import { Transform, TransformFnParams } from 'class-transformer'; import { IsOptional, IsUUID } from 'class-validator'; -import { isString } from 'lodash'; +import { isString } from 'lodash-es'; export class GetExportDto extends ActivitiesFilterDto { @IsOptional() diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 5f202f4ac4..5afa4c33dd 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -47,7 +47,7 @@ import { Account, DataSource, Prisma } from '@prisma/client'; import { Big } from 'big.js'; import { isISIN } from 'class-validator'; import { isSameSecond, parseISO } from 'date-fns'; -import { omit, uniqBy } from 'lodash'; +import { omit, uniqBy } from 'lodash-es'; import { randomUUID } from 'node:crypto'; import { ImportValidationError } from './errors/import-validation.error'; diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index a0759b1698..2a7382446c 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -26,7 +26,7 @@ import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { MarketData } from '@prisma/client'; import { subDays } from 'date-fns'; -import { isNil } from 'lodash'; +import { isNil } from 'lodash-es'; @Injectable() export class InfoService { diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index d868b65b3f..e03b530258 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -72,7 +72,7 @@ import { startOfYear, subDays } from 'date-fns'; -import { groupBy, sortBy, uniqBy } from 'lodash'; +import { groupBy, sortBy, uniqBy } from 'lodash-es'; export abstract class PortfolioCalculator { protected static readonly ENABLE_LOGGING = false; diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index 2b7b4e6dce..c0b7337350 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -32,7 +32,7 @@ import { isBefore, isThisYear } from 'date-fns'; -import { sum } from 'lodash'; +import { sum } from 'lodash-es'; export class RoaiPortfolioCalculator extends PortfolioCalculator { protected calculateOverallPerformance( diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index 79363c6344..80d37259a4 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -14,7 +14,7 @@ import { import { Injectable } from '@nestjs/common'; import { isBefore, isToday } from 'date-fns'; -import { isEmpty, uniqBy } from 'lodash'; +import { isEmpty, uniqBy } from 'lodash-es'; import { GetValueObject } from './interfaces/get-value-object.interface'; import { GetValuesObject } from './interfaces/get-values-object.interface'; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 21e615bc21..72c5b5946a 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -104,7 +104,7 @@ import { set, startOfDay } from 'date-fns'; -import { groupBy } from 'lodash'; +import { groupBy } from 'lodash-es'; import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory'; @@ -2140,6 +2140,7 @@ export class PortfolioService { const { currentValueInBaseCurrency, + dividendPercentageWithCurrencyEffect, netPerformance, netPerformancePercentage, netPerformancePercentageWithCurrencyEffect, @@ -2236,6 +2237,7 @@ export class PortfolioService { cash, currentValueInBaseCurrency, dateOfFirstActivity, + dividendPercentageWithCurrencyEffect, excludedAccountsAndActivities, netPerformance, netPerformancePercentage, diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index 9d8abb47ba..cf26d39d54 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -25,7 +25,7 @@ import { AuthGuard } from '@nestjs/passport'; import { DataSource } from '@prisma/client'; import { isValid, parseISO } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; -import { isEmpty } from 'lodash'; +import { isEmpty } from 'lodash-es'; import { GetLookupDto } from './get-lookup.dto'; import { SymbolService } from './symbol.service'; diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index ae9e7d680b..32ac353351 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -53,7 +53,7 @@ import { AuthGuard } from '@nestjs/passport'; import { Throttle } from '@nestjs/throttler'; import { User as UserModel } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; -import { merge, size } from 'lodash'; +import { merge, size } from 'lodash-es'; import { UserService } from './user.service'; diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index a74e32ca93..2e54a7ddc3 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -46,7 +46,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { InjectThrottlerStorage, ThrottlerStorage } from '@nestjs/throttler'; import { Prisma, Role, User } from '@prisma/client'; import { differenceInDays, subDays } from 'date-fns'; -import { isNil, without } from 'lodash'; +import { isNil, without } from 'lodash-es'; import { createHmac } from 'node:crypto'; @Injectable() diff --git a/apps/api/src/dtos/filter.dto.ts b/apps/api/src/dtos/filter.dto.ts index 09206984ab..84d9ffc0d0 100644 --- a/apps/api/src/dtos/filter.dto.ts +++ b/apps/api/src/dtos/filter.dto.ts @@ -9,7 +9,7 @@ import { IsUUID, MaxLength } from 'class-validator'; -import { isString } from 'lodash'; +import { isString } from 'lodash-es'; export class FilterDto { @IsOptional() diff --git a/apps/api/src/helper/activity.helper.ts b/apps/api/src/helper/activity.helper.ts index b281106c77..f7cb20505e 100644 --- a/apps/api/src/helper/activity.helper.ts +++ b/apps/api/src/helper/activity.helper.ts @@ -5,7 +5,7 @@ import { import { Prisma, Type as ActivityType } from '@prisma/client'; import { endOfToday, isAfter } from 'date-fns'; -import { uniqBy } from 'lodash'; +import { uniqBy } from 'lodash-es'; export const WHERE_ACTIVITY_NOT_DRAFT: Prisma.OrderWhereInput = { tags: { diff --git a/apps/api/src/helper/object.helper.spec.ts b/apps/api/src/helper/object.helper.spec.ts index 415851a6c6..9776232a0b 100644 --- a/apps/api/src/helper/object.helper.spec.ts +++ b/apps/api/src/helper/object.helper.spec.ts @@ -1,4 +1,5 @@ import { DEFAULT_REDACTED_PATHS } from '@ghostfolio/common/config'; +import { PortfolioDetails } from '@ghostfolio/common/interfaces'; import { query, redactPaths } from './object.helper'; @@ -133,6 +134,7 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.044900865255793135, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'STOCK', countries: [ { @@ -149,7 +151,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.3183066634822068, grossPerformancePercentWithCurrencyEffect: 0.3183066634822068, grossPerformanceWithCurrencyEffect: 2665.5, - holdings: [], investment: 0.060265768702233234, name: 'Apple Inc', netPerformance: 2664.5, @@ -185,6 +186,7 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.026912563036519527, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'STOCK', countries: [ { code: 'DE', weight: 1, continent: 'Europe', name: 'Germany' } @@ -196,7 +198,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.3719230057375532, grossPerformancePercentWithCurrencyEffect: 0.2650716044872953, grossPerformanceWithCurrencyEffect: 1386.429698978564, - holdings: [], investment: 0.03471025137190358, name: 'Allianz SE', netPerformance: 1789.1095737558583, @@ -232,6 +233,7 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.07646101417126275, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'STOCK', countries: [ { @@ -248,7 +250,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.8594552890963852, grossPerformancePercentWithCurrencyEffect: 0.8594552890963852, grossPerformanceWithCurrencyEffect: 8689.05, - holdings: [], investment: 0.07275900505029173, name: 'Amazon.com, Inc.', netPerformance: 8608.26, @@ -290,6 +291,7 @@ describe('redactAttributes', () => { ], allocationInPercentage: 0.15042891393226654, assetClass: 'LIQUIDITY', + assetProfile: { holdings: [] }, assetSubClass: 'CRYPTOCURRENCY', countries: [], dataSource: 'COINGECKO', @@ -299,7 +301,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 17.4925166352, grossPerformancePercentWithCurrencyEffect: 17.4925166352, grossPerformanceWithCurrencyEffect: 34985.0332704, - holdings: [], investment: 0.014393543993846005, name: 'Bitcoin', netPerformance: 34955.1332704, @@ -341,6 +342,7 @@ describe('redactAttributes', () => { ], allocationInPercentage: 0.009076749759365777, assetClass: 'FIXED_INCOME', + assetProfile: { holdings: [] }, assetSubClass: 'BOND', countries: [], dataSource: 'MANUAL', @@ -350,7 +352,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: -0.06153834320225245, grossPerformanceWithCurrencyEffect: -125.68932723700505, - holdings: [], investment: 0.016060638243523776, name: 'Bondora Go & Grow', netPerformance: 0, @@ -392,6 +393,7 @@ describe('redactAttributes', () => { ], allocationInPercentage: 0.09095764645669335, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'ETF', countries: [ { @@ -474,7 +476,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.27579517683678895, grossPerformancePercentWithCurrencyEffect: 0.458553421589667, grossPerformanceWithCurrencyEffect: 5322.44900391902, - holdings: [], investment: 0.13551383737034509, name: 'frankly Extreme 95 Index', netPerformance: 3533.389614611676, @@ -510,6 +511,7 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.05222646409742627, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'STOCK', countries: [ { @@ -526,7 +528,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.7865431171216295, grossPerformancePercentWithCurrencyEffect: 0.7865431171216295, grossPerformanceWithCurrencyEffect: 5653.2, - holdings: [], investment: 0.051726079050684395, name: 'Microsoft Corporation', netPerformance: 5653.2, @@ -562,6 +563,7 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.1589050142378352, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'STOCK', countries: [ { @@ -578,7 +580,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 17.184314638161936, grossPerformancePercentWithCurrencyEffect: 17.184314638161936, grossPerformanceWithCurrencyEffect: 36920.500000005, - holdings: [], investment: 0.01546226463535309, name: 'Tesla, Inc.', netPerformance: 36890.500000005, @@ -614,6 +615,85 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.057358979326040366, assetClass: 'EQUITY', + assetProfile: { + holdings: [ + { + allocationInPercentage: 0.06099941636982121, + name: 'APPLE INC', + valueInBaseCurrency: 860.2442693554036 + }, + { + allocationInPercentage: 0.05862464529372787, + name: 'MICROSOFT CORP', + valueInBaseCurrency: 826.7540602547973 + }, + { + allocationInPercentage: 0.05156070760128074, + name: 'NVIDIA CORP', + valueInBaseCurrency: 727.1348789470617 + }, + { + allocationInPercentage: 0.03301535551128066, + name: 'AMAZON COM INC', + valueInBaseCurrency: 465.5990510978355 + }, + { + allocationInPercentage: 0.01962204914568647, + name: 'FACEBOOK CLASS A INC', + valueInBaseCurrency: 276.71994807704345 + }, + { + allocationInPercentage: 0.01902835637666313, + name: 'ALPHABET INC CLASS A', + valueInBaseCurrency: 268.34739580189176 + }, + { + allocationInPercentage: 0.01555676306627245, + name: 'ALPHABET INC CLASS C', + valueInBaseCurrency: 219.3892511421072 + }, + { + allocationInPercentage: 0.01463100485016827, + name: 'BERKSHIRE HATHAWAY INC CLASS B', + valueInBaseCurrency: 206.33374589949804 + }, + { + allocationInPercentage: 0.01403731208114493, + name: 'BROADCOM INC', + valueInBaseCurrency: 197.96119362434638 + }, + { + allocationInPercentage: 0.01297067761476403, + name: 'ELI LILLY', + valueInBaseCurrency: 182.91898106220972 + }, + { + allocationInPercentage: 0.0118637927911612, + name: 'TESLA INC', + valueInBaseCurrency: 167.30913783735082 + }, + { + allocationInPercentage: 0.01152166475477487, + name: 'JPMORGAN CHASE & CO', + valueInBaseCurrency: 162.48427720421262 + }, + { + allocationInPercentage: 0.0100324015375638, + name: 'EXXON MOBIL CORP', + valueInBaseCurrency: 141.4819426834935 + }, + { + allocationInPercentage: 0.01000221376964736, + name: 'UNITEDHEALTH GROUP INC', + valueInBaseCurrency: 141.0562196864519 + }, + { + allocationInPercentage: 0.007818631890358146, + name: 'VISA INC CLASS A', + valueInBaseCurrency: 110.26225623377576 + } + ] + }, assetSubClass: 'ETF', countries: [ { @@ -660,83 +740,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.8832083851170418, grossPerformancePercentWithCurrencyEffect: 0.8832083851170418, grossPerformanceWithCurrencyEffect: 5856.3, - holdings: [ - { - allocationInPercentage: 0.06099941636982121, - name: 'APPLE INC', - valueInBaseCurrency: 860.2442693554036 - }, - { - allocationInPercentage: 0.05862464529372787, - name: 'MICROSOFT CORP', - valueInBaseCurrency: 826.7540602547973 - }, - { - allocationInPercentage: 0.05156070760128074, - name: 'NVIDIA CORP', - valueInBaseCurrency: 727.1348789470617 - }, - { - allocationInPercentage: 0.03301535551128066, - name: 'AMAZON COM INC', - valueInBaseCurrency: 465.5990510978355 - }, - { - allocationInPercentage: 0.01962204914568647, - name: 'FACEBOOK CLASS A INC', - valueInBaseCurrency: 276.71994807704345 - }, - { - allocationInPercentage: 0.01902835637666313, - name: 'ALPHABET INC CLASS A', - valueInBaseCurrency: 268.34739580189176 - }, - { - allocationInPercentage: 0.01555676306627245, - name: 'ALPHABET INC CLASS C', - valueInBaseCurrency: 219.3892511421072 - }, - { - allocationInPercentage: 0.01463100485016827, - name: 'BERKSHIRE HATHAWAY INC CLASS B', - valueInBaseCurrency: 206.33374589949804 - }, - { - allocationInPercentage: 0.01403731208114493, - name: 'BROADCOM INC', - valueInBaseCurrency: 197.96119362434638 - }, - { - allocationInPercentage: 0.01297067761476403, - name: 'ELI LILLY', - valueInBaseCurrency: 182.91898106220972 - }, - { - allocationInPercentage: 0.0118637927911612, - name: 'TESLA INC', - valueInBaseCurrency: 167.30913783735082 - }, - { - allocationInPercentage: 0.01152166475477487, - name: 'JPMORGAN CHASE & CO', - valueInBaseCurrency: 162.48427720421262 - }, - { - allocationInPercentage: 0.0100324015375638, - name: 'EXXON MOBIL CORP', - valueInBaseCurrency: 141.4819426834935 - }, - { - allocationInPercentage: 0.01000221376964736, - name: 'UNITEDHEALTH GROUP INC', - valueInBaseCurrency: 141.0562196864519 - }, - { - allocationInPercentage: 0.007818631890358146, - name: 'VISA INC CLASS A', - valueInBaseCurrency: 110.26225623377576 - } - ], investment: 0.05934602124102648, name: 'Vanguard Total Stock Market Index Fund ETF Shares', netPerformance: 5756.8, @@ -786,6 +789,85 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.09386983901959013, assetClass: 'EQUITY', + assetProfile: { + holdings: [ + { + allocationInPercentage: 0.042520261085, + name: 'APPLE INC', + valueInBaseCurrency: 981.3336460398625 + }, + { + allocationInPercentage: 0.037017038038, + name: 'MICROSOFT CORP', + valueInBaseCurrency: 854.3236559815404 + }, + { + allocationInPercentage: 0.018861883836, + name: 'AMAZON COM INC', + valueInBaseCurrency: 435.31720557783655 + }, + { + allocationInPercentage: 0.017806548325, + name: 'NVIDIA CORP', + valueInBaseCurrency: 410.9609053487602 + }, + { + allocationInPercentage: 0.012188534864, + name: 'ALPHABET INC CLASS A', + valueInBaseCurrency: 281.3016442693628 + }, + { + allocationInPercentage: 0.010831709166, + name: 'ALPHABET INC CLASS C', + valueInBaseCurrency: 249.98719145833246 + }, + { + allocationInPercentage: 0.010813551981, + name: 'TESLA INC', + valueInBaseCurrency: 249.56813813873381 + }, + { + allocationInPercentage: 0.009934819182, + name: 'FACEBOOK CLASS A INC', + valueInBaseCurrency: 229.28768737165962 + }, + { + allocationInPercentage: 0.007403227621000001, + name: 'BERKSHIRE HATHAWAY INC CLASS B', + valueInBaseCurrency: 170.8605772494153 + }, + { + allocationInPercentage: 0.007076883908, + name: 'ELI LILLY', + valueInBaseCurrency: 163.32882514892185 + }, + { + allocationInPercentage: 0.006844271583, + name: 'EXXON MOBIL CORP', + valueInBaseCurrency: 157.96031857861325 + }, + { + allocationInPercentage: 0.006718061670999999, + name: 'UNITEDHEALTH GROUP INC', + valueInBaseCurrency: 155.0474946695187 + }, + { + allocationInPercentage: 0.006456949621, + name: 'JPMORGAN CHASE & CO', + valueInBaseCurrency: 149.02123722158794 + }, + { + allocationInPercentage: 0.006293890054, + name: 'TAIWAN SEMICONDUCTOR MANUFACTURING', + valueInBaseCurrency: 145.25795272326576 + }, + { + allocationInPercentage: 0.00600392555, + name: 'VISA INC CLASS A', + valueInBaseCurrency: 138.56580369427397 + } + ] + }, assetSubClass: 'ETF', countries: [ { @@ -1060,83 +1142,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.3683200415015591, grossPerformancePercentWithCurrencyEffect: 0.5806366182968891, grossPerformanceWithCurrencyEffect: 6402.248165662604, - holdings: [ - { - allocationInPercentage: 0.042520261085, - name: 'APPLE INC', - valueInBaseCurrency: 981.3336460398625 - }, - { - allocationInPercentage: 0.037017038038, - name: 'MICROSOFT CORP', - valueInBaseCurrency: 854.3236559815404 - }, - { - allocationInPercentage: 0.018861883836, - name: 'AMAZON COM INC', - valueInBaseCurrency: 435.31720557783655 - }, - { - allocationInPercentage: 0.017806548325, - name: 'NVIDIA CORP', - valueInBaseCurrency: 410.9609053487602 - }, - { - allocationInPercentage: 0.012188534864, - name: 'ALPHABET INC CLASS A', - valueInBaseCurrency: 281.3016442693628 - }, - { - allocationInPercentage: 0.010831709166, - name: 'ALPHABET INC CLASS C', - valueInBaseCurrency: 249.98719145833246 - }, - { - allocationInPercentage: 0.010813551981, - name: 'TESLA INC', - valueInBaseCurrency: 249.56813813873381 - }, - { - allocationInPercentage: 0.009934819182, - name: 'FACEBOOK CLASS A INC', - valueInBaseCurrency: 229.28768737165962 - }, - { - allocationInPercentage: 0.007403227621000001, - name: 'BERKSHIRE HATHAWAY INC CLASS B', - valueInBaseCurrency: 170.8605772494153 - }, - { - allocationInPercentage: 0.007076883908, - name: 'ELI LILLY', - valueInBaseCurrency: 163.32882514892185 - }, - { - allocationInPercentage: 0.006844271583, - name: 'EXXON MOBIL CORP', - valueInBaseCurrency: 157.96031857861325 - }, - { - allocationInPercentage: 0.006718061670999999, - name: 'UNITEDHEALTH GROUP INC', - valueInBaseCurrency: 155.0474946695187 - }, - { - allocationInPercentage: 0.006456949621, - name: 'JPMORGAN CHASE & CO', - valueInBaseCurrency: 149.02123722158794 - }, - { - allocationInPercentage: 0.006293890054, - name: 'TAIWAN SEMICONDUCTOR MANUFACTURING', - valueInBaseCurrency: 145.25795272326576 - }, - { - allocationInPercentage: 0.00600392555, - name: 'VISA INC CLASS A', - valueInBaseCurrency: 138.56580369427397 - } - ], investment: 0.13346122254229614, name: 'Vanguard FTSE All-World UCITS ETF', netPerformance: 4438.993935069568, @@ -1194,6 +1199,80 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.03598477442100562, assetClass: 'EQUITY', + assetProfile: { + holdings: [ + { + allocationInPercentage: 0.051778373, + name: 'APPLE INC', + valueInBaseCurrency: 458.1016731945994 + }, + { + allocationInPercentage: 0.0403267055, + name: 'MICROSOFT CORP', + valueInBaseCurrency: 356.78469974280296 + }, + { + allocationInPercentage: 0.0221895862, + name: 'AMAZON COM INC', + valueInBaseCurrency: 196.3191575315778 + }, + { + allocationInPercentage: 0.0208100035, + name: 'NVIDIA CORP', + valueInBaseCurrency: 184.1134989416425 + }, + { + allocationInPercentage: 0.0139820061, + name: 'ALPHABET INC CLASS A', + valueInBaseCurrency: 123.70377858390985 + }, + { + allocationInPercentage: 0.0126263246, + name: 'ALPHABET INC CLASS C', + valueInBaseCurrency: 111.70958240727516 + }, + { + allocationInPercentage: 0.0121596126, + name: 'TESLA INC', + valueInBaseCurrency: 107.58041542669048 + }, + { + allocationInPercentage: 0.0114079282, + name: 'FACEBOOK CLASS A INC', + valueInBaseCurrency: 100.92999631533141 + }, + { + allocationInPercentage: 0.0081570352, + name: 'BERKSHIRE HATHAWAY INC CLASS B', + valueInBaseCurrency: 72.16819024860523 + }, + { + allocationInPercentage: 0.0079471416, + name: 'EXXON MOBIL CORP', + valueInBaseCurrency: 70.31118695201964 + }, + { + allocationInPercentage: 0.0078190388, + name: 'ELI LILLY', + valueInBaseCurrency: 69.1778159397456 + }, + { + allocationInPercentage: 0.0077121293, + name: 'UNITEDHEALTH GROUP INC', + valueInBaseCurrency: 68.23194958681098 + }, + { + allocationInPercentage: 0.0074484861, + name: 'JPMORGAN CHASE & CO', + valueInBaseCurrency: 65.89940447098863 + }, + { + allocationInPercentage: 0.006978079, + name: 'VISA INC CLASS A', + valueInBaseCurrency: 61.73754562709217 + } + ] + }, assetSubClass: 'ETF', countries: [ { @@ -1342,78 +1421,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.3474381850624522, grossPerformancePercentWithCurrencyEffect: 0.28744846894552306, grossPerformanceWithCurrencyEffect: 1975.348026988124, - holdings: [ - { - allocationInPercentage: 0.051778373, - name: 'APPLE INC', - valueInBaseCurrency: 458.1016731945994 - }, - { - allocationInPercentage: 0.0403267055, - name: 'MICROSOFT CORP', - valueInBaseCurrency: 356.78469974280296 - }, - { - allocationInPercentage: 0.0221895862, - name: 'AMAZON COM INC', - valueInBaseCurrency: 196.3191575315778 - }, - { - allocationInPercentage: 0.0208100035, - name: 'NVIDIA CORP', - valueInBaseCurrency: 184.1134989416425 - }, - { - allocationInPercentage: 0.0139820061, - name: 'ALPHABET INC CLASS A', - valueInBaseCurrency: 123.70377858390985 - }, - { - allocationInPercentage: 0.0126263246, - name: 'ALPHABET INC CLASS C', - valueInBaseCurrency: 111.70958240727516 - }, - { - allocationInPercentage: 0.0121596126, - name: 'TESLA INC', - valueInBaseCurrency: 107.58041542669048 - }, - { - allocationInPercentage: 0.0114079282, - name: 'FACEBOOK CLASS A INC', - valueInBaseCurrency: 100.92999631533141 - }, - { - allocationInPercentage: 0.0081570352, - name: 'BERKSHIRE HATHAWAY INC CLASS B', - valueInBaseCurrency: 72.16819024860523 - }, - { - allocationInPercentage: 0.0079471416, - name: 'EXXON MOBIL CORP', - valueInBaseCurrency: 70.31118695201964 - }, - { - allocationInPercentage: 0.0078190388, - name: 'ELI LILLY', - valueInBaseCurrency: 69.1778159397456 - }, - { - allocationInPercentage: 0.0077121293, - name: 'UNITEDHEALTH GROUP INC', - valueInBaseCurrency: 68.23194958681098 - }, - { - allocationInPercentage: 0.0074484861, - name: 'JPMORGAN CHASE & CO', - valueInBaseCurrency: 65.89940447098863 - }, - { - allocationInPercentage: 0.006978079, - name: 'VISA INC CLASS A', - valueInBaseCurrency: 61.73754562709217 - } - ], investment: 0.04725441287200783, name: 'Xtrackers MSCI World UCITS ETF 1C', netPerformance: 2247.935728632002, @@ -1454,6 +1461,7 @@ describe('redactAttributes', () => { currency: 'USD', allocationInPercentage: 0.20291717628620132, assetClass: 'LIQUIDITY', + assetProfile: { holdings: [] }, assetSubClass: 'CASH', countries: [], dividend: 0, @@ -1461,7 +1469,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: 0, grossPerformanceWithCurrencyEffect: 0, - holdings: [], investment: 0.35904695492648864, marketPrice: 0, name: 'USD', @@ -1637,6 +1644,7 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.044900865255793135, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'STOCK', countries: [ { @@ -1653,7 +1661,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.3183066634822068, grossPerformancePercentWithCurrencyEffect: 0.3183066634822068, grossPerformanceWithCurrencyEffect: null, - holdings: [], investment: null, name: 'Apple Inc', netPerformance: null, @@ -1689,6 +1696,7 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.026912563036519527, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'STOCK', countries: [ { code: 'DE', weight: 1, continent: 'Europe', name: 'Germany' } @@ -1700,7 +1708,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.3719230057375532, grossPerformancePercentWithCurrencyEffect: 0.2650716044872953, grossPerformanceWithCurrencyEffect: null, - holdings: [], investment: null, name: 'Allianz SE', netPerformance: null, @@ -1736,6 +1743,7 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.07646101417126275, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'STOCK', countries: [ { @@ -1752,7 +1760,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.8594552890963852, grossPerformancePercentWithCurrencyEffect: 0.8594552890963852, grossPerformanceWithCurrencyEffect: null, - holdings: [], investment: null, name: 'Amazon.com, Inc.', netPerformance: null, @@ -1794,6 +1801,7 @@ describe('redactAttributes', () => { ], allocationInPercentage: 0.15042891393226654, assetClass: 'LIQUIDITY', + assetProfile: { holdings: [] }, assetSubClass: 'CRYPTOCURRENCY', countries: [], dataSource: 'COINGECKO', @@ -1803,7 +1811,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 17.4925166352, grossPerformancePercentWithCurrencyEffect: 17.4925166352, grossPerformanceWithCurrencyEffect: null, - holdings: [], investment: null, name: 'Bitcoin', netPerformance: null, @@ -1845,6 +1852,7 @@ describe('redactAttributes', () => { ], allocationInPercentage: 0.009076749759365777, assetClass: 'FIXED_INCOME', + assetProfile: { holdings: [] }, assetSubClass: 'BOND', countries: [], dataSource: 'MANUAL', @@ -1854,7 +1862,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: -0.06153834320225245, grossPerformanceWithCurrencyEffect: null, - holdings: [], investment: null, name: 'Bondora Go & Grow', netPerformance: null, @@ -1896,6 +1903,7 @@ describe('redactAttributes', () => { ], allocationInPercentage: 0.09095764645669335, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'ETF', countries: [ { @@ -1958,7 +1966,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.27579517683678895, grossPerformancePercentWithCurrencyEffect: 0.458553421589667, grossPerformanceWithCurrencyEffect: null, - holdings: [], investment: null, name: 'frankly Extreme 95 Index', netPerformance: null, @@ -1994,6 +2001,7 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.05222646409742627, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'STOCK', countries: [ { @@ -2010,7 +2018,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.7865431171216295, grossPerformancePercentWithCurrencyEffect: 0.7865431171216295, grossPerformanceWithCurrencyEffect: null, - holdings: [], investment: null, name: 'Microsoft Corporation', netPerformance: null, @@ -2046,6 +2053,7 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.1589050142378352, assetClass: 'EQUITY', + assetProfile: { holdings: [] }, assetSubClass: 'STOCK', countries: [ { @@ -2062,7 +2070,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 17.184314638161936, grossPerformancePercentWithCurrencyEffect: 17.184314638161936, grossPerformanceWithCurrencyEffect: null, - holdings: [], investment: null, name: 'Tesla, Inc.', netPerformance: null, @@ -2098,6 +2105,85 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.057358979326040366, assetClass: 'EQUITY', + assetProfile: { + holdings: [ + { + allocationInPercentage: 0.06099941636982121, + name: 'APPLE INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.05862464529372787, + name: 'MICROSOFT CORP', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.05156070760128074, + name: 'NVIDIA CORP', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.03301535551128066, + name: 'AMAZON COM INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.01962204914568647, + name: 'FACEBOOK CLASS A INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.01902835637666313, + name: 'ALPHABET INC CLASS A', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.01555676306627245, + name: 'ALPHABET INC CLASS C', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.01463100485016827, + name: 'BERKSHIRE HATHAWAY INC CLASS B', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.01403731208114493, + name: 'BROADCOM INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.01297067761476403, + name: 'ELI LILLY', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0118637927911612, + name: 'TESLA INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.01152166475477487, + name: 'JPMORGAN CHASE & CO', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0100324015375638, + name: 'EXXON MOBIL CORP', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.01000221376964736, + name: 'UNITEDHEALTH GROUP INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.007818631890358146, + name: 'VISA INC CLASS A', + valueInBaseCurrency: null + } + ] + }, assetSubClass: 'ETF', countries: [ { @@ -2144,83 +2230,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.8832083851170418, grossPerformancePercentWithCurrencyEffect: 0.8832083851170418, grossPerformanceWithCurrencyEffect: null, - holdings: [ - { - allocationInPercentage: 0.06099941636982121, - name: 'APPLE INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.05862464529372787, - name: 'MICROSOFT CORP', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.05156070760128074, - name: 'NVIDIA CORP', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.03301535551128066, - name: 'AMAZON COM INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.01962204914568647, - name: 'FACEBOOK CLASS A INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.01902835637666313, - name: 'ALPHABET INC CLASS A', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.01555676306627245, - name: 'ALPHABET INC CLASS C', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.01463100485016827, - name: 'BERKSHIRE HATHAWAY INC CLASS B', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.01403731208114493, - name: 'BROADCOM INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.01297067761476403, - name: 'ELI LILLY', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0118637927911612, - name: 'TESLA INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.01152166475477487, - name: 'JPMORGAN CHASE & CO', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0100324015375638, - name: 'EXXON MOBIL CORP', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.01000221376964736, - name: 'UNITEDHEALTH GROUP INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.007818631890358146, - name: 'VISA INC CLASS A', - valueInBaseCurrency: null - } - ], investment: null, name: 'Vanguard Total Stock Market Index Fund ETF Shares', netPerformance: null, @@ -2270,6 +2279,85 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.09386983901959013, assetClass: 'EQUITY', + assetProfile: { + holdings: [ + { + allocationInPercentage: 0.042520261085, + name: 'APPLE INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.037017038038, + name: 'MICROSOFT CORP', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.018861883836, + name: 'AMAZON COM INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.017806548325, + name: 'NVIDIA CORP', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.012188534864, + name: 'ALPHABET INC CLASS A', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.010831709166, + name: 'ALPHABET INC CLASS C', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.010813551981, + name: 'TESLA INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.009934819182, + name: 'FACEBOOK CLASS A INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.007403227621000001, + name: 'BERKSHIRE HATHAWAY INC CLASS B', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.007076883908, + name: 'ELI LILLY', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.006844271583, + name: 'EXXON MOBIL CORP', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.006718061670999999, + name: 'UNITEDHEALTH GROUP INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.006456949621, + name: 'JPMORGAN CHASE & CO', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.006293890054, + name: 'TAIWAN SEMICONDUCTOR MANUFACTURING', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.00600392555, + name: 'VISA INC CLASS A', + valueInBaseCurrency: null + } + ] + }, assetSubClass: 'ETF', countries: [ { @@ -2539,83 +2627,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.3683200415015591, grossPerformancePercentWithCurrencyEffect: 0.5806366182968891, grossPerformanceWithCurrencyEffect: null, - holdings: [ - { - allocationInPercentage: 0.042520261085, - name: 'APPLE INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.037017038038, - name: 'MICROSOFT CORP', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.018861883836, - name: 'AMAZON COM INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.017806548325, - name: 'NVIDIA CORP', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.012188534864, - name: 'ALPHABET INC CLASS A', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.010831709166, - name: 'ALPHABET INC CLASS C', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.010813551981, - name: 'TESLA INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.009934819182, - name: 'FACEBOOK CLASS A INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.007403227621000001, - name: 'BERKSHIRE HATHAWAY INC CLASS B', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.007076883908, - name: 'ELI LILLY', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.006844271583, - name: 'EXXON MOBIL CORP', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.006718061670999999, - name: 'UNITEDHEALTH GROUP INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.006456949621, - name: 'JPMORGAN CHASE & CO', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.006293890054, - name: 'TAIWAN SEMICONDUCTOR MANUFACTURING', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.00600392555, - name: 'VISA INC CLASS A', - valueInBaseCurrency: null - } - ], investment: null, name: 'Vanguard FTSE All-World UCITS ETF', netPerformance: null, @@ -2670,6 +2681,80 @@ describe('redactAttributes', () => { tags: [], allocationInPercentage: 0.03598477442100562, assetClass: 'EQUITY', + assetProfile: { + holdings: [ + { + allocationInPercentage: 0.051778373, + name: 'APPLE INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0403267055, + name: 'MICROSOFT CORP', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0221895862, + name: 'AMAZON COM INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0208100035, + name: 'NVIDIA CORP', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0139820061, + name: 'ALPHABET INC CLASS A', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0126263246, + name: 'ALPHABET INC CLASS C', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0121596126, + name: 'TESLA INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0114079282, + name: 'FACEBOOK CLASS A INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0081570352, + name: 'BERKSHIRE HATHAWAY INC CLASS B', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0079471416, + name: 'EXXON MOBIL CORP', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0078190388, + name: 'ELI LILLY', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0077121293, + name: 'UNITEDHEALTH GROUP INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.0074484861, + name: 'JPMORGAN CHASE & CO', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.006978079, + name: 'VISA INC CLASS A', + valueInBaseCurrency: null + } + ] + }, assetSubClass: 'ETF', countries: [ { @@ -2818,78 +2903,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0.3474381850624522, grossPerformancePercentWithCurrencyEffect: 0.28744846894552306, grossPerformanceWithCurrencyEffect: null, - holdings: [ - { - allocationInPercentage: 0.051778373, - name: 'APPLE INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0403267055, - name: 'MICROSOFT CORP', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0221895862, - name: 'AMAZON COM INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0208100035, - name: 'NVIDIA CORP', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0139820061, - name: 'ALPHABET INC CLASS A', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0126263246, - name: 'ALPHABET INC CLASS C', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0121596126, - name: 'TESLA INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0114079282, - name: 'FACEBOOK CLASS A INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0081570352, - name: 'BERKSHIRE HATHAWAY INC CLASS B', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0079471416, - name: 'EXXON MOBIL CORP', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0078190388, - name: 'ELI LILLY', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0077121293, - name: 'UNITEDHEALTH GROUP INC', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.0074484861, - name: 'JPMORGAN CHASE & CO', - valueInBaseCurrency: null - }, - { - allocationInPercentage: 0.006978079, - name: 'VISA INC CLASS A', - valueInBaseCurrency: null - } - ], investment: null, name: 'Xtrackers MSCI World UCITS ETF 1C', netPerformance: null, @@ -2930,6 +2943,7 @@ describe('redactAttributes', () => { currency: 'USD', allocationInPercentage: 0.20291717628620132, assetClass: 'LIQUIDITY', + assetProfile: { holdings: [] }, assetSubClass: 'CASH', countries: [], dividend: null, @@ -2937,7 +2951,6 @@ describe('redactAttributes', () => { grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: 0, grossPerformanceWithCurrencyEffect: null, - holdings: [], investment: null, marketPrice: 0, name: 'USD', @@ -3069,6 +3082,20 @@ describe('redactAttributes', () => { feeInBaseCurrency: 18.2, id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b', quantity: 50, + // The relation of the database is part of the response next to the + // asset profile + SymbolProfile: { + comment: 'Reviewed by the administrator', + name: 'Apple Inc', + scraperConfiguration: { + headers: { Authorization: 'Bearer TOKEN' }, + mode: 'lazy', + selector: '.price', + url: 'https://example.org/get_price' + }, + symbol: 'AAPL', + symbolMapping: { YAHOO: 'AAPL' } + }, type: 'BUY', unitPrice: 220.79, value: 11039.5, @@ -3098,6 +3125,13 @@ describe('redactAttributes', () => { feeInBaseCurrency: null, id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b', quantity: null, + SymbolProfile: { + comment: null, + name: 'Apple Inc', + scraperConfiguration: null, + symbol: 'AAPL', + symbolMapping: null + }, type: 'BUY', // A price per unit stays visible, like the average price and the market // price of a holding @@ -3156,6 +3190,18 @@ describe('redactAttributes', () => { currency: 'USD', fee: 19.9, quantity: 50, + SymbolProfile: { + comment: 'Reviewed by the administrator', + name: 'Apple Inc', + scraperConfiguration: { + headers: { Authorization: 'Bearer TOKEN' }, + mode: 'lazy', + selector: '.price', + url: 'https://example.org/get_price' + }, + symbol: 'AAPL', + symbolMapping: { YAHOO: 'AAPL' } + }, type: 'BUY', unitPrice: 220.79, value: 11039.5, @@ -3181,6 +3227,13 @@ describe('redactAttributes', () => { currency: 'USD', fee: null, quantity: null, + SymbolProfile: { + comment: null, + name: 'Apple Inc', + scraperConfiguration: null, + symbol: 'AAPL', + symbolMapping: null + }, type: 'BUY', unitPrice: 220.79, value: null, @@ -3237,4 +3290,101 @@ describe('redactAttributes', () => { ] }); }); + + // The type makes sure that the object keeps the shape of the response, in + // which the holdings of an ETF are part of its asset profile + it('should redact the values of the holdings of an ETF', () => { + expect( + redactPaths({ + object: { + holdings: [ + { + activitiesCount: 5, + allocationInPercentage: 0.057358979326040366, + assetProfile: { + assetClass: 'EQUITY', + assetSubClass: 'ETF', + countries: [], + currency: 'USD', + dataSource: 'YAHOO', + holdings: [ + { + allocationInPercentage: 0.06099941636982121, + name: 'APPLE INC', + valueInBaseCurrency: 860.2442693554036 + }, + { + allocationInPercentage: 0.05862464529372787, + name: 'MICROSOFT CORP', + valueInBaseCurrency: 826.7540602547973 + } + ], + name: 'Vanguard Total Stock Market Index Fund ETF Shares', + sectors: [], + symbol: 'VTI' + }, + dateOfFirstActivity: new Date('2019-02-28T23:00:00.000Z'), + dividend: 0, + grossPerformance: 5856.3, + grossPerformancePercent: 0.8832083851170418, + grossPerformancePercentWithCurrencyEffect: 0.8832083851170418, + grossPerformanceWithCurrencyEffect: 5856.3, + investment: 8246.2, + marketPrice: 282.05, + netPerformance: 5756.8, + netPerformancePercent: 0.8682024540139314, + netPerformancePercentWithCurrencyEffect: 0.8328704068843998, + netPerformanceWithCurrencyEffect: 5756.8, + quantity: 50, + valueInBaseCurrency: 14102.5 + } + ] + } satisfies Pick, + paths: DEFAULT_REDACTED_PATHS + }) + ).toStrictEqual({ + holdings: [ + { + activitiesCount: 5, + allocationInPercentage: 0.057358979326040366, + assetProfile: { + assetClass: 'EQUITY', + assetSubClass: 'ETF', + countries: [], + currency: 'USD', + dataSource: 'YAHOO', + holdings: [ + { + allocationInPercentage: 0.06099941636982121, + name: 'APPLE INC', + valueInBaseCurrency: null + }, + { + allocationInPercentage: 0.05862464529372787, + name: 'MICROSOFT CORP', + valueInBaseCurrency: null + } + ], + name: 'Vanguard Total Stock Market Index Fund ETF Shares', + sectors: [], + symbol: 'VTI' + }, + dateOfFirstActivity: '2019-02-28T23:00:00.000Z', + dividend: null, + grossPerformance: null, + grossPerformancePercent: 0.8832083851170418, + grossPerformancePercentWithCurrencyEffect: 0.8832083851170418, + grossPerformanceWithCurrencyEffect: null, + investment: null, + marketPrice: 282.05, + netPerformance: null, + netPerformancePercent: 0.8682024540139314, + netPerformancePercentWithCurrencyEffect: 0.8328704068843998, + netPerformanceWithCurrencyEffect: null, + quantity: null, + valueInBaseCurrency: null + } + ] + }); + }); }); diff --git a/apps/api/src/helper/object.helper.ts b/apps/api/src/helper/object.helper.ts index 350d5fe04a..c040c5e0fc 100644 --- a/apps/api/src/helper/object.helper.ts +++ b/apps/api/src/helper/object.helper.ts @@ -1,6 +1,6 @@ import fastRedact from 'fast-redact'; import jsonpath from 'jsonpath'; -import { cloneDeep, isObject } from 'lodash'; +import { cloneDeep, isObject } from 'lodash-es'; export function hasNotDefinedValuesInObject(aObject: Object): boolean { for (const key in aObject) { diff --git a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts index 31bfb620ed..339b91a2f7 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts @@ -83,6 +83,7 @@ export class TransformDataSourceInResponseInterceptor< '["filters.dataSource"]', 'activities[*].assetProfile.dataSource', 'activities[*].dataSource', + 'activities[*].SymbolProfile.dataSource', 'assetProfile.dataSource', 'benchmarks[*].dataSource', 'errors[*].dataSource', @@ -93,6 +94,7 @@ export class TransformDataSourceInResponseInterceptor< 'items[*].dataSource', 'latestActivities[*].assetProfile.dataSource', 'settings["filters.dataSource"]', + 'SymbolProfile.dataSource', 'watchlist[*].dataSource' ] }); diff --git a/apps/api/src/models/rule.ts b/apps/api/src/models/rule.ts index 4985b26477..f7f6c53637 100644 --- a/apps/api/src/models/rule.ts +++ b/apps/api/src/models/rule.ts @@ -7,7 +7,7 @@ import { } from '@ghostfolio/common/interfaces'; import { Big } from 'big.js'; -import { groupBy } from 'lodash'; +import { groupBy } from 'lodash-es'; import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { RuleInterface } from './interfaces/rule.interface'; diff --git a/apps/api/src/services/benchmark/benchmark.service.ts b/apps/api/src/services/benchmark/benchmark.service.ts index 953939caac..4ea27b2f2e 100644 --- a/apps/api/src/services/benchmark/benchmark.service.ts +++ b/apps/api/src/services/benchmark/benchmark.service.ts @@ -23,7 +23,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; import { addHours, isPast, subDays } from 'date-fns'; -import { round, uniqBy } from 'lodash'; +import { round, uniqBy } from 'lodash-es'; import ms from 'ms'; import { BenchmarkValue } from './interfaces/benchmark-value.interface'; diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index 87c1e85c61..cfacab3231 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -42,7 +42,7 @@ import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { DataSource, MarketData, Prisma, SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; import { eachDayOfInterval, format, isValid } from 'date-fns'; -import { groupBy, isEmpty, isNumber, omit, uniqWith } from 'lodash'; +import { groupBy, isEmpty, isNumber, omit, uniqWith } from 'lodash-es'; import ms from 'ms'; import { AssetProfileInvalidError } from './errors/asset-profile-invalid.error'; diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index 6bc003fc23..7dbb3b859c 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -31,7 +31,7 @@ import { SymbolProfile } from '@prisma/client'; import { addDays, format, isSameDay, isToday } from 'date-fns'; -import { isNumber } from 'lodash'; +import { isNumber } from 'lodash-es'; @Injectable() export class EodHistoricalDataService diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index 7c840b5fc6..a8f1c6287c 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts @@ -47,7 +47,7 @@ import { isSameDay, parseISO } from 'date-fns'; -import { isArray, uniqBy } from 'lodash'; +import { isArray, uniqBy } from 'lodash-es'; @Injectable() export class FinancialModelingPrepService diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index ca6357e257..d2b741d372 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts @@ -24,7 +24,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { DataSource, SymbolProfile } from '@prisma/client'; import { addDays, format, isSameDay } from 'date-fns'; import { ReasonPhrases, StatusCodes } from 'http-status-codes'; -import { uniqBy } from 'lodash'; +import { uniqBy } from 'lodash-es'; import YahooFinance from 'yahoo-finance2'; import type { ChartResultArray } from 'yahoo-finance2/modules/chart'; import type { diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 0d7e98e2e0..824149e74f 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -26,7 +26,7 @@ import { isToday, subDays } from 'date-fns'; -import { isNumber } from 'lodash'; +import { isNumber } from 'lodash-es'; import ms from 'ms'; import { ExchangeRatesByCurrency } from './interfaces/exchange-rate-data.interface'; diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts index 4cbe6969ae..3a98757591 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts @@ -33,7 +33,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { Job, JobOptions, Queue } from 'bull'; import { format, min, subDays, subMilliseconds, subYears } from 'date-fns'; -import { isEmpty } from 'lodash'; +import { isEmpty } from 'lodash-es'; import ms, { StringValue } from 'ms'; @Injectable() diff --git a/apps/api/src/services/twitter-bot/twitter-bot.service.ts b/apps/api/src/services/twitter-bot/twitter-bot.service.ts index 2dbed82d21..6282b4363b 100644 --- a/apps/api/src/services/twitter-bot/twitter-bot.service.ts +++ b/apps/api/src/services/twitter-bot/twitter-bot.service.ts @@ -10,7 +10,7 @@ import { import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { isWeekend } from 'date-fns'; -import { round } from 'lodash'; +import { round } from 'lodash-es'; import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2'; @Injectable() diff --git a/apps/client/jest.config.ts b/apps/client/jest.config.ts index 26d772e11f..d60a24ae4f 100644 --- a/apps/client/jest.config.ts +++ b/apps/client/jest.config.ts @@ -18,6 +18,6 @@ export default { } ] }, - transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], + transformIgnorePatterns: ['node_modules/(?!.*.mjs$|lodash-es/)'], preset: '../../jest.preset.js' }; diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index 7252ad1646..fba00e1c47 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -1,4 +1,8 @@ -import { getCssVariable } from '@ghostfolio/common/helper'; +import { + getBackgroundColor, + getCssVariable, + isDarkColorScheme +} from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, InfoItem, @@ -227,7 +231,7 @@ export class GfAppComponent implements OnInit { : !!this.info?.subscriptionOffer?.coupon || !!this.info?.subscriptionOffer?.durationExtension; - this.initializeTheme(this.user?.settings.colorScheme); + this.toggleTheme(this.user?.settings.colorScheme); this.changeDetectorRef.markForCheck(); }); @@ -259,21 +263,19 @@ export class GfAppComponent implements OnInit { document.location.href = `/${document.documentElement.lang}`; } - private initializeTheme(userPreferredColorScheme?: ColorScheme) { - const isDarkTheme = userPreferredColorScheme - ? userPreferredColorScheme === 'DARK' - : window.matchMedia('(prefers-color-scheme: dark)').matches; - - this.toggleTheme(isDarkTheme); + private initializeTheme() { + this.toggleTheme(); // Default chart styles Chart.defaults.font.family = getCssVariable('--font-family-sans-serif'); - window.matchMedia('(prefers-color-scheme: dark)').addListener((event) => { - if (!this.user?.settings.colorScheme) { - this.toggleTheme(event.matches); - } - }); + window + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', () => { + if (!this.user?.settings.colorScheme) { + this.toggleTheme(); + } + }); } private openHoldingDetailDialog({ @@ -344,12 +346,8 @@ export class GfAppComponent implements OnInit { }); } - private toggleTheme(isDarkTheme: boolean) { - const themeColor = getCssVariable( - isDarkTheme ? '--dark-background' : '--light-background' - ); - - if (isDarkTheme) { + private toggleTheme(userPreferredColorScheme?: ColorScheme) { + if (isDarkColorScheme(userPreferredColorScheme)) { this.document.body.classList.add('theme-dark'); this.document.body.classList.remove('theme-light'); } else { @@ -359,6 +357,6 @@ export class GfAppComponent implements OnInit { this.document .querySelector('meta[name="theme-color"]') - ?.setAttribute('content', themeColor); + ?.setAttribute('content', getBackgroundColor(userPreferredColorScheme)); } } diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index 75186b1744..44f7280473 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -16,6 +16,7 @@ import { } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasScope, scopes } from '@ghostfolio/common/scopes'; +import type { Tag } from '@ghostfolio/prisma/browser'; import { GfAccountBalancesComponent } from '@ghostfolio/ui/account-balances'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer'; @@ -45,7 +46,6 @@ import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { NavigationStart, Router } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; -import { Tag } from '@prisma/client'; import { Big } from 'big.js'; import { format, parseISO } from 'date-fns'; import { addIcons } from 'ionicons'; @@ -55,7 +55,7 @@ import { readerOutline, swapVerticalOutline } from 'ionicons/icons'; -import { isNumber } from 'lodash'; +import { isNumber } from 'lodash-es'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { filter, forkJoin } from 'rxjs'; diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html index a87179ecb9..5a86e4aa9b 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html @@ -23,6 +23,7 @@
+@if (isLoading) { + +} + (); + protected readonly displayedColumns = [ 'icon', 'name', @@ -68,12 +71,15 @@ export class GfAdminPlatformComponent implements OnInit { 'accounts', 'actions' ]; + + protected isLoading = false; protected readonly pageSize = DEFAULT_PAGE_SIZE; protected platforms: Platform[]; private readonly deviceType = computed( () => this.deviceDetectorService.deviceInfo().deviceType ); + private readonly paginator = viewChild.required(MatPaginator); private readonly sort = viewChild.required(MatSort); @@ -149,6 +155,8 @@ export class GfAdminPlatformComponent implements OnInit { } private fetchPlatforms() { + this.isLoading = true; + this.adminService .fetchPlatforms() .pipe(takeUntilDestroyed(this.destroyRef)) @@ -162,6 +170,8 @@ export class GfAdminPlatformComponent implements OnInit { this.dataService.updateInfo(); + this.isLoading = false; + this.changeDetectorRef.markForCheck(); }); } diff --git a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/interfaces/interfaces.ts index d3b9ae5288..206cc14e79 100644 --- a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/interfaces/interfaces.ts @@ -1,4 +1,4 @@ -import { Platform } from '@prisma/client'; +import type { Platform } from '@ghostfolio/prisma/browser'; export interface CreateOrUpdatePlatformDialogParams { platform?: Platform; diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.ts b/apps/client/src/app/components/admin-settings/admin-settings.component.ts index b0c3dc2d22..d49f3fb0aa 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.component.ts +++ b/apps/client/src/app/components/admin-settings/admin-settings.component.ts @@ -42,7 +42,7 @@ import { RouterModule } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { ellipsisHorizontal, trashOutline } from 'ionicons/icons'; -import { get } from 'lodash'; +import { get } from 'lodash-es'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { catchError, filter, of } from 'rxjs'; diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.html b/apps/client/src/app/components/admin-tag/admin-tag.component.html index 6cd9c12599..2e8750093a 100644 --- a/apps/client/src/app/components/admin-tag/admin-tag.component.html +++ b/apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -77,6 +77,24 @@ + + + Total + + + + + + @@ -123,6 +141,17 @@ +@if (isLoading) { + +} + (); + protected dataSource = + new MatTableDataSource(); + protected readonly displayedColumns = [ 'name', 'userId', 'accounts', 'activities', + 'total', 'actions' ]; + + protected isLoading = false; protected readonly isSystemTag = isSystemTag; protected readonly pageSize = DEFAULT_PAGE_SIZE; - protected tags: Tag[]; + protected tags: TagWithAccountAndActivityCount[]; protected readonly translate = translate; private readonly deviceType = computed( () => this.deviceDetectorService.deviceInfo().deviceType ); + private readonly paginator = viewChild.required(MatPaginator); private readonly sort = viewChild.required(MatSort); @@ -120,6 +128,13 @@ export class GfAdminTagComponent implements OnInit { this.fetchTags(); } + protected getTotalCount({ + accountCount, + activityCount + }: TagWithAccountAndActivityCount) { + return accountCount + activityCount; + } + protected onDeleteTag(aId: string) { this.notificationService.confirm({ confirmFn: () => { @@ -130,7 +145,7 @@ export class GfAdminTagComponent implements OnInit { }); } - protected onUpdateTag({ id }: Tag) { + protected onUpdateTag({ id }: TagWithAccountAndActivityCount) { this.router.navigate([], { queryParams: { editTagDialog: true, tagId: id } }); @@ -153,6 +168,8 @@ export class GfAdminTagComponent implements OnInit { } private fetchTags() { + this.isLoading = true; + this.dataService .fetchTags() .pipe(takeUntilDestroyed(this.destroyRef)) @@ -164,13 +181,19 @@ export class GfAdminTagComponent implements OnInit { this.dataSource.sort = this.sort(); this.dataSource.sortingDataAccessor = (tag, path) => { - return path === 'name' - ? translate(tag.name).toLocaleLowerCase() - : (getLowercase(tag, path) as number | string); + if (path === 'name') { + return translate(tag.name).toLocaleLowerCase(); + } else if (path === 'totalCount') { + return this.getTotalCount(tag); + } + + return getLowercase(tag, path) as number | string; }; this.dataService.updateInfo(); + this.isLoading = false; + this.changeDetectorRef.markForCheck(); }); } diff --git a/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/interfaces/interfaces.ts index f4b1104d00..b866ea47dd 100644 --- a/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/interfaces/interfaces.ts @@ -1,4 +1,4 @@ -import { Tag } from '@prisma/client'; +import type { Tag } from '@ghostfolio/prisma/browser'; export interface CreateOrUpdateTagDialogParams { tag?: Pick; diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts index 0060b38ee0..8969583d35 100644 --- a/apps/client/src/app/components/admin-users/admin-users.component.ts +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -5,12 +5,12 @@ import { import { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { formatRelativeTime } from '@ghostfolio/client/util/date.util'; import { DEFAULT_LOCALE, DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { canDeleteUser, getCountryName, - getDateFnsLocale, getDateFormatString, getEmojiFlag } from '@ghostfolio/common/helper'; @@ -49,11 +49,6 @@ import { import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; -import { - differenceInSeconds, - formatDistanceToNowStrict, - parseISO -} from 'date-fns'; import { addIcons } from 'ionicons'; import { contractOutline, @@ -93,6 +88,7 @@ export class GfAdminUsersComponent implements OnInit { protected defaultDateFormat: string; protected displayedColumns: string[] = []; protected readonly canDeleteUser = canDeleteUser; + protected readonly formatRelativeTime = formatRelativeTime; protected readonly getCountryName = getCountryName; protected readonly getEmojiFlag = getEmojiFlag; protected hasPermissionForSubscription: boolean; @@ -209,22 +205,6 @@ export class GfAdminUsersComponent implements OnInit { }); } - protected formatDistanceToNow(aDateString: string) { - if (aDateString) { - const distanceString = formatDistanceToNowStrict(parseISO(aDateString), { - addSuffix: true, - locale: getDateFnsLocale(this.user?.settings?.language) - }); - - return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) < - 60 - ? $localize`just now` - : distanceString; - } - - return ''; - } - protected onChangePage(page: PageEvent) { this.fetchUsers({ pageIndex: page.pageIndex @@ -332,6 +312,7 @@ export class GfAdminUsersComponent implements OnInit { currentUserId: this.user?.id, deviceType: this.deviceType(), hasPermissionForSubscription: this.hasPermissionForSubscription, + language: this.user?.settings?.language, locale: this.user?.settings?.locale ?? DEFAULT_LOCALE, userId: aUserId } satisfies UserDetailDialogParams, diff --git a/apps/client/src/app/components/admin-users/admin-users.html b/apps/client/src/app/components/admin-users/admin-users.html index 1be6174ace..e0ec70c551 100644 --- a/apps/client/src/app/components/admin-users/admin-users.html +++ b/apps/client/src/app/components/admin-users/admin-users.html @@ -32,7 +32,10 @@ class="ml-1" i18n-title title="Expires {{ - formatDistanceToNow(element.subscription.expiresAt) + formatRelativeTime({ + date: element.subscription.expiresAt, + language: user?.settings?.language + }) }} ({{ element.subscription.expiresAt | date: defaultDateFormat }})" @@ -79,7 +82,12 @@ class="mat-mdc-cell px-1 py-2 text-nowrap" mat-cell > - {{ formatDistanceToNow(element.createdAt) }} + {{ + formatRelativeTime({ + date: element.createdAt, + language: user?.settings?.language + }) + }} @@ -175,17 +183,21 @@ - Last Request + Last Request - {{ formatDistanceToNow(element.lastActivity) }} + {{ + formatRelativeTime({ + date: element.lastActivity, + language: user?.settings?.language + }) + }} } diff --git a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts index 8d5e3ff62b..72241f0454 100644 --- a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts +++ b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts @@ -12,8 +12,10 @@ import { LineChartItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { ColorScheme } from '@ghostfolio/common/types'; +import type { SymbolProfile } from '@ghostfolio/prisma/browser'; import { getTimeSeriesTooltipOptions, + onPrefersColorSchemeChange, registerChartConfiguration } from '@ghostfolio/ui/chart'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; @@ -26,13 +28,13 @@ import { OnChanges, OnDestroy, output, + SimpleChanges, viewChild } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatSelectModule } from '@angular/material/select'; import { RouterModule } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; -import { SymbolProfile } from '@prisma/client'; import { Chart, ChartData, @@ -68,7 +70,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { public readonly benchmark = input>(); public readonly benchmarkDataItems = input([]); public readonly benchmarks = input[]>(); - public readonly colorScheme = input.required(); + public readonly colorScheme = input(); public readonly isLoading = input(); public readonly locale = input(getLocale()); public readonly performanceDataItems = input.required(); @@ -76,7 +78,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { public readonly benchmarkChanged = output(); - protected chart: Chart<'line'>; + protected chart?: Chart<'line'>; protected hasPermissionToAccessAdminControl: boolean; protected readonly routerLinkAdminControlMarketData = internalRoutes.adminControl.subRoutes.marketData.routerLink; @@ -96,10 +98,24 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { registerChartConfiguration(); + onPrefersColorSchemeChange(() => { + if (this.chart && !this.colorScheme()) { + this.chart.destroy(); + this.chart = undefined; + + this.initialize(); + } + }); + addIcons({ arrowForwardOutline }); } - public ngOnChanges() { + public ngOnChanges(changes: SimpleChanges) { + if (changes.colorScheme && this.chart) { + this.chart.destroy(); + this.chart = undefined; + } + this.hasPermissionToAccessAdminControl = hasPermission( this.user()?.permissions, permissions.accessAdminControl diff --git a/apps/client/src/app/components/data-provider-status/data-provider-status.component.ts b/apps/client/src/app/components/data-provider-status/data-provider-status.component.ts index a77d65961a..13218623a0 100644 --- a/apps/client/src/app/components/data-provider-status/data-provider-status.component.ts +++ b/apps/client/src/app/components/data-provider-status/data-provider-status.component.ts @@ -1,3 +1,4 @@ +import type { DataSource } from '@ghostfolio/prisma/enums'; import { DataService } from '@ghostfolio/ui/services'; import { CommonModule } from '@angular/common'; @@ -9,7 +10,6 @@ import { OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import type { DataSource } from '@prisma/client'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { catchError, map, type Observable, of } from 'rxjs'; diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index d8385d70e2..8c3ec2206e 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -25,6 +25,7 @@ import { import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { AccountWithValue } from '@ghostfolio/common/types'; +import type { MarketData, Tag } from '@ghostfolio/prisma/browser'; import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits'; @@ -69,7 +70,6 @@ import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { NavigationStart, Router, RouterModule } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; -import { MarketData, Tag } from '@prisma/client'; import { isUUID } from 'class-validator'; import { format, isSameMonth, isToday, parseISO } from 'date-fns'; import { addIcons } from 'ionicons'; @@ -82,7 +82,7 @@ import { swapVerticalOutline, walletOutline } from 'ionicons/icons'; -import { isNumber, round, uniqBy } from 'lodash'; +import { isNumber, round, uniqBy } from 'lodash-es'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { filter, switchMap } from 'rxjs/operators'; diff --git a/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts index efc10d6ac3..a03fd93992 100644 --- a/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts @@ -1,10 +1,9 @@ import { ColorScheme } from '@ghostfolio/common/types'; - -import { DataSource } from '@prisma/client'; +import { DataSource } from '@ghostfolio/prisma/enums'; export interface HoldingDetailDialogParams { baseCurrency: string; - colorScheme: ColorScheme; + colorScheme?: ColorScheme; dataSource: DataSource; deviceType: string; hasPermissionToAccessAdminControl: boolean; diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index 43ac5ff99b..a1dc6a28f9 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -14,6 +14,7 @@ import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.in import { ColorScheme, GroupBy } from '@ghostfolio/common/types'; import { getTimeSeriesTooltipOptions, + onPrefersColorSchemeChange, registerChartConfiguration } from '@ghostfolio/ui/chart'; @@ -24,6 +25,7 @@ import { Input, OnChanges, OnDestroy, + SimpleChanges, viewChild } from '@angular/core'; import { @@ -55,19 +57,19 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; export class GfInvestmentChartComponent implements OnChanges, OnDestroy { @Input() public benchmarkDataItems: InvestmentItem[] = []; @Input() public benchmarkDataLabel = ''; - @Input() public colorScheme: ColorScheme; - @Input() public currency: string; + @Input() public colorScheme?: ColorScheme; + @Input() public currency?: string; @Input() public groupBy: GroupBy; @Input() public historicalDataItems: LineChartItem[] = []; - @Input() public isInPercentage = false; + @Input() public isInPercentage?: boolean = false; @Input() public isLoading = false; - @Input() public locale = getLocale(); - @Input() public savingsRate = 0; + @Input() public locale?: string = getLocale(); + @Input() public savingsRate?: number = 0; private readonly chartCanvas = viewChild.required>('chartCanvas'); - private chart: Chart<'bar' | 'line'>; + private chart?: Chart<'bar' | 'line'>; private investments: InvestmentItem[]; private values: LineChartItem[]; @@ -84,9 +86,23 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { ); registerChartConfiguration(); + + onPrefersColorSchemeChange(() => { + if (this.chart && !this.colorScheme) { + this.chart.destroy(); + this.chart = undefined; + + this.initialize(); + } + }); } - public ngOnChanges() { + public ngOnChanges(changes: SimpleChanges) { + if (changes.colorScheme && this.chart) { + this.chart.destroy(); + this.chart = undefined; + } + if (this.benchmarkDataItems && this.historicalDataItems) { this.initialize(); } diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts index a48d77a2dd..4f7f35dd1e 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts @@ -23,7 +23,7 @@ import { IonIcon } from '@ionic/angular/standalone'; import { CountUp } from 'countup.js'; import { addIcons } from 'ionicons'; import { timeOutline } from 'ionicons/icons'; -import { isNumber } from 'lodash'; +import { isNumber } from 'lodash-es'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @Component({ diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 1e10210a6a..519fb04ab2 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -98,10 +98,29 @@ /> + @if (user?.settings?.isExperimentalFeatures) { +
+
Dividend
+
+ @if (isNumber(summary?.dividendInBaseCurrency)) { + + + } + +
+
+ }
Fees
- @if (summary?.fees || summary?.fees === 0) { + @if (isNumber(summary?.fees)) { - }
@@ -154,7 +173,7 @@ [isLoading]="isLoading" [isPercent]="true" [locale]="locale" - [value]="summary?.netPerformancePercentageWithCurrencyEffect" + [value]="netPerformancePercentageWithCurrencyEffect" /> @@ -367,10 +386,7 @@
Liabilities
- @if ( - summary?.liabilitiesInBaseCurrency || - summary?.liabilitiesInBaseCurrency === 0 - ) { + @if (isNumber(summary?.liabilitiesInBaseCurrency)) { - }
@@ -488,18 +506,20 @@ /> -
-
Dividend
-
- + @if (!user?.settings?.isExperimentalFeatures) { +
+
Dividend
+
+ +
-
+ }
diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts index dec65ddad9..13686624f7 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts @@ -23,7 +23,7 @@ import { ellipsisHorizontalCircleOutline, informationCircleOutline } from 'ionicons/icons'; -import { isNumber } from 'lodash'; +import { isNumber } from 'lodash-es'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @Component({ @@ -55,6 +55,7 @@ export class GfPortfolioSummaryComponent implements OnChanges { 'BUY_AND_SELL_ACTIVITIES_TOOLTIP' ); + protected readonly isNumber = isNumber; protected readonly PerformanceCalculationType = PerformanceCalculationType; protected isCashExpanded = false; @@ -136,6 +137,36 @@ export class GfPortfolioSummaryComponent implements OnChanges { ); } + protected get netPerformancePercentageWithCurrencyEffect() { + if ( + !this.user?.settings?.isExperimentalFeatures || + !isNumber(this.summary?.netPerformancePercentageWithCurrencyEffect) + ) { + return this.summary?.netPerformancePercentageWithCurrencyEffect; + } + + // Include the dividends (total return) + return ( + this.summary.netPerformancePercentageWithCurrencyEffect + + (this.summary.dividendPercentageWithCurrencyEffect ?? 0) + ); + } + + protected get netPerformanceWithCurrencyEffect() { + if ( + !this.user?.settings?.isExperimentalFeatures || + !isNumber(this.summary?.netPerformanceWithCurrencyEffect) + ) { + return this.summary?.netPerformanceWithCurrencyEffect; + } + + // Include the dividends (total return) + return ( + this.summary.netPerformanceWithCurrencyEffect + + this.summary.dividendInBaseCurrency + ); + } + public ngOnChanges() { if (this.summary) { if ( @@ -170,7 +201,8 @@ export class GfPortfolioSummaryComponent implements OnChanges { }, confirmLabel: $localize`Save`, defaultValue: this.summary.emergencyFund?.total?.toString() ?? '0', - title: $localize`Please set the amount of your emergency fund.` + title: $localize`Please set the amount of your emergency fund.`, + valueSuffix: this.baseCurrency }); } diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts index 9f119fc439..f58b19212b 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts @@ -15,6 +15,7 @@ import { } from '@ghostfolio/common/scopes'; import { AccessLevel, AccountWithPlatform } from '@ghostfolio/common/types'; import { validateObjectForForm } from '@ghostfolio/common/utils'; +import { AccessType } from '@ghostfolio/prisma/enums'; import { GfAccessLevelIconComponent } from '@ghostfolio/ui/access-level-icon'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { @@ -57,7 +58,6 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { IonIcon } from '@ionic/angular/standalone'; -import { AccessType } from '@prisma/client'; import { addDays, endOfDay, isValid, startOfDay } from 'date-fns'; import { StatusCodes } from 'http-status-codes'; import { addIcons } from 'ionicons'; diff --git a/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts index 41b2b210f1..2b2f85fbde 100644 --- a/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts @@ -2,6 +2,7 @@ export interface UserDetailDialogParams { currentUserId: string; deviceType: string; hasPermissionForSubscription: boolean; + language?: string; locale: string; userId: string; } diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts index 3aec4d42c7..db8c94abcd 100644 --- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts +++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts @@ -1,9 +1,11 @@ +import { formatRelativeTime } from '@ghostfolio/client/util/date.util'; import { canDeleteUser, getCountryName, getSum } from '@ghostfolio/common/helper'; import { AdminUserResponse } from '@ghostfolio/common/interfaces'; +import type { Subscription } from '@ghostfolio/prisma/browser'; import { AdminService, DataService } from '@ghostfolio/ui/services'; import { GfValueComponent } from '@ghostfolio/ui/value'; @@ -23,7 +25,6 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatMenuModule } from '@angular/material/menu'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { IonIcon } from '@ionic/angular/standalone'; -import { Subscription } from '@prisma/client'; import { Big } from 'big.js'; import { differenceInDays } from 'date-fns'; import { addIcons } from 'ionicons'; @@ -55,6 +56,7 @@ import { export class GfUserDetailDialogComponent implements OnInit { protected readonly baseCurrency: string; protected readonly canDeleteUser = canDeleteUser; + protected readonly formatRelativeTime = formatRelativeTime; protected readonly getCountryName = getCountryName; protected isLoading = true; @@ -91,25 +93,7 @@ export class GfUserDetailDialogComponent implements OnInit { } public ngOnInit() { - this.adminService - .fetchUserById(this.data.userId) - .pipe( - takeUntilDestroyed(this.destroyRef), - catchError(() => { - this.dialogRef.close(); - - return EMPTY; - }) - ) - .subscribe((user) => { - this.user = user; - - this.subscriptionsDataSource.data = this.user.subscriptions ?? []; - - this.isLoading = false; - - this.changeDetectorRef.markForCheck(); - }); + this.initialize(); } protected deleteUser() { @@ -141,6 +125,30 @@ export class GfUserDetailDialogComponent implements OnInit { : $localize`Coupon`; } + protected initialize() { + this.isLoading = true; + + this.adminService + .fetchUserById(this.data.userId) + .pipe( + takeUntilDestroyed(this.destroyRef), + catchError(() => { + this.dialogRef.close(); + + return EMPTY; + }) + ) + .subscribe((user) => { + this.user = user; + + this.subscriptionsDataSource.data = this.user.subscriptions ?? []; + + this.isLoading = false; + + this.changeDetectorRef.markForCheck(); + }); + } + protected onClose() { this.dialogRef.close(); } diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html index efc4fe5b43..c54318f856 100644 --- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html +++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -13,6 +13,10 @@ class="no-max-width" xPosition="before" > + +
- + diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts index edb9a852f5..fcc98c271e 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts @@ -1,6 +1,5 @@ import { User } from '@ghostfolio/common/interfaces'; - -import { DataSource } from '@prisma/client'; +import { DataSource } from '@ghostfolio/prisma/enums'; export interface HistoricalMarketDataEditorDialogParams { currency: string; diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts index aa6177cff9..401fd84523 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts @@ -5,6 +5,8 @@ import { getLocale } from '@ghostfolio/common/helper'; import { LineChartItem, User } from '@ghostfolio/common/interfaces'; +import type { MarketData } from '@ghostfolio/prisma/browser'; +import { DataSource } from '@ghostfolio/prisma/enums'; import { DataService } from '@ghostfolio/ui/services'; import { CommonModule } from '@angular/common'; @@ -28,7 +30,6 @@ import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatInputModule } from '@angular/material/input'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { DataSource, MarketData } from '@prisma/client'; import { addDays, addMonths, diff --git a/libs/ui/src/lib/line-chart/line-chart.component.ts b/libs/ui/src/lib/line-chart/line-chart.component.ts index 5dbb6cc31c..ad399496e2 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -4,11 +4,7 @@ import { getValueAxisOptions, getVerticalHoverLinePlugin } from '@ghostfolio/common/chart-helper'; -import { - DEFAULT_COLOR_SCHEME, - primaryColorRgb, - secondaryColorRgb -} from '@ghostfolio/common/config'; +import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; import { getBackgroundColor, getLocale } from '@ghostfolio/common/helper'; import { LineChartItem } from '@ghostfolio/common/interfaces'; import { ColorScheme } from '@ghostfolio/common/types'; @@ -22,6 +18,7 @@ import { Input, OnChanges, OnDestroy, + SimpleChanges, ViewChild } from '@angular/core'; import { @@ -41,6 +38,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { getTimeSeriesTooltipOptions, + onPrefersColorSchemeChange, registerChartConfiguration } from '../chart'; @@ -56,7 +54,7 @@ export class GfLineChartComponent { @Input() benchmarkDataItems: LineChartItem[] = []; @Input() benchmarkLabel = ''; - @Input() colorScheme: ColorScheme = DEFAULT_COLOR_SCHEME; + @Input() colorScheme?: ColorScheme; @Input() currency: string; @Input() historicalDataItems: LineChartItem[]; @Input() isAnimated = false; @@ -75,7 +73,7 @@ export class GfLineChartComponent @ViewChild('chartCanvas') chartCanvas: ElementRef; - public chart: Chart<'line'>; + public chart?: Chart<'line'>; public isLoading = true; private readonly ANIMATION_DURATION = 1200; @@ -92,6 +90,15 @@ export class GfLineChartComponent ); registerChartConfiguration(); + + onPrefersColorSchemeChange(() => { + if (this.chart && !this.colorScheme) { + this.chart.destroy(); + this.chart = undefined; + + this.initialize(); + } + }); } public ngAfterViewInit() { @@ -105,7 +112,12 @@ export class GfLineChartComponent } } - public ngOnChanges() { + public ngOnChanges(changes: SimpleChanges) { + if (changes.colorScheme && this.chart) { + this.chart.destroy(); + this.chart = undefined; + } + if (this.historicalDataItems || this.historicalDataItems === null) { setTimeout(() => { // Wait for the chartCanvas diff --git a/libs/ui/src/lib/mocks/entity-logo-image-source.service.mock.ts b/libs/ui/src/lib/mocks/entity-logo-image-source.service.mock.ts index 766a4178b0..4160485bce 100644 --- a/libs/ui/src/lib/mocks/entity-logo-image-source.service.mock.ts +++ b/libs/ui/src/lib/mocks/entity-logo-image-source.service.mock.ts @@ -1,6 +1,5 @@ import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; - -import { DataSource } from '@prisma/client'; +import { DataSource } from '@ghostfolio/prisma/enums'; // Resolves like a logo of an unknown entity, but fails to load const UNAVAILABLE_LOGO_URL = 'data:image/png;base64,unavailable'; diff --git a/libs/ui/src/lib/notifications/interfaces/interfaces.ts b/libs/ui/src/lib/notifications/interfaces/interfaces.ts index dcea7d53c9..a9e065d83d 100644 --- a/libs/ui/src/lib/notifications/interfaces/interfaces.ts +++ b/libs/ui/src/lib/notifications/interfaces/interfaces.ts @@ -26,4 +26,5 @@ export interface PromptParams { discardLabel?: string; title: string; valueLabel?: string; + valueSuffix?: string; } diff --git a/libs/ui/src/lib/notifications/notification.service.ts b/libs/ui/src/lib/notifications/notification.service.ts index b0758b5763..d1c8459a6c 100644 --- a/libs/ui/src/lib/notifications/notification.service.ts +++ b/libs/ui/src/lib/notifications/notification.service.ts @@ -3,7 +3,7 @@ import { translate } from '@ghostfolio/ui/i18n'; import { inject, Service } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { isFunction } from 'lodash'; +import { isFunction } from 'lodash-es'; import { GfAlertDialogComponent } from './alert-dialog/alert-dialog.component'; import { GfConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component'; @@ -87,7 +87,8 @@ export class NotificationService { defaultValue: aParams.defaultValue, discardLabel: aParams.discardLabel, title: aParams.title, - valueLabel: aParams.valueLabel + valueLabel: aParams.valueLabel, + valueSuffix: aParams.valueSuffix }); return dialog.afterClosed().subscribe((result: string) => { diff --git a/libs/ui/src/lib/notifications/prompt-dialog/interfaces/interfaces.ts b/libs/ui/src/lib/notifications/prompt-dialog/interfaces/interfaces.ts index 607577dfa0..d3dcccf596 100644 --- a/libs/ui/src/lib/notifications/prompt-dialog/interfaces/interfaces.ts +++ b/libs/ui/src/lib/notifications/prompt-dialog/interfaces/interfaces.ts @@ -4,4 +4,5 @@ export interface PromptDialogParams { discardLabel: string; title: string; valueLabel?: string; + valueSuffix?: string; } diff --git a/libs/ui/src/lib/notifications/prompt-dialog/prompt-dialog.component.ts b/libs/ui/src/lib/notifications/prompt-dialog/prompt-dialog.component.ts index e9750c3f52..e74056401c 100644 --- a/libs/ui/src/lib/notifications/prompt-dialog/prompt-dialog.component.ts +++ b/libs/ui/src/lib/notifications/prompt-dialog/prompt-dialog.component.ts @@ -26,6 +26,7 @@ export class GfPromptDialogComponent { public formControl = new FormControl(''); public title: string; public valueLabel?: string; + public valueSuffix?: string; protected readonly dialogRef = inject>(MatDialogRef); @@ -35,7 +36,8 @@ export class GfPromptDialogComponent { defaultValue, discardLabel, title, - valueLabel + valueLabel, + valueSuffix }: PromptDialogParams) { this.confirmLabel = confirmLabel; this.defaultValue = defaultValue; @@ -43,5 +45,6 @@ export class GfPromptDialogComponent { this.formControl.setValue(defaultValue ?? null); this.title = title; this.valueLabel = valueLabel; + this.valueSuffix = valueSuffix; } } diff --git a/libs/ui/src/lib/notifications/prompt-dialog/prompt-dialog.html b/libs/ui/src/lib/notifications/prompt-dialog/prompt-dialog.html index d73c8dabb5..0b39f22e17 100644 --- a/libs/ui/src/lib/notifications/prompt-dialog/prompt-dialog.html +++ b/libs/ui/src/lib/notifications/prompt-dialog/prompt-dialog.html @@ -8,6 +8,9 @@ {{ valueLabel }} } + @if (valueSuffix) { + {{ valueSuffix }} + } diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.util.ts b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.util.ts index 193624c768..163029258a 100644 --- a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.util.ts +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.util.ts @@ -1,8 +1,8 @@ import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; - -import { AssetClass, DataSource, Tag } from '@prisma/client'; +import type { Tag } from '@ghostfolio/prisma/browser'; +import { AssetClass, DataSource } from '@ghostfolio/prisma/enums'; import { translate } from '../i18n'; import { PortfolioFilterFormValue } from './interfaces'; diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index bf2f595c26..e27097a701 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -1,8 +1,9 @@ import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; -import { DEFAULT_COLOR_SCHEME, UNKNOWN_KEY } from '@ghostfolio/common/config'; +import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { getLocale, getSum, getTextColor } from '@ghostfolio/common/helper'; import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { ColorScheme } from '@ghostfolio/common/types'; +import { DataSource } from '@ghostfolio/prisma/enums'; import { AfterViewInit, @@ -15,7 +16,6 @@ import { output, viewChild } from '@angular/core'; -import { DataSource } from '@prisma/client'; import { Big } from 'big.js'; import { ArcElement, @@ -33,6 +33,7 @@ import Color from 'color'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import OpenColor from 'open-color'; +import { onPrefersColorSchemeChange } from '../chart'; import { PortfolioProportionChartClickEvent } from './interfaces/interfaces'; const { @@ -94,6 +95,12 @@ export class GfPortfolioProportionChartComponent public constructor() { Chart.register(ArcElement, DoughnutController, LinearScale, Tooltip); + + onPrefersColorSchemeChange(() => { + if (this.chart && !this.colorScheme) { + this.initialize(); + } + }); } public ngAfterViewInit() { @@ -124,7 +131,7 @@ export class GfPortfolioProportionChartComponent }; } = {}; - const textColor = getTextColor(this.colorScheme ?? DEFAULT_COLOR_SCHEME); + const textColor = getTextColor(this.colorScheme); this.colorMap = { [this.OTHER_KEY]: `rgba(${textColor}, 0.24)`, @@ -448,7 +455,7 @@ export class GfPortfolioProportionChartComponent ): Partial> { return { ...getTooltipOptions({ - colorScheme: this.colorScheme ?? DEFAULT_COLOR_SCHEME, + colorScheme: this.colorScheme, currency: this.baseCurrency, locale: this.locale }), diff --git a/libs/ui/src/lib/services/admin.service.ts b/libs/ui/src/lib/services/admin.service.ts index aa048faaf2..0307909dc1 100644 --- a/libs/ui/src/lib/services/admin.service.ts +++ b/libs/ui/src/lib/services/admin.service.ts @@ -21,13 +21,17 @@ import { EnhancedAssetProfile } from '@ghostfolio/common/interfaces'; import { DateRange } from '@ghostfolio/common/types'; +import type { + AssetProfileSplit, + MarketData, + Platform +} from '@ghostfolio/prisma/browser'; import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { inject, Service } from '@angular/core'; -import { AssetProfileSplit, MarketData, Platform } from '@prisma/client'; import { JobStatus } from 'bull'; -import { isNumber } from 'lodash'; +import { isNumber } from 'lodash-es'; @Service() export class AdminService { diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index 2b390df3c5..8545c36ba4 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -62,26 +62,27 @@ import { filterGlobalPermissions } from '@ghostfolio/common/permissions'; import type { AiPromptMode, DateRange, - GroupBy + GroupBy, + TagWithAccountAndActivityCount } from '@ghostfolio/common/types'; -import { translate } from '@ghostfolio/ui/i18n'; - -import { HttpClient, HttpParams } from '@angular/common/http'; -import { inject, Service } from '@angular/core'; -import { SortDirection } from '@angular/material/sort'; -import { utc } from '@date-fns/utc'; -import { +import type { Account, AccountBalance, - DataSource, MarketData, Order, SymbolProfile, Tag, User as UserModel -} from '@prisma/client'; +} from '@ghostfolio/prisma/browser'; +import { DataSource } from '@ghostfolio/prisma/enums'; +import { translate } from '@ghostfolio/ui/i18n'; + +import { HttpClient, HttpParams } from '@angular/common/http'; +import { inject, Service } from '@angular/core'; +import { SortDirection } from '@angular/material/sort'; +import { utc } from '@date-fns/utc'; import { format, parseISO } from 'date-fns'; -import { cloneDeep, groupBy, isNumber } from 'lodash'; +import { cloneDeep, groupBy, isNumber } from 'lodash-es'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -843,7 +844,7 @@ export class DataService { } public fetchTags() { - return this.http.get('/api/v1/tags'); + return this.http.get('/api/v1/tags'); } public fetchWatchlist() { diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts index 683f7c9252..d56aa177bf 100644 --- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts @@ -36,7 +36,7 @@ import { } from '@angular/material/form-field'; import { MatInput, MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { isString } from 'lodash'; +import { isString } from 'lodash-es'; import { tap } from 'rxjs'; import { debounceTime, diff --git a/libs/ui/src/lib/tags-selector/interfaces/interfaces.ts b/libs/ui/src/lib/tags-selector/interfaces/interfaces.ts index fe9af9882c..616ffa2487 100644 --- a/libs/ui/src/lib/tags-selector/interfaces/interfaces.ts +++ b/libs/ui/src/lib/tags-selector/interfaces/interfaces.ts @@ -1,4 +1,4 @@ -import { Tag } from '@prisma/client'; +import type { Tag } from '@ghostfolio/prisma/browser'; export interface NewTag extends Omit { id: undefined; diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.ts b/libs/ui/src/lib/top-holdings/top-holdings.component.ts index 5ee8813a1f..1a675a5531 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.ts +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.ts @@ -18,7 +18,7 @@ import { import { MatButtonModule } from '@angular/material/button'; import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; -import { capitalize } from 'lodash'; +import { capitalize } from 'lodash-es'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { GfValueComponent } from '../value/value.component'; @@ -39,6 +39,7 @@ import { GfValueComponent } from '../value/value.component'; }) export class GfTopHoldingsComponent implements OnChanges { @Input() baseCurrency: string; + @Input() hasPermissionToShowValues = true; @Input() locale = getLocale(); @Input() pageSize = Number.MAX_SAFE_INTEGER; @Input() topHoldings: HoldingWithParents[]; @@ -48,14 +49,18 @@ export class GfTopHoldingsComponent implements OnChanges { @ViewChild(MatPaginator) paginator: MatPaginator; public dataSource = new MatTableDataSource(); - public displayedColumns: string[] = [ - 'name', - 'valueInBaseCurrency', - 'allocationInPercentage' - ]; + public displayedColumns: string[] = []; public isLoading = true; public ngOnChanges() { + this.displayedColumns = ['name']; + + if (this.hasPermissionToShowValues) { + this.displayedColumns.push('valueInBaseCurrency'); + } + + this.displayedColumns.push('allocationInPercentage'); + this.isLoading = true; this.dataSource = new MatTableDataSource(this.topHoldings); diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts index 04ea5c65f0..72f34e8027 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts @@ -1,5 +1,3 @@ -import { DEFAULT_COLOR_SCHEME } from '@ghostfolio/common/config'; - import { CommonModule } from '@angular/common'; import { moduleMetadata } from '@storybook/angular'; import type { Meta, StoryObj } from '@storybook/angular'; @@ -38,7 +36,7 @@ export const Default: Story = { args: { holdings, baseCurrency: 'USD', - colorScheme: DEFAULT_COLOR_SCHEME, + colorScheme: 'LIGHT', cursor: undefined, dateRange: 'mtd', locale: 'en-US' 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 54c8d7f907..83b9916c6e 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -32,10 +32,11 @@ import { Chart, LinearScale, Tooltip } from 'chart.js'; import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; import { isUUID } from 'class-validator'; import { differenceInDays, max } from 'date-fns'; -import { orderBy, round } from 'lodash'; +import { orderBy, round } from 'lodash-es'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import OpenColor from 'open-color'; +import { onPrefersColorSchemeChange } from '../chart'; import type { GetColorParams, GfTreemapDataPoint, @@ -55,7 +56,7 @@ export class GfTreemapChartComponent implements AfterViewInit, OnChanges, OnDestroy { public readonly baseCurrency = input.required(); - public readonly colorScheme = input.required(); + public readonly colorScheme = input(); public readonly cursor = input.required(); public readonly dateRange = input.required(); public readonly holdings = input(); @@ -71,6 +72,12 @@ export class GfTreemapChartComponent public constructor() { Chart.register(LinearScale, Tooltip, TreemapController, TreemapElement); + + onPrefersColorSchemeChange(() => { + if (this.chart && !this.colorScheme()) { + this.initialize(); + } + }); } public ngAfterViewInit() { diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index d13acadd7a..34ce354f68 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -21,7 +21,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { IonIcon } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { checkmarkOutline, copyOutline } from 'ionicons/icons'; -import { isNumber } from 'lodash'; +import { isNumber } from 'lodash-es'; import ms from 'ms'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; diff --git a/nx.json b/nx.json index 3b8c3846a1..d42873bda8 100644 --- a/nx.json +++ b/nx.json @@ -98,6 +98,7 @@ "cache": true }, "lint": { + "inputs": ["default", "^default", "{workspaceRoot}/eslint.config.cjs"], "cache": true }, "@nx/jest:jest": { @@ -124,7 +125,8 @@ "sharedGlobals": [ "{workspaceRoot}/angular.json", "{workspaceRoot}/tsconfig.base.json", - "{workspaceRoot}/nx.json" + "{workspaceRoot}/nx.json", + "{workspaceRoot}/prisma/schema.prisma" ], "production": [ "default", diff --git a/package-lock.json b/package-lock.json index c4896f0436..a5dad5f892 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.74.0", + "version": "3.76.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.74.0", + "version": "3.76.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -83,7 +83,7 @@ "ionicons": "8.0.13", "jsonpath": "1.3.0", "katex": "0.16.47", - "lodash": "4.18.1", + "lodash-es": "4.18.1", "marked": "18.0.12", "marked-katex-extension": "5.1.12", "ms": "3.0.0-canary.1", @@ -147,13 +147,11 @@ "@types/google-spreadsheet": "3.1.5", "@types/jest": "30.0.0", "@types/jsonpath": "0.2.4", - "@types/lodash": "4.17.25", + "@types/lodash-es": "4.17.12", "@types/node": "22.20.1", "@types/papaparse": "5.5.2", "@types/passport-google-oauth20": "2.0.17", "@types/passport-openidconnect": "0.1.3", - "@typescript-eslint/eslint-plugin": "8.67.0", - "@typescript-eslint/parser": "8.67.0", "eslint": "9.35.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "2.32.0", @@ -15223,6 +15221,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/luxon": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", @@ -27044,7 +27052,6 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { diff --git a/package.json b/package.json index 3361c354ba..fcb94759f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.74.0", + "version": "3.76.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", @@ -127,7 +127,7 @@ "ionicons": "8.0.13", "jsonpath": "1.3.0", "katex": "0.16.47", - "lodash": "4.18.1", + "lodash-es": "4.18.1", "marked": "18.0.12", "marked-katex-extension": "5.1.12", "ms": "3.0.0-canary.1", @@ -191,13 +191,11 @@ "@types/google-spreadsheet": "3.1.5", "@types/jest": "30.0.0", "@types/jsonpath": "0.2.4", - "@types/lodash": "4.17.25", + "@types/lodash-es": "4.17.12", "@types/node": "22.20.1", "@types/papaparse": "5.5.2", "@types/passport-google-oauth20": "2.0.17", "@types/passport-openidconnect": "0.1.3", - "@typescript-eslint/eslint-plugin": "8.67.0", - "@typescript-eslint/parser": "8.67.0", "eslint": "9.35.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "2.32.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8d874b4de6..d2181e13be 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,7 +1,12 @@ generator client { - provider = "prisma-client-js" - previewFeatures = [] binaryTargets = ["debian-openssl-3.0.x", "linux-arm64-openssl-3.0.x", "native"] + previewFeatures = [] + provider = "prisma-client-js" +} + +generator workspaceClient { + output = "./generated" + provider = "prisma-client" } datasource db { diff --git a/tsconfig.base.json b/tsconfig.base.json index a74d9910f3..f5771c4e9d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -19,6 +19,8 @@ "@ghostfolio/api/*": ["./apps/api/src/*"], "@ghostfolio/client/*": ["./apps/client/src/app/*"], "@ghostfolio/common/*": ["./libs/common/src/lib/*"], + "@ghostfolio/prisma/browser": ["./prisma/generated/browser"], + "@ghostfolio/prisma/enums": ["./prisma/generated/enums"], "@ghostfolio/ui/*": ["./libs/ui/src/lib/*"] }, "noFallthroughCasesInSwitch": true,