사용자 검색 1차 커밋

This commit is contained in:
leejinho 2020-01-29 09:58:40 +09:00
parent fe041f1ea2
commit d2032c4625
19 changed files with 788 additions and 2 deletions

View File

@ -5,6 +5,7 @@ import { DIALOGS as MESSAGE_DIALOGS } from './message';
import { DIALOGS as NOTICE_DIALOGS } from './notice';
import { DIALOGS as ORGANIZATION_DIALOGS } from './organization';
import { DIALOGS as PROFILE_DIALOGS } from './profile';
import { DIALOGS as SEARCH_DIALOGS } from './search';
import { DIALOGS as SETTINGS_DIALOGS } from './settings';
export const DIALOGS = [
@ -15,5 +16,6 @@ export const DIALOGS = [
...NOTICE_DIALOGS,
...ORGANIZATION_DIALOGS,
...PROFILE_DIALOGS,
...SEARCH_DIALOGS,
...SETTINGS_DIALOGS
];

View File

@ -0,0 +1,3 @@
import { IntegratedSearchDialogComponent } from './integrated-search.dialog.component';
export const DIALOGS = [IntegratedSearchDialogComponent];

View File

@ -0,0 +1,11 @@
<ucap-integrated-search
[searchWord]="!!data.keyword ? data.keyword : ''"
[searchingProcessing]="searchingProcessing"
[searchUserInfos]="searchUserInfos"
[totalCount]="totalCount"
[pageCurrent]="pageCurrent"
[pageListCount]="pageListCount"
(search)="onSearch($event)"
(changePage)="onChangePage($event)"
>
</ucap-integrated-search>

View File

@ -0,0 +1,25 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IntegratedSearchDialogComponent } from './integrated-search.dialog.component';
describe('IntegratedSearchDialogComponent', () => {
let component: IntegratedSearchDialogComponent;
let fixture: ComponentFixture<IntegratedSearchDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [IntegratedSearchDialogComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IntegratedSearchDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,152 @@
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA, PageEvent } from '@angular/material';
import { KEY_LOGIN_RES_INFO } from '@app/types';
import { SessionStorageService } from '@ucap-webmessenger/web-storage';
import { Store } from '@ngrx/store';
import * as StatusStore from '@app/store/messenger/status';
import {
UserInfoSS,
QueryProtocolService,
DeptSearchType,
SSVC_TYPE_QUERY_DEPT_USER_DATA,
DeptUserData,
SSVC_TYPE_QUERY_DEPT_USER_RES,
DeptUserResponse
} from '@ucap-webmessenger/protocol-query';
import { LoginResponse } from '@ucap-webmessenger/protocol-authentication';
import { map, catchError } from 'rxjs/operators';
import { Subscription, of } from 'rxjs';
import { EnvironmentsInfo, KEY_ENVIRONMENTS_INFO } from '@app/types';
import { NGXLogger } from 'ngx-logger';
import { NativeService, UCAP_NATIVE_SERVICE } from '@ucap-webmessenger/native';
import { environment } from '../../../../../environments/environment';
import { TranslateService } from '@ngx-translate/core';
export interface IntegratedSearchDialogData {
keyword: string;
}
export interface IntegratedSearchDialogResult {}
@Component({
selector: 'app-integrated-search.dialog',
templateUrl: './integrated-search.dialog.component.html',
styleUrls: ['./integrated-search.dialog.component.scss']
})
export class IntegratedSearchDialogComponent implements OnInit, OnDestroy {
loginRes: LoginResponse;
environmentsInfo: EnvironmentsInfo;
searchSubscription: Subscription;
searchUserInfos: UserInfoSS[] = [];
searchingProcessing = false;
currentSearchWord: string;
totalCount = 0;
pageCurrent = 1;
pageListCount = 20;
constructor(
public dialogRef: MatDialogRef<
IntegratedSearchDialogData,
IntegratedSearchDialogResult
>,
@Inject(UCAP_NATIVE_SERVICE) private nativeService: NativeService,
@Inject(MAT_DIALOG_DATA) public data: IntegratedSearchDialogData,
private queryProtocolService: QueryProtocolService,
private sessionStorageService: SessionStorageService,
private store: Store<any>,
private logger: NGXLogger
) {
this.loginRes = this.sessionStorageService.get<LoginResponse>(
KEY_LOGIN_RES_INFO
);
this.environmentsInfo = this.sessionStorageService.get<EnvironmentsInfo>(
KEY_ENVIRONMENTS_INFO
);
}
ngOnInit() {
this.onSearch(this.data.keyword);
}
onSearch(searchWord: string) {
this.currentSearchWord = this.data.keyword;
if (searchWord.trim().length > 0) {
this.searchingProcessing = true;
const searchUserInfos: UserInfoSS[] = [];
this.searchSubscription = this.queryProtocolService
.deptUser({
divCd: 'INT_SRCH',
companyCode: this.loginRes.companyCode,
searchRange: DeptSearchType.All,
search: searchWord.trim(),
senderCompanyCode: this.loginRes.companyCode,
senderEmployeeType: this.loginRes.userInfo.employeeType,
pageCurrent: this.pageCurrent,
pageListCount: this.pageListCount
})
.pipe(
map(res => {
switch (res.SSVC_TYPE) {
case SSVC_TYPE_QUERY_DEPT_USER_DATA:
const userInfos = (res as DeptUserData).userInfos;
searchUserInfos.push(...userInfos);
break;
case SSVC_TYPE_QUERY_DEPT_USER_RES:
{
const response = res as DeptUserResponse;
// 검색 결과 처리.
this.searchUserInfos = searchUserInfos;
this.totalCount = response.pageTotalCount;
this.pageCurrent = response.pageCurrent;
this.pageListCount = response.pageListCount;
this.searchingProcessing = false;
// 검색 결과에 따른 프레즌스 조회.
const userSeqList: number[] = [];
if (userSeqList.length > 0) {
this.store.dispatch(
StatusStore.bulkInfo({
divCd: 'INT_SRCH',
userSeqs: this.searchUserInfos.map(user => user.seq)
})
);
}
console.log(this.searchUserInfos);
}
break;
}
}),
catchError(error => {
this.searchingProcessing = false;
return of(this.logger.error(error));
})
)
.subscribe();
} else {
// clear list.
this.searchingProcessing = false;
this.searchUserInfos = [];
}
}
ngOnDestroy(): void {
if (!!this.searchSubscription) {
this.searchSubscription.unsubscribe();
}
}
onChangePage(event: PageEvent) {
this.pageCurrent = event.pageIndex + 1;
this.pageListCount = event.pageSize;
this.onSearch(this.currentSearchWord);
}
}

View File

@ -8,6 +8,9 @@
*ngIf="!!loginRes && !!weblink"
class="app-layout-native-title-bar-link"
>
<ucap-integrated-search-form (search)="onIntegratedSearch($event)">
</ucap-integrated-search-form>
<ng-container *ngFor="let link of weblink" [ngSwitch]="link.key">
<button
mat-icon-button
@ -230,7 +233,6 @@
class="button app-layout-native-title-bar-close"
(click)="onClickClose()"
>
<!--<mat-icon>close</mat-icon>-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"

View File

@ -70,6 +70,11 @@ import {
MessageIndexType,
MessageUpdateRequest
} from '@ucap-webmessenger/protocol-status';
import {
IntegratedSearchDialogComponent,
IntegratedSearchDialogResult,
IntegratedSearchDialogData
} from '@app/layouts/messenger/dialogs/search/integrated-search.dialog.component';
const zoomFactors = [60, 70, 85, 100, 120, 145, 170, 200];
@ -555,4 +560,16 @@ export class TopBarComponent implements OnInit, OnDestroy {
onClickApplyUpdate(event: Event) {
this.nativeService.checkForUpdates(this.checkingUpdateAppVersion);
}
onIntegratedSearch(keyword: string) {
this.dialogService.open<
IntegratedSearchDialogComponent,
IntegratedSearchDialogData,
IntegratedSearchDialogResult
>(IntegratedSearchDialogComponent, {
data: {
keyword
}
});
}
}

View File

@ -73,6 +73,20 @@
"failToChangeProfileImage": "Failed to change profile image."
}
},
"search": {
"label": "User search",
"searchFormPlaceholder": "Name, department, position, phone number, email",
"fieldProfile": "Profile",
"fieldName": "Name",
"fieldDeptartment": "Department",
"fieldCompany": "Company",
"fieldGrade": "Grade",
"fieldOfficePhoneNumber": "Office",
"fieldHandphone": "Mobile",
"fieldEmail": "Email",
"fieldResponsibilities": "Responsibilities",
"fieldWorkPlace": "WorkPlace"
},
"information": {
"label": "Information",
"version": "Version",

View File

@ -73,6 +73,20 @@
"failToChangeProfileImage": "프로필 이미지 변경에 실패 하였습니다."
}
},
"search": {
"label": "사용자 검색",
"searchFormPlaceholder": "이름, 부서, 직위, 전화번호, 이메일",
"fieldProfile": "프로필",
"fieldName": "이름",
"fieldDeptartment": "부서",
"fieldCompany": "회사",
"fieldGrade": "직위",
"fieldOfficePhoneNumber": "사무실",
"fieldHandphone": "핸드폰",
"fieldEmail": "이메일",
"fieldResponsibilities": "담당업무",
"fieldWorkPlace": "근무지"
},
"information": {
"label": "정보",
"version": "버전",

View File

@ -0,0 +1,12 @@
<div class="search-container">
<mat-form-field appearance="outline">
<input
matInput
#searchWordInput
placeholder="{{ 'search.searchFormPlaceholder' | translate }}"
(keydown.enter)="onKeyDownEnter(searchWordInput.value)"
[value]="!!searchWord ? searchWord : ''"
/>
<mat-icon matPrefix>search</mat-icon>
</mat-form-field>
</div>

View File

@ -0,0 +1,3 @@
.search-container {
-webkit-app-region: no-drag;
}

View File

@ -0,0 +1,27 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { IntegratedSearchFormComponent } from './integrated-search-form.component';
describe('IntegratedSearchFormComponent', () => {
let component: IntegratedSearchFormComponent;
let fixture: ComponentFixture<IntegratedSearchFormComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [IntegratedSearchFormComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IntegratedSearchFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,31 @@
import {
Component,
OnInit,
Input,
Output,
EventEmitter,
ChangeDetectorRef
} from '@angular/core';
import { ucapAnimations } from '../animations';
@Component({
selector: 'ucap-integrated-search-form',
templateUrl: './integrated-search-form.component.html',
styleUrls: ['./integrated-search-form.component.scss'],
animations: ucapAnimations
})
export class IntegratedSearchFormComponent implements OnInit {
@Input()
searchWord?: string;
@Output()
search = new EventEmitter<string>();
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit() {}
onKeyDownEnter(keyword: string) {
this.search.emit(keyword);
}
}

View File

@ -0,0 +1,221 @@
<mat-card class="confirm-card mat-elevation-z">
<mat-card-header
cdkDrag
cdkDragRootElement=".cdk-overlay-pane"
cdkDragHandle
class="card-header"
>
<mat-card-title>{{ 'search.label' | translate }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<div fxLayout="column" class="rightDrawer-notice">
<div fxFlex="1 1 80px" class="search-area">
<ucap-integrated-search-form
[searchWord]="!!searchWord ? searchWord : ''"
(search)="onSearch($event)"
>
</ucap-integrated-search-form>
</div>
<div style="position: relative;">
<div
*ngIf="searchingProcessing"
style="position: absolute; width: 100%; z-index: 101;"
>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</div>
<div fxFlex="0 0 auto" class="table-box">
<perfect-scrollbar class="search-scrollbar">
<table mat-table [dataSource]="searchUserInfos">
<ng-container matColumnDef="profile">
<th
mat-header-cell
*matHeaderCellDef
#header
class="profile"
(mousedown)="resizeTable($event, header)"
>
{{ 'search.fieldProfile' | translate }}
</th>
<td mat-cell *matCellDef="let element">
<div class="profile">
{{ element.name }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th
mat-header-cell
*matHeaderCellDef
#header
class="name"
(mousedown)="resizeTable($event, header)"
>
{{ 'search.fieldName' | translate }}
</th>
<td mat-cell *matCellDef="let element">
<div class="name">
{{ element.name }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="deptName">
<th
mat-header-cell
*matHeaderCellDef
#header
class="deptName"
(mousedown)="resizeTable($event, header)"
>
{{ 'search.fieldDeptartment' | translate }}
</th>
<td mat-cell *matCellDef="let element">
<div class="deptName">
{{ element.deptName }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="companyName">
<th
mat-header-cell
*matHeaderCellDef
#header
class="companyName"
(mousedown)="resizeTable($event, header)"
>
{{ 'search.fieldCompany' | translate }}
</th>
<td mat-cell *matCellDef="let element">
<div class="companyName">
{{ element.companyName }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="grade">
<th
mat-header-cell
*matHeaderCellDef
#header
class="grade"
(mousedown)="resizeTable($event, header)"
>
{{ 'search.fieldGrade' | translate }}
</th>
<td mat-cell *matCellDef="let element" class="grade">
<div class="grade">
{{ element.grade }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="lineNumber">
<th
mat-header-cell
*matHeaderCellDef
#header
class="lineNumber"
(mousedown)="resizeTable($event, header)"
>
{{ 'search.fieldOfficePhoneNumber' | translate }}
</th>
<td mat-cell *matCellDef="let element" class="lineNumber">
<div class="lineNumber">
{{ element.lineNumber }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="hpNumber">
<th
mat-header-cell
*matHeaderCellDef
#header
class="hpNumber"
(mousedown)="resizeTable($event, header)"
>
{{ 'search.fieldHandphone' | translate }}
</th>
<td mat-cell *matCellDef="let element" class="hpNumber">
<div class="hpNumber">
{{ element.hpNumber }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="email">
<th
mat-header-cell
*matHeaderCellDef
#header
class="email"
(mousedown)="resizeTable($event, header)"
>
{{ 'search.fieldEmail' | translate }}
</th>
<td mat-cell *matCellDef="let element" class="email">
<div class="email">
{{ element.email }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="responsibilities">
<th
mat-header-cell
*matHeaderCellDef
#header
class="responsibilities"
(mousedown)="resizeTable($event, header)"
>
{{ 'search.fieldResponsibilities' | translate }}
</th>
<td mat-cell *matCellDef="let element" class="responsibilities">
<div class="responsibilities">
{{ element.responsibilities }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="workplace">
<th
mat-header-cell
*matHeaderCellDef
#header
class="workplace"
(mousedown)="resizeTable($event, header)"
>
{{ 'search.fieldWorkPlace' | translate }}
</th>
<td mat-cell *matCellDef="let element" class="workplace">
<div class="workplace">
{{ element.workplace }}
</div>
</td>
</ng-container>
<tr
mat-header-row
*matHeaderRowDef="displayedColumns; sticky: true"
></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</perfect-scrollbar>
</div>
<div class="footer-fix">
<mat-paginator
[length]="totalCount"
[pageSize]="pageListCount"
[pageSizeOptions]="[10, 20, 30]"
(page)="onChangePage($event)"
showFirstLastButtons
></mat-paginator>
</div>
</div>
</mat-card-content>
<!-- <mat-card-actions *ngIf="!hideAction" class="button-farm flex-row">
<button
mat-stroked-button
(click)="onClickChoice(false)"
class="mat-primary"
>
{{ 'common.messages.no' | translate }}
</button>
<button mat-flat-button (click)="onClickChoice(true)" class="mat-primary">
{{ 'common.messages.yes' | translate }}
</button>
</mat-card-actions> -->
</mat-card>

View File

@ -0,0 +1,106 @@
.search-scrollbar {
height: 550px;
}
// .search-container {
// -webkit-app-region: no-drag;
// }
// @mixin ellipsis($row) {
// overflow: hidden;
// text-overflow: ellipsis;
// @if $row == 1 {
// display: block;
// white-space: nowrap;
// word-wrap: normal;
// } @else if $row >= 2 {
// display: -webkit-box;
// -webkit-line-clamp: $row;
// -webkit-box-orient: vertical;
// word-wrap: break-word;
// line-height: 1.2em;
// }
// }
// .rightDrawer-notice {
// width: 100%;
// height: calc(100% - 60px);
// .table-box {
// height: calc(100% - 111.5px);
// overflow: auto;
// }
// }
// .mat-table {
// width: 100%;
// position: relative;
// th.infos {
// padding: 10px;
// text-align: center;
// }
// tr.mat-row {
// height: 70px;
// .notice-info {
// padding: 16px;
// display: grid;
// height: 70px;
// .title {
// font-weight: 600;
// margin-bottom: 2px;
// width: 100%;
// @include ellipsis(2);
// display: flex;
// align-items: center;
// .important {
// color: red;
// margin-right: 6px;
// }
// }
// }
// .date {
// .date {
// font-size: 0.8em;
// text-align: right;
// }
// }
// }
// }
// .mat-paginator-container {
// display: flex;
// flex-flow: column;
// }
// .mat-row:hover {
// background: rgba(0, 0, 0, 0.04);
// cursor: pointer;
// }
// .footer-fix {
// position: absolute;
// bottom: 0;
// flex-direction: column;
// box-sizing: border-box;
// display: flex;
// border-top: 1px solid #dddddd;
// .mat-paginator {
// .mat-paginator-container {
// justify-content: center;
// }
// }
// .btn-box {
// height: 50px;
// padding-bottom: 10px;
// width: 100%;
// background-color: #ffffff;
// button {
// margin: 5px;
// }
// }
// }
// .mat-form-field-appearance-legacy {
// .mat-form-field-infix {
// padding: 6px;
// }
// }

View File

@ -0,0 +1,27 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { IntegratedSearchComponent } from './integrated-search.component';
describe('IntegratedSearchComponent', () => {
let component: IntegratedSearchComponent;
let fixture: ComponentFixture<IntegratedSearchComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [IntegratedSearchComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IntegratedSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,108 @@
import {
Component,
OnInit,
Input,
Output,
EventEmitter,
ViewChild,
ChangeDetectorRef,
Renderer2
} from '@angular/core';
import { ucapAnimations } from '../animations';
import { UserInfoSS } from '@ucap-webmessenger/protocol-query';
import { MatPaginator, PageEvent, MatTabLabel } from '@angular/material';
@Component({
selector: 'ucap-integrated-search',
templateUrl: './integrated-search.component.html',
styleUrls: ['./integrated-search.component.scss'],
animations: ucapAnimations
})
export class IntegratedSearchComponent implements OnInit {
@Input()
searchWord?: string;
@Input()
searchingProcessing: boolean;
@Input()
searchUserInfos: UserInfoSS[] = [];
@Input()
totalCount = 0;
@Input()
pageCurrent = 1;
@Input()
pageListCount = 20;
@Output()
search = new EventEmitter<string>();
@Output()
changePage = new EventEmitter<PageEvent>();
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
displayedColumns: string[] = [
'profile',
'name',
'deptName',
'companyName',
'grade',
'lineNumber',
'hpNumber',
'email',
'responsibilities',
'workplace'
];
start: any = undefined;
pressed = false;
startX: any;
startWidth: any;
resizableFnMousemove: () => void;
resizableFnMouseup: () => void;
constructor(
private changeDetectorRef: ChangeDetectorRef,
private renderer: Renderer2
) {}
ngOnInit() {}
onSearch(keyword: string) {
this.search.emit(keyword);
}
onChangePage(event: PageEvent) {
this.changePage.emit(event);
}
resizeTable(event: any, column: any) {
this.start = event.target;
this.pressed = true;
this.startX = event.pageX;
this.startWidth = this.start.clientWidth;
this.mouseMove(column);
}
mouseMove(column: any) {
this.resizableFnMousemove = this.renderer.listen(
'document',
'mousemove',
event => {
if (this.pressed) {
let width = this.startWidth + (event.pageX - this.startX);
let index = this.start.cellIndex;
column.width = width;
}
}
);
this.resizableFnMouseup = this.renderer.listen(
'document',
'mouseup',
event => {
if (this.pressed) {
this.pressed = false;
this.resizableFnMousemove();
this.resizableFnMouseup();
}
}
);
}
}

View File

@ -22,7 +22,9 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
import {
MatTabsModule,
MatSelectModule,
MatSlideToggleModule
MatSlideToggleModule,
MatTableModule,
MatPaginatorModule
} from '@angular/material';
import { DragDropModule } from '@angular/cdk/drag-drop';
@ -77,6 +79,9 @@ import {
import { ClickDebounceDirective } from './directives/click-debounce.directive';
import { TranslationSectionComponent } from './components/translation-section.component';
import { AlertSnackbarComponent } from './snackbars/alert.snackbar.component';
import { IntegratedSearchFormComponent } from './components/integrated-search-form.component';
import { IntegratedSearchComponent } from './components/integrated-search.component';
import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
const COMPONENTS = [
FileUploadQueueComponent,
@ -90,6 +95,8 @@ const COMPONENTS = [
StepInputComponent,
TranslationSectionComponent,
InlineEditInputComponent,
IntegratedSearchComponent,
IntegratedSearchFormComponent,
BinaryViewerComponent,
DocumentViewerComponent,
@ -153,6 +160,10 @@ const SERVICES = [
MatDatepickerModule,
MatSelectModule,
MatSlideToggleModule,
MatTableModule,
MatPaginatorModule,
PerfectScrollbarModule,
DragDropModule,
TranslateModule
],