Browse Source

First part of the import transactions functionality

pull/212/head
Thomas 4 years ago
parent
commit
751b077bbb
  1. 2
      apps/api/src/app/app.module.ts
  2. 54
      apps/api/src/app/import/import.controller.ts
  3. 34
      apps/api/src/app/import/import.module.ts
  4. 43
      apps/api/src/app/import/import.service.ts
  5. 18
      apps/client/src/app/components/transactions-table/transactions-table.component.html
  6. 6
      apps/client/src/app/components/transactions-table/transactions-table.component.ts
  7. 21
      apps/client/src/app/pages/transactions/transactions-page.component.ts
  8. 2
      apps/client/src/app/pages/transactions/transactions-page.html
  9. 2
      apps/client/src/app/pages/transactions/transactions-page.module.ts
  10. 4
      apps/client/src/app/services/data.service.ts

2
apps/api/src/app/app.module.ts

@ -24,6 +24,7 @@ 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 { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module';
import { OrderModule } from './order/order.module';
import { PortfolioModule } from './portfolio/portfolio.module';
@ -43,6 +44,7 @@ import { UserModule } from './user/user.module';
ConfigModule.forRoot(),
ExperimentalModule,
ExportModule,
ImportModule,
InfoModule,
OrderModule,
PortfolioModule,

54
apps/api/src/app/import/import.controller.ts

@ -0,0 +1,54 @@
import { environment } from '@ghostfolio/api/environments/environment';
import { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
HttpException,
Inject,
Post,
UseGuards
} 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 { ImportService } from './import.service';
@Controller('import')
export class ImportController {
public constructor(
private readonly importService: ImportService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Post()
@UseGuards(AuthGuard('jwt'))
public async import(): Promise<void> {
if (environment.production) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
let orders: Partial<Order>[];
try {
// TODO: Wire with file upload
const data = { orders: [] };
orders = data.orders;
return await this.importService.import({
orders,
userId: this.request.user.id
});
} catch (error) {
console.error(error);
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
}
}

34
apps/api/src/app/import/import.module.ts

@ -0,0 +1,34 @@
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { OrderService } from '@ghostfolio/api/app/order/order.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 { ImportController } from './import.controller';
import { ImportService } from './import.service';
@Module({
imports: [RedisCacheModule],
controllers: [ImportController],
providers: [
AlphaVantageService,
CacheService,
ConfigurationService,
DataGatheringService,
DataProviderService,
GhostfolioScraperApiService,
ImportService,
OrderService,
PrismaService,
RakutenRapidApiService,
YahooFinanceService
]
})
export class ImportModule {}

43
apps/api/src/app/import/import.service.ts

@ -0,0 +1,43 @@
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { Injectable } from '@nestjs/common';
import { Order } from '@prisma/client';
import { parseISO } from 'date-fns';
@Injectable()
export class ImportService {
public constructor(private readonly orderService: OrderService) {}
public async import({
orders,
userId
}: {
orders: Partial<Order>[];
userId: string;
}): Promise<void> {
for (const {
currency,
dataSource,
date,
fee,
quantity,
symbol,
type,
unitPrice
} of orders) {
await this.orderService.createOrder(
{
currency,
dataSource,
fee,
quantity,
symbol,
type,
unitPrice,
date: parseISO(<string>(<unknown>date)),
User: { connect: { id: userId } }
},
userId
);
}
}
}

18
apps/client/src/app/components/transactions-table/transactions-table.component.html

@ -212,7 +212,23 @@
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #transactionsMenu="matMenu" xPosition="before">
<button i18n mat-menu-item (click)="onExport()">Export</button>
<button
*ngIf="hasPermissionToImportOrders"
class="align-items-center d-flex"
mat-menu-item
(click)="onImport()"
>
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
<span i18n>Import</span>
</button>
<button
class="align-items-center d-flex"
mat-menu-item
(click)="onExport()"
>
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
<span i18n>Export</span>
</button>
</mat-menu>
</th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>

6
apps/client/src/app/components/transactions-table/transactions-table.component.ts

@ -43,11 +43,13 @@ export class TransactionsTableComponent
{
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToImportOrders: boolean;
@Input() locale: string;
@Input() showActions: boolean;
@Input() transactions: OrderWithAccount[];
@Output() export = new EventEmitter<void>();
@Output() import = new EventEmitter<void>();
@Output() transactionDeleted = new EventEmitter<string>();
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
@ -190,6 +192,10 @@ export class TransactionsTableComponent
this.export.emit();
}
public onImport() {
this.import.emit();
}
public onOpenPositionDialog({
symbol,
title

21
apps/client/src/app/pages/transactions/transactions-page.component.ts

@ -1,5 +1,6 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
@ -9,6 +10,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 { environment } from 'apps/client/src/environments/environment';
import { format, parseISO } from 'date-fns';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, Subscription } from 'rxjs';
@ -26,6 +28,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean;
public hasPermissionToDeleteOrder: boolean;
public hasPermissionToImportOrders = !environment.production;
public routeQueryParams: Subscription;
public transactions: OrderModel[];
public user: User;
@ -43,6 +46,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
private impersonationStorageService: ImpersonationStorageService,
private route: ActivatedRoute,
private router: Router,
private snackBar: MatSnackBar,
private userService: UserService
) {
this.routeQueryParams = route.queryParams
@ -145,6 +149,23 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
});
}
public onImport() {
this.snackBar.open('⏳ Importing data...');
this.dataService
.postImport()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => {
this.fetchOrders();
this.snackBar.open('✅ Import has been completed', undefined, {
duration: 3000
});
}
});
}
public onUpdateTransaction(aTransaction: OrderModel) {
this.router.navigate([], {
queryParams: { editDialog: true, transactionId: aTransaction.id }

2
apps/client/src/app/pages/transactions/transactions-page.html

@ -5,10 +5,12 @@
<gf-transactions-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToImportOrders]="hasPermissionToImportOrders"
[locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder"
[transactions]="transactions"
(export)="onExport()"
(import)="onImport()"
(transactionDeleted)="onDeleteTransaction($event)"
(transactionToClone)="onCloneTransaction($event)"
(transactionToUpdate)="onUpdateTransaction($event)"

2
apps/client/src/app/pages/transactions/transactions-page.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 { MatSnackBarModule } from '@angular/material/snack-bar';
import { RouterModule } from '@angular/router';
import { GfTransactionsTableModule } from '@ghostfolio/client/components/transactions-table/transactions-table.module';
@ -16,6 +17,7 @@ import { TransactionsPageComponent } from './transactions-page.component';
CreateOrUpdateTransactionDialogModule,
GfTransactionsTableModule,
MatButtonModule,
MatSnackBarModule,
RouterModule,
TransactionsPageRoutingModule
],

4
apps/client/src/app/services/data.service.ts

@ -173,6 +173,10 @@ export class DataService {
return this.http.post<OrderModel>(`/api/account`, aAccount);
}
public postImport() {
return this.http.post<void>('/api/import', {});
}
public postOrder(aOrder: CreateOrderDto) {
return this.http.post<OrderModel>(`/api/order`, aOrder);
}

Loading…
Cancel
Save