mirror of
https://github.com/valitydev/control-center.git
synced 2024-11-06 02:25:17 +00:00
IMP-43: Cash input (#132)
This commit is contained in:
parent
dc71b014da
commit
32fd665ba1
45
package-lock.json
generated
45
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -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 : []);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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],
|
||||
})
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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) : '';
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
33
src/components/cash-field/cash-field.component.html
Normal file
33
src/components/cash-field/cash-field.component.html
Normal 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 }} </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>
|
120
src/components/cash-field/cash-field.component.ts
Normal file
120
src/components/cash-field/cash-field.component.ts
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
24
src/components/cash-field/cash-field.module.ts
Normal file
24
src/components/cash-field/cash-field.module.ts
Normal 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 {}
|
2
src/components/cash-field/index.ts
Normal file
2
src/components/cash-field/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './cash-field.module';
|
||||
export * from './cash-field.component';
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
@ -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>>(
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user