You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ghostfolio/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-d.../create-or-update-activity-d...

545 lines
19 KiB

import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { DataService } from '@ghostfolio/client/services/data.service';
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
import { getDateFormatString } from '@ghostfolio/common/helper';
import { translate } from '@ghostfolio/ui/i18n';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Inject,
OnDestroy,
ViewChild
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AssetClass, AssetSubClass, Tag, Type } from '@prisma/client';
import { isUUID } from 'class-validator';
import { isToday } from 'date-fns';
import { EMPTY, Observable, Subject, lastValueFrom, of } from 'rxjs';
import { catchError, delay, map, startWith, takeUntil } from 'rxjs/operators';
import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces';
@Component({
host: { class: 'h-100' },
selector: 'gf-create-or-update-activity-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./create-or-update-activity-dialog.scss'],
templateUrl: 'create-or-update-activity-dialog.html'
})
export class CreateOrUpdateActivityDialog implements OnDestroy {
@ViewChild('symbolAutocomplete') symbolAutocomplete;
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>;
public activityForm: FormGroup;
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
return { id: assetClass, label: translate(assetClass) };
});
public assetSubClasses = Object.keys(AssetSubClass).map((assetSubClass) => {
return { id: assetSubClass, label: translate(assetSubClass) };
});
public currencies: string[] = [];
public currentMarketPrice = null;
public defaultDateFormat: string;
public filteredTagsObservable: Observable<Tag[]> = of([]);
public isLoading = false;
public isToday = isToday;
public platforms: { id: string; name: string }[];
public separatorKeysCodes: number[] = [ENTER, COMMA];
public tags: Tag[] = [];
public total = 0;
public typesTranslationMap = new Map<Type, string>();
public Validators = Validators;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateActivityDialogParams,
private dataService: DataService,
private dateAdapter: DateAdapter<any>,
public dialogRef: MatDialogRef<CreateOrUpdateActivityDialog>,
private formBuilder: FormBuilder,
@Inject(MAT_DATE_LOCALE) private locale: string
) {}
public ngOnInit() {
this.locale = this.data.user?.settings?.locale;
this.dateAdapter.setLocale(this.locale);
const { currencies, platforms, tags } = this.dataService.fetchInfo();
this.currencies = currencies;
this.defaultDateFormat = getDateFormatString(this.locale);
this.platforms = platforms;
this.tags = tags.map(({ id, name }) => {
return {
id,
name: translate(name)
};
});
Object.keys(Type).forEach((type) => {
this.typesTranslationMap[Type[type]] = translate(Type[type]);
});
this.activityForm = this.formBuilder.group({
accountId: [this.data.activity?.accountId, Validators.required],
assetClass: [this.data.activity?.SymbolProfile?.assetClass],
assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass],
comment: [this.data.activity?.comment],
currency: [
this.data.activity?.SymbolProfile?.currency,
Validators.required
],
currencyOfUnitPrice: [
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],
feeInCustomCurrency: [this.data.activity?.fee, Validators.required],
name: [this.data.activity?.SymbolProfile?.name, Validators.required],
quantity: [this.data.activity?.quantity, Validators.required],
searchSymbol: [
!!this.data.activity?.SymbolProfile
? {
dataSource: this.data.activity?.SymbolProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol
}
: null,
Validators.required
],
tags: [
this.data.activity?.tags?.map(({ id, name }) => {
return {
id,
name: translate(name)
};
})
],
type: [undefined, Validators.required], // Set after value changes subscription
unitPrice: [this.data.activity?.unitPrice, Validators.required],
unitPriceInCustomCurrency: [
this.data.activity?.unitPrice,
Validators.required
],
updateAccountBalance: [false]
});
this.activityForm.valueChanges
.pipe(
// Slightly delay until the more specific form control value changes have
// completed
delay(300),
takeUntil(this.unsubscribeSubject)
)
.subscribe(async () => {
let exchangeRateOfUnitPrice = 1;
this.activityForm.get('feeInCustomCurrency').setErrors(null);
this.activityForm.get('unitPriceInCustomCurrency').setErrors(null);
const currency = this.activityForm.get('currency').value;
const currencyOfUnitPrice = this.activityForm.get(
'currencyOfUnitPrice'
).value;
const date = this.activityForm.get('date').value;
if (
currency &&
currencyOfUnitPrice &&
currency !== currencyOfUnitPrice &&
date
) {
try {
const { marketPrice } = await lastValueFrom(
this.dataService
.fetchExchangeRateForDate({
date,
symbol: `${currencyOfUnitPrice}-${currency}`
})
.pipe(takeUntil(this.unsubscribeSubject))
);
exchangeRateOfUnitPrice = marketPrice;
} catch {
this.activityForm.get('unitPriceInCustomCurrency').setErrors({
invalid: true
});
}
}
const feeInCustomCurrency =
this.activityForm.get('feeInCustomCurrency').value *
exchangeRateOfUnitPrice;
const unitPriceInCustomCurrency =
this.activityForm.get('unitPriceInCustomCurrency').value *
exchangeRateOfUnitPrice;
this.activityForm.get('fee').setValue(feeInCustomCurrency, {
emitEvent: false
});
this.activityForm.get('unitPrice').setValue(unitPriceInCustomCurrency, {
emitEvent: false
});
if (
this.activityForm.get('type').value === 'BUY' ||
this.activityForm.get('type').value === 'FEE' ||
this.activityForm.get('type').value === 'ITEM'
) {
this.total =
this.activityForm.get('quantity').value *
this.activityForm.get('unitPrice').value +
this.activityForm.get('fee').value ?? 0;
} else {
this.total =
this.activityForm.get('quantity').value *
this.activityForm.get('unitPrice').value -
this.activityForm.get('fee').value ?? 0;
}
this.changeDetectorRef.markForCheck();
});
this.activityForm.get('accountId').valueChanges.subscribe((accountId) => {
const type = this.activityForm.get('type').value;
if (
type === 'FEE' ||
type === 'INTEREST' ||
type === 'ITEM' ||
type === 'LIABILITY'
) {
const currency =
this.data.accounts.find(({ id }) => {
return id === accountId;
})?.currency ?? this.data.user.settings.baseCurrency;
this.activityForm.get('currency').setValue(currency);
this.activityForm.get('currencyOfUnitPrice').setValue(currency);
if (['FEE', 'INTEREST'].includes(type)) {
if (this.activityForm.get('accountId').value) {
this.activityForm.get('updateAccountBalance').enable();
} else {
this.activityForm.get('updateAccountBalance').disable();
this.activityForm.get('updateAccountBalance').setValue(false);
}
}
}
});
this.activityForm.get('date').valueChanges.subscribe(() => {
if (isToday(this.activityForm.get('date').value)) {
this.activityForm.get('updateAccountBalance').enable();
} else {
this.activityForm.get('updateAccountBalance').disable();
this.activityForm.get('updateAccountBalance').setValue(false);
}
this.changeDetectorRef.markForCheck();
});
this.activityForm.get('searchSymbol').valueChanges.subscribe(() => {
if (this.activityForm.get('searchSymbol').invalid) {
this.data.activity.SymbolProfile = null;
} else if (
['BUY', 'DIVIDEND', 'SELL'].includes(
this.activityForm.get('type').value
)
) {
this.activityForm
.get('dataSource')
.setValue(this.activityForm.get('searchSymbol').value.dataSource);
this.updateSymbol();
}
this.changeDetectorRef.markForCheck();
});
this.filteredTagsObservable = this.activityForm.controls[
'tags'
].valueChanges.pipe(
startWith(this.activityForm.get('tags').value),
map((aTags: Tag[] | null) => {
return aTags ? this.filterTags(aTags) : this.tags.slice();
})
);
this.activityForm
.get('type')
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((type: Type) => {
if (type === 'ITEM') {
this.activityForm
.get('accountId')
.removeValidators(Validators.required);
this.activityForm.get('accountId').updateValueAndValidity();
const currency =
this.data.accounts.find(({ id }) => {
return id === this.activityForm.get('accountId').value;
})?.currency ?? this.data.user.settings.baseCurrency;
this.activityForm.get('currency').setValue(currency);
this.activityForm.get('currencyOfUnitPrice').setValue(currency);
this.activityForm
.get('dataSource')
.removeValidators(Validators.required);
this.activityForm.get('dataSource').updateValueAndValidity();
this.activityForm.get('feeInCustomCurrency').reset();
this.activityForm.get('name').setValidators(Validators.required);
this.activityForm.get('name').updateValueAndValidity();
this.activityForm.get('quantity').setValue(1);
this.activityForm
.get('searchSymbol')
.removeValidators(Validators.required);
this.activityForm.get('searchSymbol').updateValueAndValidity();
this.activityForm.get('updateAccountBalance').disable();
this.activityForm.get('updateAccountBalance').setValue(false);
} else if (
type === 'FEE' ||
type === 'INTEREST' ||
type === 'LIABILITY'
) {
this.activityForm
.get('accountId')
.removeValidators(Validators.required);
this.activityForm.get('accountId').updateValueAndValidity();
const currency =
this.data.accounts.find(({ id }) => {
return id === this.activityForm.get('accountId').value;
})?.currency ?? this.data.user.settings.baseCurrency;
this.activityForm.get('currency').setValue(currency);
this.activityForm.get('currencyOfUnitPrice').setValue(currency);
this.activityForm
.get('dataSource')
.removeValidators(Validators.required);
this.activityForm.get('dataSource').updateValueAndValidity();
if (
(type === 'FEE' &&
this.activityForm.get('feeInCustomCurrency').value === 0) ||
type === 'INTEREST' ||
type === 'LIABILITY'
) {
this.activityForm.get('feeInCustomCurrency').reset();
}
this.activityForm.get('name').setValidators(Validators.required);
this.activityForm.get('name').updateValueAndValidity();
if (type === 'FEE') {
this.activityForm.get('quantity').setValue(0);
} else if (type === 'INTEREST' || type === 'LIABILITY') {
this.activityForm.get('quantity').setValue(1);
}
this.activityForm
.get('searchSymbol')
.removeValidators(Validators.required);
this.activityForm.get('searchSymbol').updateValueAndValidity();
if (type === 'FEE') {
this.activityForm.get('unitPriceInCustomCurrency').setValue(0);
}
if (
['FEE', 'INTEREST'].includes(type) &&
this.activityForm.get('accountId').value
) {
this.activityForm.get('updateAccountBalance').enable();
} else {
this.activityForm.get('updateAccountBalance').disable();
this.activityForm.get('updateAccountBalance').setValue(false);
}
} else {
this.activityForm.get('accountId').setValidators(Validators.required);
this.activityForm.get('accountId').updateValueAndValidity();
this.activityForm
.get('dataSource')
.setValidators(Validators.required);
this.activityForm.get('dataSource').updateValueAndValidity();
this.activityForm.get('name').removeValidators(Validators.required);
this.activityForm.get('name').updateValueAndValidity();
this.activityForm
.get('searchSymbol')
.setValidators(Validators.required);
this.activityForm.get('searchSymbol').updateValueAndValidity();
this.activityForm.get('updateAccountBalance').enable();
}
this.changeDetectorRef.markForCheck();
});
this.activityForm.get('type').setValue(this.data.activity?.type);
if (this.data.activity?.id) {
this.activityForm.get('searchSymbol').disable();
this.activityForm.get('type').disable();
}
if (this.data.activity?.SymbolProfile?.symbol) {
this.dataService
.fetchSymbolItem({
dataSource: this.data.activity?.SymbolProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketPrice }) => {
this.currentMarketPrice = marketPrice;
this.changeDetectorRef.markForCheck();
});
}
}
public applyCurrentMarketPrice() {
this.activityForm.patchValue({
currencyOfUnitPrice: this.activityForm.get('currency').value,
unitPriceInCustomCurrency: this.currentMarketPrice
});
}
public onAddTag(event: MatAutocompleteSelectedEvent) {
this.activityForm.get('tags').setValue([
...(this.activityForm.get('tags').value ?? []),
this.tags.find(({ id }) => {
return id === event.option.value;
})
]);
this.tagInput.nativeElement.value = '';
}
public onCancel() {
this.dialogRef.close();
}
public onRemoveTag(aTag: Tag) {
this.activityForm.get('tags').setValue(
this.activityForm.get('tags').value.filter(({ id }) => {
return id !== aTag.id;
})
);
}
public async onSubmit() {
const activity: CreateOrderDto | UpdateOrderDto = {
accountId: this.activityForm.get('accountId').value,
assetClass: this.activityForm.get('assetClass').value,
assetSubClass: this.activityForm.get('assetSubClass').value,
comment: this.activityForm.get('comment').value,
currency: this.activityForm.get('currency').value,
customCurrency: this.activityForm.get('currencyOfUnitPrice').value,
date: this.activityForm.get('date').value,
dataSource: this.activityForm.get('dataSource').value,
fee: this.activityForm.get('fee').value,
quantity: this.activityForm.get('quantity').value,
symbol:
this.activityForm.get('searchSymbol').value?.symbol === undefined ||
isUUID(this.activityForm.get('searchSymbol').value?.symbol)
? this.activityForm.get('name').value
: this.activityForm.get('searchSymbol').value.symbol,
tags: this.activityForm.get('tags').value,
type: this.activityForm.get('type').value,
unitPrice: this.activityForm.get('unitPrice').value
};
try {
if (this.data.activity.id) {
(activity as UpdateOrderDto).id = this.data.activity.id;
await validateObjectForForm({
classDto: UpdateOrderDto,
form: this.activityForm,
ignoreFields: ['dataSource', 'date'],
object: activity as UpdateOrderDto
});
} else {
(activity as CreateOrderDto).updateAccountBalance =
this.activityForm.get('updateAccountBalance').value;
await validateObjectForForm({
classDto: CreateOrderDto,
form: this.activityForm,
ignoreFields: ['dataSource', 'date'],
object: activity
});
}
this.dialogRef.close({ activity });
} catch (error) {
console.error(error);
}
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private filterTags(aTags: Tag[]) {
const tagIds = aTags.map((tag) => {
return tag.id;
});
return this.tags.filter((tag) => {
return !tagIds.includes(tag.id);
});
}
private updateSymbol() {
this.isLoading = true;
this.changeDetectorRef.markForCheck();
this.dataService
.fetchSymbolItem({
dataSource: this.activityForm.get('dataSource').value,
symbol: this.activityForm.get('searchSymbol').value.symbol
})
.pipe(
catchError(() => {
this.data.activity.SymbolProfile = null;
this.isLoading = false;
this.changeDetectorRef.markForCheck();
return EMPTY;
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(({ currency, dataSource, marketPrice }) => {
this.activityForm.get('currency').setValue(currency);
this.activityForm.get('currencyOfUnitPrice').setValue(currency);
this.activityForm.get('dataSource').setValue(dataSource);
this.currentMarketPrice = marketPrice;
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
}