Browse Source

Feature/add summary row to activities table (#645)

* Add summary row to activities table

* Update changelog
pull/646/head
Thomas Kaul 3 years ago
committed by GitHub
parent
commit
585f99e4df
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 5
      apps/api/src/app/import/import.module.ts
  3. 10
      apps/api/src/app/order/interfaces/activities.interface.ts
  4. 15
      apps/api/src/app/order/order.controller.ts
  5. 2
      apps/api/src/app/order/order.module.ts
  6. 25
      apps/api/src/app/order/order.service.ts
  7. 22
      apps/api/src/app/portfolio/portfolio.service.ts
  8. 2
      apps/client/src/app/components/accounts-table/accounts-table.component.html
  9. 13
      apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts
  10. 2
      apps/client/src/app/pages/portfolio/transactions/transactions-page.html
  11. 15
      apps/client/src/app/services/data.service.ts
  12. 54
      libs/ui/src/lib/activities-table/activities-table.component.html
  13. 19
      libs/ui/src/lib/activities-table/activities-table.component.scss
  14. 46
      libs/ui/src/lib/activities-table/activities-table.component.ts

4
CHANGELOG.md

@ -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

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

@ -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

@ -0,0 +1,10 @@
import { OrderWithAccount } from '@ghostfolio/common/types';
export interface Activities {
activities: Activity[];
}
export interface Activity extends OrderWithAccount {
feeInBaseCurrency: number;
valueInBaseCurrency: number;
}

15
apps/api/src/app/order/order.controller.ts

@ -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')

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

@ -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,

25
apps/api/src/app/order/order.service.ts

@ -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
)
}; };
}); });
} }

22
apps/api/src/app/portfolio/portfolio.service.ts

@ -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,

2
apps/client/src/app/components/accounts-table/accounts-table.component.html

@ -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">

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

@ -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 } });
} }

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

@ -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"

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

@ -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 };
}) })
); );
} }

54
libs/ui/src/lib/activities-table/activities-table.component.html

@ -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

19
libs/ui/src/lib/activities-table/activities-table.component.scss

@ -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),

46
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -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…
Cancel
Save