| @ViewChild('loanEditComponent', {static: true}) loanEdit: LoanEditComponent; | @ViewChild('loanEditComponent', {static: true}) loanEdit: LoanEditComponent; | ||||
| webSocketSubscription: Subscription; | webSocketSubscription: Subscription; | ||||
| wsAssignSocketIdSub: Subscription; | |||||
| wsLoginSub: Subscription; | wsLoginSub: Subscription; | ||||
| constructor(private menuService: MenuService, | constructor(private menuService: MenuService, | ||||
| this.onWSLogin(m); | this.onWSLogin(m); | ||||
| }); | }); | ||||
| this.wsAssignSocketIdSub = this.wsService.assignSocketIdEvent.subscribe(m =>{ | |||||
| this.onWSAssignSocketId(m); | |||||
| }); | |||||
| this.titleService.setTitle(this.title); | this.titleService.setTitle(this.title); | ||||
| } | } | ||||
| ngOnDestroy(): void { | ngOnDestroy(): void { | ||||
| this.webSocketSubscription.unsubscribe(); | this.webSocketSubscription.unsubscribe(); | ||||
| this.wsLoginSub.unsubscribe(); | this.wsLoginSub.unsubscribe(); | ||||
| this.wsAssignSocketIdSub.unsubscribe(); | |||||
| } | } | ||||
| onWSLogin(e: WsLoginEventModel): void { | onWSLogin(e: WsLoginEventModel): void { | ||||
| if ( e.SocketId === this.ss.SocketId ) { // our own message | |||||
| return; | |||||
| } | |||||
| console.log('on login out', this); | |||||
| if ( e.T === 'logout' ) { // regardless where are they, logout means logout | if ( e.T === 'logout' ) { // regardless where are they, logout means logout | ||||
| if ( this.ss.isLoggedIn() && this.ss.isCurrentUser(e.Uid) ) { | if ( this.ss.isLoggedIn() && this.ss.isCurrentUser(e.Uid) ) { | ||||
| if ( e.Sid === this.ss.SessionId ) { | |||||
| this.ss.logoutAndClearLocalStorage(); | |||||
| if ( e.Mid === this.ss.MachineId ) { | |||||
| this.ss.logoutWithoutPersistingStorage(); | |||||
| }else{ | }else{ | ||||
| this.ss.logout(); | this.ss.logout(); | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| onWSAssignSocketId(id: string): void{ | |||||
| console.log('get registration id', id); | |||||
| this.ss.SocketId = id; | |||||
| } | |||||
| } | } | ||||
| import { RewardsAllComponent } from './rewards-all/rewards-all.component'; | import { RewardsAllComponent } from './rewards-all/rewards-all.component'; | ||||
| import { SinglePayoutRewardsListComponent } from './single-payout-rewards-list/single-payout-rewards-list.component'; | import { SinglePayoutRewardsListComponent } from './single-payout-rewards-list/single-payout-rewards-list.component'; | ||||
| import {SessionService} from './service/session.service'; | import {SessionService} from './service/session.service'; | ||||
| import { NumberRangeFilterComponent } from './grid-filter/number-range-filter/number-range-filter.component'; | |||||
| RewardSelectComponent, | RewardSelectComponent, | ||||
| RewardsAllComponent, | RewardsAllComponent, | ||||
| SinglePayoutRewardsListComponent, | SinglePayoutRewardsListComponent, | ||||
| NumberRangeFilterComponent, | |||||
| ], | ], | ||||
| imports: [ | imports: [ | ||||
| BrowserModule, | BrowserModule, |
| if (this.ss.MachineId !== '') { | if (this.ss.MachineId !== '') { | ||||
| h = h.set('Biukop-Mid', this.ss.MachineId); | h = h.set('Biukop-Mid', this.ss.MachineId); | ||||
| } | } | ||||
| if (this.ss.SocketId !== '') { | |||||
| h = h.set('Biukop-Socket', this.ss.SocketId); | |||||
| } | |||||
| const authReq = req.clone({ | const authReq = req.clone({ | ||||
| headers: h, | headers: h, | ||||
| public setSession(bs: string): void { | public setSession(bs: string): void { | ||||
| // console.log('receive session:' , bs); | // console.log('receive session:' , bs); | ||||
| if (bs){ | if (bs){ | ||||
| if ( this.ss.loggedIn.session !== bs ){ | |||||
| this.ss.loggedIn.session = bs; | |||||
| this.ss.saveSessionInfo(); | |||||
| if ( this.ss.SessionId !== bs ){ | |||||
| this.ss.SessionId = bs; | |||||
| console.log('switch session:' , bs); | console.log('switch session:' , bs); | ||||
| } | } | ||||
| } | } |
| public onLogin(rsp: ApiV1LoginResponse): void { | public onLogin(rsp: ApiV1LoginResponse): void { | ||||
| this.loading = false; | this.loading = false; | ||||
| // console.log ('found login ' , rsp ); | |||||
| // console.log (' auth event login ' , rsp ); | |||||
| if (rsp.login) { | if (rsp.login) { | ||||
| switch ( rsp.role ) { | switch ( rsp.role ) { | ||||
| case 'admin': | case 'admin': |
| <div #anchor class="filter"> | |||||
| <div *ngIf="singleMode" class="single"> | |||||
| <kendo-dropdownbutton *ngIf="showOperatorChoice" class="thin" | |||||
| [data]="availableOperators" textField="op" look="bare" | |||||
| (itemClick)="onOperatorClick($event)"> | |||||
| {{operator.op}} | |||||
| </kendo-dropdownbutton> | |||||
| <kendo-textbox #single class="full" [ngClass]="{'with-padding': showOperatorChoice}" | |||||
| [(ngModel)]="valueFrom" (valueChange)="onChangeFrom($event)" | |||||
| [ngModelOptions]="{standalone: true}" | |||||
| [showErrorIcon]="!valueFromValid" | |||||
| [required]="required" | |||||
| (inputFocus)="onFromTouched()" | |||||
| ></kendo-textbox> | |||||
| </div> | |||||
| <div *ngIf="!singleMode" class="multiple" > | |||||
| <kendo-textbox #rangeFrom class="first-half" | |||||
| [required]="required" | |||||
| [showErrorIcon]="!valueFromValid" | |||||
| [ngModelOptions]="{standalone: true}" | |||||
| (inputFocus)="onFromTouched()" | |||||
| [(ngModel)]="valueFrom" (valueChange)="onChangeFrom($event)"> | |||||
| </kendo-textbox> | |||||
| <kendo-dropdownbutton class="minimum" | |||||
| [data]="availableOperators" textField="op" | |||||
| look="bare" (itemClick)="onOperatorClick($event)"> | |||||
| {{operator.op}} | |||||
| </kendo-dropdownbutton> | |||||
| <kendo-textbox #rangeTo class="second-half" | |||||
| [required]="required" | |||||
| [showErrorIcon]="!valueToValid" | |||||
| [(ngModel)]="valueTo" | |||||
| (inputFocus)="onToTouched()" | |||||
| [ngModelOptions]="{standalone: true}" | |||||
| (valueChange)="onChangeTo($event)"></kendo-textbox> | |||||
| </div> | |||||
| </div> | |||||
| <kendo-popup [anchor]="anchor" (anchorViewportLeave)="showError = false" *ngIf="showError" > | |||||
| <div class="content popup-content"> | |||||
| {{errorMessage}} | |||||
| </div> | |||||
| </kendo-popup> |
| div.filter{ | |||||
| display:flex; | |||||
| width: 100%; | |||||
| background: white; | |||||
| kendo-textbox{ | |||||
| display: flex; | |||||
| width: 100%; | |||||
| margin:1px; | |||||
| background: transparent; | |||||
| } | |||||
| div.single{ | |||||
| display:flex; | |||||
| flex-direction: row; | |||||
| width: 100%; | |||||
| .thin { | |||||
| width: 1px; | |||||
| z-index:1; | |||||
| } | |||||
| .full{ | |||||
| width: 100%; | |||||
| border-left:0; | |||||
| border-right:0; | |||||
| border-top:0; | |||||
| border-bottom:1px solid darkgrey; | |||||
| } | |||||
| .full.with-padding{ | |||||
| padding-left: 18px; | |||||
| } | |||||
| } | |||||
| div.multiple{ | |||||
| display:flex; | |||||
| width: 100%; | |||||
| flex-direction: column; | |||||
| align-items: center; | |||||
| .minimum { | |||||
| height: 10px; | |||||
| } | |||||
| .first-half{ | |||||
| border-left: 1px solid lightblue; | |||||
| border-right: 1px solid lightblue; | |||||
| border-top: 3px solid lightblue; | |||||
| border-bottom: 1px dotted darkgray; | |||||
| } | |||||
| .second-half{ | |||||
| border-left: 1px solid lightblue; | |||||
| border-right: 1px solid lightblue; | |||||
| border-top: 1px dotted lightblue; | |||||
| border-bottom: 3px solid darkgray; | |||||
| } | |||||
| } | |||||
| } | |||||
| .popup-content{ | |||||
| padding: 10px; | |||||
| background: black; | |||||
| opacity: 0.5; | |||||
| color: white | |||||
| } |
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||||
| import { NumberRangeFilterComponent } from './number-range-filter.component'; | |||||
| describe('NumberRangeFilterComponent', () => { | |||||
| let component: NumberRangeFilterComponent; | |||||
| let fixture: ComponentFixture<NumberRangeFilterComponent>; | |||||
| beforeEach(async () => { | |||||
| await TestBed.configureTestingModule({ | |||||
| declarations: [ NumberRangeFilterComponent ] | |||||
| }) | |||||
| .compileComponents(); | |||||
| }); | |||||
| beforeEach(() => { | |||||
| fixture = TestBed.createComponent(NumberRangeFilterComponent); | |||||
| component = fixture.componentInstance; | |||||
| fixture.detectChanges(); | |||||
| }); | |||||
| it('should create', () => { | |||||
| expect(component).toBeTruthy(); | |||||
| }); | |||||
| }); |
| import {Component, Input, OnChanges, OnInit, ViewChild} from '@angular/core'; | |||||
| import {CompositeFilterDescriptor, FilterDescriptor} from '@progress/kendo-data-query'; | |||||
| import {BaseFilterCellComponent, FilterService} from '@progress/kendo-angular-grid'; | |||||
| import {trim} from '@progress/kendo-angular-editor/dist/es2015/util'; | |||||
| import {debounce} from 'ts-debounce'; | |||||
| import {TextBoxComponent} from '@progress/kendo-angular-inputs'; | |||||
| class Operator { op: string; value: string; } | |||||
| @Component({ | |||||
| selector: 'app-number-range-filter', | |||||
| templateUrl: './number-range-filter.component.html', | |||||
| styleUrls: ['./number-range-filter.component.scss'] | |||||
| }) | |||||
| export class NumberRangeFilterComponent extends BaseFilterCellComponent implements OnInit, OnChanges { | |||||
| constructor(filterService: FilterService) { super(filterService); } | |||||
| @Input() public filter: CompositeFilterDescriptor; | |||||
| @Input() public min = -1; | |||||
| @Input() public max = -1; | |||||
| @Input() public required = false; | |||||
| @Input() public fieldName = ''; | |||||
| @Input() public options = ['=', '≠', '>', '≥', '<', '≤', '⇩']; | |||||
| // ngModel control | |||||
| @ViewChild('single') ctlSingle: TextBoxComponent; | |||||
| @ViewChild('rangeFrom') ctlRangeFrom: TextBoxComponent; | |||||
| @ViewChild('rangeTo') ctlRangeTo: TextBoxComponent; | |||||
| public operator: Operator = {op: '=', value: 'eq'}; | |||||
| private defaultOperator: Operator = {op: '=', value: 'eq'}; | |||||
| public availableOperators: Operator[] = []; | |||||
| private AllOperatorMap = [ | |||||
| {op: '=', value: 'eq'}, | |||||
| {op: '≠', value: 'neq'}, | |||||
| {op: '>', value: 'gt'}, | |||||
| {op: '≥', value: 'gte'}, | |||||
| {op: '<', value: 'lt'}, | |||||
| {op: '≤', value: 'lte'}, | |||||
| {op: '⇩', value: 'range'} | |||||
| ]; | |||||
| public singleMode = true; | |||||
| public valueFrom = ''; | |||||
| public valueTo = ''; | |||||
| public valueFromValid = true; | |||||
| public valueToValid = true; | |||||
| public fromTouched = false; | |||||
| public toTouched = false; | |||||
| public currentFocus = ''; | |||||
| public showOperatorChoice = true; | |||||
| public showError = false; | |||||
| public validateResult = new Map<string, string>(); | |||||
| public errorMessage = ''; | |||||
| private debouncedSingleFrom = debounce(this.valueFromChanged, 500); | |||||
| private debouncedRangeFrom = debounce(this.rangeFromChanged, 500); | |||||
| private debouncedUpdateTo = debounce(this.rangeToChanged, 500); | |||||
| private isEmpty(v: string): boolean{ | |||||
| return trim(v) === ''; | |||||
| } | |||||
| private isNumber(v: string): boolean{ | |||||
| const vf = Number(v); | |||||
| return v !== null && ! isNaN(vf) ; | |||||
| } | |||||
| ngOnInit(): void { | |||||
| console.log(this); | |||||
| this.initAvailableOperators(); | |||||
| this.showOperatorChoice = this.availableOperators.length > 1 || | |||||
| ( this.availableOperators[0].op !== '=' && this.availableOperators[0].op !== ' '); | |||||
| } | |||||
| ngOnChanges(changes): void { | |||||
| if ( changes.options ){ | |||||
| this.initAvailableOperators(); | |||||
| } | |||||
| } | |||||
| private initAvailableOperators(): void { | |||||
| this.availableOperators = this.AllOperatorMap.filter( v => this.options.indexOf(v.value) !== -1 ); | |||||
| if ( this.availableOperators.length === 0) { | |||||
| this.availableOperators = [this.defaultOperator]; | |||||
| this.operator = this.defaultOperator; | |||||
| }else{ | |||||
| this.operator = this.availableOperators[0]; | |||||
| } | |||||
| this.singleMode = this.operator.value !== 'range'; | |||||
| } | |||||
| // | |||||
| // Events handling | |||||
| // | |||||
| public onOperatorClick(op: Operator): void { | |||||
| if ( this.operator === op ) { // same op | |||||
| return; | |||||
| } | |||||
| this.operator = op; | |||||
| this.singleMode = op.value !== 'range'; | |||||
| if (!this.toTouched && ! this.fromTouched) { | |||||
| return ; | |||||
| } | |||||
| this.clearError(); // start validation | |||||
| if ( this.singleMode ){ | |||||
| this.valueTo = ''; // clear to | |||||
| if (this.fromTouched ){ | |||||
| this.onChangeFrom(this.valueFrom); | |||||
| } | |||||
| }else if (!this.singleMode) { | |||||
| if (this.fromTouched ){ | |||||
| this.validateFromAsRange(this.valueFrom); | |||||
| } | |||||
| if (this.toTouched ){ | |||||
| this.validateTo(this.valueTo); | |||||
| } | |||||
| } | |||||
| this.showError = this.buildErrorMessage(); | |||||
| if ( ! this.showError ){ | |||||
| this.buildFilter(); | |||||
| } | |||||
| } | |||||
| public onChangeFrom(v: string): void { | |||||
| this.clearError(); | |||||
| this.valueFromValid = this.validateFrom(v); | |||||
| if ( this.singleMode ){ | |||||
| this.debouncedSingleFrom(v).then(); | |||||
| }else{ | |||||
| this.debouncedRangeFrom(v).then(); | |||||
| } | |||||
| this.showError = this.buildErrorMessage(); | |||||
| } | |||||
| public onFromTouched(): void { | |||||
| this.fromTouched = true; | |||||
| this.currentFocus = 'from'; | |||||
| } | |||||
| public onToTouched(): void { | |||||
| this.toTouched = false; | |||||
| this.currentFocus = 'to'; | |||||
| } | |||||
| public onChangeTo(v: string): void { | |||||
| this.clearError(); | |||||
| this.valueToValid = this.validateTo(v); | |||||
| this.debouncedUpdateTo(v).then(); | |||||
| this.showError = this.buildErrorMessage(); | |||||
| } | |||||
| // | |||||
| // debounced function | |||||
| // | |||||
| private valueFromChanged(v: string): void{ | |||||
| if ( this.validateResult.size === 0 ) { | |||||
| this.buildFilter(); | |||||
| return; | |||||
| } | |||||
| // remove filter | |||||
| this.clearOurFilter(); | |||||
| } | |||||
| private rangeFromChanged(v): void{ | |||||
| if ( this.validateResult.size === 0 ) { | |||||
| this.buildFilter(); | |||||
| return; | |||||
| } | |||||
| // remove filter | |||||
| this.clearOurFilter(); | |||||
| } | |||||
| private rangeToChanged(v: string): void { | |||||
| if ( this.validateResult.size === 0 ) { | |||||
| this.buildFilter(); | |||||
| return; | |||||
| } | |||||
| // remove filter | |||||
| this.clearOurFilter(); | |||||
| if ( this.valueFromValid && this.valueToValid) { | |||||
| this.buildFilter(); | |||||
| }else{ | |||||
| if (this.fieldName && trim(this.fieldName) !== ''){ | |||||
| const f = this.removeFilter(this.fieldName); | |||||
| this.applyFilter(f); | |||||
| } | |||||
| } | |||||
| } | |||||
| private clearOurFilter(): void { | |||||
| if (this.fieldName && trim(this.fieldName) !== ''){ | |||||
| const f = this.removeFilter(this.fieldName); | |||||
| this.applyFilter(f); | |||||
| } | |||||
| } | |||||
| protected buildFilter(): void { | |||||
| if (this.fieldName === '') { | |||||
| console.warn('filed name is not specified, skip update filter', this); | |||||
| return; | |||||
| } | |||||
| if (this.singleMode) { | |||||
| this.buildSingleFilter(); | |||||
| } else { | |||||
| this.buildRangeFilter(); | |||||
| } | |||||
| } | |||||
| private buildSingleFilter(): void { | |||||
| if ( this.valueFrom === null ) { return; } | |||||
| const filter: FilterDescriptor = { | |||||
| field: this.fieldName, | |||||
| operator: this.operator.value, | |||||
| value: this.valueFrom, | |||||
| }; | |||||
| this.applyFilter(this.updateFilter(filter)); | |||||
| } | |||||
| private buildRangeFilter(): void { | |||||
| const fs: FilterDescriptor[] = []; | |||||
| this.valueFromValid = this.validateFromAsRange(this.valueFrom); | |||||
| this.valueToValid = this.validateTo(this.valueTo); | |||||
| if ( this.valueFromValid && this.valueFrom !== null && this.valueFrom !== '') { | |||||
| fs.push({ | |||||
| field: this.fieldName, | |||||
| operator: 'gte', | |||||
| value: this.valueFrom | |||||
| }); | |||||
| } | |||||
| if ( this.valueToValid && this.valueTo !== null && this.valueTo !== '') { | |||||
| fs.push({ | |||||
| field: this.fieldName, | |||||
| operator: 'lte', | |||||
| value: this.valueTo | |||||
| }); | |||||
| } | |||||
| this.removeFilter(this.fieldName); | |||||
| const root: CompositeFilterDescriptor = this.filter || { logic: 'and', | |||||
| filters: [], | |||||
| }; | |||||
| if (fs.length) { | |||||
| root.filters.push(...fs); | |||||
| } | |||||
| this.filterService.filter(root); | |||||
| } | |||||
| private validateCommon(v: string, field?: string ): boolean { | |||||
| const f = field || ''; | |||||
| if ( v === null ){ | |||||
| this.validateResult.set(f + 'null', ' value is null'); | |||||
| return false; | |||||
| } | |||||
| if (! this.isNumber(v) ) { | |||||
| this.validateResult.set(f + 'number', 'should be number'); | |||||
| return false; | |||||
| } | |||||
| return this.isWithinRange(v, field); | |||||
| } | |||||
| private validateFrom(v: string): boolean { | |||||
| if ( this.singleMode ) { | |||||
| return this.validateFromAsSingle(v); | |||||
| } else { | |||||
| return this.validateFromAsRange(v); | |||||
| } | |||||
| } | |||||
| private validateFromAsSingle(v: string): boolean { | |||||
| if ( ! this.validateCommon(v) ) { | |||||
| return false; | |||||
| } | |||||
| if (this.required && this.isEmpty(v) ) { | |||||
| this.validateResult.set('required', 'value is required'); | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| private validateFromAsRange(v: string): boolean { | |||||
| if ( ! this.validateCommon(v, 'from_') ){ | |||||
| return false; | |||||
| } | |||||
| if ( !this.isFromLessThanTo() ){ | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| private validateTo(v: string): boolean{ | |||||
| if ( ! this.validateCommon(v, 'to_') ) { | |||||
| return false; | |||||
| } | |||||
| if ( ! this.isFromLessThanTo() ) { | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| private isFromLessThanTo(): boolean{ | |||||
| if (this.isEmpty( this.valueFrom) || this.isEmpty(this.valueTo)){ | |||||
| return true; // if one of them is empty, we dont care about who is bigger | |||||
| } | |||||
| if ( ! (Number(this.valueFrom) <= Number(this.valueTo)) ){ | |||||
| this.validateResult.set('min>max', 'min must ≤ max'); | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| private buildErrorMessage(): boolean { | |||||
| if (this.singleMode) { | |||||
| return this.buildSingleError(); | |||||
| } else { | |||||
| return this.buildRangeError(); | |||||
| } | |||||
| } | |||||
| private clearError(): void { | |||||
| this.showError = false; | |||||
| this.validateResult.clear(); | |||||
| this.errorMessage = '' ; | |||||
| } | |||||
| private buildSingleError(): boolean { | |||||
| if ( this.validateResult.has('null') ){ | |||||
| this.errorMessage = 'can not be null'; | |||||
| return true; | |||||
| } | |||||
| if (this.validateResult.has('required')){ | |||||
| this.errorMessage = 'value is required' + this.hint(); | |||||
| return true; | |||||
| } | |||||
| if (this.validateResult.has('number')){ | |||||
| this.errorMessage = 'should be a number'; | |||||
| return true; | |||||
| } | |||||
| if (this.validateResult.has('min')){ | |||||
| this.errorMessage = 'should ≥ ' + this.min; | |||||
| return true; | |||||
| } | |||||
| if (this.validateResult.has('max')){ | |||||
| this.errorMessage = 'should ≤ ' + this.max; | |||||
| return true; | |||||
| } | |||||
| const touched = this.fromTouched || this.toTouched; // this.ctlRangeFrom | |||||
| this.showError = this.errorMessage !== '' && ! touched; | |||||
| return this.showError; | |||||
| } | |||||
| private buildRangeError(): boolean { | |||||
| if ( this.validateResult.has('from_null') && this.validateResult.has('to_null') ){ | |||||
| this.errorMessage = 'can not be null'; | |||||
| return true; | |||||
| } | |||||
| if (this.validateResult.has('from_number') || this.validateResult.has( 'to_number')) { | |||||
| this.errorMessage = 'should be a number'; | |||||
| return true; | |||||
| } | |||||
| if (this.validateResult.has('from_min') || this.validateResult.has('to_min')) { | |||||
| this.errorMessage = 'should ≥ ' + this.min; | |||||
| return true; | |||||
| } | |||||
| if (this.validateResult.has('from_max') || this.validateResult.has('to_max')){ | |||||
| this.errorMessage = 'should ≤ ' + this.max; | |||||
| return true; | |||||
| } | |||||
| if (this.validateResult.has('min>max')){ | |||||
| if (this.currentFocus === 'to'){ | |||||
| this.errorMessage = 'should ≥ ' + this.valueFrom; | |||||
| }else{ | |||||
| this.errorMessage = 'should ≤ ' + this.valueTo; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| private hint(): string{ | |||||
| let hint = ''; | |||||
| if ( this.min !== -1 && this.max === -1) { | |||||
| hint = ' > ' + this.min + ' '; | |||||
| } | |||||
| if (this.max !== -1 && this.min === -1 ) { | |||||
| hint = ' < ' + this.max + ' '; | |||||
| } | |||||
| if (this.min !== -1 && this.max !== -1) { | |||||
| hint = this.min + ' - ' + this.max; | |||||
| } | |||||
| if ( hint !== '' ){ | |||||
| hint = ' (' + hint + ') '; | |||||
| } | |||||
| return hint; | |||||
| } | |||||
| private isWithinRange(v: string, field?: string): boolean{ | |||||
| const f = field || ''; | |||||
| if ( this.isEmpty(v) ){ | |||||
| if (this.singleMode && this.required){ | |||||
| this.validateResult.set(f + 'required', 'must input something'); | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| const vf = Number(v); | |||||
| if (! this.isNumber(v)) { | |||||
| this.validateResult.set(f + 'number', 'must be a number'); | |||||
| return false; | |||||
| } | |||||
| if ( this.min !== -1 && vf < this.min ){ | |||||
| this.validateResult.set(f + 'min', 'cannot be lower than min'); | |||||
| return false; | |||||
| } | |||||
| if (this.max !== -1 && vf > this.max ) { | |||||
| this.validateResult.set(f + 'max', 'cannot be greater than max'); | |||||
| return false; | |||||
| } | |||||
| // if we reached here | |||||
| return true; | |||||
| } | |||||
| } |
| import {CompositeFilterDescriptor, GroupDescriptor, SortDescriptor} from '@progress/kendo-data-query'; | |||||
| import {DataStateChangeEvent} from '@progress/kendo-angular-grid'; | |||||
| export class GridStateModel implements DataStateChangeEvent { | |||||
| /** | |||||
| * The number of records to skip. | |||||
| */ | |||||
| skip: number; | |||||
| /** | |||||
| * The number of records to take. | |||||
| */ | |||||
| take: number; | |||||
| /** | |||||
| * The sort descriptors by which the data is sorted. | |||||
| */ | |||||
| sort?: Array<SortDescriptor>; | |||||
| /** | |||||
| * The group descriptors by which the data is grouped. | |||||
| */ | |||||
| group?: Array<GroupDescriptor>; | |||||
| /** | |||||
| * The filter descriptor by which the data is filtered. | |||||
| */ | |||||
| filter?: CompositeFilterDescriptor; | |||||
| public constructor(payload?: Partial<GridStateModel>) { | |||||
| if ( ! payload ) payload = {}; | |||||
| this.skip = payload.skip || 0; | |||||
| this.take = payload.take || 0; | |||||
| this.sort = payload.sort || []; | |||||
| this.group = payload.group || []; | |||||
| this.filter = payload.filter || { logic: 'and', filters: [] }; | |||||
| } | |||||
| } |
| T: string; | T: string; | ||||
| Mid: string; | Mid: string; | ||||
| Sid: string; | Sid: string; | ||||
| SocketId: string; | |||||
| Uid: string; | Uid: string; | ||||
| Role: string; | Role: string; | ||||
| constructor( payload: Partial<WsLoginEventModel>) { | constructor( payload: Partial<WsLoginEventModel>) { | ||||
| this.Sid = payload.Sid || ''; | this.Sid = payload.Sid || ''; | ||||
| this.Uid = payload.Uid || ''; | this.Uid = payload.Uid || ''; | ||||
| this.Role = payload.Uid || ''; | this.Role = payload.Uid || ''; | ||||
| this.SocketId = payload.SocketId || ''; | |||||
| } | } | ||||
| } | } |
| <kendo-grid #grid [data]="gridData" | <kendo-grid #grid [data]="gridData" | ||||
| [pageable]="pageable" | [pageable]="pageable" | ||||
| [navigable]="true" | [navigable]="true" | ||||
| [resizable]="true" | |||||
| [pageSize]="filter.Take" | [pageSize]="filter.Take" | ||||
| [skip]="filter.Skip" | [skip]="filter.Skip" | ||||
| [sortable]="sortable" | [sortable]="sortable" | ||||
| [filterable]="false" | |||||
| [filterable]="'row'" | |||||
| [loading]="loading" | [loading]="loading" | ||||
| [sort]="filter.Sort" | [sort]="filter.Sort" | ||||
| [filter]="state.filter" | |||||
| [selectable]="true" | [selectable]="true" | ||||
| kendoGridSelectBy | kendoGridSelectBy | ||||
| (edit)="editHandler($event)" | (edit)="editHandler($event)" | ||||
| (remove)="removeHandler($event)" | (remove)="removeHandler($event)" | ||||
| (dataStateChange)="dataStateChange($event)" | |||||
| (filterChange)="filterChange($event)" | |||||
| (pageChange)="pageChange($event)" | (pageChange)="pageChange($event)" | ||||
| (sortChange)="sortChange($event)" | (sortChange)="sortChange($event)" | ||||
| [ngClass]="{ 'filterByUploadMeta': uploadMeta.Id > 0 }" | [ngClass]="{ 'filterByUploadMeta': uploadMeta.Id > 0 }" | ||||
| </ng-template> | </ng-template> | ||||
| </kendo-grid-command-column> | </kendo-grid-command-column> | ||||
| <kendo-grid-column field="Id" title="Id" width="50" editable="false" > | |||||
| <kendo-grid-column field="Id" title="Id" width="100" editable="false"> | |||||
| <ng-template kendoGridFilterCellTemplate let-filter let-column="column"> | <ng-template kendoGridFilterCellTemplate let-filter let-column="column"> | ||||
| <kendo-grid-numeric-filter-cell [column]="column" [filter]="filter" [showOperators]="false" > | |||||
| </kendo-grid-numeric-filter-cell> | |||||
| <app-number-range-filter [filter]="filter" [fieldName]="'Id'" | |||||
| [options]="['eq', 'lt', 'gte', 'range' ]" [min]=1 [max]="20" | |||||
| [required]="true" | |||||
| > | |||||
| </app-number-range-filter> | |||||
| </ng-template> | </ng-template> | ||||
| </kendo-grid-column> | </kendo-grid-column> | ||||
| <kendo-grid-column field="Lender" title="Lender" width="150" > | <kendo-grid-column field="Lender" title="Lender" width="150" > |
| import {PayInListResult} from '../models/pay-in-list-result.model'; | import {PayInListResult} from '../models/pay-in-list-result.model'; | ||||
| import {Router} from '@angular/router'; | import {Router} from '@angular/router'; | ||||
| import {PopupIncomeFilterComponent} from '../popup-income-filter/popup-income-filter.component'; | import {PopupIncomeFilterComponent} from '../popup-income-filter/popup-income-filter.component'; | ||||
| import {GridComponent, PageChangeEvent, RowClassArgs, SortSettings} from '@progress/kendo-angular-grid'; | |||||
| import {SortDescriptor} from '@progress/kendo-data-query'; | |||||
| import {GridComponent, PageChangeEvent, RowClassArgs, SortSettings, DataStateChangeEvent} from '@progress/kendo-angular-grid'; | |||||
| import {CompositeFilterDescriptor, FilterDescriptor, SortDescriptor} from '@progress/kendo-data-query'; | |||||
| import {UploadMetaModel} from '../models/uploadMetaModel'; | import {UploadMetaModel} from '../models/uploadMetaModel'; | ||||
| import {debounce} from 'ts-debounce'; | import {debounce} from 'ts-debounce'; | ||||
| import {LoanModel} from '../models/loan.model'; | import {LoanModel} from '../models/loan.model'; | ||||
| import {PayInModelEx} from '../models/pay-in-ex.model'; | import {PayInModelEx} from '../models/pay-in-ex.model'; | ||||
| import {Observable} from 'rxjs'; | import {Observable} from 'rxjs'; | ||||
| import {LenderNameService} from '../service/lender-name.service'; | import {LenderNameService} from '../service/lender-name.service'; | ||||
| import {GridStateModel} from '../models/grid.state.model'; | |||||
| private filterUploadMeta: UploadMetaModel = new UploadMetaModel({}); | private filterUploadMeta: UploadMetaModel = new UploadMetaModel({}); | ||||
| public filterLoan = new LoanModel({}); | public filterLoan = new LoanModel({}); | ||||
| @Input() filter: PayInListFilterModel = new PayInListFilterModel({}); | @Input() filter: PayInListFilterModel = new PayInListFilterModel({}); | ||||
| public state = new GridStateModel(); | |||||
| @Output() errorOccurred = new EventEmitter<string>(); | @Output() errorOccurred = new EventEmitter<string>(); | ||||
| @Output() Updated = new EventEmitter<PayInModel>(); | @Output() Updated = new EventEmitter<PayInModel>(); | ||||
| @ViewChild('filterDialog', {static: true}) filterDialog: PopupIncomeFilterComponent; | @ViewChild('filterDialog', {static: true}) filterDialog: PopupIncomeFilterComponent; | ||||
| ); | ); | ||||
| } | } | ||||
| public loadFilteredPayInList(): void { | |||||
| this.loading = true; | |||||
| this.pis.getFilteredPayInList(this.state).subscribe( | |||||
| ( resp: PayInListResult) => { | |||||
| this.gridData.total = resp.total; | |||||
| this.gridData.data = []; | |||||
| resp.data.forEach(v => { | |||||
| this.gridData.data.push(new PayInModelEx(v)); | |||||
| }); | |||||
| this.loading = false; | |||||
| }, err => { | |||||
| this.loading = false; | |||||
| }, () => { | |||||
| this.loading = false; | |||||
| } | |||||
| ); | |||||
| } | |||||
| // Upload Filter | // Upload Filter | ||||
| @Input() set uploadMeta(value: UploadMetaModel) { | @Input() set uploadMeta(value: UploadMetaModel) { | ||||
| this.filterUploadMeta = value; | this.filterUploadMeta = value; | ||||
| public pageChange(event: PageChangeEvent): void { | public pageChange(event: PageChangeEvent): void { | ||||
| this.filter.Skip = event.skip; | this.filter.Skip = event.skip; | ||||
| this.loadFilteredData(); | |||||
| // this.loadFilteredData(); | |||||
| } | } | ||||
| public sortChange(sort: SortDescriptor[]): void { | public sortChange(sort: SortDescriptor[]): void { | ||||
| this.filter.Sort = sort; | this.filter.Sort = sort; | ||||
| this.loadFilteredData(); | |||||
| // this.loadFilteredData(); | |||||
| } | |||||
| public filterChange( filter: CompositeFilterDescriptor): void { | |||||
| // console.log (filter, this.state.filter); | |||||
| } | |||||
| public dataStateChange(state: DataStateChangeEvent): void { | |||||
| this.state = state; | |||||
| this.loadFilteredPayInList(); | |||||
| console.log(state, this.state); | |||||
| } | } | ||||
| public onLoanChange(loan: LoanModel): void { | public onLoanChange(loan: LoanModel): void { |
| constructor(private authService: AuthService, private router: Router) { } | constructor(private authService: AuthService, private router: Router) { } | ||||
| canActivate(route: ActivatedRouteSnapshot, | canActivate(route: ActivatedRouteSnapshot, | ||||
| state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { | |||||
| state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { | |||||
| if (this.authService.isAuthenticated()) { | if (this.authService.isAuthenticated()) { | ||||
| return true; | return true; | ||||
| } else { | } else { | ||||
| } | } | ||||
| canActivateChild(route: ActivatedRouteSnapshot, | canActivateChild(route: ActivatedRouteSnapshot, | ||||
| state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { | |||||
| state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { | |||||
| return this.canActivate(route, state); | return this.canActivate(route, state); | ||||
| } | } | ||||
| } | } |
| import {HttpClient} from '@angular/common/http'; | import {HttpClient} from '@angular/common/http'; | ||||
| import {AuthService} from './auth.service'; | import {AuthService} from './auth.service'; | ||||
| import {PayInListResult} from '../models/pay-in-list-result.model'; | import {PayInListResult} from '../models/pay-in-list-result.model'; | ||||
| import {DataStateChangeEvent} from '@progress/kendo-angular-grid'; | |||||
| public getPayInList(filter: PayInListFilterModel): Observable<PayInListResult> { | public getPayInList(filter: PayInListFilterModel): Observable<PayInListResult> { | ||||
| return this.http.post<PayInListResult>(this.auth.getUrl('pay-in-list/'), filter); | return this.http.post<PayInListResult>(this.auth.getUrl('pay-in-list/'), filter); | ||||
| } | } | ||||
| public getFilteredPayInList(state: DataStateChangeEvent): Observable<PayInListResult> { | |||||
| return this.http.post<PayInListResult>(this.auth.getUrl('pay-in-filtered-list/'), state); | |||||
| } | |||||
| } | } |
| loginResult = new EventEmitter <ApiV1LoginResponse>(); | loginResult = new EventEmitter <ApiV1LoginResponse>(); | ||||
| logoutEvent = new EventEmitter <ApiV1LoginResponse>(); | logoutEvent = new EventEmitter <ApiV1LoginResponse>(); | ||||
| private machineId = localStorage.getItem('mid') || ''; | private machineId = localStorage.getItem('mid') || ''; | ||||
| private socketId = ''; // only lives in memory | |||||
| debouncedLocalStorageMonitor = debounce( this.localStorageChange, 1000); // to avoid to frequent local storage changes | debouncedLocalStorageMonitor = debounce( this.localStorageChange, 1000); // to avoid to frequent local storage changes | ||||
| constructor(private config: AppConfig, private http: HttpClient, | constructor(private config: AppConfig, private http: HttpClient, | ||||
| public login( resp: ApiV1LoginResponse): void{ | public login( resp: ApiV1LoginResponse): void{ | ||||
| // console.log( 'login in session', this); | |||||
| this.loggedIn = new ApiV1LoginResponse(resp); | this.loggedIn = new ApiV1LoginResponse(resp); | ||||
| if ( this.MachineId !== '' && resp.machineId !== '' && this.MachineId !== resp.machineId ) { | |||||
| if ( this.MachineId === '' || (resp.machineId !== '' && this.MachineId !== resp.machineId )) { | |||||
| this.MachineId = resp.machineId; // update machine id | this.MachineId = resp.machineId; // update machine id | ||||
| } | } | ||||
| private localStorageChange(event: StorageEvent): void { | private localStorageChange(event: StorageEvent): void { | ||||
| console.log('local storage change', event); | console.log('local storage change', event); | ||||
| const sfm: ApiV1LoginResponse = JSON.parse(localStorage.getItem(this.config.storageKey)); | |||||
| if ( sfm && sfm.session && sfm.User && sfm.User.Id && this.loggedIn.User.Id) { | |||||
| if ( sfm.session === this.loggedIn.session && sfm.User.Id !== this.loggedIn.User.Id){ | |||||
| this.logoutWithoutPersistingStorage(); // silently logout without touching any storage | |||||
| if ( event.key === this.config.storageKey ){ | |||||
| const newSigin: ApiV1LoginResponse = JSON.parse(localStorage.getItem(this.config.storageKey)); | |||||
| if ( newSigin && newSigin.session && newSigin.User && newSigin.User.Id && this.loggedIn.User.Id) { | |||||
| if ( newSigin.machineId === this.loggedIn.machineId && newSigin.User.Id !== this.loggedIn.User.Id){ | |||||
| this.logoutWithoutPersistingStorage(); // silently logout without touching any storage | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| localStorage.setItem(this.config.storageKey, JSON.stringify(this.loggedIn)); | localStorage.setItem(this.config.storageKey, JSON.stringify(this.loggedIn)); | ||||
| } | } | ||||
| public isAdmin(): boolean { | public isAdmin(): boolean { | ||||
| return this.loggedIn.role === 'admin'; | return this.loggedIn.role === 'admin'; | ||||
| } | } | ||||
| return this.loggedIn.session || ''; | return this.loggedIn.session || ''; | ||||
| } | } | ||||
| public set SessionId(sid: string){ | |||||
| if ( this.loggedIn.session === '' || this.loggedIn.session !== sid ){ | |||||
| this.loggedIn.session = sid; | |||||
| this.saveSessionInfo(); | |||||
| this.ws.SessionId = sid; | |||||
| } | |||||
| } | |||||
| public get SocketId(): string{ | |||||
| return this.socketId; | |||||
| } | |||||
| public set SocketId(socketId: string ) { | |||||
| this.socketId = socketId; | |||||
| } | |||||
| } | } |
| private connected = false; | private connected = false; | ||||
| private sessionId = ''; | private sessionId = ''; | ||||
| private socketId = ''; | |||||
| public ws: WebSocket; | public ws: WebSocket; | ||||
| private readonly url: string; | private readonly url: string; | ||||
| public LoginEvent: Subject<WsLoginEventModel>; | public LoginEvent: Subject<WsLoginEventModel>; | ||||
| public assignSocketIdEvent: Subject<string>; | |||||
| // cannot have session as service, as session use us as building block | // cannot have session as service, as session use us as building block | ||||
| constructor(private config: AppConfig) { | constructor(private config: AppConfig) { | ||||
| this.url = config.apiWsUrl; | this.url = config.apiWsUrl; | ||||
| this.startWebsocket(); | this.startWebsocket(); | ||||
| this.LoginEvent = new Subject<WsLoginEventModel>(); | this.LoginEvent = new Subject<WsLoginEventModel>(); | ||||
| this.assignSocketIdEvent = new Subject<string>(); | |||||
| } | } | ||||
| private startWebsocket(): void { | private startWebsocket(): void { | ||||
| if ( e.T === 'login' || e.T === 'logout' ){ | if ( e.T === 'login' || e.T === 'logout' ){ | ||||
| this.LoginEvent.next(new WsLoginEventModel(e)); | this.LoginEvent.next(new WsLoginEventModel(e)); | ||||
| } | } | ||||
| if (e.T === 'assign-socketId') { | |||||
| this.socketId = e.socketId; | |||||
| this.assignSocketIdEvent.next(e.socketId); | |||||
| } | |||||
| }catch (e) { | }catch (e) { | ||||
| console.log(e); | console.log(e); | ||||
| } | } |
| "experimentalDecorators": true, | "experimentalDecorators": true, | ||||
| "moduleResolution": "node", | "moduleResolution": "node", | ||||
| "importHelpers": true, | "importHelpers": true, | ||||
| "target": "es5", | |||||
| "target": "Es6", | |||||
| "module": "es2020", | "module": "es2020", | ||||
| "lib": [ | "lib": [ | ||||
| "es2018", | "es2018", |