Browse Source

Add hidden tab to manage the splits of an asset profile

pull/7251/head
Thomas Kaul 4 weeks ago
parent
commit
a3e491e300
  1. 71
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  2. 108
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  3. 24
      libs/ui/src/lib/services/admin.service.ts
  4. 4
      libs/ui/src/lib/services/data.service.ts

71
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -11,6 +11,7 @@ import {
DATE_FORMAT, DATE_FORMAT,
getCountryName, getCountryName,
getCurrencyFromSymbol, getCurrencyFromSymbol,
getDateFormatString,
getStringOrNull, getStringOrNull,
getStringOrUndefined, getStringOrUndefined,
isCurrency isCurrency
@ -63,6 +64,7 @@ import {
MatCheckboxChange, MatCheckboxChange,
MatCheckboxModule MatCheckboxModule
} from '@angular/material/checkbox'; } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { import {
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
MatDialogModule, MatDialogModule,
@ -76,6 +78,7 @@ import { MatTabsModule } from '@angular/material/tabs';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { import {
AssetClass, AssetClass,
AssetProfileSplit,
AssetSubClass, AssetSubClass,
DataGatheringFrequency, DataGatheringFrequency,
DataSource, DataSource,
@ -88,11 +91,14 @@ import { format } from 'date-fns';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
calendarClearOutline,
codeSlashOutline, codeSlashOutline,
createOutline, createOutline,
ellipsisVertical, ellipsisVertical,
gitCompareOutline,
readerOutline, readerOutline,
serverOutline serverOutline,
trashOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { isBoolean } from 'lodash'; import { isBoolean } from 'lodash';
import ms from 'ms'; import ms from 'ms';
@ -116,6 +122,7 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
IonIcon, IonIcon,
MatButtonModule, MatButtonModule,
MatCheckboxModule, MatCheckboxModule,
MatDatepickerModule,
MatDialogModule, MatDialogModule,
MatInputModule, MatInputModule,
MatMenuModule, MatMenuModule,
@ -196,6 +203,18 @@ export class GfAssetProfileDialogComponent implements OnInit {
} }
); );
protected readonly assetProfileSplitForm = this.formBuilder.group({
date: new FormControl<Date | null>(null, Validators.required),
factor: new FormControl<number | null>(null, [
Validators.required,
(control: AbstractControl): ValidationErrors | null => {
return control.value > 0 && control.value !== 1
? null
: { invalidSplitFactor: true };
}
])
});
protected readonly canDeleteAssetProfile = canDeleteAssetProfile; protected readonly canDeleteAssetProfile = canDeleteAssetProfile;
protected canEditAssetProfile = true; protected canEditAssetProfile = true;
@ -247,11 +266,17 @@ export class GfAssetProfileDialogComponent implements OnInit {
value: 'max' value: 'max'
} }
]; ];
protected defaultDateFormat: string;
protected readonly getCountryName = getCountryName; protected readonly getCountryName = getCountryName;
protected historicalDataItems: LineChartItem[]; protected historicalDataItems: LineChartItem[];
protected isBenchmark = false; protected isBenchmark = false;
protected isDataGatheringEnabled: boolean; protected isDataGatheringEnabled: boolean;
protected isEditAssetProfileIdentifierMode = false; 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 readonly isUUID = isUUID;
protected marketDataItems: MarketData[] = []; protected marketDataItems: MarketData[] = [];
@ -270,6 +295,8 @@ export class GfAssetProfileDialogComponent implements OnInit {
[name: string]: { name: string; value: number }; [name: string]: { name: string; value: number };
}; };
protected splits: AssetProfileSplit[] = [];
protected readonly translate = translate; protected readonly translate = translate;
protected user: User; protected user: User;
@ -293,11 +320,14 @@ export class GfAssetProfileDialogComponent implements OnInit {
private userService: UserService private userService: UserService
) { ) {
addIcons({ addIcons({
calendarClearOutline,
codeSlashOutline, codeSlashOutline,
createOutline, createOutline,
ellipsisVertical, ellipsisVertical,
gitCompareOutline,
readerOutline, readerOutline,
serverOutline serverOutline,
trashOutline
}); });
} }
@ -310,6 +340,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
this.benchmarks = benchmarks; this.benchmarks = benchmarks;
this.currencies = currencies; this.currencies = currencies;
this.defaultDateFormat = getDateFormatString(this.data.locale);
this.initialize(); this.initialize();
} }
@ -364,8 +395,9 @@ export class GfAssetProfileDialogComponent implements OnInit {
symbol: this.data.symbol symbol: this.data.symbol
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ assetProfile, marketData }) => { .subscribe(({ assetProfile, marketData, splits }) => {
this.assetProfile = assetProfile; this.assetProfile = assetProfile;
this.splits = splits ?? [];
this.assetClassLabel = translate(this.assetProfile?.assetClass ?? ''); this.assetClassLabel = translate(this.assetProfile?.assetClass ?? '');
this.assetSubClassLabel = translate( this.assetSubClassLabel = translate(
@ -521,6 +553,39 @@ export class GfAssetProfileDialogComponent implements OnInit {
.subscribe(); .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) { protected onMarketDataChanged(withRefresh: boolean = false) {
if (withRefresh) { if (withRefresh) {
this.initialize(); this.initialize();

108
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -503,6 +503,114 @@
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
@if (isSplitsTabEnabled) {
<mat-tab>
<ng-template mat-tab-label>
<ion-icon name="git-compare-outline" />
<div class="d-none d-sm-block ml-2" i18n>Splits</div>
</ng-template>
<div class="container mt-3 p-0">
<div class="no-gutters row w-100">
<div class="col-12">
<p class="text-muted" i18n>
Splits are stored for this asset profile, but are not applied
to the portfolio calculation yet.
</p>
@if (splits.length > 0) {
<table class="gf-table mb-3 w-100">
<thead>
<tr class="mat-mdc-header-row">
<th class="mat-mdc-header-cell px-1 py-2" i18n>Date</th>
<th
class="mat-mdc-header-cell px-1 py-2 text-right"
i18n
>
Split Factor
</th>
<th class="mat-mdc-header-cell px-1 py-2"></th>
</tr>
</thead>
<tbody>
@for (split of splits; track split.id) {
<tr class="mat-mdc-row">
<td class="mat-mdc-cell px-1 py-2">
{{ split.date | date: defaultDateFormat }}
</td>
<td class="mat-mdc-cell px-1 py-2 text-right">
{{ split.factor }}
</td>
<td class="mat-mdc-cell px-1 py-2 text-right">
<button
mat-icon-button
type="button"
[disabled]="!canEditAssetProfile"
(click)="onDeleteSplit(split.id)"
>
<ion-icon name="trash-outline" />
</button>
</td>
</tr>
}
</tbody>
</table>
}
<form
class="align-items-center d-flex"
[formGroup]="assetProfileSplitForm"
(ngSubmit)="onAddSplit()"
>
<mat-form-field
appearance="outline"
class="mr-3 without-hint"
>
<mat-label i18n>Date</mat-label>
<input
formControlName="date"
matInput
[matDatepicker]="dateOfSplit"
/>
<mat-datepicker-toggle
class="mr-2"
matSuffix
[for]="dateOfSplit"
>
<ion-icon
class="text-muted"
matDatepickerToggleIcon
name="calendar-clear-outline"
/>
</mat-datepicker-toggle>
<mat-datepicker #dateOfSplit />
</mat-form-field>
<mat-form-field appearance="outline" class="mr-3">
<mat-label i18n>Split Factor</mat-label>
<input
formControlName="factor"
matInput
step="any"
type="number"
/>
<mat-hint i18n
>Shares after per 1 share before, e.g. 4 for a 4:1 split
or 0.1 for a 1:10 reverse split</mat-hint
>
</mat-form-field>
<button
color="primary"
mat-flat-button
type="submit"
[disabled]="
!assetProfileSplitForm.valid || !canEditAssetProfile
"
>
<ng-container i18n>Add</ng-container>
</button>
</form>
</div>
</div>
</div>
</mat-tab>
}
@if (assetProfile?.dataSource === 'MANUAL') { @if (assetProfile?.dataSource === 'MANUAL') {
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>

24
libs/ui/src/lib/services/admin.service.ts

@ -4,6 +4,7 @@ import {
HEADER_KEY_TOKEN HEADER_KEY_TOKEN
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
CreateAssetProfileSplitDto,
CreatePlatformDto, CreatePlatformDto,
UpdateAssetProfileDto, UpdateAssetProfileDto,
UpdatePlatformDto UpdatePlatformDto
@ -23,7 +24,7 @@ import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { MarketData, Platform } from '@prisma/client'; import { AssetProfileSplit, MarketData, Platform } from '@prisma/client';
import { JobStatus } from 'bull'; import { JobStatus } from 'bull';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
@ -61,6 +62,16 @@ export class AdminService {
return this.http.delete<void>(`/api/v1/platform/${aId}`); return this.http.delete<void>(`/api/v1/platform/${aId}`);
} }
public deleteAssetProfileSplit({
dataSource,
id,
symbol
}: AssetProfileIdentifier & { id: string }) {
return this.http.delete<void>(
`/api/v1/asset-profiles/${dataSource}/${encodeURIComponent(symbol)}/splits/${id}`
);
}
public deleteProfileData({ dataSource, symbol }: AssetProfileIdentifier) { public deleteProfileData({ dataSource, symbol }: AssetProfileIdentifier) {
return this.http.delete<void>( return this.http.delete<void>(
`/api/v1/admin/profile-data/${dataSource}/${encodeURIComponent(symbol)}` `/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<AssetProfileSplit>(
`/api/v1/asset-profiles/${dataSource}/${encodeURIComponent(symbol)}/splits`,
split
);
}
public postPlatform(aPlatform: CreatePlatformDto) { public postPlatform(aPlatform: CreatePlatformDto) {
return this.http.post<Platform>(`/api/v1/platform`, aPlatform); return this.http.post<Platform>(`/api/v1/platform`, aPlatform);
} }

4
libs/ui/src/lib/services/data.service.ts

@ -589,6 +589,10 @@ export class DataService {
item.date = parseISO(item.date); item.date = parseISO(item.date);
} }
for (const item of data.splits ?? []) {
item.date = parseISO(item.date);
}
return data; return data;
}) })
); );

Loading…
Cancel
Save