Browse Source

Refactor to FormGroup

pull/666/head
Thomas 3 years ago
parent
commit
8e0d53ebf1
  1. 4
      apps/api/src/app/order/order.service.ts
  2. 3
      apps/api/src/app/order/update-order.dto.ts
  3. 214
      apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts
  4. 76
      apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html

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

@ -203,6 +203,10 @@ export class OrderService {
}): Promise<Order> {
const { data, where } = params;
if (data.Account.connect.id_userId.id === null) {
delete data.Account;
}
if (data.type === 'ITEM') {
const name = data.symbol;

3
apps/api/src/app/order/update-order.dto.ts

@ -1,7 +1,8 @@
import { DataSource, Type } from '@prisma/client';
import { IsISO8601, IsNumber, IsString } from 'class-validator';
import { IsISO8601, IsNumber, IsOptional, IsString } from 'class-validator';
export class UpdateOrderDto {
@IsOptional()
@IsString()
accountId: string;

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

@ -6,7 +6,7 @@ import {
OnDestroy,
ViewChild
} from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { FormBuilder, FormGroup, 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';
@ -14,6 +14,7 @@ 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 { isUUID } from 'class-validator';
import { isString } from 'lodash';
import { EMPTY, Observable, Subject } from 'rxjs';
import {
@ -37,25 +38,15 @@ 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 activityForm: FormGroup;
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 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);
public Validators = Validators;
private unsubscribeSubject = new Subject<void>();
@ -63,6 +54,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
public dialogRef: MatDialogRef<CreateOrUpdateTransactionDialog>,
private formBuilder: FormBuilder,
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTransactionDialogParams
) {}
@ -72,78 +64,98 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
this.currencies = currencies;
this.platforms = platforms;
this.filteredLookupItemsObservable =
this.searchSymbolCtrl.valueChanges.pipe(
startWith(''),
debounceTime(400),
distinctUntilChanged(),
switchMap((query: string) => {
if (isString(query)) {
const filteredLookupItemsObservable =
this.dataService.fetchSymbols(query);
filteredLookupItemsObservable.subscribe((filteredLookupItems) => {
this.filteredLookupItems = filteredLookupItems;
});
return filteredLookupItemsObservable;
}
this.activityForm = this.formBuilder.group({
accountId: [this.data.activity?.accountId, Validators.required],
currency: [
this.data.activity?.SymbolProfile?.currency,
Validators.required
],
dataSource: [
this.data.activity?.SymbolProfile?.dataSource,
Validators.required
],
date: [this.data.activity?.date, Validators.required],
fee: [this.data.activity?.fee, Validators.required],
name: [this.data.activity?.SymbolProfile?.name, Validators.required],
quantity: [this.data.activity?.quantity, Validators.required],
searchSymbol: [
{
dataSource: this.data.activity?.SymbolProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol
},
Validators.required
],
type: [undefined, Validators.required], // Set after value changes subscription
unitPrice: [this.data.activity?.unitPrice, Validators.required]
});
return [];
})
);
this.filteredLookupItemsObservable = this.activityForm.controls[
'searchSymbol'
].valueChanges.pipe(
startWith(''),
debounceTime(400),
distinctUntilChanged(),
switchMap((query: string) => {
if (isString(query)) {
const filteredLookupItemsObservable =
this.dataService.fetchSymbols(query);
filteredLookupItemsObservable.subscribe((filteredLookupItems) => {
this.filteredLookupItems = filteredLookupItems;
});
return filteredLookupItemsObservable;
}
return [];
})
);
this.typeCtrl.valueChanges.subscribe((type: Type) => {
this.activityForm.controls['type'].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;
this.activityForm.controls['accountId'].removeValidators(
Validators.required
);
this.activityForm.controls['accountId'].updateValueAndValidity();
this.activityForm.controls['currency'].setValue(
this.data.user.settings.baseCurrency
);
this.activityForm.controls['dataSource'].removeValidators(
Validators.required
);
this.activityForm.controls['dataSource'].updateValueAndValidity();
this.activityForm.controls['name'].setValidators(Validators.required);
this.activityForm.controls['name'].updateValueAndValidity();
this.activityForm.controls['quantity'].setValue(1);
this.activityForm.controls['searchSymbol'].removeValidators(
Validators.required
);
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
} 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.activityForm.controls['accountId'].setValidators(
Validators.required
);
this.activityForm.controls['accountId'].updateValueAndValidity();
this.activityForm.controls['dataSource'].setValidators(
Validators.required
);
this.activityForm.controls['dataSource'].updateValueAndValidity();
this.activityForm.controls['name'].removeValidators(
Validators.required
);
this.activityForm.controls['name'].updateValueAndValidity();
this.activityForm.controls['searchSymbol'].setValidators(
Validators.required
);
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
}
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);
this.activityForm.controls['type'].setValue(this.data.activity?.type);
if (this.data.activity?.id) {
this.searchSymbolCtrl.disable();
this.typeCtrl.disable();
this.activityForm.controls['searchSymbol'].disable();
this.activityForm.controls['type'].disable();
}
if (this.data.activity?.symbol) {
@ -162,7 +174,9 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
}
public applyCurrentMarketPrice() {
this.unitPriceCtrl.setValue(this.currentMarketPrice);
this.activityForm.patchValue({
unitPrice: this.currentMarketPrice
});
}
public displayFn(aLookupItem: LookupItem) {
@ -171,13 +185,16 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
public onBlurSymbol() {
const currentLookupItem = this.filteredLookupItems.find((lookupItem) => {
return lookupItem.symbol === this.data.activity.symbol;
return (
lookupItem.symbol ===
this.activityForm.controls['searchSymbol'].value.symbol
);
});
if (currentLookupItem) {
this.updateSymbol(currentLookupItem.symbol);
} else {
this.searchSymbolCtrl.setErrors({ incorrect: true });
this.activityForm.controls['searchSymbol'].setErrors({ incorrect: true });
this.data.activity.currency = null;
this.data.activity.dataSource = null;
@ -193,15 +210,17 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
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
accountId: this.activityForm.controls['accountId'].value,
currency: this.activityForm.controls['currency'].value,
date: this.activityForm.controls['date'].value,
dataSource: this.activityForm.controls['dataSource'].value,
fee: this.activityForm.controls['fee'].value,
quantity: this.activityForm.controls['quantity'].value,
symbol: isUUID(this.activityForm.controls['searchSymbol'].value.symbol)
? this.activityForm.controls['name'].value
: this.activityForm.controls['searchSymbol'].value.symbol,
type: this.activityForm.controls['type'].value,
unitPrice: this.activityForm.controls['unitPrice'].value
};
if (this.data.activity.id) {
@ -212,7 +231,9 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
}
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
this.dataSourceCtrl.setValue(event.option.value.dataSource);
this.activityForm.controls['dataSource'].setValue(
event.option.value.dataSource
);
this.updateSymbol(event.option.value.symbol);
}
@ -224,16 +245,15 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
private updateSymbol(symbol: string) {
this.isLoading = true;
this.searchSymbolCtrl.setErrors(null);
this.searchSymbolCtrl.setValue({ symbol });
this.activityForm.controls['searchSymbol'].setErrors(null);
this.activityForm.controls['searchSymbol'].setValue({ symbol });
this.changeDetectorRef.markForCheck();
this.dataService
.fetchSymbolItem({
dataSource: this.dataSourceCtrl.value,
symbol: this.searchSymbolCtrl.value.symbol
dataSource: this.activityForm.controls['dataSource'].value,
symbol: this.activityForm.controls['searchSymbol'].value.symbol
})
.pipe(
catchError(() => {
@ -250,8 +270,8 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
takeUntil(this.unsubscribeSubject)
)
.subscribe(({ currency, dataSource, marketPrice }) => {
this.currencyCtrl.setValue(currency);
this.dataSourceCtrl.setValue(dataSource);
this.activityForm.controls['currency'].setValue(currency);
this.activityForm.controls['dataSource'].setValue(dataSource);
this.currentMarketPrice = marketPrice;

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

@ -1,11 +1,15 @@
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100">
<form
class="d-flex flex-column h-100"
[formGroup]="activityForm"
(ngSubmit)="onSubmit()"
>
<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-select formControlName="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>
@ -13,25 +17,29 @@
</mat-select>
</mat-form-field>
</div>
<div [ngClass]="{ 'd-none': !showAccountIdCtrl }">
<div
[ngClass]="{ 'd-none': !activityForm.controls['accountId'].hasValidator(Validators.required) }"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account</mat-label>
<mat-select name="accountId" [formControl]="accountIdCtrl">
<mat-select formControlName="accountId">
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
>{{ account.name }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
<div [ngClass]="{ 'd-none': !showSearchSymbolCtrl }">
<div
[ngClass]="{ 'd-none': !activityForm.controls['searchSymbol'].hasValidator(Validators.required) }"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Symbol or ISIN</mat-label>
<input
autocapitalize="off"
autocomplete="off"
autocorrect="off"
formControlName="searchSymbol"
matInput
[formControl]="searchSymbolCtrl"
[matAutocomplete]="autocomplete"
(blur)="onBlurSymbol()"
/>
@ -54,20 +62,18 @@
<mat-spinner *ngIf="isLoading" matSuffix [diameter]="20"></mat-spinner>
</mat-form-field>
</div>
<div [ngClass]="{ 'd-none': !showNameCtrl }">
<div
[ngClass]="{ 'd-none': !activityForm.controls['name'].hasValidator(Validators.required) }"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name</mat-label>
<input matInput name="name" [formControl]="nameCtrl" />
<input formControlName="name" matInput />
</mat-form-field>
</div>
<div class="d-none">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Currency</mat-label>
<mat-select
class="no-arrow"
name="currency"
[formControl]="currencyCtrl"
>
<mat-select class="no-arrow" formControlName="currency">
<mat-option *ngFor="let currency of currencies" [value]="currency"
>{{ currency }}</mat-option
>
@ -77,18 +83,13 @@
<div class="d-none">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Data Source</mat-label>
<input matInput name="dataSource" [formControl]="dataSourceCtrl" />
<input formControlName="dataSource" matInput />
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Date</mat-label>
<input
matInput
name="date"
[formControl]="dateCtrl"
[matDatepicker]="date"
/>
<input formControlName="date" matInput [matDatepicker]="date" />
<mat-datepicker-toggle matSuffix [for]="date">
<ion-icon
class="text-muted"
@ -102,29 +103,22 @@
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Quantity</mat-label>
<input
matInput
name="quantity"
type="number"
[formControl]="quantityCtrl"
/>
<input formControlName="quantity" matInput type="number" />
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Unit Price</mat-label>
<input
matInput
name="unitPrice"
type="number"
[formControl]="unitPriceCtrl"
/>
<span class="ml-2" matSuffix>{{ currencyCtrl.value }}</span>
<input formControlName="unitPrice" matInput type="number" />
<span class="ml-2" matSuffix
>{{ activityForm.controls['currency'].value }}</span
>
<button
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
mat-icon-button
matSuffix
title="Apply current market price"
type="button"
(click)="applyCurrentMarketPrice()"
>
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
@ -134,26 +128,28 @@
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label>
<input matInput name="fee" type="number" [formControl]="feeCtrl" />
<span class="ml-2" matSuffix>{{ currencyCtrl.value }}</span>
<input formControlName="fee" matInput type="number" />
<span class="ml-2" matSuffix
>{{ activityForm.controls['currency'].value }}</span
>
</mat-form-field>
</div>
</div>
<div class="d-flex" mat-dialog-actions>
<gf-value
class="flex-grow-1"
[currency]="currencyCtrl.value"
[currency]="activityForm.controls['currency'].value"
[locale]="data.user?.settings?.locale"
[value]="feeCtrl.value + (quantityCtrl.value * unitPriceCtrl.value) ?? 0"
[value]="activityForm.controls['fee'].value + (activityForm.controls['quantity'].value * activityForm.controls['unitPrice'].value) ?? 0"
></gf-value>
<div>
<button i18n mat-button (click)="onCancel()">Cancel</button>
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
<button
color="primary"
i18n
mat-flat-button
[disabled]="!(addTransactionForm.form.valid)"
(click)="onSubmit()"
type="submit"
[disabled]="!activityForm.valid"
>
Save
</button>

Loading…
Cancel
Save