Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
91ec9aa0a4 | |||
565e920f1b | |||
5d24adfa75 | |||
1dc94c0027 | |||
ebae2f4ec9 | |||
7099edc591 | |||
de973d6bda | |||
993a491d24 | |||
631efff7ae | |||
a3d1ac2ce4 | |||
4484c21757 |
9
.travis.yml
Normal file
9
.travis.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
language: node_js
|
||||||
|
git:
|
||||||
|
depth: false
|
||||||
|
node_js:
|
||||||
|
- 14
|
||||||
|
before_script:
|
||||||
|
- yarn
|
||||||
|
script:
|
||||||
|
- yarn format:check
|
26
CHANGELOG.md
26
CHANGELOG.md
@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 1.1.0 - 11.05.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a button to fetch the current market price in the create or edit transaction dialog
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the transaction filtering with multi filter support
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the filtering by account name in the transactions table
|
||||||
|
- Fixed the active menu item state when a modal has opened
|
||||||
|
|
||||||
|
## 1.0.0 - 05.05.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the functionality to clone a transaction
|
||||||
|
- Added a _Google Play_ badge on the landing page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed to maskable icons
|
||||||
|
|
||||||
## 0.99.0 - 03.05.2021
|
## 0.99.0 - 03.05.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
19
README.md
19
README.md
@ -68,19 +68,18 @@ The frontend is built with [Angular](https://angular.io).
|
|||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
1. Run `yarn install`
|
1. Run `yarn install`
|
||||||
2. Run `cd docker`
|
1. Run `cd docker`
|
||||||
3. Run `docker compose build`
|
1. Run `docker compose up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
|
||||||
4. Run `docker compose up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
|
1. Run `cd -` to go back to the project root directory
|
||||||
5. Run `cd -` to go back to the project root directory
|
1. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data
|
||||||
6. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data
|
1. Start server and client (see [_Development_](#Development))
|
||||||
7. Start server and client (see _Development_)
|
1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
|
||||||
8. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
|
1. Go to the _Admin Control Panel_ and press _Gather All Data_ to fetch historical data
|
||||||
9. Go to the _Admin Control Panel_ and press _Gather All Data_ to fetch historical data
|
1. Press _Sign out_ and check out the _Live Demo_
|
||||||
10. Press _Sign out_ and check out the _Live Demo_
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Please make sure you have completed the instructions from _Setup_
|
Please make sure you have completed the instructions from [_Setup_](#Setup)
|
||||||
|
|
||||||
### Start server
|
### Start server
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
import { Account, Order } from '@prisma/client';
|
import { Account, Order, Platform } from '@prisma/client';
|
||||||
|
|
||||||
export type OrderWithAccount = Order & { Account?: Account };
|
type AccountWithPlatform = Account & { Platform?: Platform };
|
||||||
|
|
||||||
|
export type OrderWithAccount = Order & { Account?: AccountWithPlatform };
|
||||||
|
@ -57,9 +57,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.router.events
|
this.router.events
|
||||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||||
.subscribe((test) => {
|
.subscribe(() => {
|
||||||
this.currentRoute = this.router.url.toString().substring(1);
|
this.currentRoute = this.router.url.toString().substring(1);
|
||||||
// this.initializeTheme();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tokenStorageService
|
this.tokenStorageService
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
[routerLink]="['/']"
|
[routerLink]="['/']"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'home' ? 'primary' : null"
|
[color]="currentRoute?.startsWith('home') ? 'primary' : null"
|
||||||
>Overview</a
|
>Overview</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -17,7 +17,7 @@
|
|||||||
[routerLink]="['/analysis']"
|
[routerLink]="['/analysis']"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'analysis' ? 'primary' : null"
|
[color]="currentRoute?.startsWith('analysis') ? 'primary' : null"
|
||||||
>Analysis</a
|
>Analysis</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -25,7 +25,7 @@
|
|||||||
[routerLink]="['/report']"
|
[routerLink]="['/report']"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'report' ? 'primary' : null"
|
[color]="currentRoute?.startsWith('report') ? 'primary' : null"
|
||||||
>X-ray</a
|
>X-ray</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -33,7 +33,7 @@
|
|||||||
[routerLink]="['/transactions']"
|
[routerLink]="['/transactions']"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'transactions' ? 'primary' : null"
|
[color]="currentRoute?.startsWith('transactions') ? 'primary' : null"
|
||||||
>Transactions</a
|
>Transactions</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -41,7 +41,7 @@
|
|||||||
[routerLink]="['/accounts']"
|
[routerLink]="['/accounts']"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'accounts' ? 'primary' : null"
|
[color]="currentRoute?.startsWith('accounts') ? 'primary' : null"
|
||||||
>Accounts</a
|
>Accounts</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -50,7 +50,7 @@
|
|||||||
[routerLink]="['/admin']"
|
[routerLink]="['/admin']"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'admin' ? 'primary' : null"
|
[color]="currentRoute?.startsWith('admin') ? 'primary' : null"
|
||||||
>Admin Control</a
|
>Admin Control</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -58,7 +58,7 @@
|
|||||||
[routerLink]="['/resources']"
|
[routerLink]="['/resources']"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'resources' ? 'primary' : null"
|
[color]="currentRoute?.startsWith('resources') ? 'primary' : null"
|
||||||
>Resources</a
|
>Resources</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -66,7 +66,7 @@
|
|||||||
[routerLink]="['/about']"
|
[routerLink]="['/about']"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'about' ? 'primary' : null"
|
[color]="currentRoute?.startsWith('about') ? 'primary' : null"
|
||||||
>About</a
|
>About</a
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -130,7 +130,7 @@
|
|||||||
[routerLink]="['/analysis']"
|
[routerLink]="['/analysis']"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'analysis' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute?.startsWith('analysis') }"
|
||||||
>Analysis</a
|
>Analysis</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -138,7 +138,7 @@
|
|||||||
[routerLink]="['/report']"
|
[routerLink]="['/report']"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'report' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute?.startsWith('report') }"
|
||||||
>X-ray</a
|
>X-ray</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -146,7 +146,9 @@
|
|||||||
[routerLink]="['/transactions']"
|
[routerLink]="['/transactions']"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'transactions' }"
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute?.startsWith('transactions')
|
||||||
|
}"
|
||||||
>Transactions</a
|
>Transactions</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -154,7 +156,7 @@
|
|||||||
[routerLink]="['/accounts']"
|
[routerLink]="['/accounts']"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'accounts' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute?.startsWith('accounts') }"
|
||||||
>Accounts</a
|
>Accounts</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -162,7 +164,7 @@
|
|||||||
[routerLink]="['/account']"
|
[routerLink]="['/account']"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'account' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute?.startsWith('account') }"
|
||||||
>Ghostfolio Account</a
|
>Ghostfolio Account</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -171,7 +173,7 @@
|
|||||||
[routerLink]="['/admin']"
|
[routerLink]="['/admin']"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute?.startsWith('admin') }"
|
||||||
>Admin Control</a
|
>Admin Control</a
|
||||||
>
|
>
|
||||||
<hr class="m-0" />
|
<hr class="m-0" />
|
||||||
@ -180,7 +182,9 @@
|
|||||||
[routerLink]="['/resources']"
|
[routerLink]="['/resources']"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'resources' }"
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute?.startsWith('resources')
|
||||||
|
}"
|
||||||
>Resources</a
|
>Resources</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@ -188,7 +192,7 @@
|
|||||||
[routerLink]="['/about']"
|
[routerLink]="['/about']"
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'about' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute?.startsWith('about') }"
|
||||||
>About Ghostfolio</a
|
>About Ghostfolio</a
|
||||||
>
|
>
|
||||||
<hr class="d-block d-sm-none m-0" />
|
<hr class="d-block d-sm-none m-0" />
|
||||||
@ -210,7 +214,7 @@
|
|||||||
[routerLink]="['/about']"
|
[routerLink]="['/about']"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'about' ? 'primary' : null"
|
[color]="currentRoute?.startsWith('about') ? 'primary' : null"
|
||||||
>About</a
|
>About</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
@ -1,12 +1,37 @@
|
|||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<input
|
|
||||||
#input
|
|
||||||
autocomplete="off"
|
|
||||||
matInput
|
|
||||||
placeholder="Search for transactions..."
|
|
||||||
(keyup)="applyFilter($event)"
|
|
||||||
/>
|
|
||||||
<ion-icon class="mr-1" matPrefix name="search-outline"></ion-icon>
|
<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"
|
||||||
|
matChipRemove
|
||||||
|
[removable]="true"
|
||||||
|
(removed)="removeKeyword(searchKeyword)"
|
||||||
|
>
|
||||||
|
{{ searchKeyword }}
|
||||||
|
<ion-icon class="ml-2" matPrefix name="close-outline"></ion-icon>
|
||||||
|
</mat-chip>
|
||||||
|
<input
|
||||||
|
#searchInput
|
||||||
|
name="close-outline"
|
||||||
|
placeholder="Search for account, currency, symbol or type..."
|
||||||
|
[formControl]="searchControl"
|
||||||
|
[matAutocomplete]="autocomplete"
|
||||||
|
[matChipInputFor]="chipList"
|
||||||
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||||
|
(matChipInputTokenEnd)="addKeyword($event)"
|
||||||
|
/>
|
||||||
|
</mat-chip-list>
|
||||||
|
<mat-autocomplete
|
||||||
|
#autocomplete="matAutocomplete"
|
||||||
|
(optionSelected)="keywordSelected($event)"
|
||||||
|
>
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let transaction of filteredTransactions | async"
|
||||||
|
[value]="transaction"
|
||||||
|
>
|
||||||
|
{{ transaction }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<table
|
<table
|
||||||
@ -177,6 +202,9 @@
|
|||||||
<button i18n mat-menu-item (click)="onUpdateTransaction(element)">
|
<button i18n mat-menu-item (click)="onUpdateTransaction(element)">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
<button i18n mat-menu-item (click)="onCloneTransaction(element)">
|
||||||
|
Clone
|
||||||
|
</button>
|
||||||
<button i18n mat-menu-item (click)="onDeleteTransaction(element.id)">
|
<button i18n mat-menu-item (click)="onDeleteTransaction(element.id)">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
@ -7,6 +7,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-chip {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.mat-table {
|
.mat-table {
|
||||||
td {
|
td {
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
ElementRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
@ -13,12 +14,21 @@ import { MatDialog } from '@angular/material/dialog';
|
|||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { OrderWithAccount } from '@ghostfolio/api/app/order/interfaces/order-with-account.type';
|
||||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper';
|
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { PositionDetailDialog } from '../position/position-detail-dialog/position-detail-dialog.component';
|
import { PositionDetailDialog } from '../position/position-detail-dialog/position-detail-dialog.component';
|
||||||
|
import {
|
||||||
|
MatAutocomplete,
|
||||||
|
MatAutocompleteSelectedEvent
|
||||||
|
} from '@angular/material/autocomplete';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||||
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
|
|
||||||
|
const SEARCH_STRING_SEPARATOR = ',';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-transactions-table',
|
selector: 'gf-transactions-table',
|
||||||
@ -32,19 +42,30 @@ export class TransactionsTableComponent
|
|||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() showActions: boolean;
|
@Input() showActions: boolean;
|
||||||
@Input() transactions: OrderModel[];
|
@Input() transactions: OrderWithAccount[];
|
||||||
|
|
||||||
@Output() transactionDeleted = new EventEmitter<string>();
|
@Output() transactionDeleted = new EventEmitter<string>();
|
||||||
@Output() transactionToUpdate = new EventEmitter<OrderModel>();
|
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
|
||||||
|
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
|
||||||
|
|
||||||
|
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
|
||||||
|
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
|
||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
public dataSource: MatTableDataSource<OrderModel> = new MatTableDataSource();
|
public dataSource: MatTableDataSource<OrderWithAccount> = new MatTableDataSource();
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
public displayedColumns = [];
|
public displayedColumns = [];
|
||||||
|
public filteredTransactions$: Subject<string[]> = new BehaviorSubject([]);
|
||||||
|
public filteredTransactions: Observable<
|
||||||
|
string[]
|
||||||
|
> = this.filteredTransactions$.asObservable();
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
|
public searchControl = new FormControl();
|
||||||
|
public searchKeywords: string[] = [];
|
||||||
|
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||||
|
|
||||||
|
private allFilteredTransactions: string[];
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -62,6 +83,49 @@ export class TransactionsTableComponent
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.searchControl.valueChanges.subscribe((keyword) => {
|
||||||
|
if (keyword) {
|
||||||
|
const filterValue = keyword.toLowerCase();
|
||||||
|
this.filteredTransactions$.next(
|
||||||
|
this.allFilteredTransactions.filter(
|
||||||
|
(filter) => filter.toLowerCase().indexOf(filterValue) === 0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.filteredTransactions$.next(this.allFilteredTransactions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ngOnInit() {}
|
||||||
@ -86,17 +150,23 @@ export class TransactionsTableComponent
|
|||||||
|
|
||||||
if (this.transactions) {
|
if (this.transactions) {
|
||||||
this.dataSource = new MatTableDataSource(this.transactions);
|
this.dataSource = new MatTableDataSource(this.transactions);
|
||||||
|
this.dataSource.filterPredicate = (data, filter) => {
|
||||||
|
const dataString = TransactionsTableComponent.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.dataSource.sort = this.sort;
|
||||||
|
this.updateFilter();
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public applyFilter(event: Event) {
|
|
||||||
const filterValue = (event.target as HTMLInputElement).value;
|
|
||||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
public onDeleteTransaction(aId: string) {
|
public onDeleteTransaction(aId: string) {
|
||||||
const confirmation = confirm(
|
const confirmation = confirm(
|
||||||
'Do you really want to delete this transaction?'
|
'Do you really want to delete this transaction?'
|
||||||
@ -119,10 +189,14 @@ export class TransactionsTableComponent
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateTransaction(aTransaction: OrderModel) {
|
public onUpdateTransaction(aTransaction: OrderWithAccount) {
|
||||||
this.transactionToUpdate.emit(aTransaction);
|
this.transactionToUpdate.emit(aTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onCloneTransaction(aTransaction: OrderWithAccount) {
|
||||||
|
this.transactionToClone.emit(aTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
public openPositionDialog({
|
public openPositionDialog({
|
||||||
symbol,
|
symbol,
|
||||||
title
|
title
|
||||||
@ -152,4 +226,40 @@ export class TransactionsTableComponent
|
|||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateFilter() {
|
||||||
|
this.dataSource.filter = this.searchKeywords.join(SEARCH_STRING_SEPARATOR);
|
||||||
|
const lowercaseSearchKeywords = this.searchKeywords.map((keyword) =>
|
||||||
|
keyword.trim().toLowerCase()
|
||||||
|
);
|
||||||
|
this.allFilteredTransactions = TransactionsTableComponent.getSearchableFieldValues(
|
||||||
|
this.transactions
|
||||||
|
).filter((item) => {
|
||||||
|
return !lowercaseSearchKeywords.includes(item.trim().toLowerCase());
|
||||||
|
});
|
||||||
|
this.filteredTransactions$.next(this.allFilteredTransactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getSearchableFieldValues(
|
||||||
|
transactions: OrderWithAccount[]
|
||||||
|
): string[] {
|
||||||
|
const fieldValues = new Set<string>();
|
||||||
|
for (const transaction of transactions) {
|
||||||
|
this.getFilterableValues(transaction, fieldValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...fieldValues].filter((item) => item != undefined).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getFilterableValues(
|
||||||
|
transaction,
|
||||||
|
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);
|
||||||
|
return [...fieldValues].filter((item) => item != undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,9 @@ import { GfPositionDetailDialogModule } from '../position/position-detail-dialog
|
|||||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||||
import { GfValueModule } from '../value/value.module';
|
import { GfValueModule } from '../value/value.module';
|
||||||
import { TransactionsTableComponent } from './transactions-table.component';
|
import { TransactionsTableComponent } from './transactions-table.component';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [TransactionsTableComponent],
|
declarations: [TransactionsTableComponent],
|
||||||
@ -23,12 +26,15 @@ import { TransactionsTableComponent } from './transactions-table.component';
|
|||||||
GfSymbolIconModule,
|
GfSymbolIconModule,
|
||||||
GfSymbolModule,
|
GfSymbolModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatChipsModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatSortModule,
|
MatSortModule,
|
||||||
MatTableModule,
|
MatTableModule,
|
||||||
NgxSkeletonLoaderModule,
|
NgxSkeletonLoaderModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
@ -156,4 +156,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="downloads my-5 row justify-content-center">
|
||||||
|
<a
|
||||||
|
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||||
|
title="Get Ghostfolio on Google Play"
|
||||||
|
>
|
||||||
|
<img alt="Google Play Badge" src="assets/badge-en-google-play.png" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,6 +18,12 @@
|
|||||||
background-color: var(--light-background);
|
background-color: var(--light-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.downloads {
|
||||||
|
img {
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
:host-context(.is-dark-theme) {
|
||||||
|
@ -30,6 +30,7 @@ import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
|
|||||||
})
|
})
|
||||||
export class CreateOrUpdateTransactionDialog {
|
export class CreateOrUpdateTransactionDialog {
|
||||||
public currencies: Currency[] = [];
|
public currencies: Currency[] = [];
|
||||||
|
public currentMarketPrice = null;
|
||||||
public filteredLookupItems: Observable<LookupItem[]>;
|
public filteredLookupItems: Observable<LookupItem[]>;
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public platforms: { id: string; name: string }[];
|
public platforms: { id: string; name: string }[];
|
||||||
@ -65,6 +66,20 @@ export class CreateOrUpdateTransactionDialog {
|
|||||||
return [];
|
return [];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.data.transaction.symbol) {
|
||||||
|
this.dataService
|
||||||
|
.fetchSymbolItem(this.data.transaction.symbol)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ marketPrice }) => {
|
||||||
|
this.currentMarketPrice = marketPrice;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public applyCurrentMarketPrice() {
|
||||||
|
this.data.transaction.unitPrice = this.currentMarketPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCancel(): void {
|
public onCancel(): void {
|
||||||
@ -81,7 +96,7 @@ export class CreateOrUpdateTransactionDialog {
|
|||||||
.subscribe(({ currency, dataSource, marketPrice }) => {
|
.subscribe(({ currency, dataSource, marketPrice }) => {
|
||||||
this.data.transaction.currency = currency;
|
this.data.transaction.currency = currency;
|
||||||
this.data.transaction.dataSource = dataSource;
|
this.data.transaction.dataSource = dataSource;
|
||||||
this.data.transaction.unitPrice = marketPrice;
|
this.currentMarketPrice = marketPrice;
|
||||||
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
|
@ -81,7 +81,13 @@
|
|||||||
[matDatepicker]="date"
|
[matDatepicker]="date"
|
||||||
[(ngModel)]="data.transaction.date"
|
[(ngModel)]="data.transaction.date"
|
||||||
/>
|
/>
|
||||||
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
<mat-datepicker-toggle matSuffix [for]="date">
|
||||||
|
<ion-icon
|
||||||
|
class="text-muted"
|
||||||
|
matDatepickerToggleIcon
|
||||||
|
name="calendar-clear-outline"
|
||||||
|
></ion-icon>
|
||||||
|
</mat-datepicker-toggle>
|
||||||
<mat-datepicker #date disabled="false"></mat-datepicker>
|
<mat-datepicker #date disabled="false"></mat-datepicker>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@ -110,7 +116,7 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="unit-price w-100">
|
||||||
<mat-label i18n>Unit Price</mat-label>
|
<mat-label i18n>Unit Price</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
@ -119,6 +125,15 @@
|
|||||||
type="number"
|
type="number"
|
||||||
[(ngModel)]="data.transaction.unitPrice"
|
[(ngModel)]="data.transaction.unitPrice"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
*ngIf="currentMarketPrice"
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
title="Apply current market price"
|
||||||
|
(click)="applyCurrentMarketPrice()"
|
||||||
|
>
|
||||||
|
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
|
||||||
|
</button>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@ -30,6 +30,7 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
|
|||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
],
|
],
|
||||||
providers: []
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class CreateOrUpdateTransactionDialogModule {}
|
export class CreateOrUpdateTransactionDialogModule {}
|
||||||
|
@ -14,6 +14,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-form-field-appearance-outline {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-form-field-suffix {
|
||||||
|
top: -0.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
font-size: 130%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mat-select {
|
.mat-select {
|
||||||
&.no-arrow {
|
&.no-arrow {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
|
@ -109,6 +109,10 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onCloneTransaction(aTransaction: OrderModel) {
|
||||||
|
this.openCreateTransactionDialog(aTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
public onDeleteTransaction(aId: string) {
|
public onDeleteTransaction(aId: string) {
|
||||||
this.dataService.deleteOrder(aId).subscribe({
|
this.dataService.deleteOrder(aId).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@ -175,20 +179,23 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private openCreateTransactionDialog(): void {
|
private openCreateTransactionDialog(aTransaction?: OrderModel): void {
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||||
data: {
|
data: {
|
||||||
accounts: this.user?.accounts,
|
accounts: this.user?.accounts,
|
||||||
transaction: {
|
transaction: {
|
||||||
accountId: this.user?.accounts.find((account) => {
|
accountId:
|
||||||
return account.isDefault;
|
aTransaction?.accountId ??
|
||||||
})?.id,
|
this.user?.accounts.find((account) => {
|
||||||
currency: null,
|
return account.isDefault;
|
||||||
|
})?.id,
|
||||||
|
currency: aTransaction?.currency ?? null,
|
||||||
|
dataSource: aTransaction?.dataSource ?? null,
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
fee: 0,
|
fee: 0,
|
||||||
quantity: null,
|
quantity: null,
|
||||||
symbol: null,
|
symbol: aTransaction?.symbol ?? null,
|
||||||
type: 'BUY',
|
type: aTransaction?.type ?? 'BUY',
|
||||||
unitPrice: null
|
unitPrice: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder"
|
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder"
|
||||||
[transactions]="transactions"
|
[transactions]="transactions"
|
||||||
(transactionDeleted)="onDeleteTransaction($event)"
|
(transactionDeleted)="onDeleteTransaction($event)"
|
||||||
|
(transactionToClone)="onCloneTransaction($event)"
|
||||||
(transactionToUpdate)="onUpdateTransaction($event)"
|
(transactionToUpdate)="onUpdateTransaction($event)"
|
||||||
></gf-transactions-table>
|
></gf-transactions-table>
|
||||||
</div>
|
</div>
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
BIN
apps/client/src/assets/badge-en-google-play.png
Normal file
BIN
apps/client/src/assets/badge-en-google-play.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
2
nx.json
2
nx.json
@ -6,7 +6,7 @@
|
|||||||
".eslintrc.json": "*",
|
".eslintrc.json": "*",
|
||||||
"nx.json": "*"
|
"nx.json": "*"
|
||||||
},
|
},
|
||||||
"affected": { "defaultBase": "master" },
|
"affected": { "defaultBase": "origin/main" },
|
||||||
"npmScope": "ghostfolio",
|
"npmScope": "ghostfolio",
|
||||||
"tasksRunnerOptions": {
|
"tasksRunnerOptions": {
|
||||||
"default": {
|
"default": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "0.99.0",
|
"version": "1.1.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -144,7 +144,6 @@ async function main() {
|
|||||||
date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)),
|
date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)),
|
||||||
fee: 30,
|
fee: 30,
|
||||||
id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1',
|
id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1',
|
||||||
platformId: platformDegiro.id,
|
|
||||||
quantity: 50,
|
quantity: 50,
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -158,7 +157,6 @@ async function main() {
|
|||||||
date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)),
|
date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)),
|
||||||
fee: 29.9,
|
fee: 29.9,
|
||||||
id: 'a1c5d73a-8631-44e5-ac44-356827a5212c',
|
id: 'a1c5d73a-8631-44e5-ac44-356827a5212c',
|
||||||
platformId: platformCoinbase.id,
|
|
||||||
quantity: 0.5614682,
|
quantity: 0.5614682,
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -172,7 +170,6 @@ async function main() {
|
|||||||
date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)),
|
||||||
fee: 80.79,
|
fee: 80.79,
|
||||||
id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b',
|
id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b',
|
||||||
platformId: platformInteractiveBrokers.id,
|
|
||||||
quantity: 5,
|
quantity: 5,
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -186,7 +183,6 @@ async function main() {
|
|||||||
date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e',
|
id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e',
|
||||||
platformId: platformInteractiveBrokers.id,
|
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -200,7 +196,6 @@ async function main() {
|
|||||||
date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)),
|
date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e',
|
id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e',
|
||||||
platformId: platformInteractiveBrokers.id,
|
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -214,7 +209,6 @@ async function main() {
|
|||||||
date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)),
|
date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '347b0430-a84f-4031-a0f9-390399066ad6',
|
id: '347b0430-a84f-4031-a0f9-390399066ad6',
|
||||||
platformId: platformInteractiveBrokers.id,
|
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -228,7 +222,6 @@ async function main() {
|
|||||||
date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '67ec3f47-3189-4b63-ba05-60d3a06b302f',
|
id: '67ec3f47-3189-4b63-ba05-60d3a06b302f',
|
||||||
platformId: platformInteractiveBrokers.id,
|
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -242,7 +235,6 @@ async function main() {
|
|||||||
date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2',
|
id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2',
|
||||||
platformId: platformInteractiveBrokers.id,
|
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbol: 'VTI',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
|
Reference in New Issue
Block a user