Feature/introduce lazy loaded activities table (#2729)
* Introduce lazy-loaded activities table * Add icon column * Emit paginator event * Add pagination logic * Integrate total items * Update changelog
This commit is contained in:
parent
531964636b
commit
51a0ede3e4
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Introduced a lazy-loaded activities table on the portfolio activities page (experimental)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Set the actions columns of various tables to stick at the end
|
- Set the actions columns of various tables to stick at the end
|
||||||
|
@ -2,6 +2,7 @@ import { OrderWithAccount } from '@ghostfolio/common/types';
|
|||||||
|
|
||||||
export interface Activities {
|
export interface Activities {
|
||||||
activities: Activity[];
|
activities: Activity[];
|
||||||
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Activity extends OrderWithAccount {
|
export interface Activity extends OrderWithAccount {
|
||||||
|
@ -24,7 +24,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import { Order as OrderModel, Prisma } from '@prisma/client';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
@ -90,6 +90,8 @@ export class OrderController {
|
|||||||
@Query('accounts') filterByAccounts?: string,
|
@Query('accounts') filterByAccounts?: string,
|
||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('skip') skip?: number,
|
@Query('skip') skip?: number,
|
||||||
|
@Query('sortColumn') sortColumn?: string,
|
||||||
|
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
||||||
@Query('tags') filterByTags?: string,
|
@Query('tags') filterByTags?: string,
|
||||||
@Query('take') take?: number
|
@Query('take') take?: number
|
||||||
): Promise<Activities> {
|
): Promise<Activities> {
|
||||||
@ -103,8 +105,10 @@ export class OrderController {
|
|||||||
await this.impersonationService.validateImpersonationId(impersonationId);
|
await this.impersonationService.validateImpersonationId(impersonationId);
|
||||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||||
|
|
||||||
const activities = await this.orderService.getOrders({
|
const { activities, count } = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
|
sortColumn,
|
||||||
|
sortDirection,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
includeDrafts: true,
|
includeDrafts: true,
|
||||||
skip: isNaN(skip) ? undefined : skip,
|
skip: isNaN(skip) ? undefined : skip,
|
||||||
@ -113,7 +117,7 @@ export class OrderController {
|
|||||||
withExcludedAccounts: true
|
withExcludedAccounts: true
|
||||||
});
|
});
|
||||||
|
|
||||||
return { activities };
|
return { activities, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@ -25,7 +25,7 @@ import { endOfToday, isAfter } from 'date-fns';
|
|||||||
import { groupBy } from 'lodash';
|
import { groupBy } from 'lodash';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { Activity } from './interfaces/activities.interface';
|
import { Activities, Activity } from './interfaces/activities.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrderService {
|
export class OrderService {
|
||||||
@ -51,7 +51,7 @@ export class OrderService {
|
|||||||
take?: number;
|
take?: number;
|
||||||
cursor?: Prisma.OrderWhereUniqueInput;
|
cursor?: Prisma.OrderWhereUniqueInput;
|
||||||
where?: Prisma.OrderWhereInput;
|
where?: Prisma.OrderWhereInput;
|
||||||
orderBy?: Prisma.OrderOrderByWithRelationInput;
|
orderBy?: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput>;
|
||||||
}): Promise<OrderWithAccount[]> {
|
}): Promise<OrderWithAccount[]> {
|
||||||
const { include, skip, take, cursor, where, orderBy } = params;
|
const { include, skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
@ -231,6 +231,8 @@ export class OrderService {
|
|||||||
filters,
|
filters,
|
||||||
includeDrafts = false,
|
includeDrafts = false,
|
||||||
skip,
|
skip,
|
||||||
|
sortColumn,
|
||||||
|
sortDirection,
|
||||||
take = Number.MAX_SAFE_INTEGER,
|
take = Number.MAX_SAFE_INTEGER,
|
||||||
types,
|
types,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
@ -240,12 +242,17 @@ export class OrderService {
|
|||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
includeDrafts?: boolean;
|
includeDrafts?: boolean;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
|
sortColumn?: string;
|
||||||
|
sortDirection?: Prisma.SortOrder;
|
||||||
take?: number;
|
take?: number;
|
||||||
types?: TypeOfOrder[];
|
types?: TypeOfOrder[];
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
}): Promise<Activity[]> {
|
}): Promise<Activities> {
|
||||||
|
let orderBy: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput> = [
|
||||||
|
{ date: 'asc' }
|
||||||
|
];
|
||||||
const where: Prisma.OrderWhereInput = { userId };
|
const where: Prisma.OrderWhereInput = { userId };
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -307,6 +314,10 @@ export class OrderService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sortColumn) {
|
||||||
|
orderBy = [{ [sortColumn]: sortDirection }];
|
||||||
|
}
|
||||||
|
|
||||||
if (types) {
|
if (types) {
|
||||||
where.OR = types.map((type) => {
|
where.OR = types.map((type) => {
|
||||||
return {
|
return {
|
||||||
@ -317,8 +328,9 @@ export class OrderService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const [orders, count] = await Promise.all([
|
||||||
await this.orders({
|
this.orders({
|
||||||
|
orderBy,
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
where,
|
where,
|
||||||
@ -332,10 +344,12 @@ export class OrderService {
|
|||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
SymbolProfile: true,
|
SymbolProfile: true,
|
||||||
tags: true
|
tags: true
|
||||||
},
|
}
|
||||||
orderBy: { date: 'asc' }
|
}),
|
||||||
})
|
this.prismaService.order.count({ where })
|
||||||
)
|
]);
|
||||||
|
|
||||||
|
const activities = orders
|
||||||
.filter((order) => {
|
.filter((order) => {
|
||||||
return (
|
return (
|
||||||
withExcludedAccounts ||
|
withExcludedAccounts ||
|
||||||
@ -361,6 +375,8 @@ export class OrderService {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { activities, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateOrder({
|
public async updateOrder({
|
||||||
|
@ -225,7 +225,7 @@ export class PortfolioService {
|
|||||||
}): Promise<InvestmentItem[]> {
|
}): Promise<InvestmentItem[]> {
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||||
|
|
||||||
const activities = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
userId,
|
userId,
|
||||||
types: ['DIVIDEND'],
|
types: ['DIVIDEND'],
|
||||||
@ -679,13 +679,13 @@ export class PortfolioService {
|
|||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
const userCurrency = this.getUserCurrency(user);
|
const userCurrency = this.getUserCurrency(user);
|
||||||
|
|
||||||
const orders = (
|
const { activities } = await this.orderService.getOrders({
|
||||||
await this.orderService.getOrders({
|
userCurrency,
|
||||||
userCurrency,
|
userId,
|
||||||
userId,
|
withExcludedAccounts: true
|
||||||
withExcludedAccounts: true
|
});
|
||||||
})
|
|
||||||
).filter(({ SymbolProfile }) => {
|
const orders = activities.filter(({ SymbolProfile }) => {
|
||||||
return (
|
return (
|
||||||
SymbolProfile.dataSource === aDataSource &&
|
SymbolProfile.dataSource === aDataSource &&
|
||||||
SymbolProfile.symbol === aSymbol
|
SymbolProfile.symbol === aSymbol
|
||||||
@ -1639,18 +1639,18 @@ export class PortfolioService {
|
|||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
const activities = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
const excludedActivities = (
|
let { activities: excludedActivities } = await this.orderService.getOrders({
|
||||||
await this.orderService.getOrders({
|
userCurrency,
|
||||||
userCurrency,
|
userId,
|
||||||
userId,
|
withExcludedAccounts: true
|
||||||
withExcludedAccounts: true
|
});
|
||||||
})
|
|
||||||
).filter(({ Account: account }) => {
|
excludedActivities = excludedActivities.filter(({ Account: account }) => {
|
||||||
return account?.isExcluded ?? false;
|
return account?.isExcluded ?? false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1830,7 +1830,7 @@ export class PortfolioService {
|
|||||||
const userCurrency =
|
const userCurrency =
|
||||||
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY;
|
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY;
|
||||||
|
|
||||||
const orders = await this.orderService.getOrders({
|
const { activities, count } = await this.orderService.getOrders({
|
||||||
filters,
|
filters,
|
||||||
includeDrafts,
|
includeDrafts,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
@ -1839,11 +1839,11 @@ export class PortfolioService {
|
|||||||
types: ['BUY', 'SELL']
|
types: ['BUY', 'SELL']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (orders.length <= 0) {
|
if (count <= 0) {
|
||||||
return { transactionPoints: [], orders: [], portfolioOrders: [] };
|
return { transactionPoints: [], orders: [], portfolioOrders: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = activities.map((order) => ({
|
||||||
currency: order.SymbolProfile.currency,
|
currency: order.SymbolProfile.currency,
|
||||||
dataSource: order.SymbolProfile.dataSource,
|
dataSource: order.SymbolProfile.dataSource,
|
||||||
date: format(order.date, DATE_FORMAT),
|
date: format(order.date, DATE_FORMAT),
|
||||||
@ -1877,8 +1877,8 @@ export class PortfolioService {
|
|||||||
portfolioCalculator.computeTransactionPoints();
|
portfolioCalculator.computeTransactionPoints();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
orders,
|
|
||||||
portfolioOrders,
|
portfolioOrders,
|
||||||
|
orders: activities,
|
||||||
transactionPoints: portfolioCalculator.getTransactionPoints()
|
transactionPoints: portfolioCalculator.getTransactionPoints()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1913,13 +1913,14 @@ export class PortfolioService {
|
|||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const ordersOfTypeItemOrLiability = await this.orderService.getOrders({
|
const { activities: ordersOfTypeItemOrLiability } =
|
||||||
filters,
|
await this.orderService.getOrders({
|
||||||
userCurrency,
|
filters,
|
||||||
userId,
|
userCurrency,
|
||||||
withExcludedAccounts,
|
userId,
|
||||||
types: ['ITEM', 'LIABILITY']
|
withExcludedAccounts,
|
||||||
});
|
types: ['ITEM', 'LIABILITY']
|
||||||
|
});
|
||||||
|
|
||||||
const accounts: PortfolioDetails['accounts'] = {};
|
const accounts: PortfolioDetails['accounts'] = {};
|
||||||
const platforms: PortfolioDetails['platforms'] = {};
|
const platforms: PortfolioDetails['platforms'] = {};
|
||||||
|
@ -1,5 +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 { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { PageEvent } from '@angular/material/paginator';
|
||||||
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
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 { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
@ -10,10 +12,11 @@ import { DataService } from '@ghostfolio/client/services/data.service';
|
|||||||
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
|
||||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { DataSource, Order as OrderModel, Prisma } from '@prisma/client';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
@ -30,12 +33,18 @@ import { ImportActivitiesDialogParams } from './import-activities-dialog/interfa
|
|||||||
})
|
})
|
||||||
export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||||
public activities: Activity[];
|
public activities: Activity[];
|
||||||
|
public dataSource: MatTableDataSource<Activity>;
|
||||||
public defaultAccountId: string;
|
public defaultAccountId: string;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateActivity: boolean;
|
public hasPermissionToCreateActivity: boolean;
|
||||||
public hasPermissionToDeleteActivity: boolean;
|
public hasPermissionToDeleteActivity: boolean;
|
||||||
|
public pageIndex = 0;
|
||||||
|
public pageSize = DEFAULT_PAGE_SIZE;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
|
public sortColumn = 'date';
|
||||||
|
public sortDirection: Prisma.SortOrder = 'desc';
|
||||||
|
public totalItems: number;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -103,21 +112,48 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetchActivities() {
|
public fetchActivities() {
|
||||||
this.dataService
|
if (this.user?.settings?.isExperimentalFeatures === true) {
|
||||||
.fetchActivities({})
|
this.dataService
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.fetchActivities({
|
||||||
.subscribe(({ activities }) => {
|
skip: this.pageIndex * this.pageSize,
|
||||||
this.activities = activities;
|
sortColumn: this.sortColumn,
|
||||||
|
sortDirection: this.sortDirection,
|
||||||
|
take: this.pageSize
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ activities, count }) => {
|
||||||
|
this.dataSource = new MatTableDataSource(activities);
|
||||||
|
this.totalItems = count;
|
||||||
|
|
||||||
if (
|
if (this.hasPermissionToCreateActivity && this.totalItems <= 0) {
|
||||||
this.hasPermissionToCreateActivity &&
|
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||||
this.activities?.length <= 0
|
}
|
||||||
) {
|
|
||||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.dataService
|
||||||
|
.fetchActivities({})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ activities }) => {
|
||||||
|
this.activities = activities;
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.hasPermissionToCreateActivity &&
|
||||||
|
this.activities?.length <= 0
|
||||||
|
) {
|
||||||
|
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onChangePage(page: PageEvent) {
|
||||||
|
this.pageIndex = page.pageIndex;
|
||||||
|
|
||||||
|
this.fetchActivities();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCloneActivity(aActivity: Activity) {
|
public onCloneActivity(aActivity: Activity) {
|
||||||
|
@ -2,7 +2,30 @@
|
|||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1>
|
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1>
|
||||||
|
<gf-activities-table-lazy
|
||||||
|
*ngIf="user?.settings?.isExperimentalFeatures === true"
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
[deviceType]="deviceType"
|
||||||
|
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
|
||||||
|
[hasPermissionToExportActivities]="!hasImpersonationId"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[pageIndex]="pageIndex"
|
||||||
|
[pageSize]="pageSize"
|
||||||
|
[showActions]="!hasImpersonationId && hasPermissionToDeleteActivity && !user.settings.isRestrictedView"
|
||||||
|
[totalItems]="totalItems"
|
||||||
|
(activityDeleted)="onDeleteActivity($event)"
|
||||||
|
(activityToClone)="onCloneActivity($event)"
|
||||||
|
(activityToUpdate)="onUpdateActivity($event)"
|
||||||
|
(deleteAllActivities)="onDeleteAllActivities()"
|
||||||
|
(export)="onExport($event)"
|
||||||
|
(exportDrafts)="onExportDrafts($event)"
|
||||||
|
(import)="onImport()"
|
||||||
|
(importDividends)="onImportDividends()"
|
||||||
|
(pageChanged)="onChangePage($event)"
|
||||||
|
></gf-activities-table-lazy>
|
||||||
<gf-activities-table
|
<gf-activities-table
|
||||||
|
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||||
[activities]="activities"
|
[activities]="activities"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
|
@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
||||||
|
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
|
||||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||||
|
|
||||||
import { ActivitiesPageRoutingModule } from './activities-page-routing.module';
|
import { ActivitiesPageRoutingModule } from './activities-page-routing.module';
|
||||||
@ -17,6 +18,7 @@ import { GfImportActivitiesDialogModule } from './import-activities-dialog/impor
|
|||||||
ActivitiesPageRoutingModule,
|
ActivitiesPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfActivitiesTableModule,
|
GfActivitiesTableModule,
|
||||||
|
GfActivitiesTableLazyModule,
|
||||||
GfCreateOrUpdateActivityDialogModule,
|
GfCreateOrUpdateActivityDialogModule,
|
||||||
GfImportActivitiesDialogModule,
|
GfImportActivitiesDialogModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
@ -38,7 +38,7 @@ import {
|
|||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
||||||
import { AccountWithValue, DateRange, GroupBy } from '@ghostfolio/common/types';
|
import { AccountWithValue, DateRange, GroupBy } from '@ghostfolio/common/types';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { DataSource, Order as OrderModel, Prisma } from '@prisma/client';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { cloneDeep, groupBy, isNumber } from 'lodash';
|
import { cloneDeep, groupBy, isNumber } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@ -149,23 +149,45 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetchActivities({
|
public fetchActivities({
|
||||||
filters
|
filters,
|
||||||
|
skip,
|
||||||
|
sortColumn,
|
||||||
|
sortDirection,
|
||||||
|
take
|
||||||
}: {
|
}: {
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
|
skip?: number;
|
||||||
|
sortColumn?: string;
|
||||||
|
sortDirection?: Prisma.SortOrder;
|
||||||
|
take?: number;
|
||||||
}): Observable<Activities> {
|
}): Observable<Activities> {
|
||||||
return this.http
|
let params = this.buildFiltersAsQueryParams({ filters });
|
||||||
.get<any>('/api/v1/order', {
|
|
||||||
params: this.buildFiltersAsQueryParams({ filters })
|
if (skip) {
|
||||||
|
params = params.append('skip', skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortColumn) {
|
||||||
|
params = params.append('sortColumn', sortColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDirection) {
|
||||||
|
params = params.append('sortDirection', sortDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (take) {
|
||||||
|
params = params.append('take', take);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<any>('/api/v1/order', { params }).pipe(
|
||||||
|
map(({ activities, count }) => {
|
||||||
|
for (const activity of activities) {
|
||||||
|
activity.createdAt = parseISO(activity.createdAt);
|
||||||
|
activity.date = parseISO(activity.date);
|
||||||
|
}
|
||||||
|
return { activities, count };
|
||||||
})
|
})
|
||||||
.pipe(
|
);
|
||||||
map(({ activities }) => {
|
|
||||||
for (const activity of activities) {
|
|
||||||
activity.createdAt = parseISO(activity.createdAt);
|
|
||||||
activity.date = parseISO(activity.date);
|
|
||||||
}
|
|
||||||
return { activities };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchDividends({
|
public fetchDividends({
|
||||||
|
@ -0,0 +1,499 @@
|
|||||||
|
<div *ngIf="hasPermissionToCreateActivity" class="d-flex justify-content-end">
|
||||||
|
<button
|
||||||
|
class="align-items-center d-flex"
|
||||||
|
mat-stroked-button
|
||||||
|
(click)="onImport()"
|
||||||
|
>
|
||||||
|
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||||
|
<ng-container i18n>Import Activities</ng-container>...
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="hasPermissionToExportActivities"
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-stroked-button
|
||||||
|
[matMenuTriggerFor]="activitiesMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #activitiesMenu="matMenu" xPosition="before">
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="dataSource?.data.length === 0"
|
||||||
|
(click)="onImportDividends()"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
|
||||||
|
<ng-container i18n>Import Dividends</ng-container>...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="hasPermissionToExportActivities"
|
||||||
|
class="align-items-center d-flex"
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="dataSource?.data.length === 0"
|
||||||
|
(click)="onExport()"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
|
||||||
|
<span i18n>Export Activities</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="hasPermissionToExportActivities"
|
||||||
|
class="align-items-center d-flex"
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="!hasDrafts"
|
||||||
|
(click)="onExportDrafts()"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||||
|
<span i18n>Export Drafts as ICS</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="align-items-center d-flex"
|
||||||
|
mat-menu-item
|
||||||
|
(click)="onDeleteAllActivities()"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||||
|
<span i18n>Delete all Activities</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="activities">
|
||||||
|
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||||
|
<ng-container matColumnDef="select">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||||
|
<mat-checkbox
|
||||||
|
color="primary"
|
||||||
|
[checked]="
|
||||||
|
areAllRowsSelected() && !hasErrors && selectedRows.hasValue()
|
||||||
|
"
|
||||||
|
[disabled]="hasErrors"
|
||||||
|
[indeterminate]="selectedRows.hasValue() && !areAllRowsSelected()"
|
||||||
|
(change)="$event ? toggleAllRows() : null"
|
||||||
|
></mat-checkbox>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
<mat-checkbox
|
||||||
|
color="primary"
|
||||||
|
[checked]="element.error ? false : selectedRows.isSelected(element)"
|
||||||
|
[disabled]="element.error"
|
||||||
|
(change)="$event ? selectedRows.toggle(element) : null"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
></mat-checkbox>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="importStatus">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||||
|
<ng-container i18n></ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
<div
|
||||||
|
*ngIf="element.error"
|
||||||
|
class="d-flex"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
[matTooltip]="element.error.message"
|
||||||
|
>
|
||||||
|
<ion-icon class="text-danger" name="alert-circle-outline"></ion-icon>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="icon">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
|
<gf-symbol-icon
|
||||||
|
[dataSource]="element.SymbolProfile?.dataSource"
|
||||||
|
[symbol]="element.SymbolProfile?.symbol"
|
||||||
|
[tooltip]="element.SymbolProfile?.name"
|
||||||
|
></gf-symbol-icon>
|
||||||
|
<div>{{ element.dataSource }}</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="nameWithSymbol">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="px-1"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header="SymbolProfile.symbol"
|
||||||
|
>
|
||||||
|
<ng-container i18n>Name</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div>
|
||||||
|
<span class="text-truncate">{{ element.SymbolProfile?.name }}</span>
|
||||||
|
<span
|
||||||
|
*ngIf="element.isDraft"
|
||||||
|
class="badge badge-secondary ml-1"
|
||||||
|
i18n
|
||||||
|
>Draft</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!isUUID(element.SymbolProfile?.symbol)">
|
||||||
|
<small class="text-muted">{{
|
||||||
|
element.SymbolProfile?.symbol | gfSymbol
|
||||||
|
}}</small>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="type">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
|
<ng-container i18n>Type</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
<gf-activity-type [activityType]="element.type"></gf-activity-type>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="date">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||||
|
<ng-container i18n>Date</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
<div class="d-flex">
|
||||||
|
{{ element.date | date: defaultDateFormat }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="quantity">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header
|
||||||
|
>
|
||||||
|
<ng-container i18n>Quantity</ng-container>
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : element.quantity"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="unitPrice">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header
|
||||||
|
>
|
||||||
|
<ng-container i18n>Unit Price</ng-container>
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : element.unitPrice"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="fee">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header
|
||||||
|
>
|
||||||
|
<ng-container i18n>Fee</ng-container>
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : element.fee"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="value">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header
|
||||||
|
>
|
||||||
|
<ng-container i18n>Value</ng-container>
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
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="currency">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header="SymbolProfile.currency"
|
||||||
|
>
|
||||||
|
<ng-container i18n>Currency</ng-container>
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
{{ element.SymbolProfile?.currency }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="valueInBaseCurrency">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-lg-none d-xl-none justify-content-end px-1"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header
|
||||||
|
>
|
||||||
|
<ng-container i18n>Value</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="d-lg-none d-xl-none px-1" mat-cell>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : element.valueInBaseCurrency"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="account">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="px-1"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header="Account.name"
|
||||||
|
>
|
||||||
|
<span class="d-none d-lg-block" i18n>Account</span>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
<div class="d-flex">
|
||||||
|
<gf-symbol-icon
|
||||||
|
*ngIf="element.Account?.Platform?.url"
|
||||||
|
class="mr-1"
|
||||||
|
[tooltip]="element.Account?.Platform?.name"
|
||||||
|
[url]="element.Account?.Platform?.url"
|
||||||
|
></gf-symbol-icon>
|
||||||
|
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="comment">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-header-cell
|
||||||
|
></th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
*ngIf="element.comment"
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
title="Note"
|
||||||
|
(click)="onOpenComment(element.comment); $event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="document-text-outline"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions" stickyEnd>
|
||||||
|
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
||||||
|
<button
|
||||||
|
*ngIf="
|
||||||
|
!hasPermissionToCreateActivity && hasPermissionToExportActivities
|
||||||
|
"
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[matMenuTriggerFor]="activitiesMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #activitiesMenu="matMenu" xPosition="before">
|
||||||
|
<button
|
||||||
|
*ngIf="hasPermissionToCreateActivity"
|
||||||
|
class="align-items-center d-flex"
|
||||||
|
mat-menu-item
|
||||||
|
(click)="onImport()"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||||
|
<ng-container i18n>Import Activities</ng-container>...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="hasPermissionToCreateActivity"
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="dataSource?.data.length === 0"
|
||||||
|
(click)="onImportDividends()"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
|
||||||
|
<ng-container i18n>Import Dividends</ng-container>...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="hasPermissionToExportActivities"
|
||||||
|
class="align-items-center d-flex"
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="dataSource?.data.length === 0"
|
||||||
|
(click)="onExport()"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
|
||||||
|
<span i18n>Export Activities</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="hasPermissionToExportActivities"
|
||||||
|
class="align-items-center d-flex"
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="!hasDrafts"
|
||||||
|
(click)="onExportDrafts()"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||||
|
<span i18n>Export Drafts as ICS</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
|
<button
|
||||||
|
*ngIf="showActions"
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[matMenuTriggerFor]="activityMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #activityMenu="matMenu" xPosition="before">
|
||||||
|
<button mat-menu-item (click)="onUpdateActivity(element)">
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||||
|
<span i18n>Edit</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="onCloneActivity(element)">
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="copy-outline"></ion-icon>
|
||||||
|
<span i18n>Clone</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="!element.isDraft"
|
||||||
|
(click)="onExportDraft(element.id)"
|
||||||
|
>
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||||
|
<span i18n>Export Draft as ICS</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="onDeleteActivity(element.id)">
|
||||||
|
<span class="align-items-center d-flex">
|
||||||
|
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||||
|
<span i18n>Delete</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||||
|
<tr
|
||||||
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
|
mat-row
|
||||||
|
[ngClass]="{
|
||||||
|
'cursor-pointer':
|
||||||
|
hasPermissionToOpenDetails &&
|
||||||
|
!row.isDraft &&
|
||||||
|
row.type !== 'FEE' &&
|
||||||
|
row.type !== 'INTEREST' &&
|
||||||
|
row.type !== 'ITEM' &&
|
||||||
|
row.type !== 'LIABILITY'
|
||||||
|
}"
|
||||||
|
(click)="onClickActivity(row)"
|
||||||
|
></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-paginator
|
||||||
|
[length]="totalItems"
|
||||||
|
[ngClass]="{
|
||||||
|
'd-none': (isLoading && totalItems === 0) || totalItems <= pageSize
|
||||||
|
}"
|
||||||
|
[pageSize]="pageSize"
|
||||||
|
(page)="onChangePage($event)"
|
||||||
|
></mat-paginator>
|
||||||
|
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
*ngIf="isLoading"
|
||||||
|
animation="pulse"
|
||||||
|
class="px-4 py-3"
|
||||||
|
[theme]="{
|
||||||
|
height: '1.5rem',
|
||||||
|
width: '100%'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
|
||||||
|
<div
|
||||||
|
*ngIf="
|
||||||
|
dataSource?.data.length === 0 && hasPermissionToCreateActivity && !isLoading
|
||||||
|
"
|
||||||
|
class="p-3 text-center"
|
||||||
|
>
|
||||||
|
<gf-no-transactions-info-indicator
|
||||||
|
[hasBorder]="false"
|
||||||
|
></gf-no-transactions-info-indicator>
|
||||||
|
</div>
|
@ -0,0 +1,9 @@
|
|||||||
|
@import 'apps/client/src/styles/ghostfolio-style';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.activities {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,237 @@
|
|||||||
|
import { SelectionModel } from '@angular/cdk/collections';
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||||
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
|
||||||
|
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||||
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
|
import { isUUID } from 'class-validator';
|
||||||
|
import { endOfToday, isAfter } from 'date-fns';
|
||||||
|
import { Subject, Subscription, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
selector: 'gf-activities-table-lazy',
|
||||||
|
styleUrls: ['./activities-table-lazy.component.scss'],
|
||||||
|
templateUrl: './activities-table-lazy.component.html'
|
||||||
|
})
|
||||||
|
export class ActivitiesTableLazyComponent
|
||||||
|
implements OnChanges, OnDestroy, OnInit
|
||||||
|
{
|
||||||
|
@Input() baseCurrency: string;
|
||||||
|
@Input() dataSource: MatTableDataSource<Activity>;
|
||||||
|
@Input() deviceType: string;
|
||||||
|
@Input() hasPermissionToCreateActivity: boolean;
|
||||||
|
@Input() hasPermissionToExportActivities: boolean;
|
||||||
|
@Input() hasPermissionToOpenDetails = true;
|
||||||
|
@Input() locale: string;
|
||||||
|
@Input() pageIndex: number;
|
||||||
|
@Input() pageSize = DEFAULT_PAGE_SIZE;
|
||||||
|
@Input() showActions = true;
|
||||||
|
@Input() showCheckbox = false;
|
||||||
|
@Input() showFooter = true;
|
||||||
|
@Input() showNameColumn = true;
|
||||||
|
@Input() totalItems = Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
@Output() activityDeleted = new EventEmitter<string>();
|
||||||
|
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
|
||||||
|
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
|
||||||
|
@Output() deleteAllActivities = new EventEmitter<void>();
|
||||||
|
@Output() export = new EventEmitter<string[]>();
|
||||||
|
@Output() exportDrafts = new EventEmitter<string[]>();
|
||||||
|
@Output() import = new EventEmitter<void>();
|
||||||
|
@Output() importDividends = new EventEmitter<UniqueAsset>();
|
||||||
|
@Output() pageChanged = new EventEmitter<PageEvent>();
|
||||||
|
@Output() selectedActivities = new EventEmitter<Activity[]>();
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
|
|
||||||
|
public defaultDateFormat: string;
|
||||||
|
public displayedColumns = [];
|
||||||
|
public endOfToday = endOfToday();
|
||||||
|
public hasDrafts = false;
|
||||||
|
public hasErrors = false;
|
||||||
|
public isAfter = isAfter;
|
||||||
|
public isLoading = true;
|
||||||
|
public isUUID = isUUID;
|
||||||
|
public routeQueryParams: Subscription;
|
||||||
|
public searchKeywords: string[] = [];
|
||||||
|
public selectedRows = new SelectionModel<Activity>(true, []);
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(private router: Router) {}
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
if (this.showCheckbox) {
|
||||||
|
this.toggleAllRows();
|
||||||
|
this.selectedRows.changed
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((selectedRows) => {
|
||||||
|
this.selectedActivities.emit(selectedRows.source.selected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public areAllRowsSelected() {
|
||||||
|
const numSelectedRows = this.selectedRows.selected.length;
|
||||||
|
const numTotalRows = this.dataSource.data.length;
|
||||||
|
return numSelectedRows === numTotalRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
this.defaultDateFormat = getDateFormatString(this.locale);
|
||||||
|
|
||||||
|
this.displayedColumns = [
|
||||||
|
'select',
|
||||||
|
'importStatus',
|
||||||
|
'icon',
|
||||||
|
'nameWithSymbol',
|
||||||
|
'type',
|
||||||
|
'date',
|
||||||
|
'quantity',
|
||||||
|
'unitPrice',
|
||||||
|
'fee',
|
||||||
|
'value',
|
||||||
|
'currency',
|
||||||
|
'valueInBaseCurrency',
|
||||||
|
'account',
|
||||||
|
'comment',
|
||||||
|
'actions'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!this.showCheckbox) {
|
||||||
|
this.displayedColumns = this.displayedColumns.filter((column) => {
|
||||||
|
return column !== 'importStatus' && column !== 'select';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.showNameColumn) {
|
||||||
|
this.displayedColumns = this.displayedColumns.filter((column) => {
|
||||||
|
return column !== 'nameWithSymbol';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dataSource) {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onChangePage(page: PageEvent) {
|
||||||
|
this.pageChanged.emit(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onClickActivity(activity: Activity) {
|
||||||
|
if (this.showCheckbox) {
|
||||||
|
if (!activity.error) {
|
||||||
|
this.selectedRows.toggle(activity);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
this.hasPermissionToOpenDetails &&
|
||||||
|
!activity.isDraft &&
|
||||||
|
activity.type !== 'FEE' &&
|
||||||
|
activity.type !== 'INTEREST' &&
|
||||||
|
activity.type !== 'ITEM' &&
|
||||||
|
activity.type !== 'LIABILITY'
|
||||||
|
) {
|
||||||
|
this.onOpenPositionDialog({
|
||||||
|
dataSource: activity.SymbolProfile.dataSource,
|
||||||
|
symbol: activity.SymbolProfile.symbol
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onCloneActivity(aActivity: OrderWithAccount) {
|
||||||
|
this.activityToClone.emit(aActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDeleteActivity(aId: string) {
|
||||||
|
const confirmation = confirm(
|
||||||
|
$localize`Do you really want to delete this activity?`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmation) {
|
||||||
|
this.activityDeleted.emit(aId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onExport() {
|
||||||
|
if (this.searchKeywords.length > 0) {
|
||||||
|
this.export.emit(
|
||||||
|
this.dataSource.filteredData.map((activity) => {
|
||||||
|
return activity.id;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.export.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onExportDraft(aActivityId: string) {
|
||||||
|
this.exportDrafts.emit([aActivityId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onExportDrafts() {
|
||||||
|
this.exportDrafts.emit(
|
||||||
|
this.dataSource.filteredData
|
||||||
|
.filter((activity) => {
|
||||||
|
return activity.isDraft;
|
||||||
|
})
|
||||||
|
.map((activity) => {
|
||||||
|
return activity.id;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDeleteAllActivities() {
|
||||||
|
this.deleteAllActivities.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onImport() {
|
||||||
|
this.import.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onImportDividends() {
|
||||||
|
this.importDividends.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onOpenComment(aComment: string) {
|
||||||
|
alert(aComment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateActivity(aActivity: OrderWithAccount) {
|
||||||
|
this.activityToUpdate.emit(aActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleAllRows() {
|
||||||
|
this.areAllRowsSelected()
|
||||||
|
? this.selectedRows.clear()
|
||||||
|
: this.dataSource.data.forEach((row) => this.selectedRows.select(row));
|
||||||
|
|
||||||
|
this.selectedActivities.emit(this.selectedRows.selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||||
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||||
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
|
import { GfActivityTypeModule } from '@ghostfolio/ui/activity-type';
|
||||||
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
import { ActivitiesTableLazyComponent } from './activities-table-lazy.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [ActivitiesTableLazyComponent],
|
||||||
|
exports: [ActivitiesTableLazyComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfActivityTypeModule,
|
||||||
|
GfNoTransactionsInfoModule,
|
||||||
|
GfSymbolIconModule,
|
||||||
|
GfSymbolModule,
|
||||||
|
GfValueModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatPaginatorModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
NgxSkeletonLoaderModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class GfActivitiesTableLazyModule {}
|
Loading…
x
Reference in New Issue
Block a user