Feature/add transactions to position detail dialog (#591)

* Add transactions to position detail dialog

* Update changelog
This commit is contained in:
Thomas Kaul 2021-12-28 17:45:04 +01:00 committed by GitHub
parent fa44cee781
commit ff638adf03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 193 additions and 129 deletions

View File

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Added the transactions to the position detail dialog
## 1.96.0 - 27.12.2021 ## 1.96.0 - 27.12.2021
### Changed ### Changed

View File

@ -66,28 +66,21 @@ export class OrderController {
this.request.user.id this.request.user.id
); );
let orders = await this.orderService.orders({ let orders = await this.orderService.getOrders({
include: { includeDrafts: true,
Account: { userId: impersonationUserId || this.request.user.id
include: {
Platform: true
}
},
SymbolProfile: {
select: {
name: true
}
}
},
orderBy: { date: 'desc' },
where: { userId: impersonationUserId || this.request.user.id }
}); });
if ( if (
impersonationUserId || impersonationUserId ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
orders = nullifyValuesInObjects(orders, ['fee', 'quantity', 'unitPrice']); orders = nullifyValuesInObjects(orders, [
'fee',
'quantity',
'unitPrice',
'value'
]);
} }
return orders; return orders;

View File

@ -4,6 +4,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, Order, Prisma } from '@prisma/client'; import { DataSource, Order, Prisma } from '@prisma/client';
import Big from 'big.js';
import { endOfToday, isAfter } from 'date-fns'; import { endOfToday, isAfter } from 'date-fns';
@Injectable() @Injectable()
@ -82,7 +83,7 @@ export class OrderService {
}); });
} }
public getOrders({ public async getOrders({
includeDrafts = false, includeDrafts = false,
userId userId
}: { }: {
@ -95,15 +96,29 @@ export class OrderService {
where.isDraft = false; where.isDraft = false;
} }
return this.orders({ return (
where, await this.orders({
include: { where,
// eslint-disable-next-line @typescript-eslint/naming-convention include: {
Account: true, // eslint-disable-next-line @typescript-eslint/naming-convention
// eslint-disable-next-line @typescript-eslint/naming-convention Account: {
SymbolProfile: true include: {
}, Platform: true
orderBy: { date: 'asc' } }
},
// eslint-disable-next-line @typescript-eslint/naming-convention
SymbolProfile: true
},
orderBy: { date: 'asc' }
})
).map((order) => {
return {
...order,
value: new Big(order.quantity)
.mul(order.unitPrice)
.plus(order.fee)
.toNumber()
};
}); });
} }

View File

@ -1,3 +1,4 @@
import { OrderWithAccount } from '@ghostfolio/common/types';
import { AssetClass, AssetSubClass } from '@prisma/client'; import { AssetClass, AssetSubClass } from '@prisma/client';
export interface PortfolioPositionDetail { export interface PortfolioPositionDetail {
@ -16,6 +17,7 @@ export interface PortfolioPositionDetail {
name: string; name: string;
netPerformance: number; netPerformance: number;
netPerformancePercent: number; netPerformancePercent: number;
orders: OrderWithAccount[];
quantity: number; quantity: number;
symbol: string; symbol: string;
transactionCount: number; transactionCount: number;

View File

@ -360,6 +360,7 @@ export class PortfolioController {
'grossPerformance', 'grossPerformance',
'investment', 'investment',
'netPerformance', 'netPerformance',
'orders',
'quantity', 'quantity',
'value' 'value'
]); ]);

View File

