FE-867: Payments search form (#53)

This commit is contained in:
Alexandra Usacheva 2019-07-15 17:47:31 +03:00 committed by GitHub
parent 30530003eb
commit a56093744b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 940 additions and 368 deletions

30
package-lock.json generated
View File

@ -1043,8 +1043,7 @@
"@types/lodash": {
"version": "4.14.135",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.135.tgz",
"integrity": "sha512-Ed+tSZ9qM1oYpi5kzdsBuOzcAIn1wDW+e8TFJ50IMJMlSopGdJgKAbhHzN6h1E1OfjlGOr2JepzEWtg9NIfoNg==",
"dev": true
"integrity": "sha512-Ed+tSZ9qM1oYpi5kzdsBuOzcAIn1wDW+e8TFJ50IMJMlSopGdJgKAbhHzN6h1E1OfjlGOr2JepzEWtg9NIfoNg=="
},
"@types/lodash.get": {
"version": "4.4.6",
@ -1055,6 +1054,23 @@
"@types/lodash": "*"
}
},
"@types/lodash.isempty": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/@types/lodash.isempty/-/lodash.isempty-4.4.6.tgz",
"integrity": "sha512-AauKrFlA4z3Usog5HLGDupKzkCP7h5KXGlfAcRGUfvTmL7guVuEzDSNI6lYJ7syO7J2RE2F47179pSLr26UHIw==",
"requires": {
"@types/lodash": "*"
}
},
"@types/lodash.mapvalues": {
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/@types/lodash.mapvalues/-/lodash.mapvalues-4.6.6.tgz",
"integrity": "sha512-Mt9eg3AqwAt5HShuOu8taiIYg0sLl4w3vDi0++E0VtiOtj9DqQHaxVr3wicVop0eDEqr5ENbht7vsLJlkMHL+w==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -6488,6 +6504,16 @@
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"lodash.isempty": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
"integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4="
},
"lodash.mapvalues": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz",
"integrity": "sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw="
},
"lodash.padend": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz",

View File

@ -31,6 +31,7 @@
"@angular/platform-browser": "~8.0.2",
"@angular/platform-browser-dynamic": "~8.0.2",
"@angular/router": "~8.0.2",
"@types/lodash.isempty": "^4.4.6",
"acorn": "^6.1.1",
"angular2-text-mask": "^9.0.0",
"core-js": "^2.5.4",
@ -42,6 +43,8 @@
"hammerjs": "^2.0.8",
"keycloak-angular": "^6.1.0",
"lodash.get": "^4.4.2",
"lodash.isempty": "^4.4.0",
"lodash.mapvalues": "^4.6.0",
"moment": "^2.24.0",
"pdfmake": "^0.1.56",
"rxjs": "~6.5.2",
@ -58,6 +61,7 @@
"@types/jasmine": "~3.3.12",
"@types/jasminewd2": "~2.0.6",
"@types/lodash.get": "^4.4.6",
"@types/lodash.mapvalues": "^4.6.6",
"@types/moment": "^2.13.0",
"@types/node": "^11.13.10",
"@types/pdfmake": "^0.1.5",

View File

@ -1,6 +1,6 @@
import { TextMaskConfig } from 'angular2-text-mask';
export const binMask: TextMaskConfig = {
mask: [/\d/, /\d/, /\d/, /\d/, ' ', /\d/, /\d/, /\d/, /\d/],
mask: [/\d/, /\d/, /\d/, /\d/, ' ', /\d/, /\d/],
guide: false
};

View File

@ -0,0 +1,9 @@
<input
class="dsh-card-input-element"
size="9"
[formControl]="formControl"
(change)="writeValue($event.target.value)"
placeholder="0000&nbsp;00"
[textMask]="mask"
/>
<span class="dsh-card-input-spacer">**&nbsp;****&nbsp;****</span>

View File

@ -0,0 +1,32 @@
import { Component, forwardRef, HostBinding } from '@angular/core';
import { MatFormFieldControl } from '@angular/material/form-field';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { binMask } from './bin-input-mask';
import { CustomFormControl } from '../../custom-form-control';
@Component({
selector: 'dsh-card-bin-input',
templateUrl: 'bin-input.component.html',
styleUrls: ['../card-controls.scss'],
providers: [
{ provide: MatFormFieldControl, useExisting: BINInputComponent },
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BINInputComponent),
multi: true
}
]
})
export class BINInputComponent extends CustomFormControl {
static nextId = 0;
@HostBinding('id')
id = `dsh-bin-input-${BINInputComponent.nextId++}`;
controlType = 'dsh-bin-input';
get mask() {
return binMask;
}
}

View File

@ -8,7 +8,6 @@
padding: 0;
outline: none;
font: inherit;
text-align: right;
}
.dsh-card-input-element::placeholder {

View File

@ -0,0 +1,2 @@
export * from './bin-input/bin-input.module';
export * from './last-digits-input/last-digits-input.module';

View File

@ -0,0 +1,9 @@
<span class="dsh-card-input-spacer">****&nbsp;****&nbsp;****&nbsp;</span>
<input
class="dsh-card-input-element"
[formControl]="formControl"
(change)="writeValue($event.target.value)"
size="4"
placeholder="0000"
[textMask]="mask"
/>

View File

@ -0,0 +1,3 @@
.dsh-card-input-element {
text-align: right;
}

View File

@ -0,0 +1,30 @@
import { Component, forwardRef, HostBinding } from '@angular/core';
import { MatFormFieldControl } from '@angular/material/form-field';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { cardMask } from './last-digits-input-mask';
import { CustomFormControl } from '../../custom-form-control';
@Component({
selector: 'dsh-card-last-digits-input',
templateUrl: 'last-digits-input.component.html',
styleUrls: ['../card-controls.scss', 'last-digits-input.component.scss'],
providers: [
{ provide: MatFormFieldControl, useExisting: LastDigitsInputComponent },
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => LastDigitsInputComponent),
multi: true
}
]
})
export class LastDigitsInputComponent extends CustomFormControl {
static nextId = 0;
@HostBinding('id')
id = `dsh-card-input-${LastDigitsInputComponent.nextId++}`;
get mask() {
return cardMask;
}
}

