From a08610b603a419becd23e288756ab73db3358108 Mon Sep 17 00:00:00 2001 From: Yash Solanki Date: Mon, 26 Dec 2022 20:56:51 +0530 Subject: [PATCH] Feature/improve activities import (#1531) --- .../import-activities-dialog.component.ts | 77 ++++++++++------ .../import-activities-dialog.html | 89 +++++++++++++------ .../import-activities-dialog.module.ts | 2 + .../app/services/import-activities.service.ts | 60 ++++++++++--- .../activities-table.component.html | 32 ++++--- .../activities-table.component.ts | 61 ++++++++++++- .../activities-table.module.ts | 2 + 7 files changed, 245 insertions(+), 78 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 5e4b5d5b8..0ec3d8d27 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -7,6 +7,7 @@ import { } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service'; import { isArray } from 'lodash'; import { Subject } from 'rxjs'; @@ -20,8 +21,11 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces'; templateUrl: 'import-activities-dialog.html' }) export class ImportActivitiesDialog implements OnDestroy { + public activities: Activity[] = []; public details: any[] = []; public errorMessages: string[] = []; + public isFileSelected = false; + public selectedActivities: Activity[] = []; private unsubscribeSubject = new Subject(); @@ -39,13 +43,47 @@ export class ImportActivitiesDialog implements OnDestroy { this.dialogRef.close(); } - public onImport() { + public async onImportActivities() { + try { + this.snackBar.open('⏳ ' + $localize`Importing data...`); + + await this.importActivitiesService.importSelectedActivities( + this.selectedActivities + ); + + this.snackBar.open( + '✅ ' + $localize`Import has been completed`, + undefined, + { + duration: 3000 + } + ); + } catch (error) { + this.snackBar.open( + $localize`Oops! Something went wrong.` + + ' ' + + $localize`Please try again later.`, + $localize`Okay`, + { duration: 3000 } + ); + } finally { + this.dialogRef.close(); + } + } + + public onReset() { + this.details = []; + this.errorMessages = []; + this.isFileSelected = false; + } + + public onSelectFile() { const input = document.createElement('input'); input.accept = 'application/JSON, .csv'; input.type = 'file'; input.onchange = (event) => { - this.snackBar.open('⏳ ' + $localize`Importing data...`); + this.snackBar.open('⏳ ' + $localize`Validating data...`); // Getting the file reference const file = (event.target as HTMLInputElement).files[0]; @@ -80,11 +118,10 @@ export class ImportActivitiesDialog implements OnDestroy { } try { - await this.importActivitiesService.importJson({ - content: content.activities + this.activities = await this.importActivitiesService.importJson({ + content: content.activities, + dryRun: true }); - - this.handleImportSuccess(); } catch (error) { console.error(error); this.handleImportError({ error, activities: content.activities }); @@ -93,12 +130,11 @@ export class ImportActivitiesDialog implements OnDestroy { return; } else if (file.name.endsWith('.csv')) { try { - await this.importActivitiesService.importCsv({ + this.activities = await this.importActivitiesService.importCsv({ + dryRun: true, fileContent, userAccounts: this.data.user.accounts }); - - this.handleImportSuccess(); } catch (error) { console.error(error); this.handleImportError({ @@ -119,6 +155,10 @@ export class ImportActivitiesDialog implements OnDestroy { activities: [], error: { error: { message: ['Unexpected format'] } } }); + } finally { + this.isFileSelected = true; + this.snackBar.dismiss(); + this.changeDetectorRef.markForCheck(); } }; }; @@ -126,9 +166,8 @@ export class ImportActivitiesDialog implements OnDestroy { input.click(); } - public onReset() { - this.details = []; - this.errorMessages = []; + public updateSelection(data: Activity[]) { + this.selectedActivities = data; } public ngOnDestroy() { @@ -143,8 +182,6 @@ export class ImportActivitiesDialog implements OnDestroy { activities: any[]; error: any; }) { - this.snackBar.dismiss(); - this.errorMessages = error?.error?.message; for (const message of this.errorMessages) { @@ -161,16 +198,4 @@ export class ImportActivitiesDialog implements OnDestroy { this.changeDetectorRef.markForCheck(); } - - private handleImportSuccess() { - this.snackBar.open( - '✅ ' + $localize`Import has been completed`, - undefined, - { - duration: 3000 - } - ); - - this.dialogRef.close(); - } } diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html index fc5985bba..7b5ad648b 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -6,13 +6,13 @@ >
- +
- - - - - -
-
- + + + + + + + + + +
+
+ +
+
{{ message }}
-
{{ message }}
-
- - -
{{ details[i] | json }}
- - -
- -
+ + +
{{ details[i] | json }}
+ + +
+ +
+
+
+ + +
+ { const content = csvToJson(fileContent, { dynamicTyping: true, header: true, @@ -52,14 +55,23 @@ export class ImportActivitiesService { }); } - await this.importJson({ content: activities }); + return await this.importJson({ content: activities, dryRun }); } - public importJson({ content }: { content: CreateOrderDto[] }): Promise { + public importJson({ + content, + dryRun = false + }: { + content: CreateOrderDto[]; + dryRun?: boolean; + }): Promise { return new Promise((resolve, reject) => { - this.postImport({ - activities: content - }) + this.postImport( + { + activities: content + }, + dryRun + ) .pipe( catchError((error) => { reject(error); @@ -67,13 +79,35 @@ export class ImportActivitiesService { }) ) .subscribe({ - next: () => { - resolve(); + next: (data) => { + resolve(data.activities); } }); }); } + public importSelectedActivities( + selectedActivities: Activity[] + ): Promise { + const importData: CreateOrderDto[] = []; + for (const activity of selectedActivities) { + importData.push(this.convertToCreateOrderDto(activity)); + } + return this.importJson({ content: importData }); + } + + private convertToCreateOrderDto(aActivity: Activity): CreateOrderDto { + return { + currency: aActivity.SymbolProfile.currency, + date: aActivity.date.toString(), + fee: aActivity.fee, + quantity: aActivity.quantity, + symbol: aActivity.SymbolProfile.symbol, + type: aActivity.type, + unitPrice: aActivity.unitPrice + }; + } + private lowercaseKeys(aObject: any) { return Object.keys(aObject).reduce((acc, key) => { acc[key.toLowerCase()] = aObject[key]; @@ -301,7 +335,13 @@ export class ImportActivitiesService { }; } - private postImport(aImportData: { activities: CreateOrderDto[] }) { - return this.http.post('/api/v1/import', aImportData); + private postImport( + aImportData: { activities: CreateOrderDto[] }, + dryRun = false + ) { + return this.http.post<{ activities: Activity[] }>( + `/api/v1/import?dryRun=${dryRun}`, + aImportData + ); } } 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 2f11eb2e2..6c70a0ccd 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -15,6 +15,26 @@ matSortDirection="desc" [dataSource]="dataSource" > + + + + + + + + + +
- {{ element.date | date: defaultDateFormat }} + {{ element.date | date : defaultDateFormat }}
Total @@ -432,15 +452,7 @@ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft && row.type !== 'ITEM' }" - (click)=" - hasPermissionToOpenDetails && - !row.isDraft && - row.type !== 'ITEM' && - onOpenPositionDialog({ - dataSource: row.SymbolProfile.dataSource, - symbol: row.SymbolProfile.symbol - }) - " + (click)="onClickActivity(row)" > (); @@ -49,6 +51,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { @Output() export = new EventEmitter(); @Output() exportDrafts = new EventEmitter(); @Output() import = new EventEmitter(); + @Output() selectedActivities = new EventEmitter(); @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -67,6 +70,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { public placeholder = ''; public routeQueryParams: Subscription; public searchKeywords: string[] = []; + public selectedRows = new SelectionModel(true, []); public totalFees: number; public totalValue: number; @@ -81,8 +85,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { }); } + public areAllRowsSelected() { + const numSelectedRows = this.selectedRows.selected.length; + const numTotalRows = this.dataSource.data.length; + return numSelectedRows === numTotalRows; + } + public ngOnChanges() { this.displayedColumns = [ + 'select', 'count', 'date', 'type', @@ -98,6 +109,16 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { 'actions' ]; + if (this.showCheckbox) { + this.displayedColumns = this.displayedColumns.filter((column) => { + return column !== 'count'; + }); + } else { + this.displayedColumns = this.displayedColumns.filter((column) => { + return column !== 'select'; + }); + } + if (!this.showNameColumn) { this.displayedColumns = this.displayedColumns.filter((column) => { return column !== 'nameWithSymbol'; @@ -133,6 +154,17 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { } } + ngOnInit() { + if (this.showCheckbox) { + this.toggleAllRows(); + this.selectedRows.changed + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((selectedRows) => { + this.selectedActivities.emit(selectedRows.source.selected); + }); + } + } + public onChangePage(page: PageEvent) { this.pageIndex = page.pageIndex; @@ -140,6 +172,21 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.totalValue = this.getTotalValue(); } + public onClickActivity(activity: Activity) { + if (this.showCheckbox) { + this.selectedRows.toggle(activity); + } else if ( + this.hasPermissionToOpenDetails && + !activity.isDraft && + activity.type !== 'ITEM' + ) { + this.onOpenPositionDialog({ + dataSource: activity.SymbolProfile.dataSource, + symbol: activity.SymbolProfile.symbol + }); + } + } + public onCloneActivity(aActivity: OrderWithAccount) { this.activityToClone.emit(aActivity); } @@ -200,6 +247,14 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.activityToUpdate.emit(aActivity); } + public toggleAllRows() { + this.areAllRowsSelected() + ? this.selectedRows.clear() + : this.dataSource.data.forEach((row) => this.selectedRows.select(row)); + + this.selectedActivities.emit(this.selectedRows.selected); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); @@ -237,9 +292,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { type: 'TAG' }; - fieldValueMap[format(activity.date, 'yyyy')] = { - id: format(activity.date, 'yyyy'), - label: format(activity.date, 'yyyy'), + fieldValueMap[format(new Date(activity.date), 'yyyy')] = { + id: format(new Date(activity.date), 'yyyy'), + label: format(new Date(activity.date), 'yyyy'), type: 'TAG' }; diff --git a/libs/ui/src/lib/activities-table/activities-table.module.ts b/libs/ui/src/lib/activities-table/activities-table.module.ts index 4f38bcd2a..4d5951644 100644 --- a/libs/ui/src/lib/activities-table/activities-table.module.ts +++ b/libs/ui/src/lib/activities-table/activities-table.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatMenuModule } from '@angular/material/menu'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatSortModule } from '@angular/material/sort'; @@ -26,6 +27,7 @@ import { ActivitiesTableComponent } from './activities-table.component'; GfSymbolModule, GfValueModule, MatButtonModule, + MatCheckboxModule, MatMenuModule, MatPaginatorModule, MatSortModule,