diff --git a/package-lock.json b/package-lock.json index a966d78..1b9531a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2433,6 +2433,22 @@ } } }, + "@progress/kendo-angular-upload": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@progress/kendo-angular-upload/-/kendo-angular-upload-7.1.0.tgz", + "integrity": "sha512-oezUH2BWZguJ6YunBHs5J4SLGWcbt87RTWp6AHlwaopgSALsu5VXpptoLjcrkVXska+nkiyuLY9zpT4HBoGgfQ==", + "requires": { + "@progress/kendo-schematics": "^1.0.0", + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, "@progress/kendo-charts": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@progress/kendo-charts/-/kendo-charts-1.17.1.tgz", diff --git a/package.json b/package.json index 188adc6..7216649 100644 --- a/package.json +++ b/package.json @@ -48,9 +48,10 @@ "@progress/kendo-angular-progressbar": "^2.0.0", "@progress/kendo-angular-toolbar": "^4.0.0", "@progress/kendo-angular-treeview": "^5.1.0", + "@progress/kendo-angular-upload": "^7.1.0", "@progress/kendo-data-query": "^1.5.4", "@progress/kendo-drawing": "^1.9.4", - "@progress/kendo-licensing": "^1.1.3", + "@progress/kendo-licensing": "^1.0.2", "@progress/kendo-svg-icons": "^0.1.2", "@progress/kendo-theme-default": "latest", "bootstrap": "^4.6.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4db05bf..8ae82ad 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -70,6 +70,8 @@ import { BrokerProfileComponent } from './broker-profile/broker-profile.componen import { ClientLoanListComponent } from './client-loan-list/client-loan-list.component'; import { ClientProfileComponent } from './client-profile/client-profile.component'; import { E403Component } from './e403/e403.component'; +import {FileSelectModule, UploadModule} from '@progress/kendo-angular-upload'; + @@ -140,7 +142,9 @@ import { E403Component } from './e403/e403.component'; DateInputsModule, DropDownsModule, ExcelExportModule, - EditorModule + EditorModule, + UploadModule, + FileSelectModule ], providers: [ MenuService, diff --git a/src/app/auth/auth.component.ts b/src/app/auth/auth.component.ts index d607086..ac0aa1a 100644 --- a/src/app/auth/auth.component.ts +++ b/src/app/auth/auth.component.ts @@ -4,7 +4,7 @@ import { Router } from '@angular/router'; import { NotificationService } from '@progress/kendo-angular-notification'; import { Subscription } from 'rxjs'; import { AuthService } from '../service/auth.service'; -import {apiV1LoginResponse} from '../models/api-v1-login-response'; +import {ApiV1LoginResponse} from '../models/api-v1-login-response'; @Component({ selector: 'app-auth', @@ -42,7 +42,7 @@ export class AuthComponent implements OnInit, OnDestroy{ this.authService.login(this.userForm.value.email, this.userForm.value.password); } - public onLogin(rsp: apiV1LoginResponse): void { + public onLogin(rsp: ApiV1LoginResponse): void { this.loading = false; // console.log ('found login ' , rsp ); if (rsp.login) { diff --git a/src/app/broker-profile/broker-profile.component.html b/src/app/broker-profile/broker-profile.component.html index 777b41e..09dcd25 100644 --- a/src/app/broker-profile/broker-profile.component.html +++ b/src/app/broker-profile/broker-profile.component.html @@ -1 +1,139 @@ -

broker-profile works!

+ +
+
+
+
+
Edit : {{broker.First + ' ' + broker.Last}}
+ +
+
+
+
+
+ + + + only jpg and png are allowed + +
+
+
+ +
+ +
+ + + + First name is required + +
+ + + + + Last Name is required + +
+ + + + + Display name cannot empty + +
+ + + + + license is required, key in unknown if have one + +
+ + + + + BSB required for accepting payment + +
+ + + + + ACC is required for accepting payment + +
+ + + + + + Organization is required, (only changed by admin) + +
+ +
+
+
+
+ +
+
+
+ + Change Password +
+
+ + + + Current password is needed (3-20 chars) + + + + + + New password is needed (3-20 chars) + + + + + + + New password repeat is needed (3-20 chars) + +
+ + +
+ +
+
+
+
+ + + +

