diff --git a/apps/api/src/app/import/import-data.dto.ts b/apps/api/src/app/import/import-data.dto.ts new file mode 100644 index 000000000..c53cde073 --- /dev/null +++ b/apps/api/src/app/import/import-data.dto.ts @@ -0,0 +1,7 @@ +import { Order } from '@prisma/client'; +import { IsArray } from 'class-validator'; + +export class ImportDataDto { + @IsArray() + orders: Partial[]; +} diff --git a/apps/api/src/app/import/import.controller.ts b/apps/api/src/app/import/import.controller.ts index 5f89e6644..12b77374b 100644 --- a/apps/api/src/app/import/import.controller.ts +++ b/apps/api/src/app/import/import.controller.ts @@ -1,6 +1,7 @@ import { environment } from '@ghostfolio/api/environments/environment'; import { RequestWithUser } from '@ghostfolio/common/types'; import { + Body, Controller, HttpException, Inject, @@ -9,9 +10,9 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { Order } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import { ImportDataDto } from './import-data.dto'; import { ImportService } from './import.service'; @Controller('import') @@ -23,7 +24,7 @@ export class ImportController { @Post() @UseGuards(AuthGuard('jwt')) - public async import(): Promise { + public async import(@Body() importData: ImportDataDto): Promise { if (environment.production) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), @@ -31,15 +32,9 @@ export class ImportController { ); } - let orders: Partial[]; - try { - // TODO: Wire with file upload - const data = { orders: [] }; - orders = data.orders; - return await this.importService.import({ - orders, + orders: importData.orders, userId: this.request.user.id }); } catch (error) { 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 b4869e690..2cce1877f 100644 --- a/apps/client/src/app/pages/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/transactions/transactions-page.component.ts @@ -13,8 +13,8 @@ import { Order as OrderModel } from '@prisma/client'; import { environment } from 'apps/client/src/environments/environment'; import { format, parseISO } from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; -import { Subject, Subscription } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { EMPTY, Subject, Subscription } from 'rxjs'; +import { catchError, takeUntil } from 'rxjs/operators'; import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog/create-or-update-transaction-dialog.component'; @@ -150,20 +150,51 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { } public onImport() { - this.snackBar.open('⏳ Importing data...'); + const input = document.createElement('input'); + input.type = 'file'; - this.dataService - .postImport() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe({ - next: () => { - this.fetchOrders(); + input.onchange = (event) => { + // Getting the file reference + const file = (event.target as HTMLInputElement).files[0]; - this.snackBar.open('✅ Import has been completed', undefined, { - duration: 3000 - }); + // Setting up the reader + const reader = new FileReader(); + reader.readAsText(file, 'UTF-8'); + + reader.onload = (readerEvent) => { + try { + const content = JSON.parse(readerEvent.target.result as string); + + this.snackBar.open('⏳ Importing data...'); + + this.dataService + .postImport({ + orders: content.orders + }) + .pipe( + catchError((error) => { + this.handleImportError(error); + + return EMPTY; + }), + takeUntil(this.unsubscribeSubject) + ) + .subscribe({ + next: () => { + this.fetchOrders(); + + this.snackBar.open('✅ Import has been completed', undefined, { + duration: 3000 + }); + } + }); + } catch (error) { + this.handleImportError(error); } - }); + }; + }; + + input.click(); } public onUpdateTransaction(aTransaction: OrderModel) { @@ -244,6 +275,11 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { a.click(); } + private handleImportError(aError: unknown) { + console.error(aError); + this.snackBar.open('❌ Oops, something went wrong...'); + } + private openCreateTransactionDialog(aTransaction?: OrderModel): void { const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, { data: { diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 5b8aabeb1..6c5f70915 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; +import { ImportDataDto } from '@ghostfolio/api/app/import/import-data.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { @@ -173,8 +174,8 @@ export class DataService { return this.http.post(`/api/account`, aAccount); } - public postImport() { - return this.http.post('/api/import', {}); + public postImport(aImportData: ImportDataDto) { + return this.http.post('/api/import', aImportData); } public postOrder(aOrder: CreateOrderDto) {