diff --git a/CHANGELOG.md b/CHANGELOG.md index 76256f3fa..dca83da42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Moved the endpoint to get the asset profiles from `GET api/v1/admin/market-data` to `GET api/v1/asset-profiles` +- Added the selected asset profile count to the delete menu item of the historical market data table in the admin control panel +- Added the selected asset profile count to the deletion confirmation dialog of the historical market data table in the admin control panel + +### Fixed + +- Fixed an issue with the localization of the country names + +## 3.12.0 - 2026-06-17 + +### Changed + +- Improved the styling of the checkboxes to consistently use the primary color in their states +- Improved the account name display in the accounts table +- Improved the name display in the activities table +- Improved the last activity display in the users table of the admin control panel +- Improved the registration display in the users table of the admin control panel +- Improved the user id display in the users table of the admin control panel +- Deprecated `SymbolProfile` in favor of `assetProfile` in the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol` +- Improved the language localization for German (`de`) +- Upgraded `svgmap` from version `2.19.3` to `2.21.0` + +### Fixed + +- Fixed a chart error on interaction by registering the annotation plugin early +- Fixed an issue on the allocations page where clicking an account in the _By Account_ chart did not open the detail dialog +- Restricted the maximum height of the import activities dialog +- Fixed the dark mode styling of the safe withdrawal rate selector in the _FIRE_ section (experimental) + +## 3.11.0 - 2026-06-14 + +### Added + +- Added support for a click handler in the page tabs component + +### Changed + +- Improved the styling of the tabs across various dialogs +- Improved the styling of the page tabs component on desktop +- Enabled the _Bull Dashboard_ tab in the admin control panel (experimental) +- Migrated the settings dialog to customize the rule thresholds of the _X-ray_ page from `ngModel` to form control +- Improved the language localization for Spanish (`es`) +- Upgraded `bull-board` from version `7.1.5` to `7.2.1` +- Upgraded `date-fns` from version `4.1.0` to `4.4.0` + +### Fixed + +- Improved the loading state when customizing the rule thresholds on the _X-ray_ page ## 3.10.0 - 2026-06-13 diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 24d760888..c15353521 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -957,6 +957,18 @@ export class PortfolioService { marketPriceMin, SymbolProfile, tags, + assetProfile: { + assetClass: SymbolProfile.assetClass, + assetSubClass: SymbolProfile.assetSubClass, + countries: SymbolProfile.countries, + currency: SymbolProfile.currency, + dataSource: SymbolProfile.dataSource, + isin: SymbolProfile.isin, + name: SymbolProfile.name, + sectors: SymbolProfile.sectors, + symbol: SymbolProfile.symbol, + userId: SymbolProfile.userId + }, averagePrice: averagePrice.toNumber(), dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 9d8d9da9d..0c159bc1c 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -28,14 +28,16 @@ import { DEFAULT_CURRENCY, DEFAULT_DATE_RANGE, DEFAULT_LANGUAGE_CODE, + DEFAULT_LOCALE, PROPERTY_IS_READ_ONLY_MODE, + PROPERTY_REFERRAL_PARTNERS, PROPERTY_SYSTEM_MESSAGE, - TAG_ID_EXCLUDE_FROM_ANALYSIS, - locale as defaultLocale + TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { SubscriptionType } from '@ghostfolio/common/enums'; import { User as IUser, + ReferralPartner, SystemMessage, UserSettings } from '@ghostfolio/common/interfaces'; @@ -100,7 +102,7 @@ export class UserService { public async getUser({ impersonationUserId, - locale = defaultLocale, + locale = DEFAULT_LOCALE, user }: { impersonationUserId: string; @@ -153,6 +155,17 @@ export class UserService { (impersonationUserSettings?.settings as UserSettings)?.baseCurrency ?? (settings.settings as UserSettings)?.baseCurrency; + let referralPartners: ReferralPartner[]; + + if ( + this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && + subscription.type === SubscriptionType.Basic + ) { + referralPartners = await this.propertyService.getByKey( + PROPERTY_REFERRAL_PARTNERS + ); + } + let systemMessage: SystemMessage; const systemMessageProperty = @@ -179,6 +192,7 @@ export class UserService { activitiesCount, id, permissions, + referralPartners, subscription, systemMessage, tags, 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 57643f76c..fb8ff5dc5 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 @@ -64,6 +64,7 @@ export class TransformDataSourceInResponseInterceptor< paths: [ 'activities[*].dataSource', 'activities[*].SymbolProfile.dataSource', + 'assetProfile.dataSource', 'benchmarks[*].dataSource', 'errors[*].dataSource', 'fearAndGreedIndex.CRYPTOCURRENCIES.dataSource', diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index b19508d3e..5f9d1055d 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -30,7 +30,6 @@ export class ConfigurationService { API_KEY_FINANCIAL_MODELING_PREP: str({ default: '' }), API_KEY_OPEN_FIGI: str({ default: '' }), API_KEY_RAPID_API: str({ default: '' }), - BULL_BOARD_IS_READ_ONLY: bool({ default: true }), CACHE_QUOTES_TTL: num({ default: ms('1 minute') }), CACHE_TTL: num({ default: CACHE_TTL_NO_CACHE }), DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }), diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index eb3ac86a3..57c58898e 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -10,7 +10,6 @@ export interface Environment extends CleanedEnvAccessors { API_KEY_FINANCIAL_MODELING_PREP: string; API_KEY_OPEN_FIGI: string; API_KEY_RAPID_API: string; - BULL_BOARD_IS_READ_ONLY: boolean; CACHE_QUOTES_TTL: number; CACHE_TTL: number; DATA_SOURCE_EXCHANGE_RATES: string; diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.module.ts b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts index 5672df5e8..5ac6c40c0 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.module.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts @@ -23,8 +23,7 @@ import { DataGatheringProcessor } from './data-gathering.processor'; adapter: BullAdapter, name: DATA_GATHERING_QUEUE, options: { - displayName: 'Data Gathering', - readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' + displayName: 'Data Gathering' } }), BullModule.registerQueue({ diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts index c90f826f6..0da529821 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts @@ -29,8 +29,7 @@ import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor'; adapter: BullAdapter, name: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE, options: { - displayName: 'Portfolio Snapshot Computation', - readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' + displayName: 'Portfolio Snapshot Computation' } }), BullModule.registerQueue({ diff --git a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts index d6f6d5ccd..6ef14e29c 100644 --- a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts +++ b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts @@ -20,8 +20,7 @@ import { StatisticsGatheringService } from './statistics-gathering.service'; adapter: BullAdapter, name: STATISTICS_GATHERING_QUEUE, options: { - displayName: 'Statistics Gathering', - readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' + displayName: 'Statistics Gathering' } }) ] diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts b/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts index b4c228881..fd90bff2c 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts @@ -1,8 +1,5 @@ -import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { - BULL_BOARD_COOKIE_NAME, - BULL_BOARD_ROUTE, DATA_GATHERING_QUEUE_PRIORITY_HIGH, DATA_GATHERING_QUEUE_PRIORITY_LOW, DATA_GATHERING_QUEUE_PRIORITY_MEDIUM, @@ -10,7 +7,6 @@ import { } from '@ghostfolio/common/config'; import { getDateWithTimeFormatString } from '@ghostfolio/common/helper'; import { AdminJobs, User } from '@ghostfolio/common/interfaces'; -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { AdminService } from '@ghostfolio/ui/services'; @@ -106,7 +102,6 @@ export class GfAdminJobsComponent implements OnInit { 'actions' ]; - protected hasPermissionToAccessBullBoard = false; protected isLoading = false; protected readonly statusFilterOptions = QUEUE_JOB_STATUS_LIST; @@ -116,7 +111,6 @@ export class GfAdminJobsComponent implements OnInit { private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly destroyRef = inject(DestroyRef); private readonly notificationService = inject(NotificationService); - private readonly tokenStorageService = inject(TokenStorageService); private readonly userService = inject(UserService); public constructor() { @@ -129,11 +123,6 @@ export class GfAdminJobsComponent implements OnInit { this.defaultDateTimeFormat = getDateWithTimeFormatString( this.user.settings.locale ); - - this.hasPermissionToAccessBullBoard = hasPermission( - this.user.permissions, - permissions.accessAdminControlBullBoard - ); } }); @@ -193,18 +182,6 @@ export class GfAdminJobsComponent implements OnInit { }); } - protected onOpenBullBoard() { - const token = this.tokenStorageService.getToken(); - - document.cookie = [ - `${BULL_BOARD_COOKIE_NAME}=${encodeURIComponent(token)}`, - 'path=/', - 'SameSite=Strict' - ].join('; '); - - window.open(BULL_BOARD_ROUTE, '_blank'); - } - protected onViewData(aData: AdminJobs['jobs'][0]['data']) { this.notificationService.alert({ title: JSON.stringify(aData, null, ' ') diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.html b/apps/client/src/app/components/admin-jobs/admin-jobs.html index d57704b86..e615db31b 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.html +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.html @@ -1,15 +1,6 @@
- @if (hasPermissionToAccessBullBoard) { -
- -
- } -
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index 8874da97e..e71bdd47c 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -1,8 +1,8 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_COLOR_SCHEME, - DEFAULT_PAGE_SIZE, - locale + DEFAULT_LOCALE, + DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { canDeleteAssetProfile, @@ -429,7 +429,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { colorScheme: this.user?.settings.colorScheme ?? DEFAULT_COLOR_SCHEME, deviceType: this.deviceType(), - locale: this.user?.settings?.locale ?? locale + locale: this.user?.settings?.locale ?? DEFAULT_LOCALE } satisfies AssetProfileDialogParams, height: this.deviceType() === 'mobile' ? '98vh' : '80vh', width: this.deviceType() === 'mobile' ? '100vw' : '50rem' @@ -464,7 +464,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { autoFocus: false, data: { deviceType: this.deviceType(), - locale: this.user?.settings?.locale ?? locale + locale: this.user?.settings?.locale ?? DEFAULT_LOCALE } satisfies CreateAssetProfileDialogParams, width: this.deviceType() === 'mobile' ? '100vw' : '50rem' }); diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html index 63d425513..e86e3e631 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.html +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html @@ -239,7 +239,18 @@ [disabled]="!selection.hasValue()" (click)="onDeleteAssetProfiles()" > - Delete Profiles + Delete + {{ + selection.selected.length > 1 + ? selection.selected.length + : '' + }} + {selection.selected.length, plural, + =1 {Profile} + other {Profiles} + } diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts index 77e2151ef..28c8c2d9f 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts @@ -32,6 +32,8 @@ export class AdminMarketDataService { public deleteAssetProfiles( aAssetProfileIdentifiers: AssetProfileIdentifier[] ) { + const assetProfileCount = aAssetProfileIdentifiers.length; + this.notificationService.confirm({ confirmFn: () => { const deleteRequests = aAssetProfileIdentifiers.map( @@ -44,7 +46,10 @@ export class AdminMarketDataService { .pipe( catchError(() => { this.notificationService.alert({ - title: $localize`Oops! Could not delete profiles.` + title: + assetProfileCount === 1 + ? $localize`Oops! Could not delete the asset profile.` + : $localize`Oops! Could not delete the asset profiles.` }); return EMPTY; @@ -56,7 +61,10 @@ export class AdminMarketDataService { .subscribe(); }, confirmType: ConfirmationDialogType.Warn, - title: $localize`Do you really want to delete these profiles?` + title: + assetProfileCount === 1 + ? $localize`Do you really want to delete this asset profile?` + : $localize`Do you really want to delete these ${assetProfileCount}:count: asset profiles?` }); } } diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index ce997cf27..829129b38 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -373,7 +373,7 @@ export class GfAssetProfileDialogComponent implements OnInit { ) { for (const { code, weight } of this.assetProfile.countries) { this.countries[code] = { - name: getCountryName({ code, locale: this.data.locale }), + name: getCountryName({ code }), value: weight }; } diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html index d9a5354e5..453b8cb6a 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -271,8 +271,7 @@ [locale]="data.locale" [value]=" getCountryName({ - code: assetProfile?.countries[0].code, - locale: data.locale + code: assetProfile?.countries[0].code }) " >Country -
+
{{ formatDistanceToNow(element.createdAt) }} @@ -183,7 +183,7 @@ {{ formatDistanceToNow(element.lastActivity) }} 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 8d13fb91d..c474e29ab 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 @@ -44,7 +44,6 @@ import { type TooltipOptions } from 'chart.js'; import 'chartjs-adapter-date-fns'; -import annotationPlugin from 'chartjs-plugin-annotation'; import { addIcons } from 'ionicons'; import { arrowForwardOutline } from 'ionicons/icons'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -86,7 +85,6 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { public constructor() { Chart.register( - annotationPlugin, LinearScale, LineController, LineElement, 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 416e9106d..b8dede2cd 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 @@ -16,6 +16,7 @@ import { EnhancedSymbolProfile, Filter, LineChartItem, + NullableLineChartItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -39,11 +40,16 @@ import { ChangeDetectorRef, Component, DestroyRef, - Inject, - OnInit + OnInit, + inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule +} from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatChipsModule } from '@angular/material/chips'; import { @@ -71,6 +77,7 @@ import { swapVerticalOutline, walletOutline } from 'ionicons/icons'; +import { isNumber } from 'lodash'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { switchMap } from 'rxjs/operators'; @@ -106,75 +113,78 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces'; templateUrl: 'holding-detail-dialog.html' }) export class GfHoldingDetailDialogComponent implements OnInit { - public activitiesCount: number; - public accounts: Account[]; - public assetClass: string; - public assetSubClass: string; - public averagePrice: number; - public averagePricePrecision = 2; - public benchmarkDataItems: LineChartItem[]; - public benchmarkLabel = $localize`Average Unit Price`; - public countries: { + protected accounts: Account[]; + protected activitiesCount: number; + protected assetClass: string; + protected assetSubClass: string; + protected averagePrice: number; + protected averagePricePrecision = 2; + protected benchmarkDataItems: NullableLineChartItem[]; + protected readonly benchmarkLabel = $localize`Average Unit Price`; + protected countries: { [code: string]: { name: string; value: number }; }; - public dataProviderInfo: DataProviderInfo; - public dataSource: MatTableDataSource; - public dateOfFirstActivity: string; - public dividendInBaseCurrency: number; - public dividendInBaseCurrencyPrecision = 2; - public dividendYieldPercentWithCurrencyEffect: number; - public feeInBaseCurrency: number; - public getCountryName = getCountryName; - public hasPermissionToCreateOwnTag: boolean; - public hasPermissionToReadMarketDataOfOwnAssetProfile: boolean; - public historicalDataItems: LineChartItem[]; - public holdingForm: FormGroup; - public investmentInBaseCurrencyWithCurrencyEffect: number; - public investmentInBaseCurrencyWithCurrencyEffectPrecision = 2; - public isUUID = isUUID; - public marketDataItems: MarketData[] = []; - public marketPrice: number; - public marketPriceMax: number; - public marketPriceMaxPrecision = 2; - public marketPriceMin: number; - public marketPriceMinPrecision = 2; - public marketPricePrecision = 2; - public netPerformance: number; - public netPerformancePrecision = 2; - public netPerformancePercent: number; - public netPerformancePercentWithCurrencyEffect: number; - public netPerformancePercentWithCurrencyEffectPrecision = 2; - public netPerformanceWithCurrencyEffect: number; - public netPerformanceWithCurrencyEffectPrecision = 2; - public pageIndex = 0; - public pageSize = DEFAULT_PAGE_SIZE; - public quantity: number; - public quantityPrecision = 2; - public reportDataGlitchMail: string; - public routerLinkAdminControlMarketData = + protected dataProviderInfo: DataProviderInfo; + protected dataSource: MatTableDataSource; + protected dateOfFirstActivity: Date; + protected dividendInBaseCurrency: number; + protected dividendInBaseCurrencyPrecision = 2; + protected dividendYieldPercentWithCurrencyEffect: number; + protected feeInBaseCurrency: number; + protected readonly getCountryName = getCountryName; + protected hasPermissionToCreateOwnTag: boolean; + protected hasPermissionToReadMarketDataOfOwnAssetProfile: boolean; + protected historicalDataItems: LineChartItem[]; + protected holdingForm: FormGroup<{ + tags: FormControl; + }>; + protected investmentInBaseCurrencyWithCurrencyEffect: number; + protected investmentInBaseCurrencyWithCurrencyEffectPrecision = 2; + protected readonly isUUID = isUUID; + protected marketDataItems: MarketData[] = []; + protected marketPrice: number; + protected marketPriceMax: number; + protected marketPriceMaxPrecision = 2; + protected marketPriceMin: number; + protected marketPriceMinPrecision = 2; + protected marketPricePrecision = 2; + protected netPerformancePercentWithCurrencyEffect: number; + protected netPerformancePercentWithCurrencyEffectPrecision = 2; + protected netPerformanceWithCurrencyEffect: number; + protected netPerformanceWithCurrencyEffectPrecision = 2; + protected pageIndex = 0; + protected readonly pageSize = DEFAULT_PAGE_SIZE; + protected quantity: number; + protected quantityPrecision = 2; + protected reportDataGlitchMail: string; + protected readonly routerLinkAdminControlMarketData = internalRoutes.adminControl.subRoutes.marketData.routerLink; - public sectors: { + protected sectors: { [name: string]: { name: string; value: number }; }; - public sortColumn = 'date'; - public sortDirection: SortDirection = 'desc'; - public SymbolProfile: EnhancedSymbolProfile; - public tags: Tag[]; - public tagsAvailable: Tag[]; - public translate = translate; - public user: User; - public value: number; - - public constructor( - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private destroyRef: DestroyRef, - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: HoldingDetailDialogParams, - private formBuilder: FormBuilder, - private router: Router, - private userService: UserService - ) { + protected sortColumn = 'date'; + protected sortDirection: SortDirection = 'desc'; + protected SymbolProfile: EnhancedSymbolProfile; + protected tagsAvailable: Tag[]; + protected readonly translate = translate; + protected user: User; + protected value: number; + + protected readonly data = inject(MAT_DIALOG_DATA); + protected readonly dialogRef = inject( + MatDialogRef + ); + + private tags: Tag[]; + + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly formBuilder = inject(FormBuilder); + private readonly router = inject(Router); + private readonly userService = inject(UserService); + + public constructor() { addIcons({ arrowDownCircleOutline, createOutline, @@ -190,12 +200,11 @@ export class GfHoldingDetailDialogComponent implements OnInit { const filters = this.getActivityFilters(); this.holdingForm = this.formBuilder.group({ - tags: [] as string[] + tags: new FormControl([]) }); - this.holdingForm - .get('tags') - .valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) + this.holdingForm.controls.tags.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((tags: Tag[]) => { const newTag = tags.find(({ id }) => { return id === undefined; @@ -268,8 +277,6 @@ export class GfHoldingDetailDialogComponent implements OnInit { marketPrice, marketPriceMax, marketPriceMin, - netPerformance, - netPerformancePercent, netPerformancePercentWithCurrencyEffect, netPerformanceWithCurrencyEffect, quantity, @@ -290,7 +297,11 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.benchmarkDataItems = []; this.countries = {}; this.dataProviderInfo = dataProviderInfo; - this.dateOfFirstActivity = dateOfFirstActivity; + + if (dateOfFirstActivity) { + this.dateOfFirstActivity = dateOfFirstActivity; + } + this.dividendInBaseCurrency = dividendInBaseCurrency; if ( @@ -318,12 +329,12 @@ export class GfHoldingDetailDialogComponent implements OnInit { ({ averagePrice, date, marketPrice }) => { this.benchmarkDataItems.push({ date, - value: averagePrice + value: isNumber(averagePrice) ? averagePrice : null }); return { date, - value: marketPrice + value: marketPrice ?? 0 }; } ); @@ -365,17 +376,6 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.marketPricePrecision = 0; } - this.netPerformance = netPerformance; - - if ( - this.data.deviceType === 'mobile' && - this.netPerformance >= NUMERICAL_PRECISION_THRESHOLD_5_FIGURES - ) { - this.netPerformancePrecision = 0; - } - - this.netPerformancePercent = netPerformancePercent; - this.netPerformancePercentWithCurrencyEffect = netPerformancePercentWithCurrencyEffect; @@ -438,10 +438,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { if (SymbolProfile?.countries?.length > 0) { for (const country of SymbolProfile.countries) { this.countries[country.code] = { - name: getCountryName({ - code: country.code, - locale: this.data.locale - }), + name: getCountryName({ code: country.code }), value: country.weight }; } @@ -456,16 +453,16 @@ export class GfHoldingDetailDialogComponent implements OnInit { } } - if (isToday(parseISO(this.dateOfFirstActivity))) { + if (isToday(this.dateOfFirstActivity)) { // Add average price this.historicalDataItems.push({ - date: this.dateOfFirstActivity, + date: this.dateOfFirstActivity.toISOString(), value: this.averagePrice }); // Add benchmark 1 this.benchmarkDataItems.push({ - date: this.dateOfFirstActivity, + date: this.dateOfFirstActivity.toISOString(), value: averagePrice }); @@ -496,7 +493,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { if ( this.benchmarkDataItems[0]?.value === undefined && - isSameMonth(parseISO(this.dateOfFirstActivity), new Date()) + isSameMonth(this.dateOfFirstActivity, new Date()) ) { this.benchmarkDataItems[0].value = this.averagePrice; } @@ -526,7 +523,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.hasPermissionToCreateOwnTag = hasPermission(this.user.permissions, permissions.createOwnTag) && - this.user?.settings?.isExperimentalFeatures; + (this.user?.settings?.isExperimentalFeatures ?? false); this.tagsAvailable = this.user?.tags?.map((tag) => { @@ -541,13 +538,13 @@ export class GfHoldingDetailDialogComponent implements OnInit { }); } - public onChangePage(page: PageEvent) { + protected onChangePage(page: PageEvent) { this.pageIndex = page.pageIndex; this.fetchActivities(); } - public onCloneActivity(aActivity: Activity) { + protected onCloneActivity(aActivity: Activity) { this.router.navigate( internalRoutes.portfolio.subRoutes.activities.routerLink, { @@ -558,22 +555,22 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.dialogRef.close(); } - public onClose() { + protected onClose() { this.dialogRef.close(); } - public onCloseHolding() { + protected onCloseHolding() { const today = new Date(); const activity: CreateOrderDto = { - accountId: this.accounts.length === 1 ? this.accounts[0].id : null, - comment: null, - currency: this.SymbolProfile.currency, - dataSource: this.SymbolProfile.dataSource, + accountId: this.accounts.length === 1 ? this.accounts[0].id : undefined, + comment: undefined, + currency: this.SymbolProfile?.currency ?? '', + dataSource: this.SymbolProfile?.dataSource, date: today.toISOString(), fee: 0, quantity: this.quantity, - symbol: this.SymbolProfile.symbol, + symbol: this.SymbolProfile?.symbol ?? '', tags: this.tags.map(({ id }) => { return id; }), @@ -593,7 +590,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { }); } - public onExport() { + protected onExport() { const activityIds = this.dataSource.data.map(({ id }) => { return id; }); @@ -613,13 +610,13 @@ export class GfHoldingDetailDialogComponent implements OnInit { }); } - public onMarketDataChanged(withRefresh = false) { + protected onMarketDataChanged(withRefresh = false) { if (withRefresh) { this.fetchMarketData(); } } - public onUpdateActivity(aActivity: Activity) { + protected onUpdateActivity(aActivity: Activity) { this.router.navigate( internalRoutes.portfolio.subRoutes.activities.routerLink, { diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index 609eec3c0..0d5691874 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -274,8 +274,7 @@ [locale]="data.locale" [value]=" getCountryName({ - code: SymbolProfile.countries[0].code, - locale: data.locale + code: SymbolProfile.countries[0].code }) " >Country - ) {} + public dialogRef: MatDialogRef, + private formBuilder: FormBuilder + ) { + this.settingsForm = this.formBuilder.group({ + thresholdMax: [this.data.settings.thresholdMax], + thresholdMin: [this.data.settings.thresholdMin] + }); + } + + public onSubmit() { + this.dialogRef.close({ + ...this.data.settings, + thresholdMax: this.settingsForm.get('thresholdMax')?.value, + thresholdMin: this.settingsForm.get('thresholdMin')?.value + }); + } } diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html index c88a9dc9d..d81a3c1f3 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html @@ -1,132 +1,142 @@
{{ data.categoryName }} › {{ data.rule.name }}
-
- @if ( - data.rule.configuration.thresholdMin && data.rule.configuration.thresholdMax - ) { -
-
- Threshold range: - - - - -
-
- - - - - - + +
+ @if ( + data.rule.configuration.thresholdMin && + data.rule.configuration.thresholdMax + ) { +
+
+ Threshold range: + + - + +
+
+ + + + + + +
-
- } @else { -
-
- Threshold Min: - -
-
- - - - - + } @else { +
+
+ Threshold Min: + +
+
+ + + + + +
-
-
-
- Threshold Max: - -
-
- - - - - +
+
+ Threshold Max: + +
+
+ + + + + +
-
- } -
+ } +
-
- - -
+
+ + +
+ diff --git a/apps/client/src/app/components/rules/rules.component.html b/apps/client/src/app/components/rules/rules.component.html index 0c3153c52..97b41e61b 100644 --- a/apps/client/src/app/components/rules/rules.component.html +++ b/apps/client/src/app/components/rules/rules.component.html @@ -3,9 +3,7 @@
@if (isLoading) { - } - - @if (rules !== null && rules !== undefined) { + } @else if (rules) { @for (rule of rules; track rule.key) { or start a discussion at   GitHub { + this.user = state?.user; + + this.initializeTabs(); + }); + addIcons({ flashOutline, peopleOutline, @@ -34,7 +56,12 @@ export class AdminPageComponent implements OnInit { }); } - public ngOnInit() { + private initializeTabs() { + const hasPermissionToAccessBullBoard = hasPermission( + this.user?.permissions, + permissions.accessAdminControlBullBoard + ); + this.tabs = [ { iconName: 'reader-outline', @@ -51,11 +78,19 @@ export class AdminPageComponent implements OnInit { label: internalRoutes.adminControl.subRoutes.marketData.title, routerLink: internalRoutes.adminControl.subRoutes.marketData.routerLink }, - { - iconName: 'flash-outline', - label: internalRoutes.adminControl.subRoutes.jobs.title, - routerLink: internalRoutes.adminControl.subRoutes.jobs.routerLink - }, + hasPermissionToAccessBullBoard + ? { + iconName: 'flash-outline', + label: $localize`Job Queue`, + onClick: () => { + this.onOpenBullBoard(); + } + } + : { + iconName: 'flash-outline', + label: internalRoutes.adminControl.subRoutes.jobs.title, + routerLink: internalRoutes.adminControl.subRoutes.jobs.routerLink + }, { iconName: 'people-outline', label: internalRoutes.adminControl.subRoutes.users.title, @@ -63,4 +98,16 @@ export class AdminPageComponent implements OnInit { } ]; } + + private onOpenBullBoard() { + const token = this.tokenStorageService.getToken(); + + document.cookie = [ + `${BULL_BOARD_COOKIE_NAME}=${encodeURIComponent(token)}`, + 'path=/', + 'SameSite=Strict' + ].join('; '); + + window.open(BULL_BOARD_ROUTE, '_blank'); + } } diff --git a/apps/client/src/app/pages/faq/overview/faq-overview-page.html b/apps/client/src/app/pages/faq/overview/faq-overview-page.html index 3b43ac096..ee4e0ad1a 100644 --- a/apps/client/src/app/pages/faq/overview/faq-overview-page.html +++ b/apps/client/src/app/pages/faq/overview/faq-overview-page.html @@ -193,7 +193,7 @@ } or start a discussion at GitHub.GitHub. or start a discussion at GitHub.
- @if (user?.subscription?.type === 'Basic') { + @if (user?.referralPartners?.length) {

If you plan to open an account at   @for ( - broker of referralBrokers; - track broker; + partner of user.referralPartners; + track partner.name; let i = $index; let last = $last ) { - {{ broker }} + {{ partner.name }} @if (last) { , } @else { - @if (i === referralBrokers.length - 2) { + @if (i === user.referralPartners.length - 2) {   or   diff --git a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts index 0dd14485f..1af3c6d54 100644 --- a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts +++ b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -33,7 +33,6 @@ export class GfProductPageComponent implements OnInit { ) {} public ngOnInit() { - const locale = document.documentElement.lang; const { subscriptionOffer } = this.dataService.fetchInfo(); this.price = subscriptionOffer?.price; @@ -57,7 +56,7 @@ export class GfProductPageComponent implements OnInit { 'Türkçe' ], name: 'Ghostfolio', - origin: getCountryName({ locale, code: 'CH' }), + origin: getCountryName({ code: 'CH' }), regions: [$localize`Global`], slogan: 'Open Source Wealth Management', useAnonymously: true @@ -70,10 +69,7 @@ export class GfProductPageComponent implements OnInit { }; if (this.product2.origin) { - this.product2.origin = getCountryName({ - locale, - code: this.product2.origin - }); + this.product2.origin = getCountryName({ code: this.product2.origin }); } if (this.product2.regions) { diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index fd0ca79d6..663196761 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -2143,7 +2143,7 @@ or start a discussion at - oder beginne eine Diskussion unter + oder beginne eine Diskussion bei apps/client/src/app/pages/about/overview/about-overview-page.html 94 diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index e475b9c2c..d922405a0 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -872,7 +872,7 @@ Energy - Energy + Energía libs/ui/src/lib/i18n.ts 90 @@ -1208,7 +1208,7 @@ Consumer Defensive - Consumer Defensive + Consumo Básico libs/ui/src/lib/i18n.ts 89 @@ -1276,7 +1276,7 @@ Utilities - Utilities + Servicios Públicos libs/ui/src/lib/i18n.ts 97 @@ -2004,7 +2004,7 @@ Consumer Cyclical - Consumer Cyclical + Consumo Discrecional libs/ui/src/lib/i18n.ts 88 @@ -2948,7 +2948,7 @@ Real Estate - Propiedad inmobiliaria + Inmobiliario libs/ui/src/lib/i18n.ts 52 @@ -3108,7 +3108,7 @@ Communication Services - Communication Services + Servicios de Comunicación libs/ui/src/lib/i18n.ts 87 @@ -4316,7 +4316,7 @@ Technology - Technology + Tecnología libs/ui/src/lib/i18n.ts 96 @@ -4964,7 +4964,7 @@ Basic Materials - Basic Materials + Materiales Básicos libs/ui/src/lib/i18n.ts 86 @@ -5690,7 +5690,7 @@ Industrials - Industrials + Industriales libs/ui/src/lib/i18n.ts 93 @@ -5806,7 +5806,7 @@ Healthcare - Healthcare + Salud libs/ui/src/lib/i18n.ts 92 @@ -6951,7 +6951,7 @@ Financial Services - Financial Services + Servicios Financieros libs/ui/src/lib/i18n.ts 91 diff --git a/apps/client/src/main.ts b/apps/client/src/main.ts index f596de5f4..45901baec 100644 --- a/apps/client/src/main.ts +++ b/apps/client/src/main.ts @@ -1,5 +1,6 @@ import { InfoResponse } from '@ghostfolio/common/interfaces'; import { filterGlobalPermissions } from '@ghostfolio/common/permissions'; +import { registerChartConfiguration } from '@ghostfolio/ui/chart'; import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment'; import { GfNotificationModule } from '@ghostfolio/ui/notifications'; @@ -58,6 +59,8 @@ import { environment } from './environments/environment'; enableProdMode(); } + registerChartConfiguration(); + await bootstrapApplication(GfAppComponent, { providers: [ authInterceptorProviders, diff --git a/apps/client/src/styles/theme.scss b/apps/client/src/styles/theme.scss index f8a194651..75ecc210d 100644 --- a/apps/client/src/styles/theme.scss +++ b/apps/client/src/styles/theme.scss @@ -312,7 +312,10 @@ $gf-typography: ( @include mat.checkbox-overrides( ( - selected-icon-color: var(--gf-theme-primary-500) + selected-focus-icon-color: var(--gf-theme-primary-500), + selected-hover-icon-color: var(--gf-theme-primary-500), + selected-icon-color: var(--gf-theme-primary-500), + selected-pressed-icon-color: var(--gf-theme-primary-500) ) ); @@ -392,8 +395,11 @@ $gf-typography: ( @include mat.tabs-overrides( ( + active-focus-indicator-color: var(--gf-theme-primary-500), active-focus-label-text-color: var(--gf-theme-primary-500), + active-hover-indicator-color: var(--gf-theme-primary-500), active-hover-label-text-color: var(--gf-theme-primary-500), + active-indicator-color: var(--gf-theme-primary-500), active-label-text-color: var(--gf-theme-primary-500), active-ripple-color: var(--gf-theme-primary-500), inactive-ripple-color: var(--gf-theme-primary-500) diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 7e7cd2ba5..4924aaeea 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -13,8 +13,6 @@ export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPr export const ghostfolioFearAndGreedIndexSymbolCryptocurrencies = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_CRYPTOCURRENCIES`; export const ghostfolioFearAndGreedIndexSymbolStocks = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_STOCKS`; -export const locale = 'en-US'; - export const primaryColorHex = '#36cfcc'; export const primaryColorRgb = { r: 54, @@ -85,6 +83,7 @@ export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; export const DEFAULT_DATE_RANGE: DateRange = 'max'; export const DEFAULT_HOST = '0.0.0.0'; export const DEFAULT_LANGUAGE_CODE = 'en'; +export const DEFAULT_LOCALE = 'en-US'; export const DEFAULT_PAGE_SIZE = 50; export const DEFAULT_PORT = 3333; export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1; @@ -253,6 +252,7 @@ export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE'; export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED'; export const PROPERTY_OPENROUTER_MODEL = 'OPENROUTER_MODEL'; export const PROPERTY_OPENROUTER_MODEL_WEB_FETCH = 'OPENROUTER_MODEL_WEB_FETCH'; +export const PROPERTY_REFERRAL_PARTNERS = 'REFERRAL_PARTNERS'; export const PROPERTY_SLACK_COMMUNITY_USERS = 'SLACK_COMMUNITY_USERS'; export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG'; export const PROPERTY_SYSTEM_MESSAGE = 'SYSTEM_MESSAGE'; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index a26055523..376ba7dbe 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -36,12 +36,12 @@ import { get, isNil, isString } from 'lodash'; import { DEFAULT_CURRENCY, + DEFAULT_LOCALE, DERIVED_CURRENCIES, ghostfolioFearAndGreedIndexSymbol, ghostfolioFearAndGreedIndexSymbolCryptocurrencies, ghostfolioFearAndGreedIndexSymbolStocks, - ghostfolioScraperApiSymbolPrefix, - locale + ghostfolioScraperApiSymbolPrefix } from './config'; import { AssetProfileItem, @@ -258,15 +258,13 @@ export function getCurrencyFromSymbol(aSymbol = '') { return aSymbol.replace(DEFAULT_CURRENCY, ''); } -export function getCountryName({ - code, - locale = getLocale() -}: { - code: string; - locale?: string; -}): string { +export function getCountryName({ code }: { code: string }): string { try { - return new Intl.DisplayNames([locale], { type: 'region' }).of(code) ?? code; + return ( + new Intl.DisplayNames([document.documentElement.lang || DEFAULT_LOCALE], { + type: 'region' + }).of(code) ?? code + ); } catch { return code; } @@ -340,7 +338,7 @@ export function getEmojiFlag(aCountryCode: string) { } export function getLocale() { - return navigator.language ?? locale; + return navigator.language ?? DEFAULT_LOCALE; } export function getLowercase(object: object, path: string) { diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 5e801a44b..989decdba 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -22,7 +22,10 @@ import type { HoldingWithParents } from './holding-with-parents.interface'; import type { Holding } from './holding.interface'; import type { InfoItem } from './info-item.interface'; import type { InvestmentItem } from './investment-item.interface'; -import type { LineChartItem } from './line-chart-item.interface'; +import type { + LineChartItem, + NullableLineChartItem +} from './line-chart-item.interface'; import type { LookupItem } from './lookup-item.interface'; import type { MarketData } from './market-data.interface'; import type { PortfolioChart } from './portfolio-chart.interface'; @@ -32,6 +35,7 @@ import type { PortfolioPosition } from './portfolio-position.interface'; import type { PortfolioReportRule } from './portfolio-report-rule.interface'; import type { PortfolioSummary } from './portfolio-summary.interface'; import type { Product } from './product'; +import type { ReferralPartner } from './referral-partner.interface'; import type { AccessTokenResponse } from './responses/access-token-response.interface'; import type { AccountBalancesResponse } from './responses/account-balances-response.interface'; import type { AccountResponse } from './responses/account-response.interface'; @@ -156,6 +160,7 @@ export { MarketData, MarketDataDetailsResponse, MarketDataOfMarketsResponse, + NullableLineChartItem, OAuthResponse, PlatformsResponse, PortfolioChart, @@ -175,6 +180,7 @@ export { PublicKeyCredentialRequestOptionsJSON, PublicPortfolioResponse, QuotesResponse, + ReferralPartner, ResponseError, RuleSettings, ScraperConfiguration, diff --git a/libs/common/src/lib/interfaces/line-chart-item.interface.ts b/libs/common/src/lib/interfaces/line-chart-item.interface.ts index e010ddfe6..43208f853 100644 --- a/libs/common/src/lib/interfaces/line-chart-item.interface.ts +++ b/libs/common/src/lib/interfaces/line-chart-item.interface.ts @@ -1,4 +1,6 @@ -export interface LineChartItem { +export interface LineChartItem { date: string; - value: number; + value: T; } + +export type NullableLineChartItem = LineChartItem; diff --git a/libs/common/src/lib/interfaces/referral-partner.interface.ts b/libs/common/src/lib/interfaces/referral-partner.interface.ts new file mode 100644 index 000000000..435070fa5 --- /dev/null +++ b/libs/common/src/lib/interfaces/referral-partner.interface.ts @@ -0,0 +1,3 @@ +export interface ReferralPartner { + name: string; +} diff --git a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts index 76bc7dc02..3b07666c9 100644 --- a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts @@ -9,6 +9,19 @@ import { Tag } from '@prisma/client'; export interface PortfolioHoldingResponse { activitiesCount: number; + assetProfile: Pick< + EnhancedSymbolProfile, + | 'assetClass' + | 'assetSubClass' + | 'countries' + | 'currency' + | 'dataSource' + | 'isin' + | 'name' + | 'sectors' + | 'symbol' + | 'userId' + >; averagePrice: number; dataProviderInfo: DataProviderInfo; dateOfFirstActivity: string; @@ -31,7 +44,10 @@ export interface PortfolioHoldingResponse { netPerformanceWithCurrencyEffect: number; performances: Benchmark['performances']; quantity: number; + + /* @deprecated */ SymbolProfile: EnhancedSymbolProfile; + tags: Tag[]; value: number; } diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts index e60f01915..619d4ee71 100644 --- a/libs/common/src/lib/interfaces/user.interface.ts +++ b/libs/common/src/lib/interfaces/user.interface.ts @@ -3,6 +3,7 @@ import { AccountWithPlatform } from '@ghostfolio/common/types'; import { Access, Tag } from '@prisma/client'; +import { ReferralPartner } from './referral-partner.interface'; import { SubscriptionOffer } from './subscription-offer.interface'; import { SystemMessage } from './system-message.interface'; import { UserSettings } from './user-settings.interface'; @@ -15,6 +16,7 @@ export interface User { dateOfFirstActivity: Date; id: string; permissions: string[]; + referralPartners?: ReferralPartner[]; settings: UserSettings; systemMessage?: SystemMessage; subscription: { diff --git a/libs/common/src/lib/personal-finance-tools.ts b/libs/common/src/lib/personal-finance-tools.ts index 8698c76f7..744081464 100644 --- a/libs/common/src/lib/personal-finance-tools.ts +++ b/libs/common/src/lib/personal-finance-tools.ts @@ -1,6 +1,17 @@ import { Product } from '@ghostfolio/common/interfaces'; export const personalFinanceTools: Product[] = [ + { + hasFreePlan: true, + hasSelfHostingAbility: true, + key: 'acemoney', + name: 'AceMoney', + note: 'License is a perpetual license', + origin: 'US', + pricingPerYear: '$44.95', + slogan: 'Lifetime Personal Finance Control, One Single Payment.', + url: 'https://www.mechcad.net' + }, { founded: 2023, hasSelfHostingAbility: false, @@ -361,6 +372,16 @@ export const personalFinanceTools: Product[] = [ slogan: 'Your financial superpower', url: 'https://www.etops.com' }, + { + founded: 2015, + hasFreePlan: true, + hasSelfHostingAbility: false, + key: 'everydollar', + name: 'EveryDollar', + origin: 'US', + slogan: 'Plan, track, find more margin', + url: 'https://www.ramseysolutions.com/money/everydollar' + }, { founded: 2020, hasFreePlan: true, @@ -497,6 +518,17 @@ export const personalFinanceTools: Product[] = [ slogan: 'Take control over your investments', url: 'https://www.folishare.com' }, + { + founded: 1993, + hasSelfHostingAbility: true, + key: 'fund-manager', + name: 'Fund Manager', + note: 'License is a perpetual license', + origin: 'US', + pricingPerYear: '$99', + slogan: 'Powerful portfolio management software', + url: 'https://www.fundmanagersoftware.com' + }, { hasFreePlan: true, hasSelfHostingAbility: false, @@ -528,6 +560,15 @@ export const personalFinanceTools: Product[] = [ slogan: 'Portfolio Tracker, Analysis & Community', url: 'https://www.getquin.com' }, + { + hasFreePlan: true, + hasSelfHostingAbility: false, + key: 'goodbudget', + name: 'Goodbudget', + origin: 'US', + slogan: 'Budget with a why', + url: 'https://goodbudget.com' + }, { hasFreePlan: true, hasSelfHostingAbility: false, @@ -665,6 +706,16 @@ export const personalFinanceTools: Product[] = [ slogan: 'Sustainability insights for wealth managers', url: 'https://leafs.ch' }, + { + founded: 2020, + hasFreePlan: false, + hasSelfHostingAbility: false, + key: 'lunch-money', + name: 'Lunch Money', + origin: 'CA', + slogan: 'Delightfully simple personal finance and budgeting', + url: 'https://lunchmoney.app' + }, { founded: 2018, hasFreePlan: false, @@ -725,6 +776,16 @@ export const personalFinanceTools: Product[] = [ slogan: 'The smartest way to track your crypto', url: 'https://www.merlincrypto.com' }, + { + hasFreePlan: false, + hasSelfHostingAbility: false, + key: 'mezzi', + name: 'Mezzi', + origin: 'US', + pricingPerYear: '$48', + slogan: 'Self-manage your wealth. Get fiduciary advice.', + url: 'https://www.mezzi.com' + }, { founded: 1991, hasSelfHostingAbility: true, @@ -776,6 +837,15 @@ export const personalFinanceTools: Product[] = [ slogan: 'Have total control of your financial life', url: 'https://www.moneyspire.com' }, + { + hasFreePlan: true, + hasSelfHostingAbility: true, + key: 'moneywell', + name: 'MoneyWell', + origin: 'US', + slogan: 'Personal Budgeting Software for Mac and iOS', + url: 'https://moneywell.app' + }, { key: 'moneywiz', name: 'MoneyWiz', @@ -1109,6 +1179,15 @@ export const personalFinanceTools: Product[] = [ slogan: 'Stock Portfolio Tracker', url: 'https://simpleportfolio.app' }, + { + founded: 2020, + hasFreePlan: false, + hasSelfHostingAbility: false, + key: 'simplifi', + name: 'Simplifi by Quicken', + origin: 'US', + url: 'https://www.quicken.com/products/simplifi' + }, { founded: 2014, hasFreePlan: true, diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html index 1ba0ecc56..daedd5d66 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -49,7 +49,7 @@ Name - + @if (element.platform?.url) { Name -

-
- {{ element.SymbolProfile?.name }} - @if (element.isDraft) { - Draft - } -
+
+ {{ element.SymbolProfile?.name }} + @if (element.isDraft) { + Draft + }
@if ( element.SymbolProfile?.dataSource !== 'MANUAL' && diff --git a/libs/ui/src/lib/chart/chart.registry.ts b/libs/ui/src/lib/chart/chart.registry.ts index 465d6e716..a6a8b3e55 100644 --- a/libs/ui/src/lib/chart/chart.registry.ts +++ b/libs/ui/src/lib/chart/chart.registry.ts @@ -1,6 +1,7 @@ import { getTooltipPositionerMapTop } from '@ghostfolio/common/chart-helper'; -import { Tooltip, TooltipPositionerFunction, ChartType } from 'chart.js'; +import { Chart, Tooltip, TooltipPositionerFunction, ChartType } from 'chart.js'; +import annotationPlugin from 'chartjs-plugin-annotation'; interface VerticalHoverLinePluginOptions { color?: string; @@ -23,6 +24,10 @@ export function registerChartConfiguration() { return; } + // Register the annotation plugin early so that every chart is initialized + // with its state and does not crash on interaction + Chart.register(annotationPlugin); + Tooltip.positioners.top = function (_elements, eventPosition) { return getTooltipPositionerMapTop(this.chart, eventPosition); }; diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts index 0872c2aac..ad80499d7 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts @@ -1,4 +1,4 @@ -import { locale } from '@ghostfolio/common/config'; +import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -47,7 +47,7 @@ export const Simple: Story = { annualInterestRate: 5, currency: 'USD', fireWealth: 50000, - locale: locale, + locale: DEFAULT_LOCALE, savingsRate: 1000 } }; 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 cde180dd9..f99c34b03 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 @@ -68,7 +68,7 @@ export class GfHistoricalMarketDataEditorComponent @Input() currency: string; @Input() dataSource: DataSource; - @Input() dateOfFirstActivity: string; + @Input() dateOfFirstActivity: Date; @Input() symbol: string; @Input() user: User; @@ -124,7 +124,7 @@ export class GfHistoricalMarketDataEditorComponent public ngOnChanges() { if (this.dateOfFirstActivity) { - let date = parseISO(this.dateOfFirstActivity); + let date = this.dateOfFirstActivity; const missingMarketData: { date: Date; marketPrice?: number }[] = []; @@ -174,7 +174,7 @@ export class GfHistoricalMarketDataEditorComponent const dates = Object.keys(this.marketDataByMonth).sort(); const startDateString = first(dates); const startDate = min([ - parseISO(this.dateOfFirstActivity), + this.dateOfFirstActivity, ...(startDateString ? [parseISO(startDateString)] : []) ]); const endDateString = last(dates); diff --git a/libs/ui/src/lib/page-tabs/interfaces/interfaces.ts b/libs/ui/src/lib/page-tabs/interfaces/interfaces.ts index 7b18b26ec..3d44d2870 100644 --- a/libs/ui/src/lib/page-tabs/interfaces/interfaces.ts +++ b/libs/ui/src/lib/page-tabs/interfaces/interfaces.ts @@ -1,6 +1,11 @@ -export interface TabConfiguration { +interface BaseTabConfiguration { iconName: string; label: string; - routerLink: string[]; showCondition?: boolean; } + +export type TabConfiguration = BaseTabConfiguration & + ( + | { onClick: () => void; routerLink?: never } + | { onClick?: never; routerLink: string[] } + ); diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.html b/libs/ui/src/lib/page-tabs/page-tabs.component.html index fa9af9b11..051005790 100644 --- a/libs/ui/src/lib/page-tabs/page-tabs.component.html +++ b/libs/ui/src/lib/page-tabs/page-tabs.component.html @@ -10,21 +10,40 @@ > @for (tab of tabs(); track tab) { @if (tab.showCondition !== false) { - - -
-
+ @if (tab.onClick) { + + } @else { + + + + } } } + + + +
+
diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.scss b/libs/ui/src/lib/page-tabs/page-tabs.component.scss index 0b377e57a..3415a7cb0 100644 --- a/libs/ui/src/lib/page-tabs/page-tabs.component.scss +++ b/libs/ui/src/lib/page-tabs/page-tabs.component.scss @@ -37,7 +37,12 @@ @include mat.tabs-overrides( ( - container-height: 2rem + active-focus-label-text-color: rgba(var(--palette-foreground-base), 1), + active-hover-label-text-color: rgba(var(--palette-foreground-base), 1), + active-label-text-color: rgba(var(--palette-foreground-base), 1), + active-ripple-color: rgba(var(--palette-foreground-base), 1), + container-height: 2rem, + inactive-ripple-color: rgba(var(--palette-foreground-base), 1) ) ); @@ -51,7 +56,15 @@ flex-direction: column; .mat-mdc-tab-link { + border-radius: 0.25rem; + font-weight: 400; justify-content: flex-start; + margin: 0 0.5rem 0.1rem 0.5rem; + + &.mdc-tab--active { + background-color: rgba(var(--palette-foreground-base), 0.05); + font-weight: 500; + } } } } @@ -61,8 +74,32 @@ :host-context(.theme-dark) { @media (min-width: 576px) { - .mat-mdc-tab-header { - background-color: rgba(var(--palette-foreground-base-dark), 0.02); + @include mat.tabs-overrides( + ( + active-focus-label-text-color: rgba( + var(--palette-foreground-base-dark), + 1 + ), + active-hover-label-text-color: rgba( + var(--palette-foreground-base-dark), + 1 + ), + active-label-text-color: rgba(var(--palette-foreground-base-dark), 1), + active-ripple-color: rgba(var(--palette-foreground-base-dark), 1), + inactive-ripple-color: rgba(var(--palette-foreground-base-dark), 1) + ) + ); + + ::ng-deep { + .mat-mdc-tab-header { + background-color: rgba(var(--palette-foreground-base-dark), 0.02); + + .mat-mdc-tab-link { + &.mdc-tab--active { + background-color: rgba(var(--palette-foreground-base-dark), 0.05); + } + } + } } } } diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.ts b/libs/ui/src/lib/page-tabs/page-tabs.component.ts index 61c2caf05..a6ab9cb18 100644 --- a/libs/ui/src/lib/page-tabs/page-tabs.component.ts +++ b/libs/ui/src/lib/page-tabs/page-tabs.component.ts @@ -1,3 +1,4 @@ +import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -13,7 +14,7 @@ import { TabConfiguration } from './interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [IonIcon, MatTabsModule, RouterModule], + imports: [IonIcon, MatTabsModule, NgTemplateOutlet, RouterModule], selector: 'gf-page-tabs', styleUrls: ['./page-tabs.component.scss'], templateUrl: './page-tabs.component.html' diff --git a/libs/ui/src/lib/portfolio-proportion-chart/interfaces/interfaces.ts b/libs/ui/src/lib/portfolio-proportion-chart/interfaces/interfaces.ts new file mode 100644 index 000000000..25d12b049 --- /dev/null +++ b/libs/ui/src/lib/portfolio-proportion-chart/interfaces/interfaces.ts @@ -0,0 +1,5 @@ +import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; + +export type PortfolioProportionChartClickEvent = + | AssetProfileIdentifier + | { accountId: string }; 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 e2c41e956..bf6fd193e 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,10 +1,7 @@ import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { getLocale, getSum, getTextColor } from '@ghostfolio/common/helper'; -import { - AssetProfileIdentifier, - PortfolioPosition -} from '@ghostfolio/common/interfaces'; +import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { ColorScheme } from '@ghostfolio/common/types'; import { @@ -36,6 +33,8 @@ import Color from 'color'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import OpenColor from 'open-color'; +import { PortfolioProportionChartClickEvent } from './interfaces/interfaces'; + const { blue, cyan, @@ -80,7 +79,8 @@ export class GfPortfolioProportionChartComponent public chart: Chart<'doughnut'>; public isLoading = true; - protected readonly proportionChartClicked = output(); + protected readonly proportionChartClicked = + output(); private readonly OTHER_KEY = 'OTHER'; @@ -355,11 +355,11 @@ export class GfPortfolioProportionChartComponent const dataIndex = activeElements[0].index; const symbol = chart.data.labels?.[dataIndex] as string; - const dataSource = this.data[symbol].dataSource; + const dataSource = this.data[symbol]?.dataSource; - if (dataSource) { - this.proportionChartClicked.emit({ dataSource, symbol }); - } + this.proportionChartClicked.emit( + dataSource ? { dataSource, symbol } : { accountId: symbol } + ); } catch {} }, onHover: (event, chartElement) => { diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index 2ae07708d..a3d8bec98 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -445,10 +445,27 @@ export class DataService { }: { dataSource: DataSource; symbol: string; - }) { - return this.http.get( - `/api/v1/portfolio/holding/${dataSource}/${symbol}` - ); + }): Observable< + Omit & { + dateOfFirstActivity: Date | undefined; + } + > { + return this.http + .get( + `/api/v1/portfolio/holding/${dataSource}/${symbol}` + ) + .pipe( + map((response) => { + const dateOfFirstActivity = response.dateOfFirstActivity + ? parseISO(response.dateOfFirstActivity) + : undefined; + + return { + ...response, + dateOfFirstActivity + }; + }) + ); } public fetchInfo(): InfoItem { diff --git a/libs/ui/src/lib/world-map-chart/world-map-chart.component.ts b/libs/ui/src/lib/world-map-chart/world-map-chart.component.ts index 0d5eb7387..899441b7b 100644 --- a/libs/ui/src/lib/world-map-chart/world-map-chart.component.ts +++ b/libs/ui/src/lib/world-map-chart/world-map-chart.component.ts @@ -95,7 +95,7 @@ export class GfWorldMapChartComponent implements OnChanges, OnDestroy { this.svgMapElement.options.countryNames = Object.keys( this.svgMapElement.countries ).reduce<{ [code: string]: string }>((names, code) => { - names[code] = getCountryName({ code, locale: this.locale }); + names[code] = getCountryName({ code }); return names; }, {}); diff --git a/package-lock.json b/package-lock.json index bcae6f86e..7afc46fd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.10.0", + "version": "3.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.10.0", + "version": "3.12.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -21,9 +21,9 @@ "@angular/platform-browser-dynamic": "21.2.7", "@angular/router": "21.2.7", "@angular/service-worker": "21.2.7", - "@bull-board/api": "7.1.5", - "@bull-board/express": "7.1.5", - "@bull-board/nestjs": "7.1.5", + "@bull-board/api": "7.2.1", + "@bull-board/express": "7.2.1", + "@bull-board/nestjs": "7.2.1", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.1", "@internationalized/number": "3.6.6", @@ -63,7 +63,7 @@ "countries-and-timezones": "3.9.0", "countries-list": "3.3.0", "countup.js": "2.10.0", - "date-fns": "4.1.0", + "date-fns": "4.4.0", "dotenv": "17.2.3", "dotenv-expand": "12.0.3", "envalid": "8.1.1", @@ -91,7 +91,7 @@ "reflect-metadata": "0.2.2", "rxjs": "7.8.1", "stripe": "21.0.1", - "svgmap": "2.19.3", + "svgmap": "2.21.0", "tablemark": "4.1.0", "twitter-api-v2": "1.29.0", "undici": "7.24.4", @@ -3523,33 +3523,33 @@ "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@bull-board/api": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-7.1.5.tgz", - "integrity": "sha512-EW0sbTtGIysu9vipdVpPQeToPqOpPgVZTt+pn1Ut3gbSS/GLWbEgIfFtMmSQDUoSL9WH00RzjgUY5K+43nWh0A==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-7.2.1.tgz", + "integrity": "sha512-ldRG4POJLHf6oDrbDA7AsbTKliBmV4eySlwdUAumiRDtfvtbRSdXGE4Md2uPDova1r/ck7ExEe1+pHEQAZElqw==", "license": "MIT", "dependencies": { "redis-info": "^3.1.0" }, "peerDependencies": { - "@bull-board/ui": "7.1.5" + "@bull-board/ui": "7.2.1" } }, "node_modules/@bull-board/express": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-7.1.5.tgz", - "integrity": "sha512-kp4SzhVjZlykryiQwcOhJjDhiLbBnZoAMoSgEstzqQ0raLw+jERRC6ryJ0MIQO+SO+Jv9EjjxrXCR8O2YSP/eg==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-7.2.1.tgz", + "integrity": "sha512-tBr/xV5letzKYPRGRkilTQZmfoCoy3mCuUo4M2dDoDKOhbrF360mK5v9/rIcSgYSyI9c7BgEgrve80LhmexNxQ==", "license": "MIT", "dependencies": { - "@bull-board/api": "7.1.5", - "@bull-board/ui": "7.1.5", - "ejs": "^5.0.2", + "@bull-board/api": "7.2.1", + "@bull-board/ui": "7.2.1", + "ejs": "^6.0.1", "express": "^5.2.1" } }, "node_modules/@bull-board/express/node_modules/ejs": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz", - "integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-6.0.1.tgz", + "integrity": "sha512-UaaM14yby8U3k02ihS1Bmj5Kz2d7CCQM1scxpgs4Mhkq8F1wR2gl3+Ts4h5Ne4Mnt7M9m4Dw7jsuMr3+xO4vZA==", "license": "Apache-2.0", "bin": { "ejs": "bin/cli.js" @@ -3559,12 +3559,12 @@ } }, "node_modules/@bull-board/nestjs": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-7.1.5.tgz", - "integrity": "sha512-1y+HkjnDaZoSCXJRsiYfBNBVx+PX3I8x3Uv+SSJuSpt2vHifMRwFbChO3XDxeWXetT1eR+yqPVq6ub5eJwNOYQ==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-7.2.1.tgz", + "integrity": "sha512-Uq2Z3+0ORgHJSw4TDV1kBrHdksRnK8CZdda63hrStduPnvKHPBxIZUGNfBN/vL08UqizpNkjFmNyNXiHOgf0LQ==", "license": "MIT", "peerDependencies": { - "@bull-board/api": "^7.1.5", + "@bull-board/api": "^7.2.1", "@nestjs/bull-shared": "^10.0.0 || ^11.0.0", "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", @@ -3573,12 +3573,12 @@ } }, "node_modules/@bull-board/ui": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-7.1.5.tgz", - "integrity": "sha512-2IkatKwNRx/1M9/lAZIptcxS1FPNq6icpp2M46Upwd4olVxs/ujF9Kvs+Ff9ExtIO/OgYfwx7mG2IprGZ+nQCg==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-7.2.1.tgz", + "integrity": "sha512-O4ykrXrl2UJNHnhJrCvJxrw1ar+DlUBgyZUeZ8Ci+Ne5Wbq6rBv1gfpQH54/eu3IFbLso0S/kjc6WUGb2HPqZw==", "license": "MIT", "dependencies": { - "@bull-board/api": "7.1.5" + "@bull-board/api": "7.2.1" } }, "node_modules/@cacheable/utils": { @@ -20491,9 +20491,9 @@ } }, "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", "license": "MIT", "funding": { "type": "github", @@ -36530,9 +36530,9 @@ "license": "BSD-2-Clause" }, "node_modules/svgmap": { - "version": "2.19.3", - "resolved": "https://registry.npmjs.org/svgmap/-/svgmap-2.19.3.tgz", - "integrity": "sha512-LjKVzKgANVMRiHqFQVz57FqqX7tVm+EskubySCWb5kvizdeDBanscPA/c4tmK/48VCvYxrR1ecBbqStcD6HYfQ==", + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/svgmap/-/svgmap-2.21.0.tgz", + "integrity": "sha512-KQNP4rkkYe3D4EgqSxX+QoRzl13oDaOt5Oqe/ClIJme/u/AaQXMU+SXgt7iL4rfqK+lRc+15dXp+y7+zQg5WiA==", "license": "MIT", "dependencies": { "svg-pan-zoom": "^3.6.2" diff --git a/package.json b/package.json index 730145781..f6f31747b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.10.0", + "version": "3.12.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", @@ -65,9 +65,9 @@ "@angular/platform-browser-dynamic": "21.2.7", "@angular/router": "21.2.7", "@angular/service-worker": "21.2.7", - "@bull-board/api": "7.1.5", - "@bull-board/express": "7.1.5", - "@bull-board/nestjs": "7.1.5", + "@bull-board/api": "7.2.1", + "@bull-board/express": "7.2.1", + "@bull-board/nestjs": "7.2.1", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.1", "@internationalized/number": "3.6.6", @@ -107,7 +107,7 @@ "countries-and-timezones": "3.9.0", "countries-list": "3.3.0", "countup.js": "2.10.0", - "date-fns": "4.1.0", + "date-fns": "4.4.0", "dotenv": "17.2.3", "dotenv-expand": "12.0.3", "envalid": "8.1.1", @@ -135,7 +135,7 @@ "reflect-metadata": "0.2.2", "rxjs": "7.8.1", "stripe": "21.0.1", - "svgmap": "2.19.3", + "svgmap": "2.21.0", "tablemark": "4.1.0", "twitter-api-v2": "1.29.0", "undici": "7.24.4",