diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a008d98..abe42de78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,17 +10,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for notes in the activities import +- Added the application version to the endpoint `GET api/v1/admin` -## Unreleased +## 2.8.0 - 2023-10-03 ### Added -- Added the version to the admin control panel +- Supported enter key press to submit the form of the create or update account dialog +- Added the application version to the admin control panel - Added pagination parameters (`skip`, `take`) to the endpoint `GET api/v1/order` ### Changed - Harmonized the settings icon of the user account page +- Improved the usability to set an asset profile as a benchmark +- Reload platforms after making a change in the admin control panel +- Reload tags after making a change in the admin control panel ### Fixed diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b73831d98..a950e5672 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -18,6 +18,12 @@ ### Prisma +#### Access database via GUI + +Run `yarn database:gui` + +https://www.prisma.io/studio + #### Synchronize schema with database for prototyping Run `yarn database:push` diff --git a/apps/api/src/app/account/create-account.dto.ts b/apps/api/src/app/account/create-account.dto.ts index eb24d959a..fff982ecf 100644 --- a/apps/api/src/app/account/create-account.dto.ts +++ b/apps/api/src/app/account/create-account.dto.ts @@ -12,7 +12,7 @@ import { isString } from 'lodash'; export class CreateAccountDto { @IsOptional() @IsString() - accountType: AccountType; + accountType?: AccountType; @IsNumber() balance: number; diff --git a/apps/api/src/app/account/update-account.dto.ts b/apps/api/src/app/account/update-account.dto.ts index a91914482..7ab829454 100644 --- a/apps/api/src/app/account/update-account.dto.ts +++ b/apps/api/src/app/account/update-account.dto.ts @@ -12,7 +12,7 @@ import { isString } from 'lodash'; export class UpdateAccountDto { @IsOptional() @IsString() - accountType: AccountType; + accountType?: AccountType; @IsNumber() balance: number; diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index a45fbe634..dd9e3f9ce 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -1,4 +1,5 @@ import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; +import { environment } from '@ghostfolio/api/environments/environment'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -97,7 +98,8 @@ export class AdminService { settings: await this.propertyService.get(), transactionCount: await this.prismaService.order.count(), userCount: await this.prismaService.user.count(), - users: await this.getUsersWithAnalytics() + users: await this.getUsersWithAnalytics(), + version: environment.version }; } diff --git a/apps/api/src/app/benchmark/benchmark.controller.ts b/apps/api/src/app/benchmark/benchmark.controller.ts index d59a231ff..2230ff42b 100644 --- a/apps/api/src/app/benchmark/benchmark.controller.ts +++ b/apps/api/src/app/benchmark/benchmark.controller.ts @@ -10,6 +10,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, Controller, + Delete, Get, HttpException, Inject, @@ -32,35 +33,49 @@ export class BenchmarkController { @Inject(REQUEST) private readonly request: RequestWithUser ) {} - @Get() - @UseInterceptors(TransformDataSourceInRequestInterceptor) - @UseInterceptors(TransformDataSourceInResponseInterceptor) - public async getBenchmark(): Promise { - return { - benchmarks: await this.benchmarkService.getBenchmarks() - }; - } - - @Get(':dataSource/:symbol/:startDateString') + @Post() @UseGuards(AuthGuard('jwt')) - @UseInterceptors(TransformDataSourceInRequestInterceptor) - public async getBenchmarkMarketDataBySymbol( - @Param('dataSource') dataSource: DataSource, - @Param('startDateString') startDateString: string, - @Param('symbol') symbol: string - ): Promise { - const startDate = new Date(startDateString); + public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) { + if ( + !hasPermission( + this.request.user.permissions, + permissions.accessAdminControl + ) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } - return this.benchmarkService.getMarketDataBySymbol({ - dataSource, - startDate, - symbol - }); + try { + const benchmark = await this.benchmarkService.addBenchmark({ + 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 + ); + } } - @Post() + @Delete(':dataSource/:symbol') @UseGuards(AuthGuard('jwt')) - public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) { + public async deleteBenchmark( + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ) { if ( !hasPermission( this.request.user.permissions, @@ -74,7 +89,7 @@ export class BenchmarkController { } try { - const benchmark = await this.benchmarkService.addBenchmark({ + const benchmark = await this.benchmarkService.deleteBenchmark({ dataSource, symbol }); @@ -94,4 +109,30 @@ export class BenchmarkController { ); } } + + @Get() + @UseInterceptors(TransformDataSourceInRequestInterceptor) + @UseInterceptors(TransformDataSourceInResponseInterceptor) + public async getBenchmark(): Promise { + 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 { + const startDate = new Date(startDateString); + + return this.benchmarkService.getMarketDataBySymbol({ + dataSource, + startDate, + symbol + }); + } } diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts index 785c2801a..7fe1911a4 100644 --- a/apps/api/src/app/benchmark/benchmark.service.ts +++ b/apps/api/src/app/benchmark/benchmark.service.ts @@ -245,6 +245,43 @@ export class BenchmarkService { }; } + public async deleteBenchmark({ + dataSource, + symbol + }: UniqueAsset): Promise> { + 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) { return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET'; } 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 be1892e91..bef984729 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 @@ -146,9 +146,11 @@ export class AssetProfileDialog implements OnDestroy, OnInit { .postBenchmark({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - setTimeout(() => { - window.location.reload(); - }, 300); + this.dataService.updateInfo(); + + 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() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); 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 be99df7cb..6682d004d 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 @@ -37,13 +37,6 @@ > Gather Profile Data - @@ -151,6 +144,17 @@ +
+
+ Benchmark +
+
Symbol Mapping diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts index 8672342b0..1911f5a47 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialogModule } from '@angular/material/dialog'; import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; @@ -21,6 +22,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component'; GfPortfolioProportionChartModule, GfValueModule, MatButtonModule, + MatCheckboxModule, MatDialogModule, MatInputModule, MatMenuModule, diff --git a/apps/client/src/app/components/admin-platform/admin-platform.component.ts b/apps/client/src/app/components/admin-platform/admin-platform.component.ts index c8fce18ad..ffc5810b3 100644 --- a/apps/client/src/app/components/admin-platform/admin-platform.component.ts +++ b/apps/client/src/app/components/admin-platform/admin-platform.component.ts @@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto'; import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto'; import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { Platform } from '@prisma/client'; import { get } from 'lodash'; @@ -40,6 +41,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy { public constructor( private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, private route: ActivatedRoute, @@ -119,6 +121,8 @@ export class AdminPlatformComponent implements OnInit, OnDestroy { this.dataSource.sort = this.sort; this.dataSource.sortingDataAccessor = get; + this.dataService.updateInfo(); + this.changeDetectorRef.markForCheck(); }); } diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.ts b/apps/client/src/app/components/admin-tag/admin-tag.component.ts index 48eb25554..e0dce2477 100644 --- a/apps/client/src/app/components/admin-tag/admin-tag.component.ts +++ b/apps/client/src/app/components/admin-tag/admin-tag.component.ts @@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto'; import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto'; import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { Tag } from '@prisma/client'; import { get } from 'lodash'; @@ -40,6 +41,7 @@ export class AdminTagComponent implements OnInit, OnDestroy { public constructor( private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, private route: ActivatedRoute, @@ -114,10 +116,13 @@ export class AdminTagComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((tags) => { this.tags = tags; + this.dataSource = new MatTableDataSource(this.tags); this.dataSource.sort = this.sort; this.dataSource.sortingDataAccessor = get; + this.dataService.updateInfo(); + this.changeDetectorRef.markForCheck(); }); } diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts index f0178f2f5..3babc14bc 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts @@ -4,7 +4,10 @@ import { Inject, OnDestroy } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; +import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { DataService } from '@ghostfolio/client/services/data.service'; import { Subject } from 'rxjs'; @@ -18,15 +21,17 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces'; templateUrl: 'create-or-update-account-dialog.html' }) export class CreateOrUpdateAccountDialog implements OnDestroy { + public accountForm: FormGroup; public currencies: string[] = []; public platforms: { id: string; name: string }[]; private unsubscribeSubject = new Subject(); public constructor( + @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams, private dataService: DataService, public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams + private formBuilder: FormBuilder ) {} ngOnInit() { @@ -34,12 +39,42 @@ export class CreateOrUpdateAccountDialog implements OnDestroy { this.currencies = currencies; this.platforms = platforms; + + this.accountForm = this.formBuilder.group({ + accountId: [{ disabled: true, value: this.data.account.id }], + balance: [this.data.account.balance, Validators.required], + comment: [this.data.account.comment], + currency: [this.data.account.currency, Validators.required], + isExcluded: [this.data.account.isExcluded], + name: [this.data.account.name, Validators.required], + platformId: [this.data.account.platformId] + }); } public onCancel() { this.dialogRef.close(); } + public onSubmit() { + const account: CreateAccountDto | UpdateAccountDto = { + balance: this.accountForm.controls['balance'].value, + comment: this.accountForm.controls['comment'].value, + currency: this.accountForm.controls['currency'].value, + id: this.accountForm.controls['accountId'].value, + isExcluded: this.accountForm.controls['isExcluded'].value, + name: this.accountForm.controls['name'].value, + platformId: this.accountForm.controls['platformId'].value + }; + + if (this.data.account.id) { + (account as UpdateAccountDto).id = this.data.account.id; + } else { + delete (account as CreateAccountDto).id; + } + + this.dialogRef.close({ account }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html index 7b6f399a0..69972c7db 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -1,17 +1,22 @@ -
+

Update account

Add account

Name - +
Currency - + {{ currency }} @@ -21,20 +26,14 @@
Cash Balance - + {{ data.account.currency }}
Platform - + {{ platform.name }}
- Exclude from Analysis
Account ID - +
@@ -80,8 +70,8 @@ diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 82b4acca0..5cc955af2 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -204,6 +204,10 @@ export class DataService { return this.http.delete(`/api/v1/order/`); } + public deleteBenchmark({ dataSource, symbol }: UniqueAsset) { + return this.http.delete(`/api/v1/benchmark/${dataSource}/${symbol}`); + } + public deleteOrder(aId: string) { return this.http.delete(`/api/v1/order/${aId}`); } @@ -496,4 +500,19 @@ export class DataService { couponCode }); } + + public updateInfo() { + this.http.get('/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; + }); + } } diff --git a/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index b66676346..68e1cbca4 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -12,4 +12,5 @@ export interface AdminData { lastActivity: Date; transactionCount: number; }[]; + version: string; } diff --git a/package.json b/package.json index f303e5f51..2e2f81186 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.7.0", + "version": "2.8.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio",