Feature/refactor transactions to activities table (#600)
* Refactor transactions to activities table with attributes and routes * Update changelog
This commit is contained in:
@@ -66,6 +66,13 @@ const routes: Routes = [
|
||||
(m) => m.PortfolioPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'portfolio/activities',
|
||||
loadChildren: () =>
|
||||
import('./pages/portfolio/transactions/transactions-page.module').then(
|
||||
(m) => m.TransactionsPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'portfolio/allocations',
|
||||
loadChildren: () =>
|
||||
@@ -87,13 +94,6 @@ const routes: Routes = [
|
||||
(m) => m.ReportPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'portfolio/transactions',
|
||||
loadChildren: () =>
|
||||
import('./pages/portfolio/transactions/transactions-page.module').then(
|
||||
(m) => m.TransactionsPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'pricing',
|
||||
loadChildren: () =>
|
||||
|
@@ -5,10 +5,10 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||
import { AccountsTableComponent } from './accounts-table.component';
|
||||
|
||||
@NgModule({
|
||||
|
@@ -25,8 +25,8 @@
|
||||
class="mt-3"
|
||||
i18n
|
||||
mat-button
|
||||
[routerLink]="['/portfolio', 'transactions']"
|
||||
>Manage Transactions...</a
|
||||
[routerLink]="['/portfolio', 'activities']"
|
||||
>Manage Activities...</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -125,19 +125,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gf-transactions-table
|
||||
<gf-activities-table
|
||||
*ngIf="orders?.length > 0"
|
||||
[activities]="orders"
|
||||
[baseCurrency]="data.baseCurrency"
|
||||
[deviceType]="data.deviceType"
|
||||
[hasPermissionToCreateOrder]="false"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToImportOrders]="false"
|
||||
[hasPermissionToImportActivities]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="data.locale"
|
||||
[showActions]="false"
|
||||
[showSymbolColumn]="false"
|
||||
[transactions]="orders"
|
||||
></gf-transactions-table>
|
||||
></gf-activities-table>
|
||||
</div>
|
||||
|
||||
<gf-dialog-footer
|
||||
|
@@ -4,7 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfTransactionsTableModule } from '@ghostfolio/client/components/transactions-table/transactions-table.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
@@ -16,10 +16,10 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfActivitiesTableModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
GfLineChartModule,
|
||||
GfTransactionsTableModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
|
@@ -1,315 +0,0 @@
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="w-100"
|
||||
[ngClass]="{ 'd-none': !hasPermissionToFilter }"
|
||||
>
|
||||
<ion-icon class="mr-1" matPrefix name="search-outline"></ion-icon>
|
||||
<mat-chip-list #chipList aria-label="Search keywords">
|
||||
<mat-chip
|
||||
*ngFor="let searchKeyword of searchKeywords"
|
||||
class="mx-1 my-0 px-2 py-0"
|
||||
matChipRemove
|
||||
[removable]="true"
|
||||
(removed)="removeKeyword(searchKeyword)"
|
||||
>
|
||||
{{ searchKeyword | gfSymbol }}
|
||||
<ion-icon class="ml-2" matPrefix name="close-outline"></ion-icon>
|
||||
</mat-chip>
|
||||
<input
|
||||
#searchInput
|
||||
name="close-outline"
|
||||
[formControl]="searchControl"
|
||||
[matAutocomplete]="autocomplete"
|
||||
[matChipInputFor]="chipList"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
[placeholder]="placeholder"
|
||||
(matChipInputTokenEnd)="addKeyword($event)"
|
||||
/>
|
||||
</mat-chip-list>
|
||||
<mat-autocomplete
|
||||
#autocomplete="matAutocomplete"
|
||||
(optionSelected)="keywordSelected($event)"
|
||||
>
|
||||
<mat-option *ngFor="let filter of filters | async" [value]="filter">
|
||||
{{ filter | gfSymbol }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<table
|
||||
class="gf-table w-100"
|
||||
matSort
|
||||
matSortActive="date"
|
||||
matSortDirection="desc"
|
||||
mat-table
|
||||
[dataSource]="dataSource"
|
||||
>
|
||||
<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 - i }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="date">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
||||
Date
|
||||
</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="type">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
||||
Type
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element" class="px-1">
|
||||
<div
|
||||
class="d-inline-flex p-1 type-badge"
|
||||
[ngClass]="{
|
||||
buy: element.type === 'BUY',
|
||||
dividend: element.type === 'DIVIDEND',
|
||||
sell: element.type === 'SELL'
|
||||
}"
|
||||
>
|
||||
<ion-icon
|
||||
[name]="
|
||||
element.type === 'BUY' || element.type === 'DIVIDEND'
|
||||
? 'arrow-forward-circle-outline'
|
||||
: 'arrow-back-circle-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span class="d-none d-lg-block mx-1">{{ element.type }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="symbol">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
||||
Symbol
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<div class="d-flex align-items-center">
|
||||
{{ element.symbol | gfSymbol }}
|
||||
<span *ngIf="element.isDraft" class="badge badge-secondary ml-1" i18n
|
||||
>Draft</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="currency">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Currency
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
{{ element.currency }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="quantity">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Quantity
|
||||
</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"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Unit Price
|
||||
</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"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Fee
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px1" 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="justify-content-end px-1"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Value
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px1" 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="account">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<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="actions">
|
||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="transactionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #transactionsMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
*ngIf="hasPermissionToImportOrders"
|
||||
class="align-items-center d-flex"
|
||||
mat-menu-item
|
||||
(click)="onImport()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||
<span i18n>Import</span>
|
||||
</button>
|
||||
<button
|
||||
class="align-items-center d-flex"
|
||||
mat-menu-item
|
||||
(click)="onExport()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
|
||||
<span i18n>Export</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="transactionMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||
<button i18n mat-menu-item (click)="onUpdateTransaction(element)">
|
||||
Edit
|
||||
</button>
|
||||
<button i18n mat-menu-item (click)="onCloneTransaction(element)">
|
||||
Clone
|
||||
</button>
|
||||
<button i18n mat-menu-item (click)="onDeleteTransaction(element.id)">
|
||||
Delete
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
mat-row
|
||||
(click)="
|
||||
hasPermissionToOpenDetails &&
|
||||
!row.isDraft &&
|
||||
onOpenPositionDialog({
|
||||
symbol: row.symbol
|
||||
})
|
||||
"
|
||||
[ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft }"
|
||||
></tr>
|
||||
</table>
|
||||
|
||||
<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 && hasPermissionToCreateOrder && !isLoading
|
||||
"
|
||||
class="p-3 text-center"
|
||||
>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator>
|
||||
</div>
|
@@ -1,65 +0,0 @@
|
||||
@import '~apps/client/src/styles/ghostfolio-style';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
::ng-deep {
|
||||
.mat-form-field-infix {
|
||||
border-top: 0 solid transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-chip {
|
||||
cursor: pointer;
|
||||
min-height: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mat-table {
|
||||
th {
|
||||
::ng-deep {
|
||||
.mat-sort-header-container {
|
||||
justify-content: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-row {
|
||||
.type-badge {
|
||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||
border-radius: 1rem;
|
||||
line-height: 1em;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.buy {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
&.dividend {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
&.sell {
|
||||
color: var(--orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.mat-form-field {
|
||||
color: rgba(var(--light-primary-text));
|
||||
}
|
||||
|
||||
.mat-table {
|
||||
.type-badge {
|
||||
background-color: rgba(
|
||||
var(--palette-foreground-text-dark),
|
||||
0.1
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,272 +0,0 @@
|
||||
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import {
|
||||
MatAutocomplete,
|
||||
MatAutocompleteSelectedEvent
|
||||
} from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import { endOfToday, format, isAfter } from 'date-fns';
|
||||
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...';
|
||||
const SEARCH_STRING_SEPARATOR = ',';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-transactions-table',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './transactions-table.component.html',
|
||||
styleUrls: ['./transactions-table.component.scss']
|
||||
})
|
||||
export class TransactionsTableComponent
|
||||
implements OnChanges, OnDestroy, OnInit
|
||||
{
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() hasPermissionToCreateOrder: boolean;
|
||||
@Input() hasPermissionToFilter = true;
|
||||
@Input() hasPermissionToImportOrders: boolean;
|
||||
@Input() hasPermissionToOpenDetails = true;
|
||||
@Input() locale: string;
|
||||
@Input() showActions: boolean;
|
||||
@Input() showSymbolColumn = true;
|
||||
@Input() transactions: OrderWithAccount[];
|
||||
|
||||
@Output() export = new EventEmitter<void>();
|
||||
@Output() import = new EventEmitter<void>();
|
||||
@Output() transactionDeleted = new EventEmitter<string>();
|
||||
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
|
||||
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
|
||||
|
||||
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
|
||||
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
public dataSource: MatTableDataSource<OrderWithAccount> =
|
||||
new MatTableDataSource();
|
||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||
public displayedColumns = [];
|
||||
public endOfToday = endOfToday();
|
||||
public filters$: Subject<string[]> = new BehaviorSubject([]);
|
||||
public filters: Observable<string[]> = this.filters$.asObservable();
|
||||
public isAfter = isAfter;
|
||||
public isLoading = true;
|
||||
public placeholder = '';
|
||||
public routeQueryParams: Subscription;
|
||||
public searchControl = new FormControl();
|
||||
public searchKeywords: string[] = [];
|
||||
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
|
||||
private allFilters: string[];
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(private router: Router) {
|
||||
this.searchControl.valueChanges
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((keyword) => {
|
||||
if (keyword) {
|
||||
const filterValue = keyword.toLowerCase();
|
||||
this.filters$.next(
|
||||
this.allFilters.filter(
|
||||
(filter) => filter.toLowerCase().indexOf(filterValue) === 0
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.filters$.next(this.allFilters);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public addKeyword({ input, value }: MatChipInputEvent): void {
|
||||
if (value?.trim()) {
|
||||
this.searchKeywords.push(value.trim());
|
||||
this.updateFilter();
|
||||
}
|
||||
|
||||
// Reset the input value
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
this.searchControl.setValue(null);
|
||||
}
|
||||
|
||||
public removeKeyword(keyword: string): void {
|
||||
const index = this.searchKeywords.indexOf(keyword);
|
||||
|
||||
if (index >= 0) {
|
||||
this.searchKeywords.splice(index, 1);
|
||||
this.updateFilter();
|
||||
}
|
||||
}
|
||||
|
||||
public keywordSelected(event: MatAutocompleteSelectedEvent): void {
|
||||
this.searchKeywords.push(event.option.viewValue);
|
||||
this.updateFilter();
|
||||
this.searchInput.nativeElement.value = '';
|
||||
this.searchControl.setValue(null);
|
||||
}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.displayedColumns = [
|
||||
'count',
|
||||
'date',
|
||||
'type',
|
||||
'symbol',
|
||||
'currency',
|
||||
'quantity',
|
||||
'unitPrice',
|
||||
'fee',
|
||||
'value',
|
||||
'account'
|
||||
];
|
||||
|
||||
if (this.showActions) {
|
||||
this.displayedColumns.push('actions');
|
||||
}
|
||||
|
||||
if (!this.showSymbolColumn) {
|
||||
this.displayedColumns = this.displayedColumns.filter((column) => {
|
||||
return column !== 'symbol';
|
||||
});
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
if (this.transactions) {
|
||||
this.dataSource = new MatTableDataSource(this.transactions);
|
||||
this.dataSource.filterPredicate = (data, filter) => {
|
||||
const dataString = this.getFilterableValues(data)
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
let contains = true;
|
||||
for (const singleFilter of filter.split(SEARCH_STRING_SEPARATOR)) {
|
||||
contains =
|
||||
contains && dataString.includes(singleFilter.trim().toLowerCase());
|
||||
}
|
||||
return contains;
|
||||
};
|
||||
this.dataSource.sort = this.sort;
|
||||
this.updateFilter();
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public onDeleteTransaction(aId: string) {
|
||||
const confirmation = confirm(
|
||||
'Do you really want to delete this transaction?'
|
||||
);
|
||||
|
||||
if (confirmation) {
|
||||
this.transactionDeleted.emit(aId);
|
||||
}
|
||||
}
|
||||
|
||||
public onExport() {
|
||||
this.export.emit();
|
||||
}
|
||||
|
||||
public onImport() {
|
||||
this.import.emit();
|
||||
}
|
||||
|
||||
public onOpenPositionDialog({ symbol }: { symbol: string }): void {
|
||||
this.router.navigate([], {
|
||||
queryParams: { positionDetailDialog: true, symbol }
|
||||
});
|
||||
}
|
||||
|
||||
public onUpdateTransaction(aTransaction: OrderWithAccount) {
|
||||
this.transactionToUpdate.emit(aTransaction);
|
||||
}
|
||||
|
||||
public onCloneTransaction(aTransaction: OrderWithAccount) {
|
||||
this.transactionToClone.emit(aTransaction);
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private updateFilter() {
|
||||
this.dataSource.filter = this.searchKeywords.join(SEARCH_STRING_SEPARATOR);
|
||||
const lowercaseSearchKeywords = this.searchKeywords.map((keyword) =>
|
||||
keyword.trim().toLowerCase()
|
||||
);
|
||||
|
||||
this.placeholder =
|
||||
lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : '';
|
||||
|
||||
this.allFilters = this.getSearchableFieldValues(this.transactions).filter(
|
||||
(item) => {
|
||||
return !lowercaseSearchKeywords.includes(item.trim().toLowerCase());
|
||||
}
|
||||
);
|
||||
|
||||
this.filters$.next(this.allFilters);
|
||||
}
|
||||
|
||||
private getSearchableFieldValues(transactions: OrderWithAccount[]): string[] {
|
||||
const fieldValues = new Set<string>();
|
||||
|
||||
for (const transaction of transactions) {
|
||||
this.getFilterableValues(transaction, fieldValues);
|
||||
}
|
||||
|
||||
return [...fieldValues]
|
||||
.filter((item) => {
|
||||
return item !== undefined;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aFirstChar = a.charAt(0);
|
||||
const bFirstChar = b.charAt(0);
|
||||
const isANumber = aFirstChar >= '0' && aFirstChar <= '9';
|
||||
const isBNumber = bFirstChar >= '0' && bFirstChar <= '9';
|
||||
|
||||
// Sort priority: text, followed by numbers
|
||||
if (isANumber && !isBNumber) {
|
||||
return 1;
|
||||
} else if (!isANumber && isBNumber) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getFilterableValues(
|
||||
transaction: OrderWithAccount,
|
||||
fieldValues: Set<string> = new Set<string>()
|
||||
): string[] {
|
||||
fieldValues.add(transaction.currency);
|
||||
fieldValues.add(transaction.symbol);
|
||||
fieldValues.add(transaction.type);
|
||||
fieldValues.add(transaction.Account?.name);
|
||||
fieldValues.add(transaction.Account?.Platform?.name);
|
||||
fieldValues.add(format(transaction.date, 'yyyy'));
|
||||
|
||||
return [...fieldValues].filter((item) => {
|
||||
return item !== undefined;
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||
import { TransactionsTableComponent } from './transactions-table.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [TransactionsTableComponent],
|
||||
exports: [TransactionsTableComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfNoTransactionsInfoModule,
|
||||
GfSymbolIconModule,
|
||||
GfSymbolModule,
|
||||
GfValueModule,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
MatChipsModule,
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfTransactionsTableModule {}
|
@@ -3,16 +3,16 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<mat-card class="mb-3">
|
||||
<h4 i18n>Transactions</h4>
|
||||
<p class="mb-0">Manage your transactions.</p>
|
||||
<h4 i18n>Activities</h4>
|
||||
<p class="mb-0">Manage your activities.</p>
|
||||
<p class="text-right">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-button
|
||||
[routerLink]="['/portfolio', 'transactions']"
|
||||
[routerLink]="['/portfolio', 'activities']"
|
||||
>
|
||||
Open Transactions →
|
||||
<span i18n>Open Activities</span>
|
||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||
</a>
|
||||
</p>
|
||||
</mat-card>
|
||||
@@ -31,12 +31,12 @@
|
||||
<p class="text-right">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-button
|
||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
||||
[routerLink]="['/portfolio', 'allocations']"
|
||||
>
|
||||
Open Allocations →
|
||||
<span i18n>Open Allocations</span>
|
||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||
</a>
|
||||
</p>
|
||||
</mat-card>
|
||||
@@ -57,12 +57,12 @@
|
||||
<p class="text-right">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-button
|
||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
||||
[routerLink]="['/portfolio', 'analysis']"
|
||||
>
|
||||
Open Analysis →
|
||||
<span i18n>Open Analysis</span>
|
||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||
</a>
|
||||
</p>
|
||||
</mat-card>
|
||||
@@ -84,12 +84,12 @@
|
||||
<p class="text-right">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-button
|
||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
||||
[routerLink]="['/portfolio', 'report']"
|
||||
>
|
||||
Open X-ray →
|
||||
<span i18n>Open X-ray</span>
|
||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||
</a>
|
||||
</p>
|
||||
</mat-card>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100">
|
||||
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update transaction</h1>
|
||||
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add transaction</h1>
|
||||
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update activity</h1>
|
||||
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add activity</h1>
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
|
@@ -1,21 +1,21 @@
|
||||
<div class="container">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Transactions</h3>
|
||||
<gf-transactions-table
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Activities</h3>
|
||||
<gf-activities-table
|
||||
[activities]="transactions"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[hasPermissionToImportOrders]="hasPermissionToImportOrders"
|
||||
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
||||
[hasPermissionToImportActivities]="hasPermissionToImportOrders"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView"
|
||||
[transactions]="transactions"
|
||||
(activityDeleted)="onDeleteTransaction($event)"
|
||||
(activityToClone)="onCloneTransaction($event)"
|
||||
(activityToUpdate)="onUpdateTransaction($event)"
|
||||
(export)="onExport()"
|
||||
(import)="onImport()"
|
||||
(transactionDeleted)="onDeleteTransaction($event)"
|
||||
(transactionToClone)="onCloneTransaction($event)"
|
||||
(transactionToUpdate)="onUpdateTransaction($event)"
|
||||
></gf-transactions-table>
|
||||
></gf-activities-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -3,8 +3,8 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfTransactionsTableModule } from '@ghostfolio/client/components/transactions-table/transactions-table.module';
|
||||
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
|
||||
import { GfCreateOrUpdateTransactionDialogModule } from './create-or-update-transaction-dialog/create-or-update-transaction-dialog.module';
|
||||
import { GfImportTransactionDialogModule } from './import-transaction-dialog/import-transaction-dialog.module';
|
||||
@@ -16,9 +16,9 @@ import { TransactionsPageComponent } from './transactions-page.component';
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfActivitiesTableModule,
|
||||
GfCreateOrUpdateTransactionDialogModule,
|
||||
GfImportTransactionDialogModule,
|
||||
GfTransactionsTableModule,
|
||||
MatButtonModule,
|
||||
MatSnackBarModule,
|
||||
RouterModule,
|
||||
|
Reference in New Issue
Block a user