From 263cbc22f6623c566919cb27d149f4e39cdeb361 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:16:19 +0200 Subject: [PATCH] Clone or edit activity from holding detail dialog --- apps/api/src/app/order/order.controller.ts | 34 ++++++++++++++++++- apps/client/src/app/app.component.ts | 4 +++ .../holding-detail-dialog.component.ts | 18 ++++++++++ .../holding-detail-dialog.html | 8 ++++- .../interfaces/interfaces.ts | 1 + .../activities/activities-page.component.ts | 27 ++++++++++----- apps/client/src/app/services/data.service.ts | 16 ++++++++- 7 files changed, 96 insertions(+), 12 deletions(-) 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..5b4f2d33d 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) && + !user?.settings?.isRestrictedView, hasPermissionToReportDataGlitch: hasPermission( this.user?.permissions, permissions.reportDataGlitch 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..9d0b19bb1 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,18 @@ [hasPermissionToFilter]="false" [hasPermissionToOpenDetails]="false" [locale]="data.locale" - [showActions]="false" + [showActions]=" + !data.hasImpersonationId && + data.hasPermissionToCreateOrder && + !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',