diff --git a/CHANGELOG.md b/CHANGELOG.md index b16284ab2..f5902f217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support to clone an activity from the holding detail dialog (experimental) +- Added support to edit an activity from the holding detail dialog (experimental) + ### Changed - Improved the caching of the benchmarks in the markets overview by returning cached data and recalculating in the background when it expires diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index f9190d1eb..af8a1e296 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -36,7 +36,7 @@ import { parseISO } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { CreateOrderDto } from './create-order.dto'; -import { Activities } from './interfaces/activities.interface'; +import { Activities, Activity } from './interfaces/activities.interface'; import { OrderService } from './order.service'; import { UpdateOrderDto } from './update-order.dto'; @@ -140,6 +140,38 @@ export class OrderController { return { activities, count }; } + @Get(':id') + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + @UseInterceptors(RedactValuesInResponseInterceptor) + @UseInterceptors(TransformDataSourceInResponseInterceptor) + public async getOrderById( + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, + @Param('id') id: string + ): Promise { + const impersonationUserId = + await this.impersonationService.validateImpersonationId(impersonationId); + const userCurrency = this.request.user.Settings.settings.baseCurrency; + + const { activities } = await this.orderService.getOrders({ + userCurrency, + userId: impersonationUserId || this.request.user.id, + withExcludedAccounts: true + }); + + const activity = activities.find((activity) => { + return activity.id === id; + }); + + if (!activity) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + return activity; + } + @HasPermission(permissions.createOrder) @Post() @UseGuards(AuthGuard('jwt'), HasPermissionGuard) diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index 4f1464408..b1d9a7f09 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -255,6 +255,10 @@ export class AppComponent implements OnDestroy, OnInit { colorScheme: this.user?.settings?.colorScheme, deviceType: this.deviceType, hasImpersonationId: this.hasImpersonationId, + hasPermissionToCreateOrder: + !this.hasImpersonationId && + hasPermission(this.user?.permissions, permissions.createOrder) && + !this.user?.settings?.isRestrictedView, hasPermissionToReportDataGlitch: hasPermission( this.user?.permissions, permissions.reportDataGlitch @@ -262,7 +266,7 @@ export class AppComponent implements OnDestroy, OnInit { hasPermissionToUpdateOrder: !this.hasImpersonationId && hasPermission(this.user?.permissions, permissions.updateOrder) && - !user?.settings?.isRestrictedView, + !this.user?.settings?.isRestrictedView, locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', 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 5673cd0c0..64c062c7e 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 @@ -48,6 +48,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; +import { Router } from '@angular/router'; import { Account, Tag } from '@prisma/client'; import { format, isSameMonth, isToday, parseISO } from 'date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -141,6 +142,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: HoldingDetailDialogParams, private formBuilder: FormBuilder, + private router: Router, private userService: UserService ) {} @@ -424,6 +426,14 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { this.tagInput.nativeElement.value = ''; } + public onCloneActivity(aActivity: Activity) { + this.router.navigate(['/portfolio', 'activities'], { + queryParams: { activityId: aActivity.id, createDialog: true } + }); + + this.dialogRef.close(); + } + public onClose() { this.dialogRef.close(); } @@ -456,6 +466,14 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { ); } + public onUpdateActivity(aActivity: Activity) { + this.router.navigate(['/portfolio', 'activities'], { + queryParams: { activityId: aActivity.id, editDialog: true } + }); + + this.dialogRef.close(); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); 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 b7474a7a3..04770837a 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 @@ -346,12 +346,19 @@ [hasPermissionToFilter]="false" [hasPermissionToOpenDetails]="false" [locale]="data.locale" - [showActions]="false" + [showActions]=" + !data.hasImpersonationId && + data.hasPermissionToCreateOrder && + user?.settings?.isExperimentalFeatures && + !user?.settings?.isRestrictedView + " [showNameColumn]="false" [sortColumn]="sortColumn" [sortDirection]="sortDirection" [sortDisabled]="true" [totalItems]="totalItems" + (activityToClone)="onCloneActivity($event)" + (activityToUpdate)="onUpdateActivity($event)" (export)="onExport()" /> diff --git a/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts index 8178838ab..cb98ab3a7 100644 --- a/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts @@ -8,6 +8,7 @@ export interface HoldingDetailDialogParams { dataSource: DataSource; deviceType: string; hasImpersonationId: boolean; + hasPermissionToCreateOrder: boolean; hasPermissionToReportDataGlitch: boolean; hasPermissionToUpdateOrder: boolean; locale: string; diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index d6209cdf8..7cd89d62f 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -16,7 +16,6 @@ import { PageEvent } from '@angular/material/paginator'; import { Sort, SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { ActivatedRoute, Router } from '@angular/router'; -import { Order as OrderModel } from '@prisma/client'; import { format, parseISO } from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, Subscription } from 'rxjs'; @@ -63,14 +62,24 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { if (params['createDialog']) { - this.openCreateActivityDialog(); + if (params['activityId']) { + this.dataService + .fetchActivity(params['activityId']) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((activity) => { + this.openCreateActivityDialog(activity); + }); + } else { + this.openCreateActivityDialog(); + } } else if (params['editDialog']) { - if (this.dataSource && params['activityId']) { - const activity = this.dataSource.data.find(({ id }) => { - return id === params['activityId']; - }); - - this.openUpdateActivityDialog(activity); + if (params['activityId']) { + this.dataService + .fetchActivity(params['activityId']) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((activity) => { + this.openUpdateActivityDialog(activity); + }); } else { this.router.navigate(['.'], { relativeTo: this.route }); } @@ -242,7 +251,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit { this.fetchActivities(); } - public onUpdateActivity(aActivity: OrderModel) { + public onUpdateActivity(aActivity: Activity) { this.router.navigate([], { queryParams: { activityId: aActivity.id, editDialog: true } }); diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index e74c3f74e..6de3d327d 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -4,7 +4,10 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; -import { Activities } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + Activities, + Activity +} from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { PortfolioHoldingDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-holding-detail.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; @@ -212,6 +215,17 @@ export class DataService { ); } + public fetchActivity(aActivityId: string) { + return this.http.get(`/api/v1/order/${aActivityId}`).pipe( + map((activity) => { + activity.createdAt = parseISO((activity.createdAt)); + activity.date = parseISO((activity.date)); + + return activity; + }) + ); + } + public fetchDividends({ filters, groupBy = 'month',