Feature/export transactions (#209)
* Export functionality for transactions * Update changelog
This commit is contained in:
parent
1491bf7f76
commit
ecfe694f0b
@ -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
|
||||
|
@ -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,
|
||||
|
23
apps/api/src/app/export/export.controller.ts
Normal file
23
apps/api/src/app/export/export.controller.ts
Normal file
@ -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<Export> {
|
||||
return await this.exportService.export({
|
||||
userId: this.request.user.id
|
||||
});
|
||||
}
|
||||
}
|
32
apps/api/src/app/export/export.module.ts
Normal file
32
apps/api/src/app/export/export.module.ts
Normal file
@ -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 {}
|
31
apps/api/src/app/export/export.service.ts
Normal file
31
apps/api/src/app/export/export.service.ts
Normal file
@ -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<Export> {
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
production: true,
|
||||
version: `v${require('../../../../package.json').version}`
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
export const environment = {
|
||||
production: false
|
||||
production: false,
|
||||
version: 'dev'
|
||||
};
|
||||
|
@ -202,17 +202,29 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="accountMenu"
|
||||
[matMenuTriggerFor]="transactionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<mat-menu #transactionsMenu="matMenu" xPosition="before">
|
||||
<button i18n mat-menu-item (click)="onExport()">Export</button>
|
||||
</mat-menu>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="transactionMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||
<button i18n mat-menu-item (click)="onUpdateTransaction(element)">
|
||||
Edit
|
||||
</button>
|
||||
|
@ -47,6 +47,7 @@ export class TransactionsTableComponent
|
||||
@Input() showActions: boolean;
|
||||
@Input() transactions: OrderWithAccount[];
|
||||
|
||||
@Output() export = new EventEmitter<void>();
|
||||
@Output() transactionDeleted = new EventEmitter<string>();
|
||||
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
|
||||
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
|
||||
@ -185,6 +186,10 @@ export class TransactionsTableComponent
|
||||
}
|
||||
}
|
||||
|
||||
public onExport() {
|
||||
this.export.emit();
|
||||
}
|
||||
|
||||
public onOpenPositionDialog({
|
||||
symbol,
|
||||
title
|
||||
|
@ -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: {
|
||||
|
@ -8,6 +8,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder"
|
||||
[transactions]="transactions"
|
||||
(export)="onExport()"
|
||||
(transactionDeleted)="onDeleteTransaction($event)"
|
||||
(transactionToClone)="onCloneTransaction($event)"
|
||||
(transactionToUpdate)="onUpdateTransaction($event)"
|
||||
|
@ -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<Export>('/api/export');
|
||||
}
|
||||
|
||||
public fetchInfo() {
|
||||
return this.http.get<InfoItem>('/api/info').pipe(
|
||||
map((data) => {
|
||||
|
9
libs/common/src/lib/interfaces/export.interface.ts
Normal file
9
libs/common/src/lib/interfaces/export.interface.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Order } from '@prisma/client';
|
||||
|
||||
export interface Export {
|
||||
meta: {
|
||||
date: string;
|
||||
version: string;
|
||||
};
|
||||
orders: Partial<Order>[];
|
||||
}
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user