@ -388,6 +388,7 @@ export class PortfolioService {
name: undefined, name: undefined,
netPerformance: undefined, netPerformance: undefined,
netPerformancePercent: undefined, netPerformancePercent: undefined,
orders: [],
quantity: undefined, quantity: undefined,
symbol: aSymbol, symbol: aSymbol,
transactionCount: undefined, transactionCount: undefined,
@ -521,6 +522,7 @@ export class PortfolioService {
minPrice, minPrice,
name, name,
netPerformance, netPerformance,
orders,
transactionCount, transactionCount,
averagePrice: averagePrice.toNumber(), averagePrice: averagePrice.toNumber(),
grossPerformancePercent: position.grossPerformancePercentage.toNumber(), grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
@ -578,6 +580,7 @@ export class PortfolioService {
maxPrice, maxPrice,
minPrice, minPrice,
name, name,
orders,
averagePrice: 0, averagePrice: 0,
currency: currentData[aSymbol]?.currency, currency: currentData[aSymbol]?.currency,
firstBuyDate: undefined, firstBuyDate: undefined,

View File

@ -1,4 +1,7 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { import {
RANGE, RANGE,
@ -33,9 +36,20 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private route: ActivatedRoute,
private router: Router,
private settingsStorageService: SettingsStorageService, private settingsStorageService: SettingsStorageService,
private userService: UserService private userService: UserService
) { ) {
route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['positionDetailDialog'] && params['symbol']) {
this.openDialog(params['symbol']);
}
});
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
@ -69,6 +83,27 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private openDialog(aSymbol: string): void {
const dialogRef = this.dialog.open(PositionDetailDialog, {
autoFocus: false,
data: {
baseCurrency: this.user?.settings?.baseCurrency,
deviceType: this.deviceType,
locale: this.user?.settings?.locale,
symbol: aSymbol
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
});
}
private update() { private update() {
this.dataService this.dataService
.fetchPositions({ range: this.dateRange }) .fetchPositions({ range: this.dateRange })

View File

@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module'; import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
import { HomeHoldingsComponent } from './home-holdings.component'; import { HomeHoldingsComponent } from './home-holdings.component';
@ -12,6 +13,7 @@ import { HomeHoldingsComponent } from './home-holdings.component';
exports: [], exports: [],
imports: [ imports: [
CommonModule, CommonModule,
GfPositionDetailDialogModule,
GfPositionsModule, GfPositionsModule,
MatButtonModule, MatButtonModule,
MatCardModule, MatCardModule,

View File

@ -3,11 +3,13 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
Inject, Inject,
OnDestroy OnDestroy,
OnInit
} from '@angular/core'; } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { AssetSubClass } from '@prisma/client'; import { AssetSubClass } from '@prisma/client';
import { format, isSameMonth, isToday, parseISO } from 'date-fns'; import { format, isSameMonth, isToday, parseISO } from 'date-fns';
@ -23,7 +25,7 @@ import { PositionDetailDialogParams } from './interfaces/interfaces';
templateUrl: 'position-detail-dialog.html', templateUrl: 'position-detail-dialog.html',
styleUrls: ['./position-detail-dialog.component.scss'] styleUrls: ['./position-detail-dialog.component.scss']
}) })
export class PositionDetailDialog implements OnDestroy { export class PositionDetailDialog implements OnDestroy, OnInit {
public assetSubClass: AssetSubClass; public assetSubClass: AssetSubClass;
public averagePrice: number; public averagePrice: number;
public benchmarkDataItems: LineChartItem[]; public benchmarkDataItems: LineChartItem[];
@ -39,6 +41,7 @@ export class PositionDetailDialog implements OnDestroy {
public name: string; public name: string;
public netPerformance: number; public netPerformance: number;
public netPerformancePercent: number; public netPerformancePercent: number;
public orders: OrderWithAccount[];
public quantity: number; public quantity: number;
public quantityPrecision = 2; public quantityPrecision = 2;
public symbol: string; public symbol: string;
@ -52,9 +55,11 @@ export class PositionDetailDialog implements OnDestroy {
private dataService: DataService, private dataService: DataService,
public dialogRef: MatDialogRef<PositionDetailDialog>, public dialogRef: MatDialogRef<PositionDetailDialog>,
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams @Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams
) { ) {}
public ngOnInit(): void {
this.dataService this.dataService
.fetchPositionDetail(data.symbol) .fetchPositionDetail(this.data.symbol)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe( .subscribe(
({ ({
@ -72,6 +77,7 @@ export class PositionDetailDialog implements OnDestroy {
name, name,
netPerformance, netPerformance,
netPerformancePercent, netPerformancePercent,
orders,
quantity, quantity,
symbol, symbol,
transactionCount, transactionCount,
@ -104,6 +110,7 @@ export class PositionDetailDialog implements OnDestroy {
this.name = name; this.name = name;
this.netPerformance = netPerformance; this.netPerformance = netPerformance;
this.netPerformancePercent = netPerformancePercent; this.netPerformancePercent = netPerformancePercent;
this.orders = orders;
this.quantity = quantity; this.quantity = quantity;
this.symbol = symbol; this.symbol = symbol;
this.transactionCount = transactionCount; this.transactionCount = transactionCount;

View File

@ -124,6 +124,20 @@
</div> </div>
</div> </div>
</div> </div>
<gf-transactions-table
*ngIf="orders?.length > 0"
[baseCurrency]="data.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToCreateOrder]="false"
[hasPermissionToFilter]="false"
[hasPermissionToImportOrders]="false"
[hasPermissionToOpenDetails]="false"
[locale]="data.locale"
[showActions]="false"
[showSymbolColumn]="false"
[transactions]="orders"
></gf-transactions-table>
</div> </div>
<gf-dialog-footer <gf-dialog-footer

View File

@ -2,12 +2,13 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfTransactionsTableModule } from '@ghostfolio/client/components/transactions-table/transactions-table.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfDialogFooterModule } from '../../dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '../../dialog-header/dialog-header.module';
import { PositionDetailDialog } from './position-detail-dialog.component'; import { PositionDetailDialog } from './position-detail-dialog.component';
@NgModule({ @NgModule({
@ -18,6 +19,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,
GfLineChartModule, GfLineChartModule,
GfTransactionsTableModule,
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatDialogModule, MatDialogModule,

View File

@ -5,14 +5,9 @@ import {
OnDestroy, OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { Position } from '@ghostfolio/common/interfaces'; import { Position } from '@ghostfolio/common/interfaces';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { PositionDetailDialog } from './position-detail-dialog/position-detail-dialog.component';
@Component({ @Component({
selector: 'gf-position', selector: 'gf-position',
@ -32,23 +27,7 @@ export class PositionComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor() {}
private dialog: MatDialog,
private route: ActivatedRoute,
private router: Router
) {
route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (
params['positionDetailDialog'] &&
params['symbol'] &&
params['symbol'] === this.position?.symbol
) {
this.openDialog();
}
});
}
public ngOnInit() {} public ngOnInit() {}
@ -56,25 +35,4 @@ export class PositionComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private openDialog(): void {
const dialogRef = this.dialog.open(PositionDetailDialog, {
autoFocus: false,
data: {
baseCurrency: this.baseCurrency,
deviceType: this.deviceType,
locale: this.locale,
symbol: this.position?.symbol
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
});
}
} }

View File

@ -14,13 +14,12 @@ import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { AssetClass, Order as OrderModel } from '@prisma/client'; import { AssetClass, Order as OrderModel } from '@prisma/client';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { PositionDetailDialog } from '../position/position-detail-dialog/position-detail-dialog.component';
@Component({ @Component({
selector: 'gf-positions-table', selector: 'gf-positions-table',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@ -7,12 +7,12 @@ import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfPositionDetailDialogModule } from '../position/position-detail-dialog/position-detail-dialog.module';
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module'; import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
import { PositionsTableComponent } from './positions-table.component'; import { PositionsTableComponent } from './positions-table.component';

View File

@ -1,4 +1,8 @@
<mat-form-field appearance="outline" class="w-100"> <mat-form-field
appearance="outline"
class="w-100"
[ngClass]="{ 'd-none': !hasPermissionToFilter }"
>
<ion-icon class="mr-1" matPrefix name="search-outline"></ion-icon> <ion-icon class="mr-1" matPrefix name="search-outline"></ion-icon>
<mat-chip-list #chipList aria-label="Search keywords"> <mat-chip-list #chipList aria-label="Search keywords">
<mat-chip <mat-chip
@ -179,6 +183,27 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="value">
<th
*matHeaderCellDef
class="justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
Value
</th>
<td *matCellDef="let element" class="px1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.value"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="account"> <ng-container matColumnDef="account">
<th *matHeaderCellDef class="px-1" mat-header-cell> <th *matHeaderCellDef class="px-1" mat-header-cell>
<span class="d-none d-lg-block" i18n>Account</span> <span class="d-none d-lg-block" i18n>Account</span>
@ -254,12 +279,13 @@
*matRowDef="let row; columns: displayedColumns" *matRowDef="let row; columns: displayedColumns"
mat-row mat-row
(click)=" (click)="
!row.isDraft && hasPermissionToOpenDetails &&
!row.isDraft &&
onOpenPositionDialog({ onOpenPositionDialog({
symbol: row.symbol symbol: row.symbol
}) })
" "
[ngClass]="{ 'is-draft': row.isDraft }" [ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft }"
></tr> ></tr>
</table> </table>

View File

@ -24,10 +24,6 @@
} }
.mat-row { .mat-row {
&:not(.is-draft) {
cursor: pointer;
}
.type-badge { .type-badge {
background-color: rgba(var(--palette-foreground-text), 0.05); background-color: rgba(var(--palette-foreground-text), 0.05);
border-radius: 1rem; border-radius: 1rem;

View File

@ -17,18 +17,15 @@ import {
MatAutocompleteSelectedEvent MatAutocompleteSelectedEvent
} from '@angular/material/autocomplete'; } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
import { MatDialog } from '@angular/material/dialog';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { Router } from '@angular/router';
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { endOfToday, format, isAfter } from 'date-fns'; import { endOfToday, format, isAfter } from 'date-fns';
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { PositionDetailDialog } from '../position/position-detail-dialog/position-detail-dialog.component';
const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...'; const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...';
const SEARCH_STRING_SEPARATOR = ','; const SEARCH_STRING_SEPARATOR = ',';
@ -44,9 +41,12 @@ export class TransactionsTableComponent
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToCreateOrder: boolean; @Input() hasPermissionToCreateOrder: boolean;
@Input() hasPermissionToFilter = true;
@Input() hasPermissionToImportOrders: boolean; @Input() hasPermissionToImportOrders: boolean;
@Input() hasPermissionToOpenDetails = true;
@Input() locale: string; @Input() locale: string;
@Input() showActions: boolean; @Input() showActions: boolean;
@Input() showSymbolColumn = true;
@Input() transactions: OrderWithAccount[]; @Input() transactions: OrderWithAccount[];
@Output() export = new EventEmitter<void>(); @Output() export = new EventEmitter<void>();
@ -77,21 +77,7 @@ export class TransactionsTableComponent
private allFilters: string[]; private allFilters: string[];
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(private router: Router) {
private dialog: MatDialog,
private route: ActivatedRoute,
private router: Router
) {
this.routeQueryParams = route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['positionDetailDialog'] && params['symbol']) {
this.openPositionDialog({
symbol: params['symbol']
});
}
});
this.searchControl.valueChanges this.searchControl.valueChanges
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((keyword) => { .subscribe((keyword) => {
@ -150,6 +136,7 @@ export class TransactionsTableComponent
'quantity', 'quantity',
'unitPrice', 'unitPrice',
'fee', 'fee',
'value',
'account' 'account'
]; ];
@ -157,6 +144,12 @@ export class TransactionsTableComponent
this.displayedColumns.push('actions'); this.displayedColumns.push('actions');
} }
if (!this.showSymbolColumn) {
this.displayedColumns = this.displayedColumns.filter((column) => {
return column !== 'symbol';
});
}
this.isLoading = true; this.isLoading = true;
if (this.transactions) { if (this.transactions) {
@ -210,27 +203,6 @@ export class TransactionsTableComponent
this.transactionToClone.emit(aTransaction); this.transactionToClone.emit(aTransaction);
} }
public openPositionDialog({ symbol }: { symbol: string }): void {
const dialogRef = this.dialog.open(PositionDetailDialog, {
autoFocus: false,
data: {
symbol,
baseCurrency: this.baseCurrency,
deviceType: this.deviceType,
locale: this.locale
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
});
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();

View File

@ -14,7 +14,6 @@ import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfPositionDetailDialogModule } from '../position/position-detail-dialog/position-detail-dialog.module';
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module'; import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
import { TransactionsTableComponent } from './transactions-table.component'; import { TransactionsTableComponent } from './transactions-table.component';
@ -24,7 +23,6 @@ import { TransactionsTableComponent } from './transactions-table.component';
imports: [ imports: [
CommonModule, CommonModule,
GfNoTransactionsInfoModule, GfNoTransactionsInfoModule,
GfPositionDetailDialogModule,
GfSymbolIconModule, GfSymbolIconModule,
GfSymbolModule, GfSymbolModule,
GfValueModule, GfValueModule,

View File

@ -4,6 +4,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service'; import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
@ -73,6 +74,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
} else { } else {
this.router.navigate(['.'], { relativeTo: this.route }); this.router.navigate(['.'], { relativeTo: this.route });
} }
} else if (params['positionDetailDialog'] && params['symbol']) {
this.openPositionDialog({
symbol: params['symbol']
});
} }
}); });
} }
@ -250,6 +255,27 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
}); });
} }
public openPositionDialog({ symbol }: { symbol: string }): void {
const dialogRef = this.dialog.open(PositionDetailDialog, {
autoFocus: false,
data: {
symbol,
baseCurrency: this.user?.settings?.baseCurrency,
deviceType: this.deviceType,
locale: this.user?.settings?.locale
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
});
}
public openUpdateTransactionDialog({ public openUpdateTransactionDialog({
accountId, accountId,
currency, currency,

View File

@ -212,8 +212,17 @@ export class DataService {
} }
public fetchPositionDetail(aSymbol: string) { public fetchPositionDetail(aSymbol: string) {
return this.http.get<PortfolioPositionDetail>( return this.http.get<any>(`/api/portfolio/position/${aSymbol}`).pipe(
`/api/portfolio/position/${aSymbol}` map((data) => {
if (data.orders) {
for (const order of data.orders) {
order.createdAt = parseISO(order.createdAt);
order.date = parseISO(order.date);
}
}
return data;
})
); );
} }