View File

@ -3,12 +3,12 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { A11yModule } from '@angular/cdk/a11y';
import { TextMaskModule } from 'angular2-text-mask';
import { CardInputComponent } from './card-input.component';
import { LastDigitsInputComponent } from './last-digits-input.component';
@NgModule({
imports: [FormsModule, ReactiveFormsModule, A11yModule, TextMaskModule],
entryComponents: [CardInputComponent],
declarations: [CardInputComponent],
exports: [CardInputComponent]
entryComponents: [LastDigitsInputComponent],
declarations: [LastDigitsInputComponent],
exports: [LastDigitsInputComponent]
})
export class CardInputModule {}
export class LastDigitsInputModule {}

View File

@ -1,8 +0,0 @@
<input
class="dsh-bin-input-element"
[formControl]="formControl"
size="9"
placeholder="0000&nbsp;0000"
[textMask]="mask"
/>
<span class="dsh-bin-input-spacer">&nbsp;****&nbsp;****</span>

View File

@ -1,29 +0,0 @@
:host {
display: flex;
}
.dsh-bin-input-element {
border: none;
background: none;
padding: 0;
outline: none;
font: inherit;
}
.dsh-bin-input-element::placeholder {
opacity: 0;
}
:host.floating .dsh-bin-input-element::placeholder {
opacity: 1;
}
.dsh-bin-input-spacer {
opacity: 0;
user-select: none;
transition: opacity 200ms;
}
:host.floating .dsh-bin-input-spacer {
opacity: 1;
}

View File

@ -1,110 +0,0 @@
import { FocusMonitor } from '@angular/cdk/a11y';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Component, ElementRef, Input, OnDestroy, HostBinding } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatFormFieldControl } from '@angular/material/form-field';
import { Subject } from 'rxjs';
import { binMask } from './bin-input-mask';
@Component({
selector: 'dsh-bin-input',
templateUrl: 'bin-input.component.html',
styleUrls: ['bin-input.component.scss'],
providers: [{ provide: MatFormFieldControl, useExisting: BINInputComponent }]
})
export class BINInputComponent implements MatFormFieldControl<number>, OnDestroy {
static nextId = 0;
@HostBinding('class.floating')
get shouldLabelFloat() {
return this.focused || !this.empty;
}
@HostBinding('id')
id = `dsh-bin-input-${BINInputComponent.nextId++}`;
@HostBinding('attr.aria-describedby')
describedBy = '';
formControl: FormControl;
stateChanges = new Subject<void>();
focused = false;
ngControl = null;
errorState = false;
controlType = 'dsh-bin-input';
get empty() {
return !this.formControl.value;
}
@Input()
get placeholder(): string {
return this._placeholder;
}
set placeholder(value: string) {
this._placeholder = value;
this.stateChanges.next();
}
private _placeholder: string;
@Input()
get required(): boolean {
return this._required;
}
set required(value: boolean) {
this._required = coerceBooleanProperty(value);
this.stateChanges.next();
}
private _required = false;
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = coerceBooleanProperty(value);
this._disabled ? this.formControl.disable() : this.formControl.enable();
this.stateChanges.next();
}
private _disabled = false;
@Input()
get value(): number {
return this.formControl.value.replace(/\D/g, '');
}
set value(v: number) {
this.formControl.setValue(v);
this.stateChanges.next();
}
get mask() {
return binMask;
}
constructor(private fm: FocusMonitor, private elRef: ElementRef<HTMLElement>) {
this.formControl = new FormControl();
fm.monitor(elRef, true).subscribe(origin => {
this.focused = !!origin;
this.stateChanges.next();
});
}
ngOnDestroy() {
this.stateChanges.complete();
this.fm.stopMonitoring(this.elRef);
}
setDescribedByIds(ids: string[]) {
this.describedBy = ids.join(' ');
}
onContainerClick(event: MouseEvent) {
if ((event.target as Element).tagName.toLowerCase() !== 'input') {
const input = this.elRef.nativeElement.querySelector('input');
if (input) {
input.focus();
}
}
}
}

View File

@ -1,2 +0,0 @@
<span class="dsh-card-input-spacer">****&nbsp;****&nbsp;****&nbsp;</span>
<input class="dsh-card-input-element" [formControl]="formControl" size="4" placeholder="0000" [textMask]="mask" />

View File

