Feature/add summary row to activities table (#645)
* Add summary row to activities table * Update changelog
This commit is contained in:
parent
9d907b5eb5
commit
585f99e4df
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the footer row with total fees and total value to the activities table
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Upgraded _Stripe_ dependencies
|
- Upgraded _Stripe_ dependencies
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
@ -15,10 +15,11 @@ import { ImportService } from './import.service';
|
|||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
|
OrderModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisCacheModule
|
RedisCacheModule
|
||||||
],
|
],
|
||||||
controllers: [ImportController],
|
controllers: [ImportController],
|
||||||
providers: [CacheService, ImportService, OrderService]
|
providers: [CacheService, ImportService]
|
||||||
})
|
})
|
||||||
export class ImportModule {}
|
export class ImportModule {}
|
||||||
|
10
apps/api/src/app/order/interfaces/activities.interface.ts
Normal file
10
apps/api/src/app/order/interfaces/activities.interface.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
|
export interface Activities {
|
||||||
|
activities: Activity[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Activity extends OrderWithAccount {
|
||||||
|
feeInBaseCurrency: number;
|
||||||
|
valueInBaseCurrency: number;
|
||||||
|
}
|
@ -23,6 +23,7 @@ import { parseISO } from 'date-fns';
|
|||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { CreateOrderDto } from './create-order.dto';
|
import { CreateOrderDto } from './create-order.dto';
|
||||||
|
import { Activities } from './interfaces/activities.interface';
|
||||||
import { OrderService } from './order.service';
|
import { OrderService } from './order.service';
|
||||||
import { UpdateOrderDto } from './update-order.dto';
|
import { UpdateOrderDto } from './update-order.dto';
|
||||||
|
|
||||||
@ -59,14 +60,16 @@ export class OrderController {
|
|||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getAllOrders(
|
public async getAllOrders(
|
||||||
@Headers('impersonation-id') impersonationId
|
@Headers('impersonation-id') impersonationId
|
||||||
): Promise<OrderModel[]> {
|
): Promise<Activities> {
|
||||||
const impersonationUserId =
|
const impersonationUserId =
|
||||||
await this.impersonationService.validateImpersonationId(
|
await this.impersonationService.validateImpersonationId(
|
||||||
impersonationId,
|
impersonationId,
|
||||||
this.request.user.id
|
this.request.user.id
|
||||||
);
|
);
|
||||||
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
|
|
||||||
let orders = await this.orderService.getOrders({
|
let activities = await this.orderService.getOrders({
|
||||||
|
userCurrency,
|
||||||
includeDrafts: true,
|
includeDrafts: true,
|
||||||
userId: impersonationUserId || this.request.user.id
|
userId: impersonationUserId || this.request.user.id
|
||||||
});
|
});
|
||||||
@ -75,15 +78,17 @@ export class OrderController {
|
|||||||
impersonationUserId ||
|
impersonationUserId ||
|
||||||
this.userService.isRestrictedView(this.request.user)
|
this.userService.isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
orders = nullifyValuesInObjects(orders, [
|
activities = nullifyValuesInObjects(activities, [
|
||||||
'fee',
|
'fee',
|
||||||
|
'feeInBaseCurrency',
|
||||||
'quantity',
|
'quantity',
|
||||||
'unitPrice',
|
'unitPrice',
|
||||||
'value'
|
'value',
|
||||||
|
'valueInBaseCurrency'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return orders;
|
return { activities };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@ -4,6 +4,7 @@ import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
|||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module';
|
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
@ -16,6 +17,7 @@ import { OrderService } from './order.service';
|
|||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
|
ExchangeRateDataModule,
|
||||||
ImpersonationModule,
|
ImpersonationModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
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';
|
||||||
@ -7,10 +8,13 @@ import { DataSource, Order, Prisma, Type as TypeOfOrder } from '@prisma/client';
|
|||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { endOfToday, isAfter } from 'date-fns';
|
import { endOfToday, isAfter } from 'date-fns';
|
||||||
|
|
||||||
|
import { Activity } from './interfaces/activities.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrderService {
|
export class OrderService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly cacheService: CacheService,
|
private readonly cacheService: CacheService,
|
||||||
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly dataGatheringService: DataGatheringService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
@ -86,12 +90,14 @@ export class OrderService {
|
|||||||
public async getOrders({
|
public async getOrders({
|
||||||
includeDrafts = false,
|
includeDrafts = false,
|
||||||
types,
|
types,
|
||||||
|
userCurrency,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
includeDrafts?: boolean;
|
includeDrafts?: boolean;
|
||||||
types?: TypeOfOrder[];
|
types?: TypeOfOrder[];
|
||||||
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
}) {
|
}): Promise<Activity[]> {
|
||||||
const where: Prisma.OrderWhereInput = { userId };
|
const where: Prisma.OrderWhereInput = { userId };
|
||||||
|
|
||||||
if (includeDrafts === false) {
|
if (includeDrafts === false) {
|
||||||
@ -124,12 +130,21 @@ export class OrderService {
|
|||||||
orderBy: { date: 'asc' }
|
orderBy: { date: 'asc' }
|
||||||
})
|
})
|
||||||
).map((order) => {
|
).map((order) => {
|
||||||
|
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...order,
|
...order,
|
||||||
value: new Big(order.quantity)
|
value,
|
||||||
.mul(order.unitPrice)
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
.plus(order.fee)
|
order.fee,
|
||||||
.toNumber()
|
order.currency,
|
||||||
|
userCurrency
|
||||||
|
),
|
||||||
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
|
value,
|
||||||
|
order.currency,
|
||||||
|
userCurrency
|
||||||
|
)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -388,11 +388,12 @@ export class PortfolioService {
|
|||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aSymbol: string
|
aSymbol: string
|
||||||
): Promise<PortfolioPositionDetail> {
|
): Promise<PortfolioPositionDetail> {
|
||||||
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const orders = (await this.orderService.getOrders({ userId })).filter(
|
const orders = (
|
||||||
(order) => order.symbol === aSymbol
|
await this.orderService.getOrders({ userCurrency, userId })
|
||||||
);
|
).filter((order) => order.symbol === aSymbol);
|
||||||
|
|
||||||
if (orders.length <= 0) {
|
if (orders.length <= 0) {
|
||||||
return {
|
return {
|
||||||
@ -846,24 +847,25 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
||||||
const currency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const performanceInformation = await this.getPerformance(aImpersonationId);
|
const performanceInformation = await this.getPerformance(aImpersonationId);
|
||||||
|
|
||||||
const { balance } = await this.accountService.getCashDetails(
|
const { balance } = await this.accountService.getCashDetails(
|
||||||
userId,
|
userId,
|
||||||
currency
|
userCurrency
|
||||||
);
|
);
|
||||||
const orders = await this.orderService.getOrders({
|
const orders = await this.orderService.getOrders({
|
||||||
|
userCurrency,
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
const dividend = this.getDividend(orders).toNumber();
|
const dividend = this.getDividend(orders).toNumber();
|
||||||
const fees = this.getFees(orders).toNumber();
|
const fees = this.getFees(orders).toNumber();
|
||||||
const firstOrderDate = orders[0]?.date;
|
const firstOrderDate = orders[0]?.date;
|
||||||
|
|
||||||
const totalBuy = this.getTotalByType(orders, currency, 'BUY');
|
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
|
||||||
const totalSell = this.getTotalByType(orders, currency, 'SELL');
|
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
|
||||||
|
|
||||||
const committedFunds = new Big(totalBuy).sub(totalSell);
|
const committedFunds = new Big(totalBuy).sub(totalSell);
|
||||||
|
|
||||||
@ -895,8 +897,8 @@ export class PortfolioService {
|
|||||||
}: {
|
}: {
|
||||||
cashDetails: CashDetails;
|
cashDetails: CashDetails;
|
||||||
investment: Big;
|
investment: Big;
|
||||||
value: Big;
|
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
|
value: Big;
|
||||||
}) {
|
}) {
|
||||||
const cashPositions = {};
|
const cashPositions = {};
|
||||||
|
|
||||||
@ -1025,8 +1027,11 @@ export class PortfolioService {
|
|||||||
transactionPoints: TransactionPoint[];
|
transactionPoints: TransactionPoint[];
|
||||||
orders: OrderWithAccount[];
|
orders: OrderWithAccount[];
|
||||||
}> {
|
}> {
|
||||||
|
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
||||||
|
|
||||||
const orders = await this.orderService.getOrders({
|
const orders = await this.orderService.getOrders({
|
||||||
includeDrafts,
|
includeDrafts,
|
||||||
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
types: ['BUY', 'SELL']
|
types: ['BUY', 'SELL']
|
||||||
});
|
});
|
||||||
@ -1035,7 +1040,6 @@ export class PortfolioService {
|
|||||||
return { transactionPoints: [], orders: [] };
|
return { transactionPoints: [], orders: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
dataSource: order.SymbolProfile?.dataSource ?? order.dataSource,
|
dataSource: order.SymbolProfile?.dataSource ?? order.dataSource,
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
>(Default)</span
|
>(Default)</span
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td *matFooterCellDef class="px-1" mat-footer-cell>Total</td>
|
<td *matFooterCellDef class="px-1" mat-footer-cell i18n>Total</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="currency">
|
<ng-container matColumnDef="currency">
|
||||||
|
@ -3,6 +3,7 @@ import { MatDialog } from '@angular/material/dialog';
|
|||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
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 { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
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 { 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';
|
||||||
@ -28,6 +29,7 @@ import { ImportTransactionDialog } from './import-transaction-dialog/import-tran
|
|||||||
templateUrl: './transactions-page.html'
|
templateUrl: './transactions-page.html'
|
||||||
})
|
})
|
||||||
export class TransactionsPageComponent implements OnDestroy, OnInit {
|
export class TransactionsPageComponent implements OnDestroy, OnInit {
|
||||||
|
public activities: Activity[];
|
||||||
public defaultAccountId: string;
|
public defaultAccountId: string;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
@ -35,7 +37,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
public hasPermissionToDeleteOrder: boolean;
|
public hasPermissionToDeleteOrder: boolean;
|
||||||
public hasPermissionToImportOrders: boolean;
|
public hasPermissionToImportOrders: boolean;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
public transactions: OrderModel[];
|
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private primaryDataSource: DataSource;
|
private primaryDataSource: DataSource;
|
||||||
@ -65,8 +66,8 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
if (params['createDialog']) {
|
if (params['createDialog']) {
|
||||||
this.openCreateTransactionDialog();
|
this.openCreateTransactionDialog();
|
||||||
} else if (params['editDialog']) {
|
} else if (params['editDialog']) {
|
||||||
if (this.transactions) {
|
if (this.activities) {
|
||||||
const transaction = this.transactions.find(({ id }) => {
|
const transaction = this.activities.find(({ id }) => {
|
||||||
return id === params['transactionId'];
|
return id === params['transactionId'];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -119,10 +120,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.dataService
|
this.dataService
|
||||||
.fetchOrders()
|
.fetchOrders()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe(({ activities }) => {
|
||||||
this.transactions = response;
|
this.activities = activities;
|
||||||
|
|
||||||
if (this.hasPermissionToCreateOrder && this.transactions?.length <= 0) {
|
if (this.hasPermissionToCreateOrder && this.activities?.length <= 0) {
|
||||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Activities</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>Activities</h3>
|
||||||
<gf-activities-table
|
<gf-activities-table
|
||||||
[activities]="transactions"
|
[activities]="activities"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
||||||
|
@ -4,6 +4,7 @@ import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
|||||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
|
import { Activities } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
|
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
|
||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
@ -169,14 +170,14 @@ export class DataService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchOrders(): Observable<OrderModel[]> {
|
public fetchOrders(): Observable<Activities> {
|
||||||
return this.http.get<any[]>('/api/order').pipe(
|
return this.http.get<any>('/api/order').pipe(
|
||||||
map((data) => {
|
map(({ activities }) => {
|
||||||
for (const item of data) {
|
for (const activity of activities) {
|
||||||
item.createdAt = parseISO(item.createdAt);
|
activity.createdAt = parseISO(activity.createdAt);
|
||||||
item.date = parseISO(item.date);
|
activity.date = parseISO(activity.date);
|
||||||
}
|
}
|
||||||
return data;
|
return { activities };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,11 @@
|
|||||||
>
|
>
|
||||||
{{ dataSource.data.length - i }}
|
{{ dataSource.data.length - i }}
|
||||||
</td>
|
</td>
|
||||||
|
<td
|
||||||
|
*matFooterCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-footer-cell
|
||||||
|
></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="date">
|
<ng-container matColumnDef="date">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
||||||
@ -68,6 +73,7 @@
|
|||||||
{{ element.date | date: defaultDateFormat }}
|
{{ element.date | date: defaultDateFormat }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="type">
|
<ng-container matColumnDef="type">
|
||||||
@ -93,6 +99,7 @@
|
|||||||
<span class="d-none d-lg-block mx-1">{{ element.type }}</span>
|
<span class="d-none d-lg-block mx-1">{{ element.type }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="symbol">
|
<ng-container matColumnDef="symbol">
|
||||||
@ -107,6 +114,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="currency">
|
<ng-container matColumnDef="currency">
|
||||||
@ -122,6 +130,9 @@
|
|||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||||
{{ element.currency }}
|
{{ element.currency }}
|
||||||
</td>
|
</td>
|
||||||
|
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||||
|
{{ baseCurrency }}
|
||||||
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="quantity">
|
<ng-container matColumnDef="quantity">
|
||||||
@ -143,6 +154,11 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td
|
||||||
|
*matFooterCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-footer-cell
|
||||||
|
></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="unitPrice">
|
<ng-container matColumnDef="unitPrice">
|
||||||
@ -164,6 +180,11 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td
|
||||||
|
*matFooterCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-footer-cell
|
||||||
|
></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="fee">
|
<ng-container matColumnDef="fee">
|
||||||
@ -176,7 +197,7 @@
|
|||||||
>
|
>
|
||||||
Fee
|
Fee
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px1" mat-cell>
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -185,6 +206,15 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : totalFees"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="value">
|
<ng-container matColumnDef="value">
|
||||||
@ -197,7 +227,7 @@
|
|||||||
>
|
>
|
||||||
Value
|
Value
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -206,6 +236,15 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td *matFooterCellDef class="px-1" mat-footer-cell>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : totalValue"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="account">
|
<ng-container matColumnDef="account">
|
||||||
@ -223,6 +262,11 @@
|
|||||||
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
|
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td
|
||||||
|
*matFooterCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-footer-cell
|
||||||
|
></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
@ -276,6 +320,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
|
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||||
@ -291,6 +336,11 @@
|
|||||||
"
|
"
|
||||||
[ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft }"
|
[ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft }"
|
||||||
></tr>
|
></tr>
|
||||||
|
<tr
|
||||||
|
*matFooterRowDef="displayedColumns"
|
||||||
|
mat-footer-row
|
||||||
|
[ngClass]="{ 'd-none': isLoading || dataSource.data.length === 0 }"
|
||||||
|
></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
|
@ -15,6 +15,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mat-table {
|
.mat-table {
|
||||||
|
td {
|
||||||
|
&.mat-footer-cell {
|
||||||
|
border-top: 1px solid
|
||||||
|
rgba(
|
||||||
|
var(--palette-foreground-divider),
|
||||||
|
var(--palette-foreground-divider-alpha)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-sort-header-container {
|
.mat-sort-header-container {
|
||||||
@ -55,6 +65,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mat-table {
|
.mat-table {
|
||||||
|
td {
|
||||||
|
&.mat-footer-cell {
|
||||||
|
border-top-color: rgba(
|
||||||
|
var(--palette-foreground-divider-dark),
|
||||||
|
var(--palette-foreground-divider-dark-alpha)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.type-badge {
|
.type-badge {
|
||||||
background-color: rgba(
|
background-color: rgba(
|
||||||
var(--palette-foreground-text-dark),
|
var(--palette-foreground-text-dark),
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
|
||||||
Output,
|
Output,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
@ -20,9 +19,12 @@ import { MatChipInputEvent } from '@angular/material/chips';
|
|||||||
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 { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
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 Big from 'big.js';
|
||||||
import { endOfToday, format, isAfter } from 'date-fns';
|
import { endOfToday, format, isAfter } from 'date-fns';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -36,7 +38,7 @@ const SEARCH_STRING_SEPARATOR = ',';
|
|||||||
templateUrl: './activities-table.component.html'
|
templateUrl: './activities-table.component.html'
|
||||||
})
|
})
|
||||||
export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||||
@Input() activities: OrderWithAccount[];
|
@Input() activities: Activity[];
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
@Input() hasPermissionToCreateActivity: boolean;
|
@Input() hasPermissionToCreateActivity: boolean;
|
||||||
@ -57,8 +59,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
|
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
|
||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
public dataSource: MatTableDataSource<OrderWithAccount> =
|
public dataSource: MatTableDataSource<Activity> = new MatTableDataSource();
|
||||||
new MatTableDataSource();
|
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
public displayedColumns = [];
|
public displayedColumns = [];
|
||||||
public endOfToday = endOfToday();
|
public endOfToday = endOfToday();
|
||||||
@ -71,6 +72,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
public searchControl = new FormControl();
|
public searchControl = new FormControl();
|
||||||
public searchKeywords: string[] = [];
|
public searchKeywords: string[] = [];
|
||||||
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||||
|
public totalFees: number;
|
||||||
|
public totalValue: number;
|
||||||
|
|
||||||
private allFilters: string[];
|
private allFilters: string[];
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -218,6 +221,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.filters$.next(this.allFilters);
|
this.filters$.next(this.allFilters);
|
||||||
|
|
||||||
|
this.totalFees = this.getTotalFees();
|
||||||
|
this.totalValue = this.getTotalValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSearchableFieldValues(activities: OrderWithAccount[]): string[] {
|
private getSearchableFieldValues(activities: OrderWithAccount[]): string[] {
|
||||||
@ -263,4 +269,36 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
return item !== undefined;
|
return item !== undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getTotalFees() {
|
||||||
|
let totalFees = new Big(0);
|
||||||
|
|
||||||
|
for (const activity of this.dataSource.filteredData) {
|
||||||
|
if (isNumber(activity.feeInBaseCurrency)) {
|
||||||
|
totalFees = totalFees.plus(activity.feeInBaseCurrency);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalFees.toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTotalValue() {
|
||||||
|
let totalValue = new Big(0);
|
||||||
|
|
||||||
|
for (const activity of this.dataSource.filteredData) {
|
||||||
|
if (isNumber(activity.valueInBaseCurrency)) {
|
||||||
|
if (activity.type === 'BUY') {
|
||||||
|
totalValue = totalValue.plus(activity.valueInBaseCurrency);
|
||||||
|
} else if (activity.type === 'SELL') {
|
||||||
|
totalValue = totalValue.minus(activity.valueInBaseCurrency);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalValue.toNumber();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user