From ecfe694f0b102134baaca6af1d35e5094600e4f4 Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Sun, 11 Jul 2021 17:05:58 +0200 Subject: [PATCH] Feature/export transactions (#209) * Export functionality for transactions * Update changelog --- CHANGELOG.md | 4 +++ apps/api/src/app/app.module.ts | 2 ++ apps/api/src/app/export/export.controller.ts | 23 +++++++++++++ apps/api/src/app/export/export.module.ts | 32 +++++++++++++++++++ apps/api/src/app/export/export.service.ts | 31 ++++++++++++++++++ apps/api/src/environments/environment.prod.ts | 3 +- apps/api/src/environments/environment.ts | 3 +- .../transactions-table.component.html | 18 +++++++++-- .../transactions-table.component.ts | 5 +++ .../transactions-page.component.ts | 31 ++++++++++++++++++ .../pages/transactions/transactions-page.html | 1 + apps/client/src/app/services/data.service.ts | 5 +++ .../src/lib/interfaces/export.interface.ts | 9 ++++++ libs/common/src/lib/interfaces/index.ts | 2 ++ 14 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 apps/api/src/app/export/export.controller.ts create mode 100644 apps/api/src/app/export/export.module.ts create mode 100644 apps/api/src/app/export/export.service.ts create mode 100644 libs/common/src/lib/interfaces/export.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b3ede4a..98ff866fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the export functionality for transactions + ### Changed - Respected the cash balance on the analysis page diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index c0f4ff88a..0ddeca1ad 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -23,6 +23,7 @@ import { AppController } from './app.controller'; import { AuthModule } from './auth/auth.module'; import { CacheModule } from './cache/cache.module'; import { ExperimentalModule } from './experimental/experimental.module'; +import { ExportModule } from './export/export.module'; import { InfoModule } from './info/info.module'; import { OrderModule } from './order/order.module'; import { PortfolioModule } from './portfolio/portfolio.module'; @@ -41,6 +42,7 @@ import { UserModule } from './user/user.module'; CacheModule, ConfigModule.forRoot(), ExperimentalModule, + ExportModule, InfoModule, OrderModule, PortfolioModule, diff --git a/apps/api/src/app/export/export.controller.ts b/apps/api/src/app/export/export.controller.ts new file mode 100644 index 000000000..692502015 --- /dev/null +++ b/apps/api/src/app/export/export.controller.ts @@ -0,0 +1,23 @@ +import { Export } from '@ghostfolio/common/interfaces'; +import { RequestWithUser } from '@ghostfolio/common/types'; +import { Controller, Get, Inject, UseGuards } from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; + +import { ExportService } from './export.service'; + +@Controller('export') +export class ExportController { + public constructor( + private readonly exportService: ExportService, + @Inject(REQUEST) private readonly request: RequestWithUser + ) {} + + @Get() + @UseGuards(AuthGuard('jwt')) + public async export(): Promise { + return await this.exportService.export({ + userId: this.request.user.id + }); + } +} diff --git a/apps/api/src/app/export/export.module.ts b/apps/api/src/app/export/export.module.ts new file mode 100644 index 000000000..2da4e566f --- /dev/null +++ b/apps/api/src/app/export/export.module.ts @@ -0,0 +1,32 @@ +import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; +import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; +import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; +import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; +import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service'; +import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; +import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; +import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { Module } from '@nestjs/common'; + +import { ExportController } from './export.controller'; +import { ExportService } from './export.service'; + +@Module({ + imports: [RedisCacheModule], + controllers: [ExportController], + providers: [ + AlphaVantageService, + CacheService, + ConfigurationService, + DataGatheringService, + DataProviderService, + ExportService, + GhostfolioScraperApiService, + PrismaService, + RakutenRapidApiService, + YahooFinanceService + ] +}) +export class ExportModule {} diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts new file mode 100644 index 000000000..5f2193b01 --- /dev/null +++ b/apps/api/src/app/export/export.service.ts @@ -0,0 +1,31 @@ +import { environment } from '@ghostfolio/api/environments/environment'; +import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { Export } from '@ghostfolio/common/interfaces'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ExportService { + public constructor(private prisma: PrismaService) {} + + public async export({ userId }: { userId: string }): Promise { + const orders = await this.prisma.order.findMany({ + orderBy: { date: 'desc' }, + select: { + currency: true, + dataSource: true, + date: true, + fee: true, + quantity: true, + symbol: true, + type: true, + unitPrice: true + }, + where: { userId } + }); + + return { + meta: { date: new Date().toISOString(), version: environment.version }, + orders + }; + } +} diff --git a/apps/api/src/environments/environment.prod.ts b/apps/api/src/environments/environment.prod.ts index 3612073bc..bc8aa65a4 100644 --- a/apps/api/src/environments/environment.prod.ts +++ b/apps/api/src/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { - production: true + production: true, + version: `v${require('../../../../package.json').version}` }; diff --git a/apps/api/src/environments/environment.ts b/apps/api/src/environments/environment.ts index ffe8aed76..c0ae2e7e5 100644 --- a/apps/api/src/environments/environment.ts +++ b/apps/api/src/environments/environment.ts @@ -1,3 +1,4 @@ export const environment = { - production: false + production: false, + version: 'dev' }; diff --git a/apps/client/src/app/components/transactions-table/transactions-table.component.html b/apps/client/src/app/components/transactions-table/transactions-table.component.html index 5cd389a3e..32d960751 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.component.html +++ b/apps/client/src/app/components/transactions-table/transactions-table.component.html @@ -202,17 +202,29 @@ - + + + + + + - + diff --git a/apps/client/src/app/components/transactions-table/transactions-table.component.ts b/apps/client/src/app/components/transactions-table/transactions-table.component.ts index b65d30e6c..fb20ab647 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.component.ts +++ b/apps/client/src/app/components/transactions-table/transactions-table.component.ts @@ -47,6 +47,7 @@ export class TransactionsTableComponent @Input() showActions: boolean; @Input() transactions: OrderWithAccount[]; + @Output() export = new EventEmitter(); @Output() transactionDeleted = new EventEmitter(); @Output() transactionToClone = new EventEmitter(); @Output() transactionToUpdate = new EventEmitter(); @@ -185,6 +186,10 @@ export class TransactionsTableComponent } } + public onExport() { + this.export.emit(); + } + public onOpenPositionDialog({ symbol, title diff --git a/apps/client/src/app/pages/transactions/transactions-page.component.ts b/apps/client/src/app/pages/transactions/transactions-page.component.ts index 9ee74c3d4..78db0273f 100644 --- a/apps/client/src/app/pages/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/transactions/transactions-page.component.ts @@ -9,6 +9,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Order as OrderModel } from '@prisma/client'; +import { format, parseISO } from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -128,6 +129,22 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); } + public onExport() { + this.dataService + .fetchExport() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data) => { + this.downloadAsFile( + data, + `ghostfolio-export-${format( + parseISO(data.meta.date), + 'yyyyMMddHHmm' + )}.json`, + 'text/plain' + ); + }); + } + public onUpdateTransaction(aTransaction: OrderModel) { this.router.navigate([], { queryParams: { editDialog: true, transactionId: aTransaction.id } @@ -192,6 +209,20 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { 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(); + } + private openCreateTransactionDialog(aTransaction?: OrderModel): void { const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, { data: { diff --git a/apps/client/src/app/pages/transactions/transactions-page.html b/apps/client/src/app/pages/transactions/transactions-page.html index fc7b9d3a8..e6488287e 100644 --- a/apps/client/src/app/pages/transactions/transactions-page.html +++ b/apps/client/src/app/pages/transactions/transactions-page.html @@ -8,6 +8,7 @@ [locale]="user?.settings?.locale" [showActions]="!hasImpersonationId && hasPermissionToDeleteOrder" [transactions]="transactions" + (export)="onExport()" (transactionDeleted)="onDeleteTransaction($event)" (transactionToClone)="onCloneTransaction($event)" (transactionToUpdate)="onUpdateTransaction($event)" diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 4057a7e12..c31200757 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -15,6 +15,7 @@ import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-sett import { Access, AdminData, + Export, InfoItem, PortfolioItem, PortfolioOverview, @@ -86,6 +87,10 @@ export class DataService { }); } + public fetchExport() { + return this.http.get('/api/export'); + } + public fetchInfo() { return this.http.get('/api/info').pipe( map((data) => { diff --git a/libs/common/src/lib/interfaces/export.interface.ts b/libs/common/src/lib/interfaces/export.interface.ts new file mode 100644 index 000000000..ff83b619a --- /dev/null +++ b/libs/common/src/lib/interfaces/export.interface.ts @@ -0,0 +1,9 @@ +import { Order } from '@prisma/client'; + +export interface Export { + meta: { + date: string; + version: string; + }; + orders: Partial[]; +} diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index eb84932e7..35543c31e 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -1,5 +1,6 @@ import { Access } from './access.interface'; import { AdminData } from './admin-data.interface'; +import { Export } from './export.interface'; import { InfoItem } from './info-item.interface'; import { PortfolioItem } from './portfolio-item.interface'; import { PortfolioOverview } from './portfolio-overview.interface'; @@ -15,6 +16,7 @@ import { User } from './user.interface'; export { Access, AdminData, + Export, InfoItem, PortfolioItem, PortfolioOverview,