From a3e491e300acdcf3d38e679098843401ecc0774a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 2 Aug 2026 10:12:58 +0200 Subject: [PATCH] Add hidden tab to manage the splits of an asset profile --- .../asset-profile-dialog.component.ts | 71 +++++++++++- .../asset-profile-dialog.html | 108 ++++++++++++++++++ libs/ui/src/lib/services/admin.service.ts | 24 +++- libs/ui/src/lib/services/data.service.ts | 4 + 4 files changed, 203 insertions(+), 4 deletions(-) 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 330688da18..026f676e7c 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 @@ -11,6 +11,7 @@ import { DATE_FORMAT, getCountryName, getCurrencyFromSymbol, + getDateFormatString, getStringOrNull, getStringOrUndefined, isCurrency @@ -63,6 +64,7 @@ import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDatepickerModule } from '@angular/material/datepicker'; import { MAT_DIALOG_DATA, MatDialogModule, @@ -76,6 +78,7 @@ import { MatTabsModule } from '@angular/material/tabs'; import { IonIcon } from '@ionic/angular/standalone'; import { AssetClass, + AssetProfileSplit, AssetSubClass, DataGatheringFrequency, DataSource, @@ -88,11 +91,14 @@ import { format } from 'date-fns'; import { StatusCodes } from 'http-status-codes'; import { addIcons } from 'ionicons'; import { + calendarClearOutline, codeSlashOutline, createOutline, ellipsisVertical, + gitCompareOutline, readerOutline, - serverOutline + serverOutline, + trashOutline } from 'ionicons/icons'; import { isBoolean } from 'lodash'; import ms from 'ms'; @@ -116,6 +122,7 @@ import { AssetProfileDialogParams } from './interfaces/interfaces'; IonIcon, MatButtonModule, MatCheckboxModule, + MatDatepickerModule, MatDialogModule, MatInputModule, MatMenuModule, @@ -196,6 +203,18 @@ export class GfAssetProfileDialogComponent implements OnInit { } ); + protected readonly assetProfileSplitForm = this.formBuilder.group({ + date: new FormControl(null, Validators.required), + factor: new FormControl(null, [ + Validators.required, + (control: AbstractControl): ValidationErrors | null => { + return control.value > 0 && control.value !== 1 + ? null + : { invalidSplitFactor: true }; + } + ]) + }); + protected readonly canDeleteAssetProfile = canDeleteAssetProfile; protected canEditAssetProfile = true; @@ -247,11 +266,17 @@ export class GfAssetProfileDialogComponent implements OnInit { value: 'max' } ]; + protected defaultDateFormat: string; protected readonly getCountryName = getCountryName; protected historicalDataItems: LineChartItem[]; protected isBenchmark = false; protected isDataGatheringEnabled: boolean; protected isEditAssetProfileIdentifierMode = false; + + // Splits are not applied to the portfolio calculation yet, hence the tab to + // manage them is hidden + protected readonly isSplitsTabEnabled = false; + protected readonly isUUID = isUUID; protected marketDataItems: MarketData[] = []; @@ -270,6 +295,8 @@ export class GfAssetProfileDialogComponent implements OnInit { [name: string]: { name: string; value: number }; }; + protected splits: AssetProfileSplit[] = []; + protected readonly translate = translate; protected user: User; @@ -293,11 +320,14 @@ export class GfAssetProfileDialogComponent implements OnInit { private userService: UserService ) { addIcons({ + calendarClearOutline, codeSlashOutline, createOutline, ellipsisVertical, + gitCompareOutline, readerOutline, - serverOutline + serverOutline, + trashOutline }); } @@ -310,6 +340,7 @@ export class GfAssetProfileDialogComponent implements OnInit { this.benchmarks = benchmarks; this.currencies = currencies; + this.defaultDateFormat = getDateFormatString(this.data.locale); this.initialize(); } @@ -364,8 +395,9 @@ export class GfAssetProfileDialogComponent implements OnInit { symbol: this.data.symbol }) .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(({ assetProfile, marketData }) => { + .subscribe(({ assetProfile, marketData, splits }) => { this.assetProfile = assetProfile; + this.splits = splits ?? []; this.assetClassLabel = translate(this.assetProfile?.assetClass ?? ''); this.assetSubClassLabel = translate( @@ -521,6 +553,39 @@ export class GfAssetProfileDialogComponent implements OnInit { .subscribe(); } + protected onAddSplit() { + const { date, factor } = this.assetProfileSplitForm.getRawValue(); + + this.adminService + .postAssetProfileSplit({ + dataSource: this.data.dataSource, + split: { + factor, + date: format(date, DATE_FORMAT) + }, + symbol: this.data.symbol + }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.assetProfileSplitForm.reset(); + + this.initialize(); + }); + } + + protected onDeleteSplit(aId: string) { + this.adminService + .deleteAssetProfileSplit({ + id: aId, + dataSource: this.data.dataSource, + symbol: this.data.symbol + }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.initialize(); + }); + } + protected onMarketDataChanged(withRefresh: boolean = false) { if (withRefresh) { this.initialize(); 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 f0b914b1cc..c9a16ceb05 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 @@ -503,6 +503,114 @@ + @if (isSplitsTabEnabled) { + + + +
Splits
+
+
+
+
+

