조회 부서원 리스트 표현 1차.

This commit is contained in:
leejinho 2020-03-04 10:24:44 +09:00
parent 3064213ed5
commit d52ace1f03
3 changed files with 540 additions and 9 deletions

View File

@ -1,12 +1,33 @@
<div class="container" fxFlex fxLayout="column"> <div class="container" fxFlex fxLayout="column">
<mat-toolbar class="organization-toolbar"> <mat-toolbar class="organization-toolbar">
<div fxFlex fxLayout="row" class="organization-header"> <div fxFlex fxLayout="row" class="organization-header">
<div fxLayout="row" fxLayoutAlign="start center" class="profile-img"> <div
icon fxLayout="row"
</div> fxLayoutAlign="start center"
class="profile-img"
></div>
<div class="organization-info"> <div class="organization-info">
<h3 class="organization-name"> <h3 class="organization-name">
title <ng-container *ngIf="!(isSearch$ | async)">
<ng-container
*ngIf="
!!(selectedDepartmentProcessing$ | async);
else useSelectedDepartmentName
"
>
{{ 'common.messages.searching' | translate }} ...
</ng-container>
<ng-template #useSelectedDepartmentName>
{{ selectedDepartment$ | async | ucapTranslate: 'name' }}
</ng-template>
</ng-container>
<ng-container *ngIf="!!(isSearch$ | async)">
{{ 'common.searchResult' | translate
}}<span class="text-accent-color"
>({{ (searchDepartmentUserInfoList$ | async).length }}
{{ 'common.units.persons' | translate }})</span
>
</ng-container>
</h3> </h3>
</div> </div>
<div class="organization-option"> <div class="organization-option">
@ -15,10 +36,165 @@
</button> </button>
</div> </div>
</div> </div>
<div class="progress">
<div
*ngIf="selectedDepartmentProcessing$ | async"
style="position: absolute; width: 100%;"
>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</div>
</mat-toolbar> </mat-toolbar>
<div fxFlex="1 1 auto" class="organization-content"> <div fxFlex="1 1 auto" class="organization-content">
contents <div fxFlex="0 0 auto" class="table-box">
<perfect-scrollbar class="scrollbar">
<table mat-table [dataSource]="departmentUserInfoList$ | async">
<ng-container matColumnDef="profile">
<th mat-header-cell *matHeaderCellDef class="profile">
{{ 'search.fieldProfile' | translate }}
</th>
<td mat-cell *matCellDef="let element">
<div class="table-item">
<div class="profile">
<span
class="presence"
[ngClass]="getPresence(element, PresenceType.PC)"
[matTooltip]="getPresenceMsg(element)"
matTooltipPosition="after"
></span>
<span class="thumbnail-mask">
<img
class="thumbnail"
ucapImage
[base]="profileImageRoot"
[path]="element.profileImageFile"
[default]="'assets/images/img_nophoto_50.png'"
(click)="onClickOpenProfile($event, element.seq)"
/>
</span>
<span
*ngIf="
getPresence(element, PresenceType.MOBILE) === 'mobileOn'
"
class="text-accent-color marker-mobile-state"
>
<mat-icon>phone_android</mat-icon>
</span>
</div>
<div>
<span
class="work-status"
[ngClass]="getWorkstatusInfo(element, 'style')"
>
{{ getWorkstatusInfo(element, 'text') }}
</span>
<span class="name">
{{ element.name }}
</span>
</div>
</div>
</td>
</ng-container>
<ng-container matColumnDef="grade">
<th mat-header-cell *matHeaderCellDef class="grade">
{{ 'search.fieldGrade' | translate }}
</th>
<td mat-cell *matCellDef="let element" class="grade">
<div class="grade">
{{ element.grade }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="deptName">
<th mat-header-cell *matHeaderCellDef class="deptName">
{{ '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 class="companyName">
{{ 'search.fieldCompany' | translate }}
</th>
<td mat-cell *matCellDef="let element">
<div class="companyName">
{{ element.companyName }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="lineNumber">
<th mat-header-cell *matHeaderCellDef class="lineNumber">
{{ '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 class="hpNumber">
{{ '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 class="email">
{{ '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 class="responsibilities">
{{ '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 class="workplace">
{{ '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
class="no-search-result"
fxFlexFill
*ngIf="
!(departmentUserInfoList$ | async) ||
0 === (departmentUserInfoList$ | async).length
"
>
{{ 'common.noSearchResult' | translate }}
</div>
</div>
</div> </div>
<div fxFlex="0 0 auto" fxLayout="column"></div> <div fxFlex="0 0 auto" fxLayout="column"></div>

View File

@ -36,6 +36,7 @@
padding: 0; padding: 0;
.organization-header { .organization-header {
width: 100%; width: 100%;
height: 50px;
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -64,6 +65,10 @@
} }
} }
} }
.progress {
position: relative;
width: 100%;
}
} }
.organization-content { .organization-content {
@ -71,4 +76,137 @@
background: transparent; background: transparent;
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
margin: 20px;
}
@mixin disable-selection {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */
}
.scrollbar {
height: 550px;
}
.table-box {
font-size: 13px;
width: 100%;
overflow: hidden;
display: flex;
table {
table-layout: fixed;
width: 100%;
th {
@include disable-selection;
text-align: center;
&.profile {
width: 200px;
min-width: 200px;
}
}
td.mat-cell {
padding: 6px;
position: relative;
width: 100px;
div {
@include ellipsis(1);
&.table-item {
display: flex;
width: 200px;
min-width: 200px;
font-size: 1em;
div {
display: inline-flex;
height: 100%;
align-self: center;
.name {
@include ellipsis(1);
font-size: 1em;
font-weight: 600;
}
.status {
font-size: 0.84em;
}
&.profile {
width: 70px;
text-overflow: unset;
flex: 0 0 auto;
.presence {
transform: translateY(6px);
}
.thumbnail {
cursor: pointer;
&-mask {
display: inline-block;
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
margin-right: 0;
position: relative;
img {
width: 40px;
height: auto;
background-color: #efefef;
}
}
}
.marker-mobile-state {
position: absolute;
background-color: #ffffff;
width: 20px;
height: 20px;
border-radius: 50%;
bottom: 4px;
left: 50px;
display: flex;
align-items: center;
align-content: center;
justify-content: center;
.mat-icon {
font-size: 0.9em;
width: 18px;
height: 18px;
line-height: 18px;
min-width: 18px;
min-height: 18px;
}
}
}
}
}
}
}
}
}
.work-status {
display: inline-block;
justify-content: center;
justify-items: center;
color: #ffffff;
height: 100%;
min-width: 32px;
margin-right: 4px;
border-radius: 24px;
flex: 0 0 auto;
font-size: 0.8em;
text-align: center;
}
.no-search-result {
display: flex;
width: 100%;
margin-top: 40px;
justify-content: center;
justify-items: center;
font-size: 1.1em;
} }

View File

@ -1,12 +1,229 @@
import { Component, OnInit } from '@angular/core'; import {
Component,
OnInit,
ChangeDetectorRef,
OnDestroy,
Output,
EventEmitter
} from '@angular/core';
import { SelectedDept, UserInfoSS } from '@ucap-webmessenger/protocol-query';
import { NGXLogger } from 'ngx-logger';
import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@ucap-webmessenger/ui';
import { SessionStorageService } from '@ucap-webmessenger/web-storage';
import { Store, select } from '@ngrx/store';
import * as AppStore from '@app/store';
import * as QueryStore from '@app/store/messenger/query';
import * as SyncStore from '@app/store/messenger/sync';
import * as ChatStore from '@app/store/messenger/chat';
import * as StatusStore from '@app/store/messenger/status';
import { Observable, Subscription } from 'rxjs';
import { PresenceType, StatusCode } from '@ucap-webmessenger/core';
import {
StatusBulkInfo,
WorkStatusType
} from '@ucap-webmessenger/protocol-status';
import { VersionInfo2Response } from '@ucap-webmessenger/api-public';
import { KEY_VER_INFO } from '@app/types';
@Component({ @Component({
selector: 'app-layout-messenger-organization', selector: 'app-layout-messenger-organization',
templateUrl: './organization.component.html', templateUrl: './organization.component.html',
styleUrls: ['./organization.component.scss'] styleUrls: ['./organization.component.scss']
}) })
export class OrganizationComponent implements OnInit { export class OrganizationComponent implements OnInit, OnDestroy {
constructor() {} @Output()
openProfile = new EventEmitter<number>();
ngOnInit() {} sessionVerinfo: VersionInfo2Response;
isSearch$: Observable<boolean>;
selectedDepartmentProcessing$: Observable<boolean>;
selectedDepartment$: Observable<SelectedDept>;
departmentUserInfoList$: Observable<UserInfoSS[]>;
searchDepartmentUserInfoList$: Observable<UserInfoSS[]>;
profileImageRoot: string;
PresenceType = PresenceType;
presenceSubscription: Subscription;
presence: StatusBulkInfo[] = [];
displayedColumns: string[] = [
'profile',
'deptName',
'companyName',
'grade',
'lineNumber',
'hpNumber',
'email',
'responsibilities',
'workplace'
];
constructor(
private store: Store<any>,
private sessionStorageService: SessionStorageService,
private dialogService: DialogService,
private translateService: TranslateService,
private changeDetectorRef: ChangeDetectorRef,
private logger: NGXLogger
) {
this.sessionVerinfo = this.sessionStorageService.get<VersionInfo2Response>(
KEY_VER_INFO
);
}
ngOnInit() {
this.isSearch$ = this.store.pipe(
select(AppStore.MessengerSelector.QuerySelector.isSearch)
);
this.selectedDepartmentProcessing$ = this.store.pipe(
select(
AppStore.MessengerSelector.QuerySelector.selectedDepartmentProcessing
)
);
this.selectedDepartment$ = this.store.pipe(
select(AppStore.MessengerSelector.QuerySelector.selectedDepartment)
);
this.departmentUserInfoList$ = this.store.pipe(
select(AppStore.MessengerSelector.QuerySelector.departmentUserInfoList)
);
this.searchDepartmentUserInfoList$ = this.store.pipe(
select(
AppStore.MessengerSelector.QuerySelector.searchDepartmentUserInfoList
)
);
this.presenceSubscription = this.store
.pipe(
select(
AppStore.MessengerSelector.StatusSelector.selectAllStatusBulkInfo
)
)
.subscribe(presence => {
this.presence = presence;
});
this.profileImageRoot = this.sessionVerinfo.profileRoot;
}
ngOnDestroy(): void {
if (!!this.presenceSubscription) {
this.presenceSubscription.unsubscribe();
}
}
/*******************************
* ABOUT TABLE
*******************************/
getPresence(userInfo: UserInfoSS, type: PresenceType): string {
const presences = this.presence.filter(p => p.userSeq === userInfo.seq);
let status: string;
let rtnClass = '';
if (!!presences && presences.length > 0) {
const presence = presences[0];
switch (type) {
case PresenceType.PC:
status = !!presence ? presence.pcStatus : undefined;
break;
case PresenceType.MOBILE:
status = !!presence ? presence.mobileStatus : undefined;
break;
}
if (!!status) {
switch (status) {
case StatusCode.OnLine:
rtnClass = type + 'On';
break;
case StatusCode.Away:
rtnClass = type + 'Out';
break;
case StatusCode.Busy:
rtnClass = type + 'Other';
break;
default:
rtnClass = type + 'Off';
break;
}
}
} else {
rtnClass = type + 'Off';
}
return rtnClass;
}
getPresenceMsg(userInfo: UserInfoSS): string {
const presences = this.presence.filter(p => p.userSeq === userInfo.seq);
if (!!presences && presences.length > 0) {
const presence = presences[0];
if (
!!presence &&
!!presence.statusMessage &&
presence.statusMessage !== '.'
) {
return presence.statusMessage;
}
}
return '';
}
getWorkstatusInfo(userInfo: UserInfoSS, type: string): string {
let workstatus = userInfo.workstatus;
const presences = this.presence.filter(p => p.userSeq === userInfo.seq);
if (!!presences && presences.length > 0) {
const presence = presences[0];
if (
!!presence &&
!!presence.workstatus &&
presence.workstatus.trim().length > 0
) {
workstatus = presence.workstatus;
}
}
let text = '';
// morning-off: 오전 afternoon-off: 오후 day-off: 휴가 long-time: 장기 leave-of-absence: 휴직
let style = '';
switch (workstatus) {
case WorkStatusType.VacationAM:
style = 'morning-off';
text = '오전';
break;
case WorkStatusType.VacationPM:
style = 'afternoon-off';
text = '오후';
break;
case WorkStatusType.VacationAll:
style = 'day-off';
text = '휴가';
break;
case WorkStatusType.LeaveOfAbsence:
style = 'leave-of-absence';
text = '휴직';
break;
case WorkStatusType.LongtermRefresh:
style = 'long-time';
text = '장기';
break;
}
return type === 'text' ? text : style;
}
onClickOpenProfile(event: MouseEvent, userSeq: number) {
event.preventDefault();
event.stopPropagation();
this.openProfile.emit(userSeq);
}
} }