From 32fd665ba1df268002d213b8728c82fbb29d9b16 Mon Sep 17 00:00:00 2001 From: Rinat Arsaev <11846445+A77AY@users.noreply.github.com> Date: Tue, 13 Sep 2022 20:05:16 +0300 Subject: [PATCH] IMP-43: Cash input (#132) --- package-lock.json | 45 +++++++ package.json | 3 + .../withdrawals/withdrawals.component.html | 4 +- .../extension-field.component.html | 8 ++ .../extension-field.component.ts | 73 +++++++++++ .../metadata-form.component.html | 59 +++++---- .../metadata-form/metadata-form.module.ts | 4 + .../types/metadata-form-extension.ts | 8 +- src/app/shared/pipes/amount-currency.pipe.ts | 4 +- src/app/shared/pipes/enum-key.pipe.ts | 3 +- ...domain-metadata-form-extensions.service.ts | 45 ++++++- .../cash-field/cash-field.component.html | 33 +++++ .../cash-field/cash-field.component.ts | 120 ++++++++++++++++++ .../cash-field/cash-field.module.ts | 24 ++++ src/components/cash-field/index.ts | 2 + src/utils/forms/get-form-value-changes.ts | 16 ++- src/utils/get-enum-keys.ts | 2 +- src/utils/to-major.ts | 5 +- src/utils/to-minor.ts | 3 +- 19 files changed, 421 insertions(+), 40 deletions(-) create mode 100644 src/app/shared/components/metadata-form/components/extension-field/extension-field.component.html create mode 100644 src/app/shared/components/metadata-form/components/extension-field/extension-field.component.ts create mode 100644 src/components/cash-field/cash-field.component.html create mode 100644 src/components/cash-field/cash-field.component.ts create mode 100644 src/components/cash-field/cash-field.module.ts create mode 100644 src/components/cash-field/index.ts diff --git a/package-lock.json b/package-lock.json index 50e830bf..007ca77c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@angular/platform-browser-dynamic": "14.0.4", "@angular/platform-server": "14.0.4", "@angular/router": "14.0.4", + "@ngneat/input-mask": "5.2.0", "@ngneat/reactive-forms": "5.0.0", "@ngneat/until-destroy": "9.0.0", "@s-libs/js-core": "14.0.0", @@ -46,6 +47,7 @@ "css-element-queries": "1.2.3", "element-resize-detector": "1.2.4", "humanize-duration": "3.21.0", + "inputmask": "5.0.7", "jsonc-parser": "2.0.2", "keycloak-angular": "12.0.0", "keycloak-js": "18.0.1", @@ -72,6 +74,7 @@ "@angular/compiler-cli": "14.0.4", "@types/element-resize-detector": "1.1.3", "@types/humanize-duration": "3.18.0", + "@types/inputmask": "5.0.3", "@types/jasmine": "4.0.3", "@types/jwt-decode": "2.2.1", "@types/lodash-es": "4.17.6", @@ -3529,6 +3532,18 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "node_modules/@ngneat/input-mask": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@ngneat/input-mask/-/input-mask-5.2.0.tgz", + "integrity": "sha512-zrKjhAYe+zvvqQOs0sappBhO+iPDlZq4OIIp4WD78hS0tYQzHZnScrUk4l7ygTzkWgolIdL+PxBJEcivFDg6xQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": ">=13.0.0", + "inputmask": "^5.0.5" + } + }, "node_modules/@ngneat/reactive-forms": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@ngneat/reactive-forms/-/reactive-forms-5.0.0.tgz", @@ -4697,6 +4712,12 @@ "integrity": "sha512-11QHl+GvEQ5TlCjA9xqQKNv4S0P8XFq5uHeZe2UPjngESBl7tA1tai/60eEYwWMFWIyQOl7ybarYF0B33K3Qtg==", "dev": true }, + "node_modules/@types/inputmask": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.3.tgz", + "integrity": "sha512-9aTXacRhf9D3+porVydLR28ll8/y3TQIrXEXv7ZWY0c3Lzl/1r4nHoaesZeh2Fd+UIefpWZrg2tkSnDn/dKUsw==", + "dev": true + }, "node_modules/@types/jasmine": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", @@ -13575,6 +13596,11 @@ "tslib": "^2.0.0" } }, + "node_modules/inputmask": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.7.tgz", + "integrity": "sha512-rUxbRDS25KEib+c/Ow+K01oprU/+EK9t9SOPC8ov94/ftULGDqj1zOgRU/Hko6uzoKRMdwCfuhAafJ/Wk2wffQ==" + }, "node_modules/inquirer": { "version": "8.2.4", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", @@ -25007,6 +25033,14 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "@ngneat/input-mask": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@ngneat/input-mask/-/input-mask-5.2.0.tgz", + "integrity": "sha512-zrKjhAYe+zvvqQOs0sappBhO+iPDlZq4OIIp4WD78hS0tYQzHZnScrUk4l7ygTzkWgolIdL+PxBJEcivFDg6xQ==", + "requires": { + "tslib": "^2.0.0" + } + }, "@ngneat/reactive-forms": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@ngneat/reactive-forms/-/reactive-forms-5.0.0.tgz", @@ -25936,6 +25970,12 @@ "integrity": "sha512-11QHl+GvEQ5TlCjA9xqQKNv4S0P8XFq5uHeZe2UPjngESBl7tA1tai/60eEYwWMFWIyQOl7ybarYF0B33K3Qtg==", "dev": true }, + "@types/inputmask": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.3.tgz", + "integrity": "sha512-9aTXacRhf9D3+porVydLR28ll8/y3TQIrXEXv7ZWY0c3Lzl/1r4nHoaesZeh2Fd+UIefpWZrg2tkSnDn/dKUsw==", + "dev": true + }, "@types/jasmine": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", @@ -32799,6 +32839,11 @@ "tslib": "^2.0.0" } }, + "inputmask": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.7.tgz", + "integrity": "sha512-rUxbRDS25KEib+c/Ow+K01oprU/+EK9t9SOPC8ov94/ftULGDqj1zOgRU/Hko6uzoKRMdwCfuhAafJ/Wk2wffQ==" + }, "inquirer": { "version": "8.2.4", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", diff --git a/package.json b/package.json index af7e184b..1c91198c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@angular/platform-browser-dynamic": "14.0.4", "@angular/platform-server": "14.0.4", "@angular/router": "14.0.4", + "@ngneat/input-mask": "5.2.0", "@ngneat/reactive-forms": "5.0.0", "@ngneat/until-destroy": "9.0.0", "@s-libs/js-core": "14.0.0", @@ -60,6 +61,7 @@ "css-element-queries": "1.2.3", "element-resize-detector": "1.2.4", "humanize-duration": "3.21.0", + "inputmask": "5.0.7", "jsonc-parser": "2.0.2", "keycloak-angular": "12.0.0", "keycloak-js": "18.0.1", @@ -86,6 +88,7 @@ "@angular/compiler-cli": "14.0.4", "@types/element-resize-detector": "1.1.3", "@types/humanize-duration": "3.18.0", + "@types/inputmask": "5.0.3", "@types/jasmine": "4.0.3", "@types/jwt-decode": "2.2.1", "@types/lodash-es": "4.17.6", diff --git a/src/app/sections/withdrawals/withdrawals.component.html b/src/app/sections/withdrawals/withdrawals.component.html index c3aee8ea..dc5de657 100644 --- a/src/app/sections/withdrawals/withdrawals.component.html +++ b/src/app/sections/withdrawals/withdrawals.component.html @@ -44,7 +44,9 @@ - + + + diff --git a/src/app/shared/components/metadata-form/components/extension-field/extension-field.component.ts b/src/app/shared/components/metadata-form/components/extension-field/extension-field.component.ts new file mode 100644 index 00000000..b89bcc9c --- /dev/null +++ b/src/app/shared/components/metadata-form/components/extension-field/extension-field.component.ts @@ -0,0 +1,73 @@ +import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Validator, ValidationErrors, FormControl, Validators } from '@angular/forms'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { FormComponentSuperclass } from '@s-libs/ng-core'; +import { ThriftType } from '@vality/thrift-ts'; +import { defer, switchMap, ReplaySubject, Observable } from 'rxjs'; +import { shareReplay, first, map } from 'rxjs/operators'; + +import { createControlProviders } from '@cc/utils'; + +import { ComponentChanges } from '../../../../utils'; +import { MetadataFormData } from '../../types/metadata-form-data'; +import { Converter } from '../../types/metadata-form-extension'; + +@UntilDestroy() +@Component({ + selector: 'cc-extension-field', + templateUrl: './extension-field.component.html', + providers: createControlProviders(ExtensionFieldComponent), +}) +export class ExtensionFieldComponent + extends FormComponentSuperclass + implements Validator, OnChanges, OnInit +{ + @Input() data: MetadataFormData; + + control = new FormControl(null); + + extensionResult$ = defer(() => this.data$).pipe( + switchMap((data) => data.extensionResult$), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + private data$ = new ReplaySubject(1); + private converter$: Observable = this.extensionResult$.pipe( + map( + ({ converter }) => + converter || { + outputToInternal: (v: unknown) => v, + internalToOutput: (v: unknown) => v, + } + ), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + ngOnInit() { + this.control.valueChanges + .pipe( + switchMap(() => this.converter$), + untilDestroyed(this) + ) + .subscribe((converter) => { + this.emitOutgoingValue(converter.internalToOutput(this.control.value) as never); + }); + } + + handleIncomingValue(value: T) { + this.converter$.pipe(first(), untilDestroyed(this)).subscribe((converter) => { + this.control.setValue(converter.outputToInternal(value) as never); + }); + } + + validate(): ValidationErrors | null { + return null; + } + + ngOnChanges(changes: ComponentChanges>) { + if (changes.data) { + this.data$.next(this.data); + this.control.setValidators(this.data.isRequired ? Validators.required : []); + } + } +} diff --git a/src/app/shared/components/metadata-form/metadata-form.component.html b/src/app/shared/components/metadata-form/metadata-form.component.html index 5eb346a6..1a7fc1f4 100644 --- a/src/app/shared/components/metadata-form/metadata-form.component.html +++ b/src/app/shared/components/metadata-form/metadata-form.component.html @@ -1,30 +1,45 @@
- - - - + + - + - - - + > + + + + + + +
diff --git a/src/app/shared/components/metadata-form/metadata-form.module.ts b/src/app/shared/components/metadata-form/metadata-form.module.ts index 57ce3289..c6aa04a5 100644 --- a/src/app/shared/components/metadata-form/metadata-form.module.ts +++ b/src/app/shared/components/metadata-form/metadata-form.module.ts @@ -22,8 +22,10 @@ import { JsonViewerModule } from '@cc/app/shared/components/json-viewer'; import { ThriftPipesModule } from '@cc/app/shared/pipes/thrift'; import { ValueTypeTitleModule } from '@cc/app/shared/pipes/value-type-title'; +import { CashModule } from '../../../../components/cash-field'; import { ComplexFormComponent } from './components/complex-form/complex-form.component'; import { EnumFieldComponent } from './components/enum-field/enum-field.component'; +import { ExtensionFieldComponent } from './components/extension-field/extension-field.component'; import { LabelComponent } from './components/label/label.component'; import { PrimitiveFieldComponent } from './components/primitive-field/primitive-field.component'; import { StructFormComponent } from './components/struct-form/struct-form.component'; @@ -56,6 +58,7 @@ import { FieldLabelPipe } from './pipes/field-label.pipe'; MatDatepickerModule, DatetimeComponent, PipesModule, + CashModule, ], declarations: [ MetadataFormComponent, @@ -67,6 +70,7 @@ import { FieldLabelPipe } from './pipes/field-label.pipe'; EnumFieldComponent, LabelComponent, FieldLabelPipe, + ExtensionFieldComponent, ], exports: [MetadataFormComponent], }) diff --git a/src/app/shared/components/metadata-form/types/metadata-form-extension.ts b/src/app/shared/components/metadata-form/types/metadata-form-extension.ts index 93b80459..abe19302 100644 --- a/src/app/shared/components/metadata-form/types/metadata-form-extension.ts +++ b/src/app/shared/components/metadata-form/types/metadata-form-extension.ts @@ -8,12 +8,18 @@ export type MetadataFormExtension = { extension: (data: MetadataFormData) => Observable; }; +export interface Converter { + outputToInternal: (outputValue: O) => I; + internalToOutput: (inputValue: I) => O; +} + export interface MetadataFormExtensionResult { options?: MetadataFormExtensionOption[]; generate?: () => Observable; isIdentifier?: boolean; label?: string; - type?: 'datetime'; + type?: 'datetime' | 'cash'; + converter?: Converter; } export interface MetadataFormExtensionOption { diff --git a/src/app/shared/pipes/amount-currency.pipe.ts b/src/app/shared/pipes/amount-currency.pipe.ts index 1ded2bed..4c47efa7 100644 --- a/src/app/shared/pipes/amount-currency.pipe.ts +++ b/src/app/shared/pipes/amount-currency.pipe.ts @@ -1,10 +1,10 @@ import { formatCurrency, getCurrencySymbol } from '@angular/common'; import { Pipe, Inject, LOCALE_ID, DEFAULT_CURRENCY_CODE, PipeTransform } from '@angular/core'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import round from 'lodash-es/round'; import { ReplaySubject, combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; +import { toMajor } from '../../../utils'; import { DomainStoreService } from '../../thrift-services/damsel/domain-store.service'; @UntilDestroy() @@ -36,7 +36,7 @@ export class AmountCurrencyPipe implements PipeTransform { const exponent = currencies.find((c) => c.data.symbolic_code === currencyCode) .data.exponent; return formatCurrency( - round(amount / 10 ** exponent, exponent), + toMajor(amount, exponent), this._locale, getCurrencySymbol(currencyCode, 'narrow', this._locale), currencyCode, diff --git a/src/app/shared/pipes/enum-key.pipe.ts b/src/app/shared/pipes/enum-key.pipe.ts index 61a69dc3..e0499589 100644 --- a/src/app/shared/pipes/enum-key.pipe.ts +++ b/src/app/shared/pipes/enum-key.pipe.ts @@ -1,5 +1,4 @@ import { Pipe, PipeTransform } from '@angular/core'; -import isNil from 'lodash-es/isNil'; import { ValuesType } from 'utility-types'; import { getEnumKey } from '@cc/utils'; @@ -10,6 +9,6 @@ import { getEnumKey } from '@cc/utils'; }) export class EnumKeyPipe implements PipeTransform { transform>(value: ValuesType, 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);