Change UX to set an asset profile as a benchmark (#2409)
* Add checkbox functionality to set / unset benchmark * Update changelog --------- Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
This commit is contained in:
parent
ad5ae938ef
commit
90e18338f6
@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Harmonized the settings icon of the user account page
|
- Harmonized the settings icon of the user account page
|
||||||
|
- Improved the usability to set an asset profile as a benchmark
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types';
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
Delete,
|
||||||
Get,
|
Get,
|
||||||
HttpException,
|
HttpException,
|
||||||
Inject,
|
Inject,
|
||||||
@ -32,32 +33,6 @@ export class BenchmarkController {
|
|||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
|
||||||
public async getBenchmark(): Promise<BenchmarkResponse> {
|
|
||||||
return {
|
|
||||||
benchmarks: await this.benchmarkService.getBenchmarks()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':dataSource/:symbol/:startDateString')
|
|
||||||
@UseGuards(AuthGuard('jwt'))
|
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
|
||||||
public async getBenchmarkMarketDataBySymbol(
|
|
||||||
@Param('dataSource') dataSource: DataSource,
|
|
||||||
@Param('startDateString') startDateString: string,
|
|
||||||
@Param('symbol') symbol: string
|
|
||||||
): Promise<BenchmarkMarketDataDetails> {
|
|
||||||
const startDate = new Date(startDateString);
|
|
||||||
|
|
||||||
return this.benchmarkService.getMarketDataBySymbol({
|
|
||||||
dataSource,
|
|
||||||
startDate,
|
|
||||||
symbol
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) {
|
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) {
|
||||||
@ -94,4 +69,70 @@ export class BenchmarkController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete(':dataSource/:symbol')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async deleteBenchmark(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
this.request.user.permissions,
|
||||||
|
permissions.accessAdminControl
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const benchmark = await this.benchmarkService.deleteBenchmark({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!benchmark) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return benchmark;
|
||||||
|
} catch {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
|
||||||
|
StatusCodes.INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
|
public async getBenchmark(): Promise<BenchmarkResponse> {
|
||||||
|
return {
|
||||||
|
benchmarks: await this.benchmarkService.getBenchmarks()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':dataSource/:symbol/:startDateString')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
|
public async getBenchmarkMarketDataBySymbol(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('startDateString') startDateString: string,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
): Promise<BenchmarkMarketDataDetails> {
|
||||||
|
const startDate = new Date(startDateString);
|
||||||
|
|
||||||
|
return this.benchmarkService.getMarketDataBySymbol({
|
||||||
|
dataSource,
|
||||||
|
startDate,
|
||||||
|
symbol
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,6 +245,43 @@ export class BenchmarkService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async deleteBenchmark({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: UniqueAsset): Promise<Partial<SymbolProfile>> {
|
||||||
|
const assetProfile = await this.prismaService.symbolProfile.findFirst({
|
||||||
|
where: {
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!assetProfile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let benchmarks =
|
||||||
|
((await this.propertyService.getByKey(
|
||||||
|
PROPERTY_BENCHMARKS
|
||||||
|
)) as BenchmarkProperty[]) ?? [];
|
||||||
|
|
||||||
|
benchmarks = benchmarks.filter(({ symbolProfileId }) => {
|
||||||
|
return symbolProfileId !== assetProfile.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.propertyService.put({
|
||||||
|
key: PROPERTY_BENCHMARKS,
|
||||||
|
value: JSON.stringify(benchmarks)
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
id: assetProfile.id,
|
||||||
|
name: assetProfile.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private getMarketCondition(aPerformanceInPercent: number) {
|
private getMarketCondition(aPerformanceInPercent: number) {
|
||||||
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
||||||
}
|
}
|
||||||
|
@ -146,9 +146,11 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
.postBenchmark({ dataSource, symbol })
|
.postBenchmark({ dataSource, symbol })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
setTimeout(() => {
|
this.dataService.updateInfo();
|
||||||
window.location.reload();
|
|
||||||
}, 300);
|
this.isBenchmark = true;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,6 +187,19 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onUnsetBenchmark({ dataSource, symbol }: UniqueAsset) {
|
||||||
|
this.dataService
|
||||||
|
.deleteBenchmark({ dataSource, symbol })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.dataService.updateInfo();
|
||||||
|
|
||||||
|
this.isBenchmark = false;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -37,13 +37,6 @@
|
|||||||
>
|
>
|
||||||
<ng-container i18n>Gather Profile Data</ng-container>
|
<ng-container i18n>Gather Profile Data</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
mat-menu-item
|
|
||||||
[disabled]="isBenchmark"
|
|
||||||
(click)="onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
|
|
||||||
>
|
|
||||||
<ng-container i18n>Set as Benchmark</ng-container>
|
|
||||||
</button>
|
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -151,6 +144,17 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-flex my-3">
|
||||||
|
<div class="w-50">
|
||||||
|
<mat-checkbox
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
[checked]="isBenchmark"
|
||||||
|
(change)="isBenchmark ? onUnsetBenchmark({dataSource: data.dataSource, symbol: data.symbol}) : onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
|
||||||
|
>Benchmark</mat-checkbox
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Symbol Mapping</mat-label>
|
<mat-label i18n>Symbol Mapping</mat-label>
|
||||||
|
@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
@ -21,6 +22,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
|
|||||||
GfPortfolioProportionChartModule,
|
GfPortfolioProportionChartModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatCheckboxModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
|
@ -204,6 +204,10 @@ export class DataService {
|
|||||||
return this.http.delete<any>(`/api/v1/order/`);
|
return this.http.delete<any>(`/api/v1/order/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public deleteBenchmark({ dataSource, symbol }: UniqueAsset) {
|
||||||
|
return this.http.delete<any>(`/api/v1/benchmark/${dataSource}/${symbol}`);
|
||||||
|
}
|
||||||
|
|
||||||
public deleteOrder(aId: string) {
|
public deleteOrder(aId: string) {
|
||||||
return this.http.delete<any>(`/api/v1/order/${aId}`);
|
return this.http.delete<any>(`/api/v1/order/${aId}`);
|
||||||
}
|
}
|
||||||
@ -496,4 +500,19 @@ export class DataService {
|
|||||||
couponCode
|
couponCode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateInfo() {
|
||||||
|
this.http.get<InfoItem>('/api/v1/info').subscribe((info) => {
|
||||||
|
const utmSource = <'ios' | 'trusted-web-activity'>(
|
||||||
|
window.localStorage.getItem('utm_source')
|
||||||
|
);
|
||||||
|
|
||||||
|
info.globalPermissions = filterGlobalPermissions(
|
||||||
|
info.globalPermissions,
|
||||||
|
utmSource
|
||||||
|
);
|
||||||
|
|
||||||
|
(window as any).info = info;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user