@ -1,29 +1,16 @@
import { FocusMonitor } from '@angular/cdk/a11y';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Component, ElementRef, Input, OnDestroy, HostBinding } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatFormFieldControl } from '@angular/material/form-field';
import { MatFormFieldControl } from '@angular/material';
import { DoCheck, ElementRef, HostBinding, Injector, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, FormControl, NgControl } from '@angular/forms';
import { Subject } from 'rxjs';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { FocusMonitor } from '@angular/cdk/a11y';
import { cardMask } from './card-input-mask';
@Component({
selector: 'dsh-card-input',
templateUrl: 'card-input.component.html',
styleUrls: ['card-input.component.scss'],
providers: [{ provide: MatFormFieldControl, useExisting: CardInputComponent }]
})
export class CardInputComponent implements MatFormFieldControl<number>, OnDestroy {
static nextId = 0;
export class CustomFormControl implements MatFormFieldControl<any>, OnInit, OnDestroy, DoCheck, ControlValueAccessor {
@HostBinding('class.floating')
get shouldLabelFloat() {
return this.focused || !this.empty;
}
@HostBinding('id')
id = `dsh-card-input-${CardInputComponent.nextId++}`;
@HostBinding('attr.aria-describedby')
describedBy = '';
@ -31,13 +18,14 @@ export class CardInputComponent implements MatFormFieldControl<number>, OnDestro
stateChanges = new Subject<void>();
focused = false;
ngControl = null;
id = null;
errorState = false;
controlType = 'dsh-card-input';
get empty() {
return !this.formControl.value;
return !this._value;
}
private _placeholder: string;
@Input()
get placeholder(): string {
return this._placeholder;
@ -46,8 +34,8 @@ export class CardInputComponent implements MatFormFieldControl<number>, OnDestro
this._placeholder = value;
this.stateChanges.next();
}
private _placeholder: string;
private _required = false;
@Input()
get required(): boolean {
return this._required;
@ -56,8 +44,8 @@ export class CardInputComponent implements MatFormFieldControl<number>, OnDestro
this._required = coerceBooleanProperty(value);
this.stateChanges.next();
}
private _required = false;
private _disabled = false;
@Input()
get disabled(): boolean {
return this._disabled;
@ -67,22 +55,19 @@ export class CardInputComponent implements MatFormFieldControl<number>, OnDestro
this._disabled ? this.formControl.disable() : this.formControl.enable();
this.stateChanges.next();
}
private _disabled = false;
@Input()
get value(): number {
return this.formControl.value.replace(/\D/g, '');
_value: any = '';
get value(): any {
return this.formControl.value;
}
set value(v: number) {
this.formControl.setValue(v);
set value(value) {
this._value = value;
this.formControl.setValue(this._value);
this.onChange(value);
this.stateChanges.next();
}
get mask() {
return cardMask;
}
constructor(private fm: FocusMonitor, private elRef: ElementRef<HTMLElement>) {
constructor(private fm: FocusMonitor, private elRef: ElementRef<HTMLElement>, public injector: Injector) {
this.formControl = new FormControl();
fm.monitor(elRef, true).subscribe(origin => {
this.focused = !!origin;
@ -90,11 +75,25 @@ export class CardInputComponent implements MatFormFieldControl<number>, OnDestro
});
}
ngOnInit() {
this.ngControl = this.injector.get(NgControl);
if (this.ngControl != null) {
this.ngControl.valueAccessor = this;
}
}
ngOnDestroy() {
this.stateChanges.complete();
this.fm.stopMonitoring(this.elRef);
}
ngDoCheck(): void {
if (this.ngControl) {
this.errorState = this.ngControl.invalid && this.ngControl.touched;
this.stateChanges.next();
}
}
setDescribedByIds(ids: string[]) {
this.describedBy = ids.join(' ');
}
@ -107,4 +106,21 @@ export class CardInputComponent implements MatFormFieldControl<number>, OnDestro
}
}
}
onChange = (value: any) => {};
onTouched = () => {};
writeValue(value: any): void {
this.value = value;
this.formControl.setValue(value);
}
registerOnChange(fn: (v: any) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
}

View File

@ -1,11 +1,10 @@
import { NgModule } from '@angular/core';
import { TextMaskModule } from 'angular2-text-mask';
import { BinInputModule } from './bin-input/bin-input.module';
import { CardInputModule } from './card-input/card-input.module';
import { BinInputModule, LastDigitsInputModule } from './bank-card-controls';
@NgModule({
imports: [BinInputModule, CardInputModule, TextMaskModule],
exports: [BinInputModule, CardInputModule, TextMaskModule]
imports: [BinInputModule, LastDigitsInputModule, TextMaskModule],
exports: [BinInputModule, LastDigitsInputModule, TextMaskModule]
})
export class FormControlsModule {}

View File

@ -7,26 +7,28 @@
(dshResized)="cardHeight = $event.height"
>
<div (dshResized)="baseContentHeight = $event.height">
<div class="dsh-float-panel-base" fxLayout="row" fxLayoutGap="20px" fxLayoutAlign=" center">
<div fxFlex><ng-content></ng-content></div>
<button
dsh-icon-button
(click)="expandToggle()"
@hide
*ngIf="!expanded"
class="dsh-float-panel-base-action"
>
<div fxLayout fxLayout.sm="column" fxLayout.xs="column" [fxLayoutGap]="layoutGap">
<!--need this additional div because invisible resize-sensor affects render-->
<div fxFlex>
<ng-content></ng-content>
</div>
<div *ngIf="!expanded" fxLayout class="dsh-float-panel-actions" fxLayoutAlign="center center">
<button dsh-icon-button (click)="expandToggle()" @hide fxHide.sm fxHide.xs>
<mat-icon>expand_more</mat-icon>
</button>
<button dsh-button (click)="expandToggle()" fxFlex fxHide fxShow.sm fxShow.xs>
{{ 'common.showMore' | lc }}
</button>
</div>
</div>
</div>
<div class="dsh-float-panel-more" [@expand]="expandTrigger" *ngIf="expanded">
<div (dshResized)="setMoreContentHeight($event.height)">
<div fxLayout="column" fxLayoutGap="20px">
<div fxLayout="column" [fxLayoutGap]="layoutGap">
<div class="dsh-float-panel-more-content">
<ng-container *ngTemplateOutlet="floatPanelMore?.templateRef"></ng-container>
</div>
<div fxLayout="row" fxLayoutGap="20px" fxLayoutAlign="space-between">
<div fxLayout [fxLayoutGap]="layoutGap" fxLayoutAlign="space-between">
<button dsh-icon-button (click)="pinToggle()">
<mat-icon svgIcon="place_outline"></mat-icon>
</button>

View File

@ -15,15 +15,12 @@ $card-without-actions-padding: $card-padding - $actions-padding;
}
}
&-base {
width: 100%;
&-action {
&-actions {
margin-right: -$card-without-actions-padding;
}
}
&-more {
margin-top: $card-padding;
height: 0;
overflow: hidden;
margin-left: -$card-without-actions-padding;
@ -31,7 +28,7 @@ $card-without-actions-padding: $card-padding - $actions-padding;
&-content {
padding-left: $actions-padding;
padding-right: $actions-padding;
padding-right: $actions-padding; // actions padding + head button width + margin compensation
}
}
}