{{ Message }}

+ + + +
diff --git a/src/app/broker-profile/broker-profile.component.scss b/src/app/broker-profile/broker-profile.component.scss index e69de29..7d990e4 100644 --- a/src/app/broker-profile/broker-profile.component.scss +++ b/src/app/broker-profile/broker-profile.component.scss @@ -0,0 +1,33 @@ +div.container { + max-width: 500px; +} + +div.vertical-spacer { + height:1px; + margin-bottom: 30px; +} + + +.dropzoneInvisible{ + opacity:0.1; +} + +.avatar { + width: 100%; +} + +.broker-photo { + display: inline-block; + width: 256px; + height: 256px; + border-radius: 50%; + background-size: 256px 256px; + background-position: center center; + vertical-align: middle; + line-height: 132px; + box-shadow: inset 0 0 1px #999, inset 0 0 10px rgba(0,0,0,.2); + margin-left: 5px; + margin-bottom: 10px; + background-repeat: no-repeat; + box-shadow: 1px 1px 10px black; +} diff --git a/src/app/broker-profile/broker-profile.component.ts b/src/app/broker-profile/broker-profile.component.ts index 508e3ab..576c585 100644 --- a/src/app/broker-profile/broker-profile.component.ts +++ b/src/app/broker-profile/broker-profile.component.ts @@ -1,4 +1,13 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, Input, OnInit, ViewChild} from '@angular/core'; +import {PeopleModel} from '../models/people.model'; +import {LoanSummaryService} from '../service/loan_summary.service'; +import {AuthService} from '../service/auth.service'; +import {FormControl, FormGroup, NgForm} from '@angular/forms'; +import {FileInfo, FileRestrictions, FileSelectComponent, SelectEvent} from '@progress/kendo-angular-upload'; +import {PeopleService} from '../service/people.service'; +import {BrokerModel} from '../models/broker.model'; +import {ConfirmedValidator} from '../validator/confirmed.validator'; + @Component({ selector: 'app-broker-profile', @@ -7,9 +16,94 @@ import { Component, OnInit } from '@angular/core'; }) export class BrokerProfileComponent implements OnInit { - constructor() { } + @Input() public broker: BrokerModel = BrokerModel.EmptyNew(); + @ViewChild('fileSelect', {static: true}) fs: FileSelectComponent; + public avatarUrl = 'url(https://svr2021.lawipac.com:8080/api/v1/avatar/1000)' ; + public myRestrictions: FileRestrictions = { + allowedExtensions: ['.jpg', '.png', '.jpeg'], + maxFileSize: 2194304 + }; + + public opened = false; // dialog box + public Message = ''; // dialog message + + public changePassword = false; + public ChangePassForm: FormGroup = new FormGroup({ + OldPassword: new FormControl(), + NewPass: new FormControl(), + NewPass1: new FormControl(), + }); + + constructor( private auth: AuthService, private ps: PeopleService) { } ngOnInit(): void { + this.broker = BrokerModel.getFromUserAndExtra(this.auth.loggedIn.user, this.auth.loggedIn.userExtra); + this.avatarUrl = 'url(' + this.auth.getUrl('avatar/' + this.broker.Id) + ')'; + } + + public save(brokerForm: NgForm): void{ + if (! brokerForm.touched || brokerForm.form.pristine) { + return; + } + this.ps.saveBroker(this.broker).subscribe( () => { + this.showDialog('updated successfully '); + brokerForm.form.markAsPristine(); + }, err => { + this.showDialog('Failed to Update: ' + err.toString()); + }); + } + + public savePassword(): void{ + this.ps.savePassword(this.broker.Id, this.ChangePassForm.value).subscribe( + () => { + this.showDialog('Password Changed'); + }, err => { + this.showDialog('Failed to Change Password :' + err.toString()); + } + ); } + public passEqual(): boolean{ + const v = this.ChangePassForm.value; + if ( this.ChangePassForm.valid && v.NewPass === v.NewPass1 && this.ChangePassForm.touched) { + return true; + }else{ + this.ChangePassForm.controls['NewPass1'].setErrors({'incorrect': true}); + } + return false; + } + + public onSelect(ev: SelectEvent): void { + if (ev.files) { + ev.files.every((file: FileInfo) => { + if (file.rawFile && !file.validationErrors) { + const reader = new FileReader(); + + reader.onloadend = () => { + const str = reader.result as string; + this.ps.updateAvatar(str, this.broker.Id).subscribe( resp => { + this.avatarUrl = 'url(' + str + ' )'; + }, err => { + this.showDialog('Failed to Update Avatar: ' + err.toString()); + }); + this.fs.clearFiles(); + }; + reader.readAsDataURL(file.rawFile); + }else{ + this.showDialog('Only jpg, and png are supported (max 2MB)'); + setTimeout(() => { this.fs.clearFiles(); }, 10); + } + return false; // we only take first file + }); + } + } + + public showDialog(msg: string): void { + this.Message = msg; + this.opened = true; // open dialog + } + + public close(status): void { + this.opened = false; + } } diff --git a/src/app/broker-reward/broker-reward.component.html b/src/app/broker-reward/broker-reward.component.html index fee1874..d38a67b 100644 --- a/src/app/broker-reward/broker-reward.component.html +++ b/src/app/broker-reward/broker-reward.component.html @@ -10,10 +10,10 @@ {{ dataItem.Amount | currency }} - + - - + + {{dataItem.Status}} @@ -28,6 +28,6 @@ - + diff --git a/src/app/dashboard/dashboard.component.scss b/src/app/dashboard/dashboard.component.scss index 1305331..4343ee4 100644 --- a/src/app/dashboard/dashboard.component.scss +++ b/src/app/dashboard/dashboard.component.scss @@ -14,4 +14,3 @@ /* background-color:chartreuse; */ } - diff --git a/src/app/models/api-v1-login-response.ts b/src/app/models/api-v1-login-response.ts index c0b3bee..d3fc83d 100644 --- a/src/app/models/api-v1-login-response.ts +++ b/src/app/models/api-v1-login-response.ts @@ -2,8 +2,9 @@ // tslint:disable-next-line:class-name import {PeopleModel} from './people.model'; +import {UserExtraModel} from './user-extra.model'; -export class apiV1LoginResponse { +export class ApiV1LoginResponse { constructor( public login: boolean, @@ -11,7 +12,8 @@ export class apiV1LoginResponse { public session: string, public sessionExpire: number, // unix timestamp public role: string, - public user: PeopleModel + public user: PeopleModel, + public userExtra?: UserExtraModel // extra user informaiton ) { this.login = login; this.machineId = machineId; @@ -19,8 +21,8 @@ export class apiV1LoginResponse { this.sessionExpire = sessionExpire; } - public static EmptyNew(): apiV1LoginResponse{ - return new apiV1LoginResponse( + public static EmptyNew(): ApiV1LoginResponse{ + return new ApiV1LoginResponse( false, '', '', 0, '', PeopleModel.EmptyNew() ); } diff --git a/src/app/models/broker.model.ts b/src/app/models/broker.model.ts index f6232dd..2058dc0 100644 --- a/src/app/models/broker.model.ts +++ b/src/app/models/broker.model.ts @@ -1,4 +1,5 @@ import {PeopleModel} from './people.model'; +import {UserExtraModel} from './user-extra.model'; export class BrokerModel{ constructor( @@ -25,4 +26,10 @@ export class BrokerModel{ return new BrokerModel('', '', '', '', '', '', '', '', false, '', '', '', ''); } + + public static getFromUserAndExtra(u: PeopleModel, ex: UserExtraModel): BrokerModel { + return new BrokerModel( + u.Id, u.First, u.Last, u.Middle, u.Title, u.Display, u.Nick, ex.Login, ex.Enabled, ex.BSB, ex.ACC, ex.License, ex.Organization + ); + } } diff --git a/src/app/models/change-password.model.ts b/src/app/models/change-password.model.ts new file mode 100644 index 0000000..a753a4b --- /dev/null +++ b/src/app/models/change-password.model.ts @@ -0,0 +1,8 @@ + + + +export class ChangePassword { + public OldPassword: string; + public NewPass: string; + public NewPass1: string; +} diff --git a/src/app/models/user-extra.model.ts b/src/app/models/user-extra.model.ts new file mode 100644 index 0000000..4e68539 --- /dev/null +++ b/src/app/models/user-extra.model.ts @@ -0,0 +1,15 @@ + +export class UserExtraModel { + public BSB: string; + public ACC: string; + public License: string; + public Organization: string; + public Enabled: boolean; + public Login: string; + + constructor(payload: Partial) { + Object.assign(this, payload); + } + +} + diff --git a/src/app/service/auth.service.ts b/src/app/service/auth.service.ts index 46ed0c8..30e0909 100644 --- a/src/app/service/auth.service.ts +++ b/src/app/service/auth.service.ts @@ -1,8 +1,9 @@ import {EventEmitter, Injectable, OnDestroy, OnInit} from '@angular/core'; import {HttpClient, HttpEvent, HttpHandler, HttpInterceptor, HttpParams, HttpRequest} from '@angular/common/http'; -import {apiV1LoginResponse} from '../models/api-v1-login-response'; +import {ApiV1LoginResponse} from '../models/api-v1-login-response'; import {Router} from '@angular/router'; import {PeopleModel} from '../models/people.model'; +import {UserExtraModel} from '../models/user-extra.model'; @Injectable() export class AuthService { @@ -11,8 +12,8 @@ export class AuthService { public apiWsUrl = 'wss://svr2021.lawipac.com:8080/api/v1/ws'; // public apiUrl = 'https://c5016.biukop.com.au:8080/api/v1/'; // public apiWsUrl = 'wss://c5016.biukop.com.au:8080/api/v1/ws'; - public loggedIn = apiV1LoginResponse.EmptyNew(); - loginSuccess = new EventEmitter (); + public loggedIn = ApiV1LoginResponse.EmptyNew(); + loginSuccess = new EventEmitter (); constructor( private http: HttpClient , private router: Router) { @@ -20,21 +21,24 @@ export class AuthService { public AutoLogin(): void { - const sfm: apiV1LoginResponse = JSON.parse(localStorage.getItem('sfm')); + const sfm: ApiV1LoginResponse = JSON.parse(localStorage.getItem('sfm')); if (!sfm) { console.log('no auto login'); return; } - this.loggedIn = new apiV1LoginResponse( + this.loggedIn = new ApiV1LoginResponse( sfm.login, sfm.machineId, sfm.session, sfm.sessionExpire, sfm.role, - new PeopleModel( - sfm.user.Id, sfm.user.First, sfm.user.Last, sfm.user.Middle, sfm.user.Title, sfm.user.Display, sfm.user.Nick - ) + new PeopleModel(sfm.user.Id, sfm.user.First, sfm.user.Last, sfm.user.Middle, sfm.user.Title, sfm.user.Display, sfm.user.Nick) ); + + if ( sfm.userExtra !== undefined ) { + this.loggedIn.userExtra = new UserExtraModel(sfm.userExtra); + } + this.loginSuccess.emit(this.loggedIn); // console.log ( 'auto login emit events', this.loggedIn); } @@ -55,7 +59,7 @@ export class AuthService { login(email: string, password: string): void { - this.http.post(this.apiUrl + 'login', {u: email, p: password}).subscribe( + this.http.post(this.apiUrl + 'login', {u: email, p: password}).subscribe( responseData => { this.loggedIn.session = responseData['Biukop-Session']; this.loggedIn.login = responseData.login; @@ -72,14 +76,19 @@ export class AuthService { responseData.user.Display, responseData.user.Nick ); + + if (responseData.userExtra !== undefined ) { + this.loggedIn.userExtra = new UserExtraModel(responseData.userExtra); + } }else{ this.loggedIn.user = PeopleModel.EmptyNew(); } + this.saveSessionInfo(); this.loginSuccess.emit(responseData); }, error => { - const fail = apiV1LoginResponse.EmptyNew(); + const fail = ApiV1LoginResponse.EmptyNew(); this.loggedIn = fail; console.log('login error', error); this.loginSuccess.emit(this.loggedIn); @@ -89,7 +98,7 @@ export class AuthService { } logout(): void { - this.loggedIn = apiV1LoginResponse.EmptyNew(); + this.loggedIn = ApiV1LoginResponse.EmptyNew(); localStorage.removeItem('sfm'); this.loginSuccess.emit(this.loggedIn); this.router.navigate(['/login']).then(r => { diff --git a/src/app/service/people.service.ts b/src/app/service/people.service.ts index bb574bd..5884c16 100644 --- a/src/app/service/people.service.ts +++ b/src/app/service/people.service.ts @@ -5,6 +5,7 @@ import {Observable} from 'rxjs'; import {PeopleModel} from '../models/people.model'; import {BrokerModel} from '../models/broker.model'; import {LoanModel} from '../models/loan.model'; +import {ChangePassword} from '../models/change-password.model'; @Injectable({providedIn: 'root'}) export class PeopleService { @@ -28,4 +29,16 @@ export class PeopleService { return this.http.post(this.auth.getUrl('sync-people/'), loan); } + public updateAvatar(avatar: string, id: string ): Observable { + return this.http.post(this.auth.getUrl('avatar/' + id), avatar); + } + + public savePassword(id: string, change: ChangePassword): Observable{ + return this.http.post(this.auth.getUrl('change-pass/' + id), change); + } + + public saveBroker(broker: BrokerModel): Observable{ + return this.http.post(this.auth.getUrl('broker/' + broker.Id), broker); + } + } diff --git a/src/app/top-bar/top-bar.component.ts b/src/app/top-bar/top-bar.component.ts index 7836675..1726b28 100644 --- a/src/app/top-bar/top-bar.component.ts +++ b/src/app/top-bar/top-bar.component.ts @@ -2,7 +2,7 @@ import {Component, OnDestroy, OnInit} from '@angular/core'; import {MenuService} from '../service/menu.service'; import {AuthService} from '../service/auth.service'; import {brokerMenuItems, mainMenuItems, peopleMenuItems, userMenuItems} from '../main-menu-items'; -import {apiV1LoginResponse} from '../models/api-v1-login-response'; +import {ApiV1LoginResponse} from '../models/api-v1-login-response'; import {Subscription} from 'rxjs'; import {PeopleModel} from '../models/people.model'; @@ -38,7 +38,7 @@ export class TopBarComponent implements OnInit , OnDestroy { this.selectMenuShow(this.authService.loggedIn.role); this.loginSub = this.authService.loginSuccess.subscribe( - (rsp: apiV1LoginResponse) => { + (rsp: ApiV1LoginResponse) => { this.login = rsp.login; this.LoggedInUser = this.authService.loggedIn.user; this.selectMenuShow(rsp.role); diff --git a/src/app/validator/confirmed.validator.ts b/src/app/validator/confirmed.validator.ts new file mode 100644 index 0000000..571a830 --- /dev/null +++ b/src/app/validator/confirmed.validator.ts @@ -0,0 +1,16 @@ +import { FormGroup } from '@angular/forms'; + +export function ConfirmedValidator(controlName: string, matchingControlName: string) { + return (formGroup: FormGroup) => { + const control = formGroup.controls[controlName]; + const matchingControl = formGroup.controls[matchingControlName]; + if (matchingControl.errors && !matchingControl.errors.confirmedValidator) { + return; + } + if (control.value !== matchingControl.value) { + matchingControl.setErrors({ confirmedValidator: true }); + } else { + matchingControl.setErrors(null); + } + }; +}