Merge remote-tracking branch 'origin/main' into wilm
# Conflicts: # shopdb/__init__.py
This commit is contained in:
@@ -11,7 +11,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-search">
|
||||
<!-- Global search is authenticated; offering the box to a signed-out
|
||||
reader gives them a control that can only answer 401. -->
|
||||
<div v-if="authStore.isAuthenticated" class="sidebar-search">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
@@ -34,6 +36,8 @@
|
||||
<a v-if="hasRoute('/tv')" :href="withBase('/tv')" target="_blank" class="external-link">TV Slideshow</a>
|
||||
<a v-if="hasRoute('/parts-kiosk')" :href="withBase('/parts-kiosk')"
|
||||
target="_blank" class="external-link">Parts Kiosk</a>
|
||||
<a v-if="hasRoute('/usb-kiosk')" :href="withBase('/usb-kiosk')"
|
||||
target="_blank" class="external-link">USB Kiosk</a>
|
||||
|
||||
<router-link v-if="authStore.isAdmin" to="/settings">Settings</router-link>
|
||||
</nav>
|
||||
@@ -136,6 +140,7 @@ const hasRoute = (path) => stagedRoutePaths.has(path)
|
||||
const showShopfloor = hasRoute('/shopfloor')
|
||||
&& (hasRoute('/notifications') || hasRoute('/calendar'))
|
||||
const showDisplays = showShopfloor || hasRoute('/tv') || hasRoute('/parts-kiosk')
|
||||
|| hasRoute('/usb-kiosk')
|
||||
const routeViewKey = computed(() =>
|
||||
route.path.startsWith('/settings') ? '/settings' : route.path
|
||||
)
|
||||
@@ -183,50 +188,81 @@ const iconMap = {
|
||||
|
||||
// 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: 'Machines', 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 },
|
||||
{ name: 'Dashboard', icon: 'layout-dashboard', route: '/', position: 0, permission: 'assets.view' },
|
||||
{ name: 'Notifications', icon: 'bell', route: '/notifications', position: 5, permission: 'notifications.view' },
|
||||
{ name: 'Calendar', icon: 'calendar', route: '/calendar', position: 6, permission: 'notifications.view' },
|
||||
{ name: 'Map', icon: 'map', route: '/map', position: 4, permission: 'assets.view' },
|
||||
{ name: 'Machines', icon: 'cog', route: '/machines', position: 10, section: 'assets', permission: 'machines.view' },
|
||||
{ name: 'PCs', icon: 'desktop', route: '/pcs', position: 15, section: 'assets', permission: 'computers.view' },
|
||||
{ name: 'Network', icon: 'network-wired', route: '/network', position: 18, section: 'assets', permission: 'network.view' },
|
||||
{ name: 'Printers', icon: 'printer', route: '/printers', position: 20, section: 'assets', permission: 'printers.view' },
|
||||
{ name: 'USB Devices', icon: 'usb', route: '/usb', position: 25, section: 'assets', permission: 'usb.view' },
|
||||
{ name: 'Applications', icon: 'app-window', route: '/applications', position: 30, section: 'information', permission: 'applications.view' },
|
||||
{ name: 'Knowledge Base', icon: 'book-open', route: '/knowledgebase', position: 35, section: 'information', permission: 'kb.view' },
|
||||
{ name: 'Reports', icon: 'bar-chart-3', route: '/reports', position: 40, section: 'information', permission: 'reports.view' },
|
||||
]
|
||||
|
||||
// The canonical sidebar sections, in the order they render. A nav item DECLARES
|
||||
// which one it belongs to; nothing is inferred.
|
||||
//
|
||||
// Sections used to be derived from the position number - 10-29 became Assets,
|
||||
// 30-49 became Information - so a plugin landed in whichever bucket its number
|
||||
// happened to fall in. That is how USB Devices, an asset, came to sit under
|
||||
// Information next to GE-Enforce and Tech Tools, and why two plugins picking
|
||||
// the same number silently reordered each other. A number is for ORDER; it was
|
||||
// never a statement about meaning.
|
||||
//
|
||||
// An item with no section (or an unknown one) stays in the unlabelled group at
|
||||
// the top, which is the safe default: it appears, it is just not filed.
|
||||
const NAV_SECTIONS = [
|
||||
{ key: null, label: null },
|
||||
{ key: 'assets', label: 'Assets' },
|
||||
{ key: 'information', label: 'Information' },
|
||||
{ key: 'fleet', label: 'Fleet' },
|
||||
]
|
||||
|
||||
function buildNavItems(items) {
|
||||
// Drop items the user cannot reach: a nav item may carry a `permission`
|
||||
// (e.g. Slides -> slides.manage); hide it from anyone who lacks it so the
|
||||
// link does not dead-end at the router guard. Admins hold every permission.
|
||||
const visible = items.filter(item =>
|
||||
!item.permission || authStore.hasPermission(item.permission))
|
||||
//
|
||||
// A signed-out visitor sees only what a plugin has declared guest-reachable.
|
||||
// Without this the sidebar offers a signed-out reader the whole app and every
|
||||
// link bounces them to the login page, which reads as a broken site rather
|
||||
// than as a closed door.
|
||||
// A nav item may name one permission or several; holding ANY of them is
|
||||
// enough. GE-Enforce and Slides need that: the page is reachable by a
|
||||
// manager and by a plain viewer, and they hold different strings.
|
||||
const holds = item => {
|
||||
if (!item.permission) return true
|
||||
const names = Array.isArray(item.permission) ? item.permission : [item.permission]
|
||||
return names.some(name => authStore.hasPermission(name))
|
||||
}
|
||||
// A signed-out visitor gets whatever the server sent, which is already
|
||||
// filtered to what the guest role can reach - hasPermission knows nothing
|
||||
// about guests, so asking it here would hide everything.
|
||||
const visible = authStore.isAuthenticated ? items.filter(holds) : items
|
||||
|
||||
// Sort by position
|
||||
const sorted = [...visible].sort((a, b) => (a.position || 99) - (b.position || 99))
|
||||
const known = new Set(NAV_SECTIONS.map(s => s.key).filter(Boolean))
|
||||
const bucket = item => {
|
||||
const declared = (item.section || '').toLowerCase()
|
||||
return known.has(declared) ? declared : null
|
||||
}
|
||||
|
||||
// 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
|
||||
for (const { key, label } of NAV_SECTIONS) {
|
||||
const inSection = visible
|
||||
.filter(item => bucket(item) === key)
|
||||
// ?? not ||: position 0 is a real position, and `0 || 99` is 99, which
|
||||
// sent Dashboard - the only item that uses 0 - to the end of its group.
|
||||
.sort((a, b) => (a.position ?? 99) - (b.position ?? 99))
|
||||
inSection.forEach((item, index) => {
|
||||
result.push({
|
||||
...item,
|
||||
// Only the first item in a section carries its heading.
|
||||
section: index === 0 ? label : null,
|
||||
iconComponent: iconMap[item.icon] || null,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -234,6 +270,11 @@ function buildNavItems(items) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Refresh the signed-in user (and their permissions) on every app load. The
|
||||
// login payload is cached in localStorage, so without this a permission
|
||||
// granted mid-session stays invisible until the person logs out and back in.
|
||||
if (authStore.isAuthenticated) authStore.fetchUser()
|
||||
|
||||
// Half-hourly: cheap, and it crosses midnight into the new week without a
|
||||
// reload on a screen nobody touches.
|
||||
fiscalWeekTimer = setInterval(() => { currentFiscalWeek.value = fiscalWeek() }, 30 * 60 * 1000)
|
||||
|
||||
Reference in New Issue
Block a user