View File

@ -25,6 +25,8 @@ export class FloatPanelComponent {
@coerce(v => coerceBooleanProperty(v), (v, self) => self.pinnedChange.emit(v))
pinned = false;
@Input() layoutGap = '20px';
@ContentChild(FloatPanelMoreTemplateComponent, { static: false }) floatPanelMore: FloatPanelMoreTemplateComponent;
@ContentChild(FloatPanelActionsTemplateComponent, { static: false })

View File

@ -9,9 +9,10 @@ import { FloatPanelActionsTemplateComponent } from './templates/float-panel-acti
import { ButtonModule } from '../../button';
import { ResizedModule } from '../../resized';
import { CardModule } from '../card';
import { LocaleModule } from '../../locale';
@NgModule({
imports: [MatIconModule, FlexLayoutModule, CommonModule, ButtonModule, ResizedModule, CardModule],
imports: [MatIconModule, FlexLayoutModule, CommonModule, ButtonModule, ResizedModule, CardModule, LocaleModule],
declarations: [FloatPanelComponent, FloatPanelMoreTemplateComponent, FloatPanelActionsTemplateComponent],
exports: [FloatPanelComponent, FloatPanelMoreTemplateComponent, FloatPanelActionsTemplateComponent]
})

View File

@ -1,3 +1 @@
<div class="dsh-justify-wrapper" fxLayout fxLayoutAlign="start center" fxLayoutGap="20px">
<ng-content></ng-content>
</div>
<ng-content></ng-content>

View File

@ -1,3 +1,3 @@
.dsh-justify-wrapper > mat-form-field.mat-form-field > .mat-form-field-wrapper {
dsh-justify-wrapper > mat-form-field.mat-form-field > .mat-form-field-wrapper {
padding-bottom: 0;
}

View File

@ -2,12 +2,12 @@
<dsh-card-content fxLayout="column" [formGroup]="formGroup">
<mat-form-field>
<mat-label>BIN банка-эмитента карты</mat-label>
<dsh-bin-input></dsh-bin-input>
<dsh-card-bin-input></dsh-card-bin-input>
<mat-hint>Первые 4-8 цифр</mat-hint>
</mat-form-field>
<mat-form-field>
<mat-label>Последние цифры номера карты</mat-label>
<dsh-card-input></dsh-card-input>
<dsh-card-last-digits-input></dsh-card-last-digits-input>
<mat-hint>Последние 2-4 цифр</mat-hint>
</mat-form-field>
<mat-form-field>

View File

@ -0,0 +1,6 @@
<dsh-button-toggle-group>
<dsh-button-toggle *ngFor="let item of items" [checked]="item.checked" (click)="selectUnit(item.value)">
<div *ngIf="item.dicPath; else moreBlock">{{ item.dicPath | lc }}</div>
</dsh-button-toggle>
</dsh-button-toggle-group>
<ng-template #moreBlock> <mat-icon>more_horiz</mat-icon> </ng-template>

View File

@ -0,0 +1,37 @@
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { SearchFormValue } from '../search-form-value';
import { DaterangeSelectorService } from './daterange-selector.service';
import { SelectorItem } from './select-item';
@Component({
selector: 'dsh-daterange-selector',
templateUrl: 'daterange-selector.component.html',
providers: [DaterangeSelectorService]
})
export class DaterangeSelectorComponent implements OnChanges {
@Input() value: SearchFormValue;
@Output() selectDaterange: EventEmitter<SearchFormValue> = new EventEmitter();
@Output() selectMore: EventEmitter<void> = new EventEmitter();
items: SelectorItem[];
constructor(private daterangeSelectorService: DaterangeSelectorService) {}
ngOnChanges({ value }: SimpleChanges) {
if (value) {
this.items = this.daterangeSelectorService.changeSelectorItems(this.value);
if (this.daterangeSelectorService.isMoreChecked(this.items)) {
this.selectMore.emit();
}
}
}
selectUnit(unit: 'today' | 'week' | 'month' | 'more') {
if (unit === 'more') {
this.selectMore.emit();
return;
}
this.selectDaterange.emit(this.daterangeSelectorService.toSearchFormValue(unit));
}
}

View File

@ -0,0 +1,14 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatIconModule } from '@angular/material';
import { DaterangeSelectorComponent } from './daterange-selector.component';
import { LocaleModule } from '../../../../locale';
import { ButtonToggleModule } from '../../../../button-toggle';
@NgModule({
imports: [CommonModule, LocaleModule, ButtonToggleModule, MatIconModule],
declarations: [DaterangeSelectorComponent],
exports: [DaterangeSelectorComponent]
})
export class DaterangeSelectorModule {}

View File

@ -0,0 +1,95 @@
import { Injectable } from '@angular/core';
import * as moment from 'moment';
import { Moment } from 'moment';
import { SearchFormValue } from '../search-form-value';
import { SelectorItem } from './select-item';
@Injectable()
export class DaterangeSelectorService {
private endOfToday = moment().endOf('d');
changeSelectorItems(
value: SearchFormValue,
defaultState: SelectorItem[] = [
{
value: 'today',
checked: false,
dicPath: 'common.today'
},
{
value: 'week',
checked: false,
dicPath: 'common.week'
},
{
value: 'month',
checked: false,
dicPath: 'common.month'
},
{
value: 'more',
checked: true
}
]
): SelectorItem[] {
if (!value) {
return defaultState;
}
const today = value.toTime.diff(this.endOfToday, 'd') === 0;
if (today) {
return this.applyValueToItems(defaultState, value);
}
return defaultState;
}
toSearchFormValue(unit: 'today' | 'week' | 'month'): SearchFormValue {
return {
fromTime: this.toFromTime(unit),
toTime: this.endOfToday
};
}
isMoreChecked(items: SelectorItem[]): boolean {
return !!items.find(({ value, checked }) => value === 'more' && checked);
}
private toFromTime(unit: 'today' | 'week' | 'month'): Moment {
const m = moment().startOf('d');
switch (unit) {
case 'today':
return m;
case 'week':
return m.subtract(1, 'w');
case 'month':
return m.subtract(1, 'M');
}
}
private applyValueToItems(items: SelectorItem[], { fromTime, toTime }: SearchFormValue): SelectorItem[] {
const days = toTime.diff(fromTime, 'd');
if (days < 0) {
return items;
}
if (days === 0) {
return this.changeChecked(items, 'today');
}
if (toTime.diff(fromTime, 'M') === 1) {
return this.changeChecked(items, 'month');
}
if (toTime.diff(fromTime, 'w') === 1) {
return this.changeChecked(items, 'week');
}
return items;
}
private changeChecked(items: SelectorItem[], changeUnit: 'today' | 'week' | 'month' | 'more'): SelectorItem[] {
return items.map(item => {
let checked = false;
if (item.value === changeUnit) {
checked = true;
}
return { ...item, checked };
});
}
}

View File

@ -0,0 +1 @@
export * from './daterange-selector.module';

View File

@ -0,0 +1,5 @@
export interface SelectorItem {
value: 'today' | 'week' | 'month' | 'more';
checked: boolean;
dicPath?: string;
}

View File

@ -1,36 +1,5 @@
<div fxLayout="column" fxLayoutGap="20px">
<dsh-float-panel>
<dsh-justify-wrapper>
<dsh-button-toggle-group fxFlex="324px">
<dsh-button-toggle value="today">
{{ 'common.today' | lc }}
</dsh-button-toggle>
<dsh-button-toggle value="week">
{{ 'common.week' | lc }}
</dsh-button-toggle>
<dsh-button-toggle value="month">
{{ 'common.month' | lc }}
</dsh-button-toggle>
<dsh-button-toggle value="more">
<mat-icon>more_horiz</mat-icon>
</dsh-button-toggle>
</dsh-button-toggle-group>
<mat-form-field>
<mat-label>{{ 'sections.operations.payments.filter.status' | lc }}</mat-label>
<input matInput />
</mat-form-field>
</dsh-justify-wrapper>
<dsh-float-panel-actions>
<button dsh-button>
{{ 'common.resetSearchParams' | lc }}
</button>
</dsh-float-panel-actions>
<dsh-float-panel-more>
<div style="height: 250px">
text
</div>
</dsh-float-panel-more>
</dsh-float-panel>
<dsh-search-form (formValueChanges)="search($event)"></dsh-search-form>
<dsh-card>
<dsh-card-content fxLayout="column" fxLayoutGap="15px">

View File

@ -1,6 +1,8 @@
import { Component } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { PaymentSearchFormValue } from './search-form/payment-search-form-value';
export interface PeriodicElement {
name: string;
position: number;
@ -65,4 +67,8 @@ const ELEMENT_DATA: PeriodicElement[] = [
export class PaymentsComponent {
displayedColumns: string[] = ['amount', 'status', 'statusChanged', 'invoice', 'attributes', 'actions'];
dataSource = new MatTableDataSource(ELEMENT_DATA);
search(paymentSearchFormValue: PaymentSearchFormValue) {
console.log('Search!', paymentSearchFormValue);
}
}

View File

@ -1,15 +1,24 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatFormFieldModule, MatInputModule, MatIconModule } from '@angular/material';
import { ReactiveFormsModule } from '@angular/forms';
import {
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatDatepickerModule,
MatSelectModule
} from '@angular/material';
import { PaymentsRoutingModule } from './payments-routing.module';
import { PaymentsComponent } from './payments.component';
import { LayoutModule } from '../../../../layout';
import { ButtonModule } from '../../../../button';
import { TableModule } from '../../../../table';
import { ButtonToggleModule } from '../../../../button-toggle';
import { LocaleModule } from '../../../../locale/locale.module';
import { LocaleModule } from '../../../../locale';
import { SearchFormComponent } from './search-form/search-form.component';
import { FormControlsModule } from '../../../../form-controls';
import { DaterangeSelectorModule } from '../daterange-selector';
@NgModule({
imports: [
@ -21,10 +30,14 @@ import { LocaleModule } from '../../../../locale/locale.module';
MatFormFieldModule,
MatInputModule,
TableModule,
ButtonToggleModule,
MatIconModule,
LocaleModule
LocaleModule,
ReactiveFormsModule,
MatDatepickerModule,
MatSelectModule,
FormControlsModule,
DaterangeSelectorModule
],
declarations: [PaymentsComponent]
declarations: [PaymentsComponent, SearchFormComponent]
})
export class PaymentsModule {}

View File

@ -0,0 +1,31 @@
import {
BankCardPaymentSystem,
BankCardTokenProvider,
PaymentFlow,
PaymentMethod,
PaymentStatus,
PaymentTerminalProvider
} from '../../../../../api/capi/swagger-codegen';
import { SearchFormValue } from '../../search-form-value';
export interface PaymentSearchFormValue extends SearchFormValue {
limit: string;
shopID?: string;
paymentStatus?: PaymentStatus.StatusEnum;
paymentFlow?: PaymentFlow.TypeEnum;
paymentMethod?: PaymentMethod.MethodEnum;
paymentTerminalProvider?: PaymentTerminalProvider;
invoiceID?: string;
paymentID?: string;
payerEmail?: string;
payerIP?: string;
payerFingerprint?: string;
customerID?: string;
first6?: string;
last4?: string;
bankCardTokenProvider?: BankCardTokenProvider;
bankCardPaymentSystem?: BankCardPaymentSystem;
paymentAmount?: number;
continuationToken?: string;
rnn?: string;
}

View File

@ -0,0 +1,186 @@
<dsh-float-panel [formGroup]="searchForm" novalidate [(expanded)]="expanded">
<div
fxLayout
[fxLayoutGap]="layoutGap"
fxLayout.sm="column"
fxLayout.xs="column"
fxLayoutAlign="space-between center"
fxLayoutAlign.sm="space-between stretch"
fxLayoutAlign.xs="space-between stretch"
>
<dsh-daterange-selector
[value]="searchForm.value"
(selectDaterange)="selectDaterange($event)"
(selectMore)="expanded = true"
fxFlex
></dsh-daterange-selector>
<dsh-justify-wrapper fxFlex fxLayout fxLayout.sm="column" fxLayout.xs="column" [fxLayoutGap]="layoutGap">
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.shopID' | lc }}</mat-label>
<input matInput formControlName="shopID" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.paymentStatus' | lc }}</mat-label>
<mat-select formControlName="paymentStatus">
<mat-option>
{{ 'common.any' | lc }}
</mat-option>
<mat-option *ngFor="let status of statuses" [value]="status">
{{ 'common.paymentStatus.' + status | lc }}
</mat-option>
</mat-select>
</mat-form-field>
</dsh-justify-wrapper>
</div>
<dsh-float-panel-actions>
<button dsh-button (click)="reset()">
{{ 'common.resetSearchParams' | lc }}
</button>
</dsh-float-panel-actions>
<dsh-float-panel-more>
<div
fxLayout
fxLayout.sm="column"
fxLayout.xs="column"
[fxLayoutGap]="layoutGap"
fxLayoutGap.sm="0"
fxLayoutGap.xs="0"
>
<div fxFlex fxLayout fxLayout.xs="column" [fxLayoutGap]="layoutGap" fxLayoutGap.xs="0">
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.fromTime' | lc }}</mat-label>
<input required matInput formControlName="fromTime" [matDatepicker]="fromTime" />
<mat-datepicker-toggle matSuffix [for]="fromTime"></mat-datepicker-toggle>
<mat-datepicker #fromTime></mat-datepicker>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.toTime' | lc }}</mat-label>
<input required matInput formControlName="toTime" [matDatepicker]="toTime" />
<mat-datepicker-toggle matSuffix [for]="toTime"></mat-datepicker-toggle>
<mat-datepicker #toTime></mat-datepicker>
</mat-form-field>
</div>
<div fxFlex fxLayout fxLayout.xs="column" [fxLayoutGap]="layoutGap" fxLayoutGap.xs="0">
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.payerEmail' | lc }}</mat-label>
<input matInput type="email" formControlName="payerEmail" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.paymentFlow' | lc }}</mat-label>
<mat-select formControlName="paymentFlow">
<mat-option>
{{ 'common.any' | lc }}
</mat-option>
<mat-option *ngFor="let flow of flows" [value]="flow">
{{ 'common.paymentFlow.' + flow | lc }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div
fxLayout
fxLayout.sm="column"
fxLayout.xs="column"
[fxLayoutGap]="layoutGap"
fxLayoutGap.sm="0"
fxLayoutGap.xs="0"
>
<div fxFlex fxLayout fxLayout.xs="column" [fxLayoutGap]="layoutGap" fxLayoutGap.xs="0">
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.rnn' | lc }}</mat-label>
<input matInput formControlName="rnn" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.invoiceID' | lc }}</mat-label>
<input matInput formControlName="invoiceID" />
</mat-form-field>
</div>
<div fxFlex fxLayout fxLayout.xs="column" [fxLayoutGap]="layoutGap" fxLayoutGap.xs="0">
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.bankCardTokenProvider' | lc }}</mat-label>
<mat-select formControlName="bankCardTokenProvider">
<mat-option>
{{ 'common.any' | lc }}
</mat-option>
<mat-option *ngFor="let tokenProvider of tokenProviders" [value]="tokenProvider">
{{ 'common.bankCardTokenProvider.' + tokenProvider | lc }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.paymentMethod' | lc }}</mat-label>
<mat-select formControlName="paymentMethod">
<mat-option>
{{ 'common.any' | lc }}
</mat-option>
<mat-option *ngFor="let method of methods" [value]="method">
{{ 'common.paymentMethod.' + method | lc }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div
fxLayout
fxLayout.sm="column"
fxLayout.xs="column"
[fxLayoutGap]="layoutGap"
fxLayoutGap.sm="0"
fxLayoutGap.xs="0"
>
<div fxFlex fxLayout fxLayout.xs="column" [fxLayoutGap]="layoutGap" fxLayoutGap.xs="0">
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.paymentAmount' | lc }}</mat-label>
<input matInput type="number" formControlName="paymentAmount" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.bankCardPaymentSystem' | lc }}</mat-label>
<mat-select formControlName="bankCardPaymentSystem">
<mat-option>
{{ 'common.any' | lc }}
</mat-option>
<mat-option *ngFor="let method of bankCardPaymentSystems" [value]="method">
{{ 'common.bankCardPaymentSystem.' + method | lc }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex fxLayout fxLayout.xs="column" [fxLayoutGap]="layoutGap" fxLayoutGap.xs="0">
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.last4' | lc }}</mat-label>
<dsh-card-last-digits-input formControlName="last4"></dsh-card-last-digits-input>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.first6' | lc }}</mat-label>
<dsh-card-bin-input formControlName="first6"></dsh-card-bin-input>
</mat-form-field>
</div>
</div>
<div
fxLayout
fxLayout.sm="column"
fxLayout.xs="column"
[fxLayoutGap]="layoutGap"
fxLayoutGap.sm="0"
fxLayoutGap.xs="0"
>
<div fxFlex fxLayout fxLayout.xs="column" [fxLayoutGap]="layoutGap" fxLayoutGap.xs="0">
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.paymentID' | lc }}</mat-label>
<input matInput formControlName="paymentID" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.payerIP' | lc }}</mat-label>
<input matInput formControlName="payerIP" />
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field fxFlex>
<mat-label>{{ localeBaseDir + '.payerFingerprint' | lc }}</mat-label>
<input matInput formControlName="payerFingerprint" />
</mat-form-field>
</div>
</div>
</dsh-float-panel-more>
</dsh-float-panel>

View File

@ -0,0 +1,57 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { SearchFormService } from './search-form.service';
import { PaymentSearchFormValue } from './payment-search-form-value';
import { SearchFormValue } from '../../search-form-value';
import { BankCardPaymentSystem, BankCardTokenProvider, PaymentStatus } from '../../../../../api/capi/swagger-codegen';
@Component({
selector: 'dsh-search-form',
templateUrl: 'search-form.component.html',
providers: [SearchFormService]
})
export class SearchFormComponent implements OnInit {
@Input() valueDebounceTime = 300;
@Input() layoutGap = '20px';
@Output() formValueChanges: EventEmitter<PaymentSearchFormValue> = new EventEmitter<PaymentSearchFormValue>();
localeBaseDir = 'sections.operations.payments.filter';
searchForm: FormGroup;
expanded = false;
statuses: PaymentStatus.StatusEnum[] = ['pending', 'processed', 'captured', 'cancelled', 'refunded', 'failed'];
flows = ['instant', 'hold'] as const;
methods = ['bankCard', 'paymentTerminal'] as const;
tokenProviders: BankCardTokenProvider[] = ['applepay', 'googlepay', 'samsungpay'];
bankCardPaymentSystems: BankCardPaymentSystem[] = [
'visa',
'mastercard',
'visaelectron',
'maestro',
'forbrugsforeningen',
'dankort',
'amex',
'dinersclub',
'discover',
'unionpay',
'jcb',
'nspkmir'
];
constructor(private searchFormService: SearchFormService) {}
ngOnInit() {
this.searchForm = this.searchFormService.searchForm;
this.formValueChanges.emit(this.searchForm.value);
this.searchFormService.formValueChanges(this.valueDebounceTime).subscribe(v => this.formValueChanges.emit(v));
}
selectDaterange(v: SearchFormValue) {
this.searchFormService.applySearchFormValue(v);
}
reset() {
this.formValueChanges.emit(this.searchFormService.reset());
}
}

View File

@ -0,0 +1,80 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import * as moment from 'moment';
import { filter, map, debounceTime } from 'rxjs/operators';
import { Observable } from 'rxjs';
import isEmpty from 'lodash.isempty';
import { PaymentSearchFormValue } from './payment-search-form-value';
import { toQueryParams } from './to-query-params';
import { toFormValue } from './to-form-value';
import { SearchFormValue } from '../../search-form-value';
@Injectable()
export class SearchFormService {
searchForm: FormGroup;
private defaultValues: PaymentSearchFormValue;
constructor(private fb: FormBuilder, private router: Router, private route: ActivatedRoute) {
this.searchForm = this.initForm();
this.defaultValues = this.searchForm.value;
this.route.queryParams
.pipe(
filter(queryParams => !isEmpty(queryParams)),
map(queryParams => toFormValue<PaymentSearchFormValue>(queryParams))
)
.subscribe(formValue => this.searchForm.patchValue(formValue));
this.searchForm.valueChanges
.pipe(map(formValues => toQueryParams<PaymentSearchFormValue>(formValues)))
.subscribe(queryParams => this.router.navigate([location.pathname], { queryParams }));
}
formValueChanges(valueDebounceTime: number): Observable<PaymentSearchFormValue> {
return this.searchForm.valueChanges.pipe(
filter(() => this.searchForm.status === 'VALID'),
debounceTime(valueDebounceTime)
);
}
reset(): PaymentSearchFormValue {
this.searchForm.reset(this.defaultValues);
return this.defaultValues;
}
applySearchFormValue(v: SearchFormValue) {
if (!v || !this.searchForm) {
return;
}
this.searchForm.patchValue(v);
}
private initForm(defaultLimit = 20): FormGroup {
const form = this.fb.group({
fromTime: moment()
.subtract(1, 'month')
.startOf('day'),
toTime: moment().endOf('day'),
limit: [defaultLimit, Validators.required],
shopID: '',
paymentStatus: '',
paymentFlow: '',
paymentMethod: '',
paymentTerminalProvider: '',
invoiceID: '',
paymentID: '',
payerEmail: '',
payerIP: '',
payerFingerprint: '',
customerID: '',
first6: '',
last4: '',
bankCardTokenProvider: '',
bankCardPaymentSystem: '',
paymentAmount: '',
continuationToken: '',
rnn: ''
});
return form;
}
}

View File

@ -0,0 +1,12 @@
import * as moment from 'moment';
import { Params } from '@angular/router';
import { SearchFormValue } from '../../search-form-value';
export function toFormValue<T extends SearchFormValue>(obj: Params): T {
return {
...obj,
fromTime: moment(obj.fromTime),
toTime: moment(obj.toTime)
} as T;
}

View File

@ -0,0 +1,14 @@
import { Params } from '@angular/router';
import mapValues from 'lodash.mapvalues';
import isEmpty from 'lodash.isempty';
import { SearchFormValue } from '../../search-form-value';
export function toQueryParams<T extends SearchFormValue>(obj: T): Params {
const mapped = mapValues(obj, value => (isEmpty(value) ? null : value));
return {
...mapped,
fromTime: obj.fromTime.utc().format(),
toTime: obj.toTime.utc().format()
};
}

View File

@ -0,0 +1,6 @@
import { Moment } from 'moment';
export interface SearchFormValue {
fromTime: Moment;
toTime: Moment;
}

View File

@ -1,12 +1,49 @@
{
"common": {
"showMore": "Показать еще",
"collapse": "Свернуть",
"resetSearchParams": "Сбросить параметры поиска",
"back": "Назад",
"next": "Далее",
"today": "Сегодня",
"week": "Неделя",
"month": "Месяц"
"month": "Месяц",
"any": "Любой",
"paymentStatus": {
"pending": "Запущен",
"processed": "Обработан",
"captured": "Подтвержден",
"cancelled": "Отменен",
"refunded": "Возвращен",
"failed": "Неуспешен"
},
"paymentFlow": {
"instant": "Мгновенный",
"hold": "С удержанием"
},
"bankCardTokenProvider": {
"applepay": "Apple Pay",
"googlepay": "Google Pay",
"samsungpay": "Samsung Pay"
},
"bankCardPaymentSystem": {
"visa": "Visa",
"mastercard": "Mastercard",
"visaelectron": "Visa Electron",
"maestro": "Maestro",
"forbrugsforeningen": "Forbrugsforeningen",
"dankort": "Dankort",
"amex": "American Express",
"dinersclub": "Diners Club International",
"discover": "Discover Card",
"unionpay": "UnionPay",
"jcb": "JCB",
"nspkmir": "MIR"
},
"paymentMethod": {
"bankCard": "Банковская карта",
"paymentTerminal": "Терминал"
}
},
"sections": {
"main": {
@ -77,7 +114,23 @@
"payments": {
"title": "Платежи",
"filter": {
"status": "Статус платежа"
"shopID": "Магазин",
"fromTime": "Начало периода",
"toTime": "Конец периода",
"payerEmail": "Email плательщика",
"paymentID": "Идентификатор платежа",
"invoiceID": "Идентификатор инвойса",
"paymentAmount": "Сумма платежа",
"first6": "BIN банка-эмитента карты",
"last4": "Последние цифры номера карты",
"payerFingerprint": "Fingerprint плательщика",
"payerIP": "IP-адрес плательщика",
"paymentStatus": "Статус платежа",
"paymentFlow": "Тип проведения платежа",
"bankCardTokenProvider": "Провайдер платежных токенов",
"bankCardPaymentSystem": "Платежаня система",
"paymentMethod": "Метод оплаты",
"rnn": "RNN платежа"
},
"refresh": "Обновить",
"lastUpdate": "Последнее обновление",

View File

@ -21,6 +21,7 @@
@import './overrides/mat-dialog';
@import './overrides/mat-snack-bar';
@import './overrides/mat-radio-button';
@import './overrides/mat-form-field';
@import '../app/timeline/timeline-theme';
@import '../app/expand-panel/expand-panel-theme';
@import '../app/dadata/dadata-theme';
@ -31,6 +32,7 @@
@include mat-theme-loaded-marker-override();
@include mat-dialog-override();
@include mat-snack-bar-override();
@include mat-form-field-infix-override();
}
@mixin dsh-typography($config) {

View File

@ -0,0 +1,5 @@
@mixin mat-form-field-infix-override() {
.mat-form-field-infix {
width: auto !important;
}
}