mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
* Move assistant from experimental to general availability * Update changelogpull/2978/head
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