+ Splits are stored for this asset profile, but are not applied + to the portfolio calculation yet. +

+ @if (splits.length > 0) { + + + + + + + + + + @for (split of splits; track split.id) { + + + + + + } + +
Date + Split Factor +
+ {{ split.date | date: defaultDateFormat }} + + {{ split.factor }} + + +
+ } +
+ + Date + + + + + + + + Split Factor + + Shares after per 1 share before, e.g. 4 for a 4:1 split + or 0.1 for a 1:10 reverse split + + +
+
+
+
+
+ } @if (assetProfile?.dataSource === 'MANUAL') { diff --git a/libs/ui/src/lib/services/admin.service.ts b/libs/ui/src/lib/services/admin.service.ts index 6a06ef76b7..8510f1729f 100644 --- a/libs/ui/src/lib/services/admin.service.ts +++ b/libs/ui/src/lib/services/admin.service.ts @@ -4,6 +4,7 @@ import { HEADER_KEY_TOKEN } from '@ghostfolio/common/config'; import { + CreateAssetProfileSplitDto, CreatePlatformDto, UpdateAssetProfileDto, UpdatePlatformDto @@ -23,7 +24,7 @@ import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; -import { MarketData, Platform } from '@prisma/client'; +import { AssetProfileSplit, MarketData, Platform } from '@prisma/client'; import { JobStatus } from 'bull'; import { isNumber } from 'lodash'; @@ -61,6 +62,16 @@ export class AdminService { return this.http.delete(`/api/v1/platform/${aId}`); } + public deleteAssetProfileSplit({ + dataSource, + id, + symbol + }: AssetProfileIdentifier & { id: string }) { + return this.http.delete( + `/api/v1/asset-profiles/${dataSource}/${encodeURIComponent(symbol)}/splits/${id}` + ); + } + public deleteProfileData({ dataSource, symbol }: AssetProfileIdentifier) { return this.http.delete( `/api/v1/admin/profile-data/${dataSource}/${encodeURIComponent(symbol)}` @@ -217,6 +228,17 @@ export class AdminService { ); } + public postAssetProfileSplit({ + dataSource, + split, + symbol + }: AssetProfileIdentifier & { split: CreateAssetProfileSplitDto }) { + return this.http.post( + `/api/v1/asset-profiles/${dataSource}/${encodeURIComponent(symbol)}/splits`, + split + ); + } + public postPlatform(aPlatform: CreatePlatformDto) { return this.http.post(`/api/v1/platform`, aPlatform); } diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index bc1512550b..7ccd9d1d9e 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -589,6 +589,10 @@ export class DataService { item.date = parseISO(item.date); } + for (const item of data.splits ?? []) { + item.date = parseISO(item.date); + } + return data; }) );