IMP-43: Cash input (#132)

This commit is contained in:
Rinat Arsaev 2022-09-13 20:05:16 +03:00 committed by GitHub
parent dc71b014da
commit 32fd665ba1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 421 additions and 40 deletions

45
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -44,7 +44,9 @@
</button>
</div>
<cc-empty-search-result *ngIf="!(withdrawals$ | async).length"></cc-empty-search-result>
<cc-empty-search-result
*ngIf="!(withdrawals$ | async)?.length"
></cc-empty-search-result>
<mat-card *ngIf="(withdrawals$ | async).length" fxLayout="column" fxLayoutGap="18px">
<table [dataSource]="withdrawals$ | async" mat-table>
<cc-select-column

View File

@ -0,0 +1,8 @@
<ng-container [ngSwitch]="(extensionResult$ | async)?.type">
<cc-cash-field
*ngSwitchCase="'cash'"
[formControl]="control"
[required]="data.isRequired"
label="{{ data.type | fieldLabel: data.field }} (amount)"
></cc-cash-field>
</ng-container>

View File

@ -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<T>
extends FormComponentSuperclass<T>
implements Validator, OnChanges, OnInit
{
@Input() data: MetadataFormData<ThriftType>;
control = new FormControl<T>(null);
extensionResult$ = defer(() => this.data$).pipe(
switchMap((data) => data.extensionResult$),
shareReplay({ refCount: true, bufferSize: 1 })
);
private data$ = new ReplaySubject<MetadataFormData>(1);
private converter$: Observable<Converter> = 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<ExtensionFieldComponent<T>>) {
if (changes.data) {
this.data$.next(this.data);
this.control.setValidators(this.data.isRequired ? Validators.required : []);
}
}
}

View File

@ -1,30 +1,45 @@
<div [ngSwitch]="data?.typeGroup">
<cc-primitive-field
*ngSwitchCase="'primitive'"
<cc-extension-field
*ngIf="
(data?.extensionResult$ | async)?.type &&
(data?.extensionResult$ | async)?.type !== 'datetime';
else defaultFields
"
[data]="data"
[formControl]="control"
></cc-primitive-field>
<cc-complex-form
*ngSwitchCase="'complex'"
[data]="data"
[formControl]="control"
></cc-complex-form>
<ng-container *ngSwitchCase="'object'" [ngSwitch]="data.objectType">
<cc-struct-form
*ngSwitchCase="'struct'"
></cc-extension-field>
<ng-template #defaultFields>
<cc-primitive-field
*ngSwitchCase="'primitive'"
[data]="data"
[formControl]="control"
></cc-struct-form>
<cc-union-field
*ngSwitchCase="'union'"
></cc-primitive-field>
<cc-complex-form
*ngSwitchCase="'complex'"
[data]="data"
[formControl]="control"
></cc-union-field>
<cc-enum-field *ngSwitchCase="'enum'" [data]="data" [formControl]="control"></cc-enum-field>
<cc-typedef-form
*ngSwitchCase="'typedef'"
[data]="data"
[formControl]="control"
></cc-typedef-form>
</ng-container>
></cc-complex-form>
<ng-container *ngSwitchCase="'object'" [ngSwitch]="data.objectType">
<cc-struct-form
*ngSwitchCase="'struct'"
[data]="data"
[formControl]="control"
></cc-struct-form>
<cc-union-field
*ngSwitchCase="'union'"
[data]="data"
[formControl]="control"
></cc-union-field>
<cc-enum-field
*ngSwitchCase="'enum'"
[data]="data"
[formControl]="control"
></cc-enum-field>
<cc-typedef-form
*ngSwitchCase="'typedef'"
[data]="data"
[formControl]="control"
></cc-typedef-form>
</ng-container>
</ng-template>
</div>

View File

@ -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],
})

View File

@ -8,12 +8,18 @@ export type MetadataFormExtension = {
extension: (data: MetadataFormData) => Observable<MetadataFormExtensionResult>;
};
export interface Converter<O = unknown, I = O> {
outputToInternal: (outputValue: O) => I;
internalToOutput: (inputValue: I) => O;
}
export interface MetadataFormExtensionResult {
options?: MetadataFormExtensionOption[];
generate?: () => Observable<unknown>;
isIdentifier?: boolean;
label?: string;
type?: 'datetime';
type?: 'datetime' | 'cash';
converter?: Converter;
}
export interface MetadataFormExtensionOption {

View File

@ -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,

View File

@ -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<E extends Record<PropertyKey, unknown>>(value: ValuesType<E>, enumObj: E): keyof E {
return isNil(value) ? '' : getEnumKey(enumObj, value);
return value && enumObj ? getEnumKey(enumObj, value) : '';
}
}

View File

@ -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)

View File

@ -0,0 +1,33 @@
<div fxLayout fxLayoutAlign fxLayoutGap="8px">
<mat-form-field style="width: 100%">
<mat-label>{{ label || 'Amount' }}</mat-label>
<span matPrefix>{{ prefix }}&nbsp;</span>
<input
[formControl]="amountControl"
[inputMask]="amountMask$ | async"
[required]="required"
matInput
/>
</mat-form-field>
<mat-form-field style="min-width: 28px; width: 50%">
<mat-label>Currency</mat-label>
<input
[formControl]="currencyCodeControl"
[inputMask]="currencyMask"
[matAutocomplete]="auto"
[required]="required"
matInput
/>
<mat-autocomplete
#auto="matAutocomplete"
(optionSelected)="this.currencyCodeControl.setValue($event.option.value)"
>
<mat-option
*ngFor="let currency of currencies$ | async"
[value]="currency.data.symbolic_code"
>
{{ currency.data.symbolic_code }} ({{ currency.data.name }})
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>

View File

@ -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<Cash> implements Validator, OnInit {
@Input() label?: string;
@Input() @coerceBoolean required: boolean = false;
amountControl = new FormControl<string>(null);
currencyCodeControl = new FormControl<string>(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()
);
}
}

View File

@ -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 {}

View File

@ -0,0 +1,2 @@
export * from './cash-field.module';
export * from './cash-field.component';

View File

@ -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<T>(form: AbstractControl<T>, hasStart = false): Observable<T> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return form.valueChanges.pipe(
export function getFormValueChanges<T>(
form: FormControl<T> | FormArray<T> | FormGroup<T>,
hasStart = false
): Observable<T> {
return (form.valueChanges as Observable<T>).pipe(
...((hasStart ? [startWith(form.value)] : []) as []),
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
map(() => getValue(form))
map(() => getValue(form) as T)
);
}

View File

@ -28,7 +28,7 @@ export function getEnumKey<E extends Record<PropertyKey, unknown>>(
srcEnum: E,
value: ValuesType<E>
): 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<E extends Record<PropertyKey, unknown>>(

View File

@ -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);
};

View File

@ -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);