When the employees directory is enabled, look up the current user by their SSO (= username) and show their full name + photo in the sidebar user menu. - auth store: enrichFromDirectory() runs on login + fetchUser; sets directoryname/directorypicture from employeesApi.lookup(sso). Best-effort - falls back to the raw SSO when employees is off, the SSO is not in the directory, or the lookup fails. - displayName / avatarUrl getters; sidebar shows the photo (/static/employees/ <Picture>) + name over the SSO. Broken photos hide gracefully. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
217 lines
7.5 KiB
Vue
217 lines
7.5 KiB
Vue
<template>
|
|
<div class="app-layout">
|
|
<aside class="sidebar">
|
|
<div class="sidebar-header">
|
|
<img src="/ge-aerospace-logo.svg" alt="GE Aerospace" class="sidebar-logo" />
|
|
<h1>West Jefferson</h1>
|
|
</div>
|
|
|
|
<div class="sidebar-search">
|
|
<input
|
|
v-model="searchQuery"
|
|
type="text"
|
|
placeholder="Search..."
|
|
@keyup.enter="performSearch"
|
|
/>
|
|
</div>
|
|
|
|
<nav class="sidebar-nav">
|
|
<template v-for="item in navItems" :key="item.route">
|
|
<div v-if="item.section" class="nav-section">{{ item.section }}</div>
|
|
<router-link :to="item.route">
|
|
<component v-if="item.iconComponent" :is="item.iconComponent" :size="16" />
|
|
{{ item.name }}
|
|
</router-link>
|
|
</template>
|
|
|
|
<div class="nav-section">Displays</div>
|
|
<a href="/shopfloor" target="_blank" class="external-link">Shopfloor Dashboard</a>
|
|
<a href="/tv" target="_blank" class="external-link">TV Slideshow</a>
|
|
|
|
<router-link v-if="authStore.isAdmin" to="/settings">Settings</router-link>
|
|
</nav>
|
|
|
|
<div class="sidebar-footer">
|
|
<button class="theme-toggle" @click="toggleTheme" :title="currentTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'">
|
|
<span v-if="currentTheme === 'dark'"><Sun :size="14" /> Light</span>
|
|
<span v-else><Moon :size="14" /> Dark</span>
|
|
</button>
|
|
|
|
<div class="user-menu">
|
|
<template v-if="authStore.isAuthenticated">
|
|
<img v-if="authStore.avatarUrl" :src="authStore.avatarUrl" class="user-avatar"
|
|
:alt="authStore.displayName" @error="onAvatarError" />
|
|
<div class="user-ident">
|
|
<div class="username">{{ authStore.displayName }}</div>
|
|
<div v-if="authStore.displayName !== authStore.username" class="user-sso">{{ authStore.username }}</div>
|
|
</div>
|
|
<button class="btn btn-secondary" @click="handleLogout">Logout</button>
|
|
</template>
|
|
<router-link v-else to="/login" class="btn btn-primary">Login</router-link>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="main-content">
|
|
<div v-if="activeNotifications.length" class="notification-banner">
|
|
<div
|
|
v-for="n in activeNotifications"
|
|
:key="n.notificationid"
|
|
class="notification-item"
|
|
:class="getNotificationType(n)"
|
|
>
|
|
<span class="notification-text">{{ n.notification }}</span>
|
|
<span class="notification-meta">
|
|
<span v-if="n.starttime" class="notification-date">{{ formatDate(n.starttime) }}</span>
|
|
<a
|
|
v-if="n.ticketnumber"
|
|
:href="`https://geit.service-now.com/now/nav/ui/search/0f8b85d0c7922010099a308dc7c2606a/params/search-term/${n.ticketnumber}/global-search-data-config-id/c861cea2c7022010099a308dc7c26041/back-button-label/IT4IT%20Homepage/search-context/now%2Fnav%2Fui`"
|
|
target="_blank"
|
|
class="notification-ticket"
|
|
>{{ n.ticketnumber }}</a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<router-view />
|
|
</main>
|
|
<ToastHost />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import ToastHost from '../components/ToastHost.vue'
|
|
import {
|
|
Sun, Moon, LayoutDashboard, Calendar, Map, Cog, Monitor,
|
|
Printer, Globe, Usb, AppWindow, BookOpen, BarChart3, Bell, Image, ShieldCheck
|
|
} from 'lucide-vue-next'
|
|
import { useAuthStore } from '../stores/auth'
|
|
import { currentTheme, toggleTheme } from '../stores/theme'
|
|
import { dashboardApi, notificationsApi } from '../api'
|
|
|
|
const router = useRouter()
|
|
const authStore = useAuthStore()
|
|
const searchQuery = ref('')
|
|
const navItems = ref([])
|
|
const activeNotifications = ref([])
|
|
|
|
// Map backend icon names to Lucide components
|
|
const iconMap = {
|
|
'layout-dashboard': LayoutDashboard,
|
|
'calendar': Calendar,
|
|
'map': Map,
|
|
'cog': Cog,
|
|
'desktop': Monitor,
|
|
'printer': Printer,
|
|
'network-wired': Globe,
|
|
'usb': Usb,
|
|
'bell': Bell,
|
|
'app-window': AppWindow,
|
|
'book-open': BookOpen,
|
|
'bar-chart-3': BarChart3,
|
|
'image': Image,
|
|
'shield': ShieldCheck,
|
|
}
|
|
|
|
// Default navigation (used as fallback if API fails)
|
|
const defaultNav = [
|
|
{ name: 'Dashboard', icon: 'layout-dashboard', route: '/', position: 0 },
|
|
{ name: 'Notifications', icon: 'bell', route: '/notifications', position: 5 },
|
|
{ name: 'Calendar', icon: 'calendar', route: '/calendar', position: 6 },
|
|
{ name: 'Map', icon: 'map', route: '/map', position: 4 },
|
|
{ name: 'Equipment', icon: 'cog', route: '/machines', position: 10 },
|
|
{ name: 'PCs', icon: 'desktop', route: '/pcs', position: 15 },
|
|
{ name: 'Network', icon: 'network-wired', route: '/network', position: 18 },
|
|
{ name: 'Printers', icon: 'printer', route: '/printers', position: 20 },
|
|
{ name: 'USB Devices', icon: 'usb', route: '/usb', position: 45 },
|
|
{ name: 'Applications', icon: 'app-window', route: '/applications', position: 30, section: 'information' },
|
|
{ name: 'Knowledge Base', icon: 'book-open', route: '/knowledgebase', position: 35 },
|
|
{ name: 'Reports', icon: 'bar-chart-3', route: '/reports', position: 40 },
|
|
]
|
|
|
|
function buildNavItems(items) {
|
|
// Sort by position
|
|
const sorted = [...items].sort((a, b) => (a.position || 99) - (b.position || 99))
|
|
|
|
// Assign section headers based on position ranges
|
|
const result = []
|
|
let currentSection = null
|
|
|
|
for (const item of sorted) {
|
|
let section = null
|
|
if (item.position >= 10 && item.position < 30 && currentSection !== 'Assets') {
|
|
section = 'Assets'
|
|
currentSection = 'Assets'
|
|
} else if (item.section === 'information' || (item.position >= 30 && item.position < 50 && currentSection !== 'Information')) {
|
|
if (currentSection !== 'Information') {
|
|
section = 'Information'
|
|
currentSection = 'Information'
|
|
}
|
|
}
|
|
|
|
result.push({
|
|
...item,
|
|
section,
|
|
iconComponent: iconMap[item.icon] || null
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const response = await dashboardApi.navigation()
|
|
navItems.value = buildNavItems(response.data.data || [])
|
|
} catch (err) {
|
|
navItems.value = buildNavItems(defaultNav)
|
|
}
|
|
|
|
try {
|
|
const resp = await notificationsApi.active()
|
|
activeNotifications.value = resp.data.data?.notifications || []
|
|
} catch (err) {
|
|
// Notifications banner is non-critical
|
|
}
|
|
})
|
|
|
|
function getNotificationType(n) {
|
|
const type = (n.typename || '').toLowerCase()
|
|
if (type.includes('incident')) return 'type-incident'
|
|
if (type.includes('change')) return 'type-change'
|
|
if (type.includes('awareness')) return 'type-awareness'
|
|
return 'type-default'
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return ''
|
|
const d = new Date(dateStr)
|
|
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })
|
|
}
|
|
|
|
function performSearch() {
|
|
if (searchQuery.value.trim()) {
|
|
router.push({ path: '/search', query: { q: searchQuery.value.trim() } })
|
|
searchQuery.value = ''
|
|
}
|
|
}
|
|
|
|
async function handleLogout() {
|
|
await authStore.logout()
|
|
router.push('/login')
|
|
}
|
|
|
|
// Hide a broken avatar (photo filename set but file missing).
|
|
function onAvatarError(event) {
|
|
event.target.style.display = 'none'
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.user-menu { display: flex; align-items: center; gap: 0.6rem; }
|
|
.user-avatar { width: 34px; height: 34px; border-radius: 50%; object-fit: cover; border: 1px solid var(--border); }
|
|
.user-ident { display: flex; flex-direction: column; line-height: 1.1; }
|
|
.user-sso { font-size: 0.72rem; color: var(--text-light); }
|
|
</style>
|