Browse Source

Extend form

pull/666/head
Thomas 3 years ago
parent
commit
beb4ee23fb
  1. 3
      apps/api/src/app/export/export.service.ts
  2. 17
      apps/api/src/app/order/order.service.ts
  3. 131
      apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts
  4. 79
      apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html
  5. 5
      apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/interfaces/interfaces.ts
  6. 32
      apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts
  7. 2
      apps/client/src/app/services/admin.service.ts
  8. 6
      libs/common/src/lib/helper.ts
  9. 7
      libs/ui/src/lib/activities-table/activities-table.component.html
  10. 8
      libs/ui/src/lib/activities-table/activities-table.component.ts

3
apps/api/src/app/export/export.service.ts

@ -1,5 +1,6 @@
import { environment } from '@ghostfolio/api/environments/environment';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { isUUID } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
@ -59,7 +60,7 @@ export class ExportService {
type,
unitPrice,
dataSource: SymbolProfile.dataSource,
symbol: SymbolProfile.symbol
symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol
};
}
)

17
apps/api/src/app/order/order.service.ts

@ -59,7 +59,7 @@ export class OrderService {
return account.isDefault === true;
});
const Account = {
let Account = {
connect: {
id_userId: {
userId: data.userId,
@ -70,14 +70,22 @@ export class OrderService {
if (data.type === 'ITEM') {
const currency = data.currency;
const dataSource: DataSource = 'MANUAL';
const id = uuidv4();
const name = data.SymbolProfile.connectOrCreate.create.symbol;
Account = undefined;
data.dataSource = dataSource;
data.id = id;
data.symbol = null;
data.SymbolProfile.connectOrCreate.create.currency = currency;
data.SymbolProfile.connectOrCreate.create.dataSource = dataSource;
data.SymbolProfile.connectOrCreate.create.name = name;
data.SymbolProfile.connectOrCreate.create.symbol = id;
data.SymbolProfile.connectOrCreate.where.dataSource_symbol = {
dataSource,
symbol: id
};
} else {
data.SymbolProfile.connectOrCreate.create.symbol =
data.SymbolProfile.connectOrCreate.create.symbol.toUpperCase();
@ -195,6 +203,13 @@ export class OrderService {
}): Promise<Order> {
const { data, where } = params;
if (data.type === 'ITEM') {
const name = data.symbol;
data.symbol = null;
data.SymbolProfile = { update: { name } };
}
const isDraft = isAfter(data.date as Date, endOfToday());
if (!isDraft) {

131
apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts

@ -9,8 +9,11 @@ import {
import { FormControl, Validators } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { DataService } from '@ghostfolio/client/services/data.service';
import { Type } from '@prisma/client';
import { isString } from 'lodash';
import { EMPTY, Observable, Subject } from 'rxjs';
import {
@ -34,19 +37,25 @@ import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
export class CreateOrUpdateTransactionDialog implements OnDestroy {
@ViewChild('autocomplete') autocomplete;
public accountIdCtrl = new FormControl({}, Validators.required);
public currencyCtrl = new FormControl({}, Validators.required);
public currencies: string[] = [];
public currentMarketPrice = null;
public dataSourceCtrl = new FormControl({}, Validators.required);
public dateCtrl = new FormControl({}, Validators.required);
public feeCtrl = new FormControl({}, Validators.required);
public filteredLookupItems: LookupItem[];
public filteredLookupItemsObservable: Observable<LookupItem[]>;
public isLoading = false;
public nameCtrl = new FormControl({}, Validators.required);
public platforms: { id: string; name: string }[];
public searchSymbolCtrl = new FormControl(
{
dataSource: this.data.transaction.dataSource,
symbol: this.data.transaction.symbol
},
Validators.required
);
public quantityCtrl = new FormControl({}, Validators.required);
public searchSymbolCtrl = new FormControl({}, Validators.required);
public showAccountIdCtrl = true;
public showNameCtrl = true;
public showSearchSymbolCtrl = true;
public typeCtrl = new FormControl({}, Validators.required);
public unitPriceCtrl = new FormControl({}, Validators.required);
private unsubscribeSubject = new Subject<void>();
@ -84,15 +93,64 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
})
);
if (this.data.transaction.id) {
this.typeCtrl.valueChanges.subscribe((type: Type) => {
if (type === 'ITEM') {
this.accountIdCtrl.removeValidators(Validators.required);
this.accountIdCtrl.updateValueAndValidity();
this.currencyCtrl.setValue(this.data.user.settings.baseCurrency);
this.dataSourceCtrl.removeValidators(Validators.required);
this.dataSourceCtrl.updateValueAndValidity();
this.nameCtrl.setValidators(Validators.required);
this.nameCtrl.updateValueAndValidity();
this.quantityCtrl.setValue(1);
this.searchSymbolCtrl.removeValidators(Validators.required);
this.searchSymbolCtrl.updateValueAndValidity();
this.showAccountIdCtrl = false;
this.showNameCtrl = true;
this.showSearchSymbolCtrl = false;
} else {
this.accountIdCtrl.setValidators(Validators.required);
this.accountIdCtrl.updateValueAndValidity();
this.currencyCtrl.setValue(undefined);
this.dataSourceCtrl.setValidators(Validators.required);
this.dataSourceCtrl.updateValueAndValidity();
this.nameCtrl.removeValidators(Validators.required);
this.nameCtrl.updateValueAndValidity();
this.quantityCtrl.setValue(undefined);
this.searchSymbolCtrl.setValidators(Validators.required);
this.searchSymbolCtrl.updateValueAndValidity();
this.showAccountIdCtrl = true;
this.showNameCtrl = false;
this.showSearchSymbolCtrl = true;
}
this.changeDetectorRef.markForCheck();
});
this.accountIdCtrl.setValue(this.data.activity?.accountId);
this.currencyCtrl.setValue(this.data.activity?.currency);
this.dataSourceCtrl.setValue(this.data.activity?.dataSource);
this.dateCtrl.setValue(this.data.activity?.date);
this.feeCtrl.setValue(this.data.activity?.fee);
this.nameCtrl.setValue(this.data.activity?.SymbolProfile?.name);
this.quantityCtrl.setValue(this.data.activity?.quantity);
this.searchSymbolCtrl.setValue({
dataSource: this.data.activity?.dataSource,
symbol: this.data.activity?.symbol
});
this.typeCtrl.setValue(this.data.activity?.type);
this.unitPriceCtrl.setValue(this.data.activity?.unitPrice);
if (this.data.activity?.id) {
this.searchSymbolCtrl.disable();
this.typeCtrl.disable();
}
if (this.data.transaction.symbol) {
if (this.data.activity?.symbol) {
this.dataService
.fetchSymbolItem({
dataSource: this.data.transaction.dataSource,
symbol: this.data.transaction.symbol
dataSource: this.data.activity?.dataSource,
symbol: this.data.activity?.symbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketPrice }) => {
@ -104,7 +162,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
}
public applyCurrentMarketPrice() {
this.data.transaction.unitPrice = this.currentMarketPrice;
this.unitPriceCtrl.setValue(this.currentMarketPrice);
}
public displayFn(aLookupItem: LookupItem) {
@ -113,7 +171,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
public onBlurSymbol() {
const currentLookupItem = this.filteredLookupItems.find((lookupItem) => {
return lookupItem.symbol === this.data.transaction.symbol;
return lookupItem.symbol === this.data.activity.symbol;
});
if (currentLookupItem) {
@ -121,9 +179,9 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
} else {
this.searchSymbolCtrl.setErrors({ incorrect: true });
this.data.transaction.currency = null;
this.data.transaction.dataSource = null;
this.data.transaction.symbol = null;
this.data.activity.currency = null;
this.data.activity.dataSource = null;
this.data.activity.symbol = null;
}
this.changeDetectorRef.markForCheck();
@ -133,8 +191,28 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
this.dialogRef.close();
}
public onSubmit() {
const activity: CreateOrderDto | UpdateOrderDto = {
accountId: this.accountIdCtrl.value,
currency: this.currencyCtrl.value,
date: this.dateCtrl.value,
dataSource: this.dataSourceCtrl.value,
fee: this.feeCtrl.value,
quantity: this.quantityCtrl.value,
symbol: this.searchSymbolCtrl.value.symbol ?? this.nameCtrl.value,
type: this.typeCtrl.value,
unitPrice: this.unitPriceCtrl.value
};
if (this.data.activity.id) {
(activity as UpdateOrderDto).id = this.data.activity.id;
}
this.dialogRef.close({ activity });
}
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
this.data.transaction.dataSource = event.option.value.dataSource;
this.dataSourceCtrl.setValue(event.option.value.dataSource);
this.updateSymbol(event.option.value.symbol);
}
@ -148,18 +226,20 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
this.searchSymbolCtrl.setErrors(null);
this.data.transaction.symbol = symbol;
this.searchSymbolCtrl.setValue({ symbol });
this.changeDetectorRef.markForCheck();
this.dataService
.fetchSymbolItem({
dataSource: this.data.transaction.dataSource,
symbol: this.data.transaction.symbol
dataSource: this.dataSourceCtrl.value,
symbol: this.searchSymbolCtrl.value.symbol
})
.pipe(
catchError(() => {
this.data.transaction.currency = null;
this.data.transaction.dataSource = null;
this.data.transaction.unitPrice = null;
this.data.activity.currency = null;
this.data.activity.dataSource = null;
this.data.activity.unitPrice = null;
this.isLoading = false;
@ -170,8 +250,9 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
takeUntil(this.unsubscribeSubject)
)
.subscribe(({ currency, dataSource, marketPrice }) => {
this.data.transaction.currency = currency;
this.data.transaction.dataSource = dataSource;
this.currencyCtrl.setValue(currency);
this.dataSourceCtrl.setValue(dataSource);
this.currentMarketPrice = marketPrice;
this.isLoading = false;

79
apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html

@ -1,22 +1,29 @@
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100">
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update activity</h1>
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add activity</h1>
<h1 *ngIf="data.activity.id" mat-dialog-title i18n>Update activity</h1>
<h1 *ngIf="!data.activity.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">
<mat-label i18n>Type</mat-label>
<mat-select name="type" [formControl]="typeCtrl">
<mat-option value="BUY" i18n>BUY</mat-option>
<mat-option value="DIVIDEND" i18n>DIVIDEND</mat-option>
<mat-option value="ITEM" i18n>ITEM</mat-option>
<mat-option value="SELL" i18n>SELL</mat-option>
</mat-select>
</mat-form-field>
</div>
<div [ngClass]="{ 'd-none': !showAccountIdCtrl }">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account</mat-label>
<mat-select
name="accountId"
required
[(value)]="data.transaction.accountId"
>
<mat-select name="accountId" [formControl]="accountIdCtrl">
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
>{{ account.name }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
<div>
<div [ngClass]="{ 'd-none': !showSearchSymbolCtrl }">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Symbol or ISIN</mat-label>
<input
@ -24,7 +31,6 @@
autocomplete="off"
autocorrect="off"
matInput
required
[formControl]="searchSymbolCtrl"
[matAutocomplete]="autocomplete"
(blur)="onBlurSymbol()"
@ -48,15 +54,10 @@
<mat-spinner *ngIf="isLoading" matSuffix [diameter]="20"></mat-spinner>
</mat-form-field>
</div>
<div>
<div [ngClass]="{ 'd-none': !showNameCtrl }">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Type</mat-label>
<mat-select name="type" required [(value)]="data.transaction.type">
<mat-option value="BUY" i18n>BUY</mat-option>
<mat-option value="DIVIDEND" i18n>DIVIDEND</mat-option>
<mat-option value="ITEM" i18n>ITEM</mat-option>
<mat-option value="SELL" i18n>SELL</mat-option>
</mat-select>
<mat-label i18n>Name</mat-label>
<input matInput name="name" [formControl]="nameCtrl" />
</mat-form-field>
</div>
<div class="d-none">
@ -64,10 +65,8 @@
<mat-label i18n>Currency</mat-label>
<mat-select
class="no-arrow"
disabled
name="currency"
required
[(value)]="data.transaction.currency"
[formControl]="currencyCtrl"
>
<mat-option *ngFor="let currency of currencies" [value]="currency"
>{{ currency }}</mat-option
@ -78,25 +77,17 @@
<div class="d-none">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Data Source</mat-label>
<input
disabled
matInput
name="dataSource"
required
[(ngModel)]="data.transaction.dataSource"
/>
<input matInput name="dataSource" [formControl]="dataSourceCtrl" />
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Date</mat-label>
<input
disabled
matInput
name="date"
required
[formControl]="dateCtrl"
[matDatepicker]="date"
[(ngModel)]="data.transaction.date"
/>
<mat-datepicker-toggle matSuffix [for]="date">
<ion-icon
@ -114,9 +105,8 @@
<input
matInput
name="quantity"
required
type="number"
[(ngModel)]="data.transaction.quantity"
[formControl]="quantityCtrl"
/>
</mat-form-field>
</div>
@ -126,13 +116,12 @@
<input
matInput
name="unitPrice"
required
type="number"
[(ngModel)]="data.transaction.unitPrice"
[formControl]="unitPriceCtrl"
/>
<span class="ml-2" matSuffix>{{ data.transaction.currency }}</span>
<span class="ml-2" matSuffix>{{ currencyCtrl.value }}</span>
<button
*ngIf="currentMarketPrice && (data.transaction.type === 'BUY' || data.transaction.type === 'SELL')"
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
mat-icon-button
matSuffix
title="Apply current market price"
@ -145,23 +134,17 @@
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label>
<input
matInput
name="fee"
required
type="number"
[(ngModel)]="data.transaction.fee"
/>
<span class="ml-2" matSuffix>{{ data.transaction.currency }}</span>
<input matInput name="fee" type="number" [formControl]="feeCtrl" />
<span class="ml-2" matSuffix>{{ currencyCtrl.value }}</span>
</mat-form-field>
</div>
</div>
<div class="d-flex" mat-dialog-actions>
<gf-value
class="flex-grow-1"
[currency]="data.transaction.currency"
[currency]="currencyCtrl.value"
[locale]="data.user?.settings?.locale"
[value]="data.transaction.fee + (data.transaction.quantity * data.transaction.unitPrice)"
[value]="feeCtrl.value + (quantityCtrl.value * unitPriceCtrl.value) ?? 0"
></gf-value>
<div>
<button i18n mat-button (click)="onCancel()">Cancel</button>
@ -169,8 +152,8 @@
color="primary"
i18n
mat-flat-button
[disabled]="!(addTransactionForm.form.valid && data.transaction.currency && data.transaction.symbol)"
[mat-dialog-close]="data"
[disabled]="!(addTransactionForm.form.valid)"
(click)="onSubmit()"
>
Save
</button>

5
apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/interfaces/interfaces.ts

@ -1,9 +1,10 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { User } from '@ghostfolio/common/interfaces';
import { Account, Order } from '@prisma/client';
import { Account } from '@prisma/client';
export interface CreateOrUpdateTransactionDialogParams {
accountId: string;
accounts: Account[];
transaction: Order;
activity: Activity;
user: User;
}

32
apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts

@ -242,35 +242,13 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
});
}
public openUpdateTransactionDialog({
accountId,
currency,
dataSource,
date,
fee,
id,
quantity,
symbol,
type,
unitPrice
}: OrderModel): void {
public openUpdateTransactionDialog(activity: Activity): void {
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
data: {
activity,
accounts: this.user?.accounts?.filter((account) => {
return account.accountType === 'SECURITIES';
}),
transaction: {
accountId,
currency,
dataSource,
date,
fee,
id,
quantity,
symbol,
type,
unitPrice
},
user: this.user
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
@ -281,7 +259,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data: any) => {
const transaction: UpdateOrderDto = data?.transaction;
const transaction: UpdateOrderDto = data?.activity;
if (transaction) {
this.dataService
@ -336,7 +314,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
accounts: this.user?.accounts?.filter((account) => {
return account.accountType === 'SECURITIES';
}),
transaction: {
activity: {
accountId: aTransaction?.accountId ?? this.defaultAccountId,
currency: aTransaction?.currency ?? null,
dataSource: aTransaction?.dataSource ?? null,
@ -357,7 +335,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data: any) => {
const transaction: CreateOrderDto = data?.transaction;
const transaction: CreateOrderDto = data?.activity;
if (transaction) {
this.dataService.postOrder(transaction).subscribe({

2
apps/client/src/app/services/admin.service.ts

@ -6,7 +6,7 @@ import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { AdminMarketDataDetails } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client';
import { format, parseISO } from 'date-fns';
import { map, Observable } from 'rxjs';
import { Observable, map } from 'rxjs';
@Injectable({
providedIn: 'root'

6
libs/common/src/lib/helper.ts

@ -106,6 +106,12 @@ export function isGhostfolioScraperApiSymbol(aSymbol = '') {
return aSymbol.startsWith(ghostfolioScraperApiSymbolPrefix);
}
export function isUUID(aString: string) {
const regexExp =
/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi;
return regexExp.test(aString);
}
export function resetHours(aDate: Date) {
const year = getYear(aDate);
const month = getMonth(aDate);

7
libs/ui/src/lib/activities-table/activities-table.component.html

@ -115,7 +115,12 @@
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex align-items-center">
{{ element.SymbolProfile.symbol | gfSymbol }}
<span *ngIf="isUUID(element.SymbolProfile.symbol); else symbol">
{{ element.SymbolProfile.name }}
</span>
<ng-template #symbol>
{{ element.SymbolProfile.symbol | gfSymbol }}
</ng-template>
<span *ngIf="element.isDraft" class="badge badge-secondary ml-1" i18n
>Draft</span
>

8
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -21,6 +21,7 @@ import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
import { isUUID } from '@ghostfolio/common/helper';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { DataSource } from '@prisma/client';
import Big from 'big.js';
@ -69,6 +70,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
public filters: Observable<string[]> = this.filters$.asObservable();
public isAfter = isAfter;
public isLoading = true;
public isUUID = isUUID;
public placeholder = '';
public routeQueryParams: Subscription;
public searchControl = new FormControl();
@ -274,7 +276,11 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
fieldValues.add(activity.Account?.name);
fieldValues.add(activity.Account?.Platform?.name);
fieldValues.add(activity.SymbolProfile.currency);
fieldValues.add(activity.SymbolProfile.symbol);
fieldValues.add(
isUUID(activity.SymbolProfile.symbol)
? activity.SymbolProfile.name
: activity.SymbolProfile.symbol
);
fieldValues.add(activity.type);
fieldValues.add(format(activity.date, 'yyyy'));

Loading…
Cancel
Save