diff --git a/apps/api/src/app/export/export.controller.ts b/apps/api/src/app/export/export.controller.ts index ca318ce81..3617ebe24 100644 --- a/apps/api/src/app/export/export.controller.ts +++ b/apps/api/src/app/export/export.controller.ts @@ -1,6 +1,13 @@ import { Export } from '@ghostfolio/common/interfaces'; import type { RequestWithUser } from '@ghostfolio/common/types'; -import { Controller, Get, Inject, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Headers, + Inject, + Query, + UseGuards +} from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -15,8 +22,11 @@ export class ExportController { @Get() @UseGuards(AuthGuard('jwt')) - public async export(): Promise { - return await this.exportService.export({ + public async export( + @Query('activityIds') activityIds?: string[] + ): Promise { + return this.exportService.export({ + activityIds, userId: this.request.user.id }); } diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 30b1ed082..301f13cea 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -7,8 +7,14 @@ import { Injectable } from '@nestjs/common'; export class ExportService { public constructor(private readonly prismaService: PrismaService) {} - public async export({ userId }: { userId: string }): Promise { - const orders = await this.prismaService.order.findMany({ + public async export({ + activityIds, + userId + }: { + activityIds?: string[]; + userId: string; + }): Promise { + let orders = await this.prismaService.order.findMany({ orderBy: { date: 'desc' }, select: { accountId: true, @@ -16,6 +22,7 @@ export class ExportService { dataSource: true, date: true, fee: true, + id: true, quantity: true, SymbolProfile: true, type: true, @@ -24,6 +31,12 @@ export class ExportService { where: { userId } }); + if (activityIds) { + orders = orders.filter((order) => { + return activityIds.includes(order.id); + }); + } + return { meta: { date: new Date().toISOString(), version: environment.version }, orders: orders.map( diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index c7e964372..50c452ea8 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -3,6 +3,7 @@ import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { RANGE, SettingsStorageService @@ -26,6 +27,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { public dateRange: DateRange; public dateRangeOptions = defaultDateRangeOptions; public deviceType: string; + public hasImpersonationId: boolean; public hasPermissionToCreateOrder: boolean; public positions: Position[]; public user: User; @@ -40,6 +42,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, + private impersonationStorageService: ImpersonationStorageService, private route: ActivatedRoute, private router: Router, private settingsStorageService: SettingsStorageService, @@ -82,6 +85,13 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { public ngOnInit() { this.deviceType = this.deviceService.getDeviceInfo().deviceType; + this.impersonationStorageService + .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((aId) => { + this.hasImpersonationId = !!aId; + }); + this.dateRange = this.settingsStorageService.getSetting(RANGE) || 'max'; @@ -119,6 +129,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, + hasImpersonationId: this.hasImpersonationId, locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts index daac4065a..791c2b46a 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts @@ -4,6 +4,7 @@ export interface PositionDetailDialogParams { baseCurrency: string; dataSource: DataSource; deviceType: string; + hasImpersonationId: boolean; locale: string; symbol: string; } diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index 1802619a0..f24dc9f17 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -185,8 +185,42 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.dialogRef.close(); } + public onExport() { + this.dataService + .fetchExport( + this.orders.map((order) => { + return order.id; + }) + ) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data) => { + this.downloadAsFile( + data, + `ghostfolio-export-${format( + parseISO(data.meta.date), + 'yyyyMMddHHmm' + )}.json`, + 'text/plain' + ); + }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); } + + private downloadAsFile( + aContent: unknown, + aFileName: string, + aContentType: string + ) { + const a = document.createElement('a'); + const file = new Blob([JSON.stringify(aContent, undefined, ' ')], { + type: aContentType + }); + a.href = URL.createObjectURL(file); + a.download = aFileName; + a.click(); + } } diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 00d949263..db8f78bc3 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -131,12 +131,14 @@ [baseCurrency]="data.baseCurrency" [deviceType]="data.deviceType" [hasPermissionToCreateActivity]="false" + [hasPermissionToExportActivities]="!hasImpersonationId" [hasPermissionToFilter]="false" [hasPermissionToImportActivities]="false" [hasPermissionToOpenDetails]="false" [locale]="data.locale" [showActions]="false" [showSymbolColumn]="false" + (export)="onExport()" > diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index f89297403..82a7fdcc4 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -316,6 +316,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, + hasImpersonationId: this.hasImpersonationId, locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 8172ed080..7e1d1faf4 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -90,11 +90,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { public ngOnInit() { const { globalPermissions } = this.dataService.fetchInfo(); - this.hasPermissionToImportOrders = hasPermission( - globalPermissions, - permissions.enableImport - ); - this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.impersonationStorageService @@ -102,6 +97,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((aId) => { this.hasImpersonationId = !!aId; + + this.hasPermissionToImportOrders = + hasPermission(globalPermissions, permissions.enableImport) && + !this.hasImpersonationId; }); this.userService.stateChanged @@ -406,6 +405,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, + hasImpersonationId: this.hasImpersonationId, locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html index db365c2d9..4cd2f462c 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html @@ -7,6 +7,7 @@ [baseCurrency]="user?.settings?.baseCurrency" [deviceType]="deviceType" [hasPermissionToCreateActivity]="hasPermissionToCreateOrder" + [hasPermissionToExportActivities]="!hasImpersonationId" [hasPermissionToImportActivities]="hasPermissionToImportOrders" [locale]="user?.settings?.locale" [showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView" diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index bf03b3f5f..fac56a1f2 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -94,8 +94,16 @@ export class DataService { }); } - public fetchExport() { - return this.http.get('/api/export'); + public fetchExport(activityIds?: string[]) { + let params = new HttpParams(); + + if (activityIds) { + params = params.append('activityIds', activityIds.join(',')); + } + + return this.http.get('/api/export', { + params + }); } public fetchInfo(): InfoItem { diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index 5af6013bb..31475941f 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -268,6 +268,9 @@