From dc1948016f42a2ec0695dde5e4b1a362db6c50a0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 7 Aug 2024 20:45:03 +0200 Subject: [PATCH] Feature/clone or edit activity from holding detail dialog (#3644) * Clone or edit activity from holding detail dialog * Update changelog --- CHANGELOG.md | 5 +++ apps/api/src/app/order/order.controller.ts | 34 ++++++++++++++++++- apps/client/src/app/app.component.ts | 6 +++- .../holding-detail-dialog.component.ts | 18 ++++++++++ .../holding-detail-dialog.html | 9 ++++- .../interfaces/interfaces.ts | 1 + .../activities/activities-page.component.ts | 27 ++++++++++----- apps/client/src/app/services/data.service.ts | 16 ++++++++- 8 files changed, 103 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b16284ab..f5902f21 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 f9190d1e..af8a1e29 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 4f146440..b1d9a7f0 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 5673cd0c..64c062c7 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 b7474a7a..04770837 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 8178838a..cb98ab3a 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 d6209cdf..7cd89d62 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 e74c3f74..6de3d327 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',