, enumObj: E): keyof E {
- return isNil(value) ? '' : getEnumKey(enumObj, value);
+ return value && enumObj ? getEnumKey(enumObj, value) : '';
}
}
diff --git a/src/app/shared/services/domain-metadata-form-extensions/domain-metadata-form-extensions.service.ts b/src/app/shared/services/domain-metadata-form-extensions/domain-metadata-form-extensions.service.ts
index 9d9ee33a..84887c77 100644
--- a/src/app/shared/services/domain-metadata-form-extensions/domain-metadata-form-extensions.service.ts
+++ b/src/app/shared/services/domain-metadata-form-extensions/domain-metadata-form-extensions.service.ts
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
-import { DomainObject } from '@vality/domain-proto/lib/domain';
+import { DomainObject, Cash } from '@vality/domain-proto/lib/domain';
import { Field } from '@vality/thrift-ts';
import moment from 'moment';
import { from, Observable, of } from 'rxjs';
@@ -8,6 +8,8 @@ import * as short from 'short-uuid';
import { ThriftAstMetadata } from '@cc/app/api/utils';
+import { Cash as CashField } from '../../../../components/cash-field';
+import { toMajor, toMinor } from '../../../../utils';
import { DomainStoreService } from '../../../thrift-services/damsel/domain-store.service';
import {
MetadataFormData,
@@ -38,7 +40,46 @@ export class DomainMetadataFormExtensionsService {
},
{
determinant: (data) => of(isTypeWithAliases(data, 'Timestamp', 'base')),
- extension: () => of({ type: 'datetime', generate: () => of(moment()) }),
+ extension: () =>
+ of({ type: 'datetime', generate: () => of(moment().toISOString()) }),
+ },
+ {
+ determinant: (data) => of(isTypeWithAliases(data, 'Cash', 'domain')),
+ extension: () =>
+ this.domainStoreService.getObjects('currency').pipe(
+ map((currencies) => ({
+ type: 'cash',
+ converter: {
+ internalToOutput: (cash: CashField): Cash =>
+ cash
+ ? {
+ amount: toMinor(
+ cash.amount,
+ currencies.find(
+ (c) =>
+ c.data.symbolic_code === cash.currencyCode
+ )?.data?.exponent
+ ),
+ currency: { symbolic_code: cash.currencyCode },
+ }
+ : null,
+ outputToInternal: (cash: Cash) =>
+ cash
+ ? {
+ amount: toMajor(
+ cash.amount,
+ currencies.find(
+ (c) =>
+ c.data.symbolic_code ===
+ cash.currency.symbolic_code
+ )?.data?.exponent
+ ),
+ currency: cash.currency.symbolic_code,
+ }
+ : null,
+ },
+ }))
+ ),
},
]),
shareReplay(1)
diff --git a/src/components/cash-field/cash-field.component.html b/src/components/cash-field/cash-field.component.html
new file mode 100644
index 00000000..c1fdc573
--- /dev/null
+++ b/src/components/cash-field/cash-field.component.html
@@ -0,0 +1,33 @@
+
+
+ {{ label || 'Amount' }}
+ {{ prefix }}
+
+
+
+ Currency
+
+
+
+ {{ currency.data.symbolic_code }} ({{ currency.data.name }})
+
+
+
+
diff --git a/src/components/cash-field/cash-field.component.ts b/src/components/cash-field/cash-field.component.ts
new file mode 100644
index 00000000..ed3830ec
--- /dev/null
+++ b/src/components/cash-field/cash-field.component.ts
@@ -0,0 +1,120 @@
+import { getCurrencySymbol } from '@angular/common';
+import { Component, Input, Injector, Inject, LOCALE_ID, OnInit } from '@angular/core';
+import { Validator, ValidationErrors, FormControl, FormBuilder } from '@angular/forms';
+import { createMask } from '@ngneat/input-mask';
+import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
+import { FormComponentSuperclass } from '@s-libs/ng-core';
+import { coerceBoolean } from 'coerce-property';
+import sortBy from 'lodash-es/sortBy';
+import { combineLatest } from 'rxjs';
+import { map, switchMap, first, distinctUntilChanged } from 'rxjs/operators';
+
+import { DomainStoreService } from '../../app/thrift-services/damsel/domain-store.service';
+import { createControlProviders, getFormValueChanges } from '../../utils';
+
+export interface Cash {
+ amount: number;
+ currencyCode: string;
+}
+
+const GROUP_SEPARATOR = ' ';
+
+@UntilDestroy()
+@Component({
+ selector: 'cc-cash-field',
+ templateUrl: './cash-field.component.html',
+ providers: createControlProviders(CashFieldComponent),
+})
+export class CashFieldComponent extends FormComponentSuperclass implements Validator, OnInit {
+ @Input() label?: string;
+ @Input() @coerceBoolean required: boolean = false;
+
+ amountControl = new FormControl(null);
+ currencyCodeControl = new FormControl(null);
+
+ currencies$ = combineLatest([
+ getFormValueChanges(this.currencyCodeControl, true),
+ this.domainStoreService.getObjects('currency'),
+ ]).pipe(
+ map(([code, currencies]) =>
+ sortBy(currencies, 'data', 'symbolic_code').filter(
+ (c) =>
+ c.data.symbolic_code.toUpperCase().includes(code) || c.data.name.includes(code)
+ )
+ )
+ );
+
+ amountMask$ = getFormValueChanges(this.currencyCodeControl, true).pipe(
+ switchMap((code) => this.getCurrencyByCode(code)),
+ map((c) => c?.data?.exponent || 2),
+ distinctUntilChanged(),
+ map((digits) =>
+ createMask({
+ alias: 'numeric',
+ groupSeparator: GROUP_SEPARATOR,
+ digits,
+ digitsOptional: true,
+ placeholder: '',
+ })
+ )
+ );
+ currencyMask = createMask({ mask: 'AAA', placeholder: '' });
+
+ get prefix() {
+ return getCurrencySymbol(this.currencyCodeControl.value, 'narrow', this._locale);
+ }
+
+ constructor(
+ injector: Injector,
+ @Inject(LOCALE_ID) private _locale: string,
+ private domainStoreService: DomainStoreService,
+ private fb: FormBuilder
+ ) {
+ super(injector);
+ }
+
+ ngOnInit() {
+ combineLatest([
+ getFormValueChanges(this.currencyCodeControl, true),
+ getFormValueChanges(this.amountControl, true),
+ ])
+ .pipe(
+ switchMap(([currencyCode]) => this.getCurrencyByCode(currencyCode)),
+ untilDestroyed(this)
+ )
+ .subscribe((currency) => {
+ const amountStr = this.amountControl.value;
+ if (amountStr && currency && !this.validate()) {
+ const [whole, fractional] = amountStr.split('.');
+ if (fractional?.length > currency.data.exponent)
+ this.amountControl.setValue(
+ `${whole}.${fractional.slice(0, currency.data.exponent)}`
+ );
+ const amount = Number(this.amountControl.value.replaceAll(GROUP_SEPARATOR, ''));
+ this.emitOutgoingValue({ amount, currencyCode: currency.data.symbolic_code });
+ } else {
+ this.emitOutgoingValue(null);
+ }
+ });
+ }
+
+ validate(): ValidationErrors | null {
+ return !this.amountControl.value || this.currencyCodeControl.value?.length !== 3
+ ? { invalidCash: true }
+ : null;
+ }
+
+ handleIncomingValue(value: Cash) {
+ this.amountControl.setValue(
+ typeof value?.amount === 'number' ? String(value.amount) : null
+ );
+ this.currencyCodeControl.setValue(value?.currencyCode);
+ }
+
+ private getCurrencyByCode(currencyCode: string) {
+ return this.domainStoreService.getObjects('currency').pipe(
+ map((c) => c.find((v) => v.data.symbolic_code === currencyCode)),
+ first()
+ );
+ }
+}
diff --git a/src/components/cash-field/cash-field.module.ts b/src/components/cash-field/cash-field.module.ts
new file mode 100644
index 00000000..12d5bb36
--- /dev/null
+++ b/src/components/cash-field/cash-field.module.ts
@@ -0,0 +1,24 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FlexModule } from '@angular/flex-layout';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatAutocompleteModule } from '@angular/material/autocomplete';
+import { MatInputModule } from '@angular/material/input';
+import { InputMaskModule } from '@ngneat/input-mask';
+
+import { CashFieldComponent } from './cash-field.component';
+
+@NgModule({
+ declarations: [CashFieldComponent],
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatInputModule,
+ FlexModule,
+ MatAutocompleteModule,
+ InputMaskModule,
+ ReactiveFormsModule,
+ ],
+ exports: [CashFieldComponent],
+})
+export class CashModule {}
diff --git a/src/components/cash-field/index.ts b/src/components/cash-field/index.ts
new file mode 100644
index 00000000..5f89918d
--- /dev/null
+++ b/src/components/cash-field/index.ts
@@ -0,0 +1,2 @@
+export * from './cash-field.module';
+export * from './cash-field.component';
diff --git a/src/utils/forms/get-form-value-changes.ts b/src/utils/forms/get-form-value-changes.ts
index 143bad68..36cb4a36 100644
--- a/src/utils/forms/get-form-value-changes.ts
+++ b/src/utils/forms/get-form-value-changes.ts
@@ -1,14 +1,16 @@
-import { AbstractControl } from '@angular/forms';
+import { FormControl } from '@angular/forms';
+import { FormArray, FormGroup } from '@ngneat/reactive-forms';
import { Observable } from 'rxjs';
-import { map, startWith } from 'rxjs/operators';
+import { startWith, map } from 'rxjs/operators';
import { getValue } from './get-value';
-export function getFormValueChanges(form: AbstractControl, hasStart = false): Observable {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- return form.valueChanges.pipe(
+export function getFormValueChanges(
+ form: FormControl | FormArray | FormGroup,
+ hasStart = false
+): Observable {
+ return (form.valueChanges as Observable).pipe(
...((hasStart ? [startWith(form.value)] : []) as []),
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- map(() => getValue(form))
+ map(() => getValue(form) as T)
);
}
diff --git a/src/utils/get-enum-keys.ts b/src/utils/get-enum-keys.ts
index e2b6d9bc..09de9420 100644
--- a/src/utils/get-enum-keys.ts
+++ b/src/utils/get-enum-keys.ts
@@ -28,7 +28,7 @@ export function getEnumKey>(
srcEnum: E,
value: ValuesType
): keyof E {
- return getEnumKeyValues(srcEnum).find((e) => e.value === String(value)).key;
+ return getEnumKeyValues(srcEnum).find((e) => e.value === String(value))?.key;
}
export function enumHasValue>(
diff --git a/src/utils/to-major.ts b/src/utils/to-major.ts
index b70cf65c..308c6fe0 100644
--- a/src/utils/to-major.ts
+++ b/src/utils/to-major.ts
@@ -1,4 +1,7 @@
import isNil from 'lodash-es/isNil';
import round from 'lodash-es/round';
-export const toMajor = (amount: number): number => (isNil(amount) ? null : round(amount / 100, 2));
+export const toMajor = (amount: number, exponent = 2): number => {
+ if (isNil(amount)) return null;
+ return round(amount / 10 ** exponent, exponent);
+};
diff --git a/src/utils/to-minor.ts b/src/utils/to-minor.ts
index 39a9fde6..2d1aa49a 100644
--- a/src/utils/to-minor.ts
+++ b/src/utils/to-minor.ts
@@ -1 +1,2 @@
-export const toMinor = (amount: number): number => Math.round(amount * 100);
+export const toMinor = (amount: number, exponent = 2): number =>
+ Math.round(amount * 10 ** exponent);