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
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the export functionality for transactions
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Respected the cash balance on the analysis page
|
- Respected the cash balance on the analysis page
|
||||||
|
@ -23,6 +23,7 @@ import { AppController } from './app.controller';
|
|||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { CacheModule } from './cache/cache.module';
|
import { CacheModule } from './cache/cache.module';
|
||||||
import { ExperimentalModule } from './experimental/experimental.module';
|
import { ExperimentalModule } from './experimental/experimental.module';
|
||||||
|
import { ExportModule } from './export/export.module';
|
||||||
import { InfoModule } from './info/info.module';
|
import { InfoModule } from './info/info.module';
|
||||||
import { OrderModule } from './order/order.module';
|
import { OrderModule } from './order/order.module';
|
||||||
import { PortfolioModule } from './portfolio/portfolio.module';
|
import { PortfolioModule } from './portfolio/portfolio.module';
|
||||||
@ -41,6 +42,7 @@ import { UserModule } from './user/user.module';
|
|||||||
CacheModule,
|
CacheModule,
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
ExperimentalModule,
|
ExperimentalModule,
|
||||||
|
ExportModule,
|
||||||
InfoModule,
|
InfoModule,
|
||||||
OrderModule,
|
OrderModule,
|
||||||
PortfolioModule,
|
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 = {
|
export const environment = {
|
||||||
production: true
|
production: true,
|
||||||
|
version: `v${require('../../../../package.json').version}`
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false
|
production: false,
|
||||||
|
version: 'dev'
|
||||||
};
|
};
|
||||||
|
@ -202,17 +202,29 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
|
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
||||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
|
||||||
<button
|
<button
|
||||||
class="mx-1 no-min-width px-2"
|
class="mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
[matMenuTriggerFor]="accountMenu"
|
[matMenuTriggerFor]="transactionsMenu"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</button>
|
</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)">
|
<button i18n mat-menu-item (click)="onUpdateTransaction(element)">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
@ -47,6 +47,7 @@ export class TransactionsTableComponent
|
|||||||
@Input() showActions: boolean;
|
@Input() showActions: boolean;
|
||||||
@Input() transactions: OrderWithAccount[];
|
@Input() transactions: OrderWithAccount[];
|
||||||
|
|
||||||
|
@Output() export = new EventEmitter<void>();
|
||||||
@Output() transactionDeleted = new EventEmitter<string>();
|
@Output() transactionDeleted = new EventEmitter<string>();
|
||||||
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
|
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
|
||||||
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
|
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
|
||||||
@ -185,6 +186,10 @@ export class TransactionsTableComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onExport() {
|
||||||
|
this.export.emit();
|
||||||
|
}
|
||||||
|
|
||||||
public onOpenPositionDialog({
|
public onOpenPositionDialog({
|
||||||
symbol,
|
symbol,
|
||||||
title
|
title
|
||||||
|
@ -9,6 +9,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
|
|||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import { Order as OrderModel } from '@prisma/client';
|
||||||
|
import { format, parseISO } from 'date-fns';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
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) {
|
public onUpdateTransaction(aTransaction: OrderModel) {
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { editDialog: true, transactionId: aTransaction.id }
|
queryParams: { editDialog: true, transactionId: aTransaction.id }
|
||||||
@ -192,6 +209,20 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
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 {
|
private openCreateTransactionDialog(aTransaction?: OrderModel): void {
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||||
data: {
|
data: {
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder"
|
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder"
|
||||||
[transactions]="transactions"
|
[transactions]="transactions"
|
||||||
|
(export)="onExport()"
|
||||||
(transactionDeleted)="onDeleteTransaction($event)"
|
(transactionDeleted)="onDeleteTransaction($event)"
|
||||||
(transactionToClone)="onCloneTransaction($event)"
|
(transactionToClone)="onCloneTransaction($event)"
|
||||||
(transactionToUpdate)="onUpdateTransaction($event)"
|
(transactionToUpdate)="onUpdateTransaction($event)"
|
||||||
|
@ -15,6 +15,7 @@ import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-sett
|
|||||||
import {
|
import {
|
||||||
Access,
|
Access,
|
||||||
AdminData,
|
AdminData,
|
||||||
|
Export,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
PortfolioOverview,
|
PortfolioOverview,
|
||||||
@ -86,6 +87,10 @@ export class DataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchExport() {
|
||||||
|
return this.http.get<Export>('/api/export');
|
||||||
|
}
|
||||||
|
|
||||||
public fetchInfo() {
|
public fetchInfo() {
|
||||||
return this.http.get<InfoItem>('/api/info').pipe(
|
return this.http.get<InfoItem>('/api/info').pipe(
|
||||||
map((data) => {
|
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 { Access } from './access.interface';
|
||||||
import { AdminData } from './admin-data.interface';
|
import { AdminData } from './admin-data.interface';
|
||||||
|
import { Export } from './export.interface';
|
||||||
import { InfoItem } from './info-item.interface';
|
import { InfoItem } from './info-item.interface';
|
||||||
import { PortfolioItem } from './portfolio-item.interface';
|
import { PortfolioItem } from './portfolio-item.interface';
|
||||||
import { PortfolioOverview } from './portfolio-overview.interface';
|
import { PortfolioOverview } from './portfolio-overview.interface';
|
||||||
@ -15,6 +16,7 @@ import { User } from './user.interface';
|
|||||||
export {
|
export {
|
||||||
Access,
|
Access,
|
||||||
AdminData,
|
AdminData,
|
||||||
|
Export,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
PortfolioOverview,
|
PortfolioOverview,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user