| @@ -21,6 +21,7 @@ export class AppComponent implements OnInit , OnDestroy { | |||
| @ViewChild('loanEditComponent', {static: true}) loanEdit: LoanEditComponent; | |||
| webSocketSubscription: Subscription; | |||
| wsAssignSocketIdSub: Subscription; | |||
| wsLoginSub: Subscription; | |||
| constructor(private menuService: MenuService, | |||
| @@ -36,6 +37,10 @@ export class AppComponent implements OnInit , OnDestroy { | |||
| this.onWSLogin(m); | |||
| }); | |||
| this.wsAssignSocketIdSub = this.wsService.assignSocketIdEvent.subscribe(m =>{ | |||
| this.onWSAssignSocketId(m); | |||
| }); | |||
| this.titleService.setTitle(this.title); | |||
| } | |||
| @@ -57,13 +62,18 @@ export class AppComponent implements OnInit , OnDestroy { | |||
| ngOnDestroy(): void { | |||
| this.webSocketSubscription.unsubscribe(); | |||
| this.wsLoginSub.unsubscribe(); | |||
| this.wsAssignSocketIdSub.unsubscribe(); | |||
| } | |||
| 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 ( 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{ | |||
| this.ss.logout(); | |||
| } | |||
| @@ -76,5 +86,10 @@ export class AppComponent implements OnInit , OnDestroy { | |||
| } | |||
| } | |||
| } | |||
| onWSAssignSocketId(id: string): void{ | |||
| console.log('get registration id', id); | |||
| this.ss.SocketId = id; | |||
| } | |||
| } | |||
| @@ -112,6 +112,7 @@ import { RewardSelectComponent } from './reward-select/reward-select.component'; | |||
| import { RewardsAllComponent } from './rewards-all/rewards-all.component'; | |||
| import { SinglePayoutRewardsListComponent } from './single-payout-rewards-list/single-payout-rewards-list.component'; | |||
| import {SessionService} from './service/session.service'; | |||
| import { NumberRangeFilterComponent } from './grid-filter/number-range-filter/number-range-filter.component'; | |||
| @@ -192,6 +193,7 @@ export function initializeApp(appConfig: AppConfig): () => Promise<void> { | |||
| RewardSelectComponent, | |||
| RewardsAllComponent, | |||
| SinglePayoutRewardsListComponent, | |||
| NumberRangeFilterComponent, | |||
| ], | |||
| imports: [ | |||
| BrowserModule, | |||
| @@ -20,6 +20,9 @@ export class AuthHttpInterceptor implements HttpInterceptor { | |||
| if (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({ | |||
| headers: h, | |||
| @@ -43,9 +46,8 @@ export class AuthHttpInterceptor implements HttpInterceptor { | |||
| public setSession(bs: string): void { | |||
| // console.log('receive session:' , 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); | |||
| } | |||
| } | |||
| @@ -48,7 +48,7 @@ export class AuthComponent implements OnInit, OnDestroy{ | |||
| public onLogin(rsp: ApiV1LoginResponse): void { | |||
| this.loading = false; | |||
| // console.log ('found login ' , rsp ); | |||
| // console.log (' auth event login ' , rsp ); | |||
| if (rsp.login) { | |||
| switch ( rsp.role ) { | |||
| case 'admin': | |||
| @@ -0,0 +1,46 @@ | |||
| <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> | |||
| @@ -0,0 +1,63 @@ | |||
| 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 | |||
| } | |||
| @@ -0,0 +1,25 @@ | |||
| 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(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,473 @@ | |||
| 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; | |||
| } | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| 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: [] }; | |||
| } | |||
| } | |||
| @@ -3,6 +3,7 @@ export class WsLoginEventModel{ | |||
| T: string; | |||
| Mid: string; | |||
| Sid: string; | |||
| SocketId: string; | |||
| Uid: string; | |||
| Role: string; | |||
| constructor( payload: Partial<WsLoginEventModel>) { | |||
| @@ -11,5 +12,6 @@ export class WsLoginEventModel{ | |||
| this.Sid = payload.Sid || ''; | |||
| this.Uid = payload.Uid || ''; | |||
| this.Role = payload.Uid || ''; | |||
| this.SocketId = payload.SocketId || ''; | |||
| } | |||
| } | |||
| @@ -1,13 +1,15 @@ | |||
| <kendo-grid #grid [data]="gridData" | |||
| [pageable]="pageable" | |||
| [navigable]="true" | |||
| [resizable]="true" | |||
| [pageSize]="filter.Take" | |||
| [skip]="filter.Skip" | |||
| [sortable]="sortable" | |||
| [filterable]="false" | |||
| [filterable]="'row'" | |||
| [loading]="loading" | |||
| [sort]="filter.Sort" | |||
| [filter]="state.filter" | |||
| [selectable]="true" | |||
| kendoGridSelectBy | |||
| @@ -21,6 +23,8 @@ | |||
| (edit)="editHandler($event)" | |||
| (remove)="removeHandler($event)" | |||
| (dataStateChange)="dataStateChange($event)" | |||
| (filterChange)="filterChange($event)" | |||
| (pageChange)="pageChange($event)" | |||
| (sortChange)="sortChange($event)" | |||
| [ngClass]="{ 'filterByUploadMeta': uploadMeta.Id > 0 }" | |||
| @@ -49,12 +53,15 @@ | |||
| </ng-template> | |||
| </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"> | |||
| <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> | |||
| </kendo-grid-column> | |||
| <kendo-grid-column field="Lender" title="Lender" width="150" > | |||
| @@ -6,8 +6,8 @@ import {PayInService} from '../service/pay-in.service'; | |||
| import {PayInListResult} from '../models/pay-in-list-result.model'; | |||
| import {Router} from '@angular/router'; | |||
| 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 {debounce} from 'ts-debounce'; | |||
| import {LoanModel} from '../models/loan.model'; | |||
| @@ -15,6 +15,7 @@ import {LoanSingleService} from '../service/loan.single.service'; | |||
| import {PayInModelEx} from '../models/pay-in-ex.model'; | |||
| import {Observable} from 'rxjs'; | |||
| import {LenderNameService} from '../service/lender-name.service'; | |||
| import {GridStateModel} from '../models/grid.state.model'; | |||
| @@ -32,6 +33,7 @@ export class PayInComponent implements OnInit { | |||
| private filterUploadMeta: UploadMetaModel = new UploadMetaModel({}); | |||
| public filterLoan = new LoanModel({}); | |||
| @Input() filter: PayInListFilterModel = new PayInListFilterModel({}); | |||
| public state = new GridStateModel(); | |||
| @Output() errorOccurred = new EventEmitter<string>(); | |||
| @Output() Updated = new EventEmitter<PayInModel>(); | |||
| @ViewChild('filterDialog', {static: true}) filterDialog: PopupIncomeFilterComponent; | |||
| @@ -98,6 +100,24 @@ export class PayInComponent implements OnInit { | |||
| ); | |||
| } | |||
| 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 | |||
| @Input() set uploadMeta(value: UploadMetaModel) { | |||
| this.filterUploadMeta = value; | |||
| @@ -316,12 +336,21 @@ export class PayInComponent implements OnInit { | |||
| public pageChange(event: PageChangeEvent): void { | |||
| this.filter.Skip = event.skip; | |||
| this.loadFilteredData(); | |||
| // this.loadFilteredData(); | |||
| } | |||
| public sortChange(sort: SortDescriptor[]): void { | |||
| 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 { | |||
| @@ -15,7 +15,7 @@ export class AuthGuard implements CanActivate, CanActivateChild { | |||
| constructor(private authService: AuthService, private router: Router) { } | |||
| canActivate(route: ActivatedRouteSnapshot, | |||
| state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { | |||
| state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { | |||
| if (this.authService.isAuthenticated()) { | |||
| return true; | |||
| } else { | |||
| @@ -24,7 +24,7 @@ export class AuthGuard implements CanActivate, CanActivateChild { | |||
| } | |||
| canActivateChild(route: ActivatedRouteSnapshot, | |||
| state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { | |||
| state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { | |||
| return this.canActivate(route, state); | |||
| } | |||
| } | |||
| @@ -4,6 +4,7 @@ import {PayInListFilterModel} from '../models/pay-in-list.filter.model'; | |||
| import {HttpClient} from '@angular/common/http'; | |||
| import {AuthService} from './auth.service'; | |||
| import {PayInListResult} from '../models/pay-in-list-result.model'; | |||
| import {DataStateChangeEvent} from '@progress/kendo-angular-grid'; | |||
| @@ -14,4 +15,8 @@ export class PayInService { | |||
| public getPayInList(filter: PayInListFilterModel): Observable<PayInListResult> { | |||
| 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); | |||
| } | |||
| } | |||
| @@ -15,6 +15,7 @@ export class SessionService { | |||
| loginResult = new EventEmitter <ApiV1LoginResponse>(); | |||
| logoutEvent = new EventEmitter <ApiV1LoginResponse>(); | |||
| private machineId = localStorage.getItem('mid') || ''; | |||
| private socketId = ''; // only lives in memory | |||
| debouncedLocalStorageMonitor = debounce( this.localStorageChange, 1000); // to avoid to frequent local storage changes | |||
| constructor(private config: AppConfig, private http: HttpClient, | |||
| @@ -41,9 +42,10 @@ export class SessionService { | |||
| public login( resp: ApiV1LoginResponse): void{ | |||
| // console.log( 'login in session', this); | |||
| 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 | |||
| } | |||
| @@ -59,10 +61,12 @@ export class SessionService { | |||
| private localStorageChange(event: StorageEvent): void { | |||
| 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 | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -71,7 +75,6 @@ export class SessionService { | |||
| localStorage.setItem(this.config.storageKey, JSON.stringify(this.loggedIn)); | |||
| } | |||
| public isAdmin(): boolean { | |||
| return this.loggedIn.role === 'admin'; | |||
| } | |||
| @@ -190,4 +193,18 @@ export class SessionService { | |||
| 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; | |||
| } | |||
| } | |||
| @@ -10,9 +10,11 @@ export class WebSocketService extends Subject<string>{ | |||
| private connected = false; | |||
| private sessionId = ''; | |||
| private socketId = ''; | |||
| public ws: WebSocket; | |||
| private readonly url: string; | |||
| public LoginEvent: Subject<WsLoginEventModel>; | |||
| public assignSocketIdEvent: Subject<string>; | |||
| // cannot have session as service, as session use us as building block | |||
| constructor(private config: AppConfig) { | |||
| @@ -20,6 +22,7 @@ export class WebSocketService extends Subject<string>{ | |||
| this.url = config.apiWsUrl; | |||
| this.startWebsocket(); | |||
| this.LoginEvent = new Subject<WsLoginEventModel>(); | |||
| this.assignSocketIdEvent = new Subject<string>(); | |||
| } | |||
| private startWebsocket(): void { | |||
| @@ -72,6 +75,10 @@ export class WebSocketService extends Subject<string>{ | |||
| if ( e.T === 'login' || e.T === 'logout' ){ | |||
| this.LoginEvent.next(new WsLoginEventModel(e)); | |||
| } | |||
| if (e.T === 'assign-socketId') { | |||
| this.socketId = e.socketId; | |||
| this.assignSocketIdEvent.next(e.socketId); | |||
| } | |||
| }catch (e) { | |||
| console.log(e); | |||
| } | |||
| @@ -10,7 +10,7 @@ | |||
| "experimentalDecorators": true, | |||
| "moduleResolution": "node", | |||
| "importHelpers": true, | |||
| "target": "es5", | |||
| "target": "Es6", | |||
| "module": "es2020", | |||
| "lib": [ | |||
| "es2018", | |||