TimeTrex/interface/html5/components/TTTopbar.vue

544 lines
21 KiB
Vue

<!-- This file is originally from the primevue apollo layout, but has been copied into our components for customization -->
<template>
<div class="topbar">
<div class="left-group">
<button class="p-link menu-button" @click="onMenuButtonClick">
<i class="pi pi-bars"></i>
</button>
<div class="company-logo-container">
<router-link to="/" class="logo-link">
<img class="logo" id="topbar-company-logo" :src="company_logo" alt="company logo" @click="onCompanyLogoClicked"/>
</router-link>
</div>
<div class="slash-line company-name">{{ company_name }}</div>
</div>
<div class="middle-group">
<div v-if="isSandboxMode()" class="sandbox-title">{{ sandbox_mode_text }}</div>
</div>
<div class="right-group">
<ProgressSpinner class="job-queue-spinner" v-if="progress_bar_visible" v-tooltip.bottom="tooltips.job_queue" style="width:30px;height:30px" strokeWidth="5" animationDuration="2s" @click="toggleJobQueuePanel"/>
<OverlayPanel ref="job-queue-panel" @show="getRunningJobsData" @hide="onHideJobQueuePanel">
<ul class="job-queue-list">
<div class="job-queue-item" v-if="pending_job_queue_tasks.length === 0">{{ job_queue_complete_text }}</div>
<div class="job-queue-item" v-for="item in pending_job_queue_tasks">
<div class="job-queue-item-summary">{{ item.name }}</div>
<div class="job-queue-item-detail">Started {{ item.elapsed_time }} ago</div>
</div>
</ul>
</OverlayPanel>
<span class="topbar-icon topbar-inout" v-if="show_punch_in_out">
<i class="tticon tticon-timer_black_24dp p-mr-4 p-text-secondary" id="profile-in-out" v-tooltip.bottom="tooltips.in_out" @click="onInOutClick"></i>
</span>
<span class="topbar-icon topbar-notification-bell">
<i v-if="notification_count === 0" class="tticon tticon-notifications_black_24dp p-mr-4 p-text-secondary" id="profile-notifications" v-tooltip.bottom="tooltips.notifications" @click="onNotificationBellClick"></i>
<i v-else class="tticon tticon-notifications_black_24dp p-mr-4 p-text-secondary" id="profile-notifications" v-tooltip.bottom="tooltips.notifications" v-badge.info="notification_count" @click="onNotificationBellClick"></i>
</span>
<span class="topbar-icon topbar-help" v-show="help_menu_items.length > 0">
<i class="tticon tticon-help_center_black_24dp p-mr-4 p-text-secondary" id="profile-help" v-tooltip.bottom="tooltips.help" @click="onTopbarMenuButtonClickHelp"></i>
</span>
<ul :class="topbarItemsClassHelp" role="menu" id="profile-help-items">
<li class="profile-menu-item" v-for="item in help_menu_items">
<div v-if="item.separator === true" class="profile-menu-separator"></div>
<button v-else class="p-link" :id="createMenuId(item.id)" @click=handleMenuClick(item)>
<i class="topbar-icon" :class="item.icon"></i>
<span class="topbar-item-name">{{ item.label }}</span>
<span v-if="item.badge_number" v-badge.info="item.badge_number"></span>
</button>
</li>
</ul>
<div class="slash-line">&nbsp;</div> <!--None breaking space required to show slash-->
<button class="p-link profile" id="profile-button" @click="onTopbarMenuButtonClickProfile" v-tooltip.bottom="tooltips.profile">
<span class="username">{{ current_user.first_name }} {{ current_user.last_name }}</span>
<div class="profile-image-holder">
<img class="profile-image" :src="user_profile_image_url" alt="apollo-layout"/>
</div>
<i v-if="totalPendingCount === 0" class="pi pi-angle-down"></i>
<i v-else class="pi pi-angle-down" v-badge.info="totalPendingCount"></i>
</button>
<ul :class="topbarItemsClassProfile" role="menu" id="profile-menu-items">
<li class="profile-menu-item" v-for="item in profile_menu_items">
<div v-if="item.separator === true" class="profile-menu-separator"></div>
<button v-else class="p-link" :id="createMenuId(item.id)" @click=handleMenuClick(item)>
<i class="topbar-icon" :class="item.icon"></i>
<span class="topbar-item-name">{{ item.label }}</span>
<span v-if="item.badge_number" v-badge.info="item.badge_number"></span>
</button>
</li>
</ul>
</div>
</div>
</template>
<script>
import InputText from 'primevue/inputtext';
import { Global } from '@/global/Global';
import ProgressSpinner from 'primevue/progressspinner';
import OverlayPanel from 'primevue/overlaypanel';
export default {
created() {
this.event_bus = new TTEventBus( {
component_id: this.component_id,
} );
this.event_bus.on( this.component_id, 'notification_bell', ( event_data ) => {
this.updateNotificationCount( event_data.notification_count );
} );
this.event_bus.on( this.component_id, 'refresh_login_data', this.refreshCompanyAndUserInfo );
this.event_bus.on( this.component_id, 'profile_menu_data', ( event_data ) => {
// Get the account menu data from the main menu (MenuManager)
this.refreshProfileMenuData( event_data.profile_menu_data );
} );
this.event_bus.on( this.component_id, 'help_menu_data', ( event_data ) => {
// Get the help menu data from the main menu (MenuManager)
this.refreshHelpMenuData( event_data.help_menu_data );
} );
this.event_bus.on( this.component_id, 'profile_pending_counts', ( event_data ) => {
// Get the number of pending authorizations, notifications and messages for the current user.
this.updateProfilePendingTotals( event_data.object_types );
} );
this.event_bus.on( 'global', 'reset_vue_data', this.resetData );
this.event_bus.on( this.component_id, 'toggle_job_queue_spinner', ( event_data ) => {
if ( event_data.get_job_data ) {
this.getRunningJobsData();
}
if ( event_data.show ) {
this.showJobQueueSpinner();
}
if ( event_data.check_completed ) {
this.checkForJobQueuePendingTasks();
}
} );
TTPromise.resolve( 'VueMenu', 'waitOnTopBarCreated' ); //Final promise before main menu is built, as this component needs to be created first.
this.refreshCompanyAndUserInfo();
},
beforeUnmount() {
this.hideJobQueueSpinner( false );
},
props: {
topbarMenuActive: Boolean,
activeTopbarItem: String
},
data() {
return {
component_id: 'tt_topbar',
company_name: '',
company_logo: '',
current_user: {},
notification_count: 0,
profile_menu_items: [],
help_menu_items: [],
pending_job_queue_tasks: [],
progress_bar_visible: false,
show_punch_in_out: true,
active_dropdown_menu: '', //Instead of using activeTopbarItem prop, only need to set active_dropdown_menu in this component.
tooltips: {
in_out: $.i18n._( 'In/Out' ),
notifications: $.i18n._( 'Notifications' ),
help: $.i18n._( 'Help' ),
profile: $.i18n._( 'Employee Profile' ),
job_queue: $.i18n._( 'Running Tasks' ),
},
sandbox_mode_text: $.i18n._( 'Sandbox Mode' ),
job_queue_complete_text: $.i18n._( 'All Tasks Completed' )
};
},
interval: null,
computed: {
topbarItemsClassProfile() {
return ['topbar-menu fadeInDown', {
'topbar-menu-visible': this.topbarMenuActive && this.active_dropdown_menu === 'profile',
}];
},
topbarItemsClassHelp() {
return ['topbar-menu fadeInDown', {
'topbar-menu-visible': this.topbarMenuActive && this.active_dropdown_menu === 'help',
}];
},
user_profile_image_url() {
return ServiceCaller.getURLByObjectType( 'user_photo' ) + '&object_id=' + this.current_user.id;
},
totalPendingCount() {
return this.profile_menu_items.filter( ( item ) => item.badge_number > 0 ).reduce( ( total, item ) => total + item.badge_number, 0 );
}
/*notificationsCount() {
Notifications currently are not shown on profile dropdown but we may wish to show it there in the future;
let notification_item = this.profile_menu_items.find( ( item ) => item.id === 'notification' );
return notification_item && notification_item.badge_number ? notification_item.badge_number : 0;
},*/
},
methods: {
resetData() {
this.hideJobQueueSpinner( false ); //Make sure job queue spinner is hidden and cancelled (especially during logout).
Object.assign( this.$data, this.$options.data() );
},
handleMenuClick( item ) {
item.command();
},
refreshProfileMenuData( data ) {
this.profile_menu_items.length = 0;
this.profile_menu_items.push( ...data );
},
refreshHelpMenuData( data ) {
this.help_menu_items.length = 0;
this.help_menu_items.push( ...data );
},
updateProfilePendingTotals( object_types ) {
TTAPI.APIUser.getUserPendingTotals( object_types, {
onResult: ( result ) => {
let pending_counts = result.getResult();
//Assign totals to the corresponding profile menu item badge number.
this.profile_menu_items.forEach( item => {
if ( pending_counts[item.id] || pending_counts[item.id] === 0 ) {
if ( Global.UNIT_TEST_MODE == true ) {
pending_counts[item.id] = 999;
}
item.badge_number = pending_counts[item.id];
}
} );
if ( pending_counts['notification'] ) {
if ( Global.UNIT_TEST_MODE == true ) {
pending_counts['notification'] = 999;
}
this.notification_count = pending_counts['notification'];
}
}
} );
},
onMenuButtonClick( event ) {
this.$emit( 'menubutton-click', event );
},
onTopbarMenuButtonClickProfile( event ) {
this.active_dropdown_menu = 'profile';
if ( this.profile_menu_items.length === 0 ) {
//This should never happen. It previously would happen consistently on FireFox and rarely Chrome/Other Browsers due to race conditions.
Debug.Error( 'Error: Profile Menu Items are empty. Rebuilding the menu.', 'ContextMenuManager.js', 'ContextMenuManager', 'getMenuModelByMenuId', 1 );
this.event_bus.emit( 'tt_left_container', 'rebuild_menu' );
}
this.$emit( 'topbar-menubutton-click', event );
},
onTopbarMenuButtonClickHelp( event ) {
this.active_dropdown_menu = 'help';
this.$emit( 'topbar-menubutton-click', event );
},
onCompanyLogoClicked() {
Global.closeEditViews( function() {
MenuManager.goToView( 'Home' );
} );
},
onNotificationBellClick() {
Global.closeEditViews( function() {
MenuManager.goToView( 'Notification' );
} );
},
onInOutClick() {
if ( LocalCacheData.getLastPunchTime() === null || ( ( new Date().getTime() - LocalCacheData.getLastPunchTime() ) / 1000 ) > 60 ) {
Global.closeEditViews( function() {
MenuManager.openSelectView( 'InOut' );
} );
} else {
let seconds_remaining = ( 60 - ( new Date().getTime() - LocalCacheData.getLastPunchTime() ) / 1000 );
TAlertManager.showAlert( $.i18n._( 'Please wait at least ' ) + Math.round( seconds_remaining ) + $.i18n._( ' seconds to punch again.' ) );
}
},
updateNotificationCount( count ) {
if ( Global.UNIT_TEST_MODE == true ) {
count = 999;
}
this.notification_count = count;
},
refreshCompanyAndUserInfo( force ) {
if ( !this.company_name || force ) {
if ( !this.current_user.id || ( this.current_user.id && LocalCacheData.getLoginUser().id !== this.current_user.id ) ) {
//This condition triggers when a new user logs in or page is refreshed.
//Setup notifications and ping check for the new user.
IndexViewController.initializeNotifications( 'login' );
Global.setupPing();
//This ensures the main menu updates and shows the correct menu items for the current users permissions.
//Otherwise the user may see menu items from the last logged in user.
this.event_bus.emit( 'tt_left_container', 'rebuild_menu' );
this.updateProfilePendingTotals( [] );
}
this.company_name = LocalCacheData.getCurrentCompany().name;
this.current_user = LocalCacheData.getLoginUser();
this.company_logo = ServiceCaller.getURLByObjectType( 'company_logo' );
this.show_punch_in_out = PermissionManager.validate( 'punch', 'punch_in_out' );
this.event_bus.emit( 'tt_main_ui', 'get_user_saved_layout_mode' );
}
},
isSandboxMode() {
return APIGlobal.pre_login_data['sandbox'];
},
toggleJobQueuePanel( event ) {
this.$refs['job-queue-panel'].toggle( event );
},
showJobQueueSpinner() {
this.progress_bar_visible = true;
this.startIntervalJobQueueTimer();
},
hideJobQueueSpinner( update_timesheet ) {
this.progress_bar_visible = false;
this.pending_job_queue_tasks = [];
clearInterval( this.interval );
this.interval = null;
this.$refs['job-queue-panel'].hide();
LocalCacheData.setJobQueuePunchData( null );
if ( update_timesheet && LocalCacheData.current_open_primary_controller && LocalCacheData.current_open_primary_controller.viewId === 'TimeSheet' ) {
LocalCacheData.current_open_primary_controller.search();
}
},
getRunningJobsData() {
let data = {};
data.filter_data = { user_id: this.current_user.id, status_id: [10, 20] };
TTAPI.APISystemJobQueue.getSystemJobQueue( data, {
onResult: ( result ) => {
if ( result.isValid() ) {
let result_data = result.getResult();
if ( Array.isArray( result_data ) ) {
this.pending_job_queue_tasks = result_data;
} else {
this.pending_job_queue_tasks = [];
}
}
}
} );
},
startIntervalJobQueueTimer() {
clearInterval( this.interval );
this.interval = setInterval( () => {
this.checkForJobQueuePendingTasks();
}, 60000 );
},
checkForJobQueuePendingTasks() {
TTAPI.APISystemJobQueue.getPendingAndRunningSystemJobQueue( {
onResult: ( result ) => {
let pending_counts = result.getResult();
if ( pending_counts == 0 ) {
this.hideJobQueueSpinner( true );
}
}
} );
},
onHideJobQueuePanel() {
if ( this.pending_job_queue_tasks.length === 0 ) {
this.hideJobQueueSpinner( true );
}
},
createMenuId( menu_id ) {
return 'profile-menu-' + menu_id;
},
},
components: {
InputText, // Added by RT
ProgressSpinner,
OverlayPanel
}
};
</script>
<style scoped>
.layout-wrapper .topbar {
padding: 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.left-group {
display: flex;
align-items: center;
/* No padding-left here, as it's put in the menu-button to make for better shaped selection border/accessibility border) */
}
.right-group {
display: flex;
align-items: center;
padding-right: 5px;
}
.layout-wrapper .topbar .menu-button {
width: auto;
margin-top: 0; /* This overrides the apollo -10px but does not work with our flexbox model */
margin-left: 5px; /* This overrides the apollo margin-left: 30px and balances the left and right spacing around the menu button in static and overlay menu mode. */
margin-right: 5px;
padding-left: 10px;
padding-right: 10px;
}
.layout-wrapper .topbar .menu-button:focus {
outline: none;
box-shadow: none;
}
.layout-wrapper.layout-slim .topbar .menu-button, .layout-wrapper.layout-horizontal .topbar .menu-button {
display: inline-block; /* Override hidden on slim and horizontal layout. Matches the behaviour for static menu, so that the menu button is visible on slim and horizontal mode, and we can use it to toggle between static, slim and horizontal. */
}
.layout-wrapper .topbar .menu-button i {
font-size: 20px;
}
.layout-wrapper .topbar .logo-link {
/* Overrides the fixed 185px width for the logo from PrimeVue */
width: auto;
max-width: 185px;
}
.company-logo-container {
background: #ffffff;
margin-left: 5px; /* Separates the logo from the left egde of the page in Slim Menu mode. */
padding: 5px;
border-radius: 4px;
}
.slash-line {
position: relative;
display: inline-block;
margin-left: 20px;
/*padding-left: 5px;*/
color: #ffffff;
}
.slash-line:before {
content: "";
position: absolute;
top: -50%;
left: -10px;
height: 200%;
width: 1px;
background: #ffffff;
/*transform: skewX(338deg);*/
/*transform-origin: bottom right;*/
}
.profile {
padding-left: 8px; /* Ensures the active outline box for accessibility looks nicer with a slight gap all around rather than no gap between border and text. */
padding-right: 5px; /* Ensures the active outline box for accessibility looks nicer with a slight gap all around rather than no gap between border and text. */
margin-right: 5px;
margin-top: 0 !important;
}
.topbar-icon {
margin: 5px 7px;
}
.topbar-icon .tticon {
color: #ffffff;
font-size: 2rem !important;
cursor: pointer;
}
.layout-wrapper .topbar .username {
font-weight: 700;
font-size: 13px;
font-family: Lucida Grande, Lucida Sans, Arial, sans-serif;
}
.layout-wrapper .topbar .company-name {
font-size: 13px;
font-weight: bold;
}
.layout-wrapper .topbar .topbar-menu {
right: 5px; /* Bring it closer to the edge, so it looks less like its under the bell, and more under the profile menu. */
}
.topbar .topbar-item-name {
font-size: 13px;
font-family: Lucida Grande, Lucida Sans, Arial, sans-serif;
}
.profile-image-holder {
width: 40px;
height: 40px;
vertical-align: middle;
overflow: hidden;
margin-right: 8px;
display: inline-block;
border-radius: 50%;
}
.profile-image {
height: 40px;
object-fit: cover;
}
.profile-menu-separator {
border-top: 1px solid #dee2e6;
margin: .25rem 0;
}
.sandbox-title {
font-size: 24px;
color: white;
}
::v-deep(.profile .p-overlay-badge .p-badge) {
right: 23px;
top: -5px;
}
::v-deep(.topbar-notification-bell .p-badge) {
top: 1px; /* This is to ensure notification badge is at same height as the profile badge */
right: 3px; /* Reduce overlap with help icon */
}
.topbar .profile-menu-item .p-link {
display: flex;
justify-content: space-between;
align-items: center;
}
.profile-menu-item .topbar-item-name {
flex: 1;
}
.topbar .profile-menu-item .p-overlay-badge {
margin-right: 9px;
}
.job-queue-spinner {
margin-right: 5px;
}
.job-queue-list {
padding: 0 10px 0 10px;
}
.job-queue-item {
margin: 10px;
}
.job-queue-item-summary {
font-weight: 600;
}
.job-queue-item-detail {
color: #6c757d;
}
::v-deep(.p-progress-spinner-circle) {
animation: p-progress-spinner-dash 1.5s ease-in-out infinite, custom-progress-spinner-color 6s ease-in-out infinite;
}
@keyframes custom-progress-spinner-color {
100%,
0% {
stroke: #ffffff;
}
40% {
stroke: #ffffff;
}
66% {
stroke: #ffffff;
}
80%,
90% {
stroke: #ffffff;
}
}
</style>