mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
* Move assistant from experimental to general availability * Update changelogpull/2978/head
Thomas Kaul
11 months ago
committed by
GitHub
40 changed files with 979 additions and 3648 deletions
@ -1,591 +0,0 @@ |
|||
<gf-activities-filter |
|||
[allFilters]="allFilters" |
|||
[isLoading]="isLoading" |
|||
[ngClass]="{ 'd-none': !hasPermissionToFilter }" |
|||
[placeholder]="placeholder" |
|||
(valueChanged)="filters$.next($event)" |
|||
/> |
|||
|
|||
<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" /> |
|||
<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" /> |
|||
</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" /> |
|||
<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" /> |
|||
<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" /> |
|||
<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" /> |
|||
<span i18n>Delete all Activities</span> |
|||
</span> |
|||
</button> |
|||
</mat-menu> |
|||
</div> |
|||
|
|||
<div class="activities"> |
|||
<table |
|||
class="gf-table w-100" |
|||
mat-table |
|||
matSort |
|||
matSortActive="date" |
|||
matSortDirection="desc" |
|||
[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> |
|||
<td *matFooterCellDef class="px-1" mat-footer-cell></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" /> |
|||
</div> |
|||
</td> |
|||
<td *matFooterCellDef class="px-1" mat-footer-cell></td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="count"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="d-none d-lg-table-cell px-1 text-right" |
|||
i18n |
|||
mat-header-cell |
|||
></th> |
|||
<td |
|||
*matCellDef="let element; let i = index" |
|||
class="d-none d-lg-table-cell px-1 text-right" |
|||
mat-cell |
|||
> |
|||
{{ |
|||
dataSource.data.length > pageSize |
|||
? dataSource.data.length - pageSize * pageIndex - i |
|||
: dataSource.data.length - i |
|||
}} |
|||
</td> |
|||
<td |
|||
*matFooterCellDef |
|||
class="d-none d-lg-table-cell px-1" |
|||
mat-footer-cell |
|||
></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> |
|||
<td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</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" /> |
|||
</td> |
|||
<td *matFooterCellDef class="px-1" mat-footer-cell></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> |
|||
<td *matFooterCellDef class="px-1" mat-footer-cell></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> |
|||
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell> |
|||
{{ baseCurrency }} |
|||
</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" |
|||
/> |
|||
</div> |
|||
</td> |
|||
<td |
|||
*matFooterCellDef |
|||
class="d-none d-lg-table-cell px-1" |
|||
mat-footer-cell |
|||
></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" |
|||
/> |
|||
</div> |
|||
</td> |
|||
<td |
|||
*matFooterCellDef |
|||
class="d-none d-lg-table-cell px-1" |
|||
mat-footer-cell |
|||
></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" |
|||
/> |
|||
</div> |
|||
</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" |
|||
/> |
|||
</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" |
|||
/> |
|||
</div> |
|||
</td> |
|||
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell> |
|||
<div class="d-flex justify-content-end"> |
|||
<gf-value |
|||
*ngIf="totalValue !== null" |
|||
[isAbsolute]="true" |
|||
[isCurrency]="true" |
|||
[locale]="locale" |
|||
[value]="isLoading ? undefined : totalValue" |
|||
/> |
|||
</div> |
|||
</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" |
|||
/> |
|||
</div> |
|||
</td> |
|||
<td *matFooterCellDef class="d-lg-none d-xl-none px-1" mat-footer-cell> |
|||
<div class="d-flex justify-content-end"> |
|||
<gf-value |
|||
*ngIf="totalValue !== null" |
|||
[isAbsolute]="true" |
|||
[isCurrency]="true" |
|||
[locale]="locale" |
|||
[value]="isLoading ? undefined : totalValue" |
|||
/> |
|||
</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" |
|||
/> |
|||
<span class="d-none d-lg-block">{{ element.Account?.name }}</span> |
|||
</div> |
|||
</td> |
|||
<td *matFooterCellDef class="px-1" mat-footer-cell></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" /> |
|||
</button> |
|||
</td> |
|||
<td |
|||
*matFooterCellDef |
|||
class="d-none d-lg-table-cell px-1" |
|||
mat-footer-cell |
|||
></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" /> |
|||
</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" /> |
|||
<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" /> |
|||
<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" /> |
|||
<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" /> |
|||
<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" /> |
|||
</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" /> |
|||
<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" /> |
|||
<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" /> |
|||
<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" /> |
|||
<span i18n>Delete</span> |
|||
</span> |
|||
</button> |
|||
</mat-menu> |
|||
</td> |
|||
<td *matFooterCellDef class="px-1" mat-footer-cell></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> |
|||
<tr |
|||
*matFooterRowDef="displayedColumns" |
|||
mat-footer-row |
|||
[ngClass]="{ |
|||
'd-none': |
|||
isLoading || dataSource.data.length === 0 || showFooter === false |
|||
}" |
|||
></tr> |
|||
</table> |
|||
</div> |
|||
|
|||
<mat-paginator |
|||
[ngClass]="{ |
|||
'd-none': |
|||
(isLoading && dataSource.data.length === 0) || |
|||
dataSource.data.length <= pageSize |
|||
}" |
|||
[pageSize]="pageSize" |
|||
[showFirstLastButtons]="true" |
|||
(page)="onChangePage($event)" |
|||
></mat-paginator> |
|||
|
|||
<ngx-skeleton-loader |
|||
*ngIf="isLoading" |
|||
animation="pulse" |
|||
class="px-4 py-3" |
|||
[theme]="{ |
|||
height: '1.5rem', |
|||
width: '100%' |
|||
}" |
|||
/> |
|||
|
|||
<div |
|||
*ngIf=" |
|||
dataSource.data.length === 0 && hasPermissionToCreateActivity && !isLoading |
|||
" |
|||
class="p-3 text-center" |
|||
> |
|||
<gf-no-transactions-info-indicator [hasBorder]="false" /> |
|||
</div> |
@ -1,19 +0,0 @@ |
|||
@import 'apps/client/src/styles/ghostfolio-style'; |
|||
|
|||
:host { |
|||
display: block; |
|||
|
|||
.activities { |
|||
overflow-x: auto; |
|||
|
|||
.mat-mdc-table { |
|||
th { |
|||
::ng-deep { |
|||
.mat-sort-header-container { |
|||
justify-content: inherit; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -1,442 +0,0 @@ |
|||
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 { MatSort } from '@angular/material/sort'; |
|||
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 { Filter, UniqueAsset } from '@ghostfolio/common/interfaces'; |
|||
import { OrderWithAccount } from '@ghostfolio/common/types'; |
|||
import { translate } from '@ghostfolio/ui/i18n'; |
|||
import Big from 'big.js'; |
|||
import { isUUID } from 'class-validator'; |
|||
import { endOfToday, format, isAfter } from 'date-fns'; |
|||
import { get, isNumber } from 'lodash'; |
|||
import { Subject, Subscription, distinctUntilChanged, takeUntil } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
selector: 'gf-activities-table', |
|||
styleUrls: ['./activities-table.component.scss'], |
|||
templateUrl: './activities-table.component.html' |
|||
}) |
|||
export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit { |
|||
@Input() activities: Activity[]; |
|||
@Input() baseCurrency: string; |
|||
@Input() deviceType: string; |
|||
@Input() hasPermissionToCreateActivity: boolean; |
|||
@Input() hasPermissionToExportActivities: boolean; |
|||
@Input() hasPermissionToFilter = true; |
|||
@Input() hasPermissionToOpenDetails = true; |
|||
@Input() locale: string; |
|||
@Input() pageSize = DEFAULT_PAGE_SIZE; |
|||
@Input() showActions = true; |
|||
@Input() showCheckbox = false; |
|||
@Input() showFooter = true; |
|||
@Input() showNameColumn = true; |
|||
|
|||
@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() selectedActivities = new EventEmitter<Activity[]>(); |
|||
|
|||
@ViewChild(MatPaginator) paginator: MatPaginator; |
|||
@ViewChild(MatSort) sort: MatSort; |
|||
|
|||
public allFilters: Filter[]; |
|||
public dataSource: MatTableDataSource<Activity> = new MatTableDataSource(); |
|||
public defaultDateFormat: string; |
|||
public displayedColumns = []; |
|||
public endOfToday = endOfToday(); |
|||
public filters$ = new Subject<Filter[]>(); |
|||
public hasDrafts = false; |
|||
public hasErrors = false; |
|||
public isAfter = isAfter; |
|||
public isLoading = true; |
|||
public isUUID = isUUID; |
|||
public pageIndex = 0; |
|||
public placeholder = ''; |
|||
public routeQueryParams: Subscription; |
|||
public searchKeywords: string[] = []; |
|||
public selectedRows = new SelectionModel<Activity>(true, []); |
|||
public totalFees: number; |
|||
public totalValue: number; |
|||
|
|||
private readonly SEARCH_STRING_SEPARATOR = ','; |
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor(private router: Router) { |
|||
this.filters$ |
|||
.pipe(distinctUntilChanged(), takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((filters) => { |
|||
this.updateFilters(filters); |
|||
}); |
|||
} |
|||
|
|||
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.displayedColumns = [ |
|||
'select', |
|||
'importStatus', |
|||
'count', |
|||
'date', |
|||
'type', |
|||
'nameWithSymbol', |
|||
'quantity', |
|||
'unitPrice', |
|||
'fee', |
|||
'value', |
|||
'currency', |
|||
'valueInBaseCurrency', |
|||
'account', |
|||
'comment', |
|||
'actions' |
|||
]; |
|||
|
|||
if (this.showCheckbox) { |
|||
this.displayedColumns = this.displayedColumns.filter((column) => { |
|||
return column !== 'count'; |
|||
}); |
|||
} else { |
|||
this.displayedColumns = this.displayedColumns.filter((column) => { |
|||
return column !== 'importStatus' && column !== 'select'; |
|||
}); |
|||
} |
|||
|
|||
if (!this.showNameColumn) { |
|||
this.displayedColumns = this.displayedColumns.filter((column) => { |
|||
return column !== 'nameWithSymbol'; |
|||
}); |
|||
} |
|||
|
|||
this.defaultDateFormat = getDateFormatString(this.locale); |
|||
|
|||
if (this.activities) { |
|||
this.activities = this.activities.map((activity) => { |
|||
return { |
|||
...activity, |
|||
error: activity.error |
|||
? { |
|||
...activity.error, |
|||
message: translate( |
|||
`IMPORT_ACTIVITY_ERROR_${activity.error.code}` |
|||
) |
|||
} |
|||
: undefined |
|||
}; |
|||
}); |
|||
|
|||
this.allFilters = this.getSearchableFieldValues(this.activities); |
|||
|
|||
this.dataSource = new MatTableDataSource(this.activities); |
|||
this.dataSource.filterPredicate = (data, filter) => { |
|||
const filterableLabels = this.getFilterableValues(data).map( |
|||
({ label }) => { |
|||
return label.toLowerCase(); |
|||
} |
|||
); |
|||
|
|||
let includes = true; |
|||
for (const singleFilter of filter.split(this.SEARCH_STRING_SEPARATOR)) { |
|||
includes = |
|||
includes && |
|||
filterableLabels.includes(singleFilter.trim().toLowerCase()); |
|||
} |
|||
return includes; |
|||
}; |
|||
this.dataSource.paginator = this.paginator; |
|||
this.dataSource.sort = this.sort; |
|||
this.dataSource.sortingDataAccessor = get; |
|||
|
|||
this.updateFilters(); |
|||
|
|||
this.hasErrors = this.activities.some(({ error }) => { |
|||
return !!error; |
|||
}); |
|||
} else { |
|||
this.hasErrors = false; |
|||
} |
|||
} |
|||
|
|||
public onChangePage(page: PageEvent) { |
|||
this.pageIndex = page.pageIndex; |
|||
|
|||
this.totalFees = this.getTotalFees(); |
|||
this.totalValue = this.getTotalValue(); |
|||
} |
|||
|
|||
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(); |
|||
} |
|||
|
|||
private getFilterableValues( |
|||
activity: OrderWithAccount, |
|||
fieldValueMap: { [id: string]: Filter } = {} |
|||
): Filter[] { |
|||
if (activity.Account?.id) { |
|||
fieldValueMap[activity.Account.id] = { |
|||
id: activity.Account.id, |
|||
label: activity.Account.name, |
|||
type: 'ACCOUNT' |
|||
}; |
|||
} |
|||
|
|||
if (activity.SymbolProfile?.currency) { |
|||
fieldValueMap[activity.SymbolProfile.currency] = { |
|||
id: activity.SymbolProfile.currency, |
|||
label: activity.SymbolProfile.currency, |
|||
type: 'TAG' |
|||
}; |
|||
} |
|||
|
|||
if ( |
|||
activity.SymbolProfile?.symbol && |
|||
!isUUID(activity.SymbolProfile.symbol) |
|||
) { |
|||
fieldValueMap[activity.SymbolProfile.symbol] = { |
|||
id: activity.SymbolProfile.symbol, |
|||
label: activity.SymbolProfile.symbol, |
|||
type: 'SYMBOL' |
|||
}; |
|||
} |
|||
|
|||
fieldValueMap[activity.type] = { |
|||
id: activity.type, |
|||
label: activity.type, |
|||
type: 'TAG' |
|||
}; |
|||
|
|||
fieldValueMap[format(new Date(activity.date), 'yyyy')] = { |
|||
id: format(new Date(activity.date), 'yyyy'), |
|||
label: format(new Date(activity.date), 'yyyy'), |
|||
type: 'TAG' |
|||
}; |
|||
|
|||
return Object.values(fieldValueMap); |
|||
} |
|||
|
|||
private getPaginatedData() { |
|||
if (this.dataSource.data.length > this.pageSize) { |
|||
const sortedData = this.dataSource.sortData( |
|||
this.dataSource.filteredData, |
|||
this.dataSource.sort |
|||
); |
|||
|
|||
return sortedData.slice( |
|||
this.pageIndex * this.pageSize, |
|||
(this.pageIndex + 1) * this.pageSize |
|||
); |
|||
} |
|||
return this.dataSource.filteredData; |
|||
} |
|||
|
|||
private getSearchableFieldValues(activities: OrderWithAccount[]): Filter[] { |
|||
const fieldValueMap: { [id: string]: Filter } = {}; |
|||
|
|||
for (const activity of activities) { |
|||
this.getFilterableValues(activity, fieldValueMap); |
|||
} |
|||
|
|||
return Object.values(fieldValueMap); |
|||
} |
|||
|
|||
private getTotalFees() { |
|||
let totalFees = new Big(0); |
|||
const paginatedData = this.getPaginatedData(); |
|||
for (const activity of paginatedData) { |
|||
if (isNumber(activity.feeInBaseCurrency)) { |
|||
totalFees = totalFees.plus(activity.feeInBaseCurrency); |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
return totalFees.toNumber(); |
|||
} |
|||
|
|||
private getTotalValue() { |
|||
const paginatedData = this.getPaginatedData(); |
|||
let totalValue = new Big(0); |
|||
|
|||
for (const { type, valueInBaseCurrency } of paginatedData) { |
|||
if (isNumber(valueInBaseCurrency)) { |
|||
if (type === 'BUY' || type === 'ITEM') { |
|||
totalValue = totalValue.plus(valueInBaseCurrency); |
|||
} else if ( |
|||
type === 'DIVIDEND' || |
|||
type === 'FEE' || |
|||
type === 'INTEREST' || |
|||
type === 'LIABILITY' || |
|||
type === 'SELL' |
|||
) { |
|||
return null; |
|||
} |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
return totalValue.toNumber(); |
|||
} |
|||
|
|||
private updateFilters(filters: Filter[] = []) { |
|||
this.isLoading = true; |
|||
|
|||
this.dataSource.filter = filters |
|||
.map((filter) => { |
|||
return filter.label; |
|||
}) |
|||
.join(this.SEARCH_STRING_SEPARATOR); |
|||
|
|||
const lowercaseSearchKeywords = filters.map((filter) => { |
|||
return filter.label.trim().toLowerCase(); |
|||
}); |
|||
|
|||
this.placeholder = |
|||
lowercaseSearchKeywords.length <= 0 |
|||
? $localize`Filter by account, currency, symbol or type...` |
|||
: ''; |
|||
|
|||
this.searchKeywords = filters.map((filter) => { |
|||
return filter.label; |
|||
}); |
|||
|
|||
this.hasDrafts = this.dataSource.filteredData.some((activity) => { |
|||
return activity.isDraft === true; |
|||
}); |
|||
this.totalFees = this.getTotalFees(); |
|||
this.totalValue = this.getTotalValue(); |
|||
|
|||
this.isLoading = false; |
|||
} |
|||
} |
@ -1,44 +0,0 @@ |
|||
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 { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.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 { ActivitiesTableComponent } from './activities-table.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [ActivitiesTableComponent], |
|||
exports: [ActivitiesTableComponent], |
|||
imports: [ |
|||
CommonModule, |
|||
GfActivitiesFilterModule, |
|||
GfActivityTypeModule, |
|||
GfNoTransactionsInfoModule, |
|||
GfSymbolIconModule, |
|||
GfSymbolModule, |
|||
GfValueModule, |
|||
MatButtonModule, |
|||
MatCheckboxModule, |
|||
MatMenuModule, |
|||
MatPaginatorModule, |
|||
MatSortModule, |
|||
MatTableModule, |
|||
MatTooltipModule, |
|||
NgxSkeletonLoaderModule, |
|||
RouterModule |
|||
], |
|||
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
|||
}) |
|||
export class GfActivitiesTableModule {} |
Loading…
Reference in new issue