Files
shopdb-flask/frontend/src/router/index.js
cproudlock 6439d1ccd9
Some checks failed
CI / backend (push) Failing after 1m42s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s
CI / migrations-mysql (push) Failing after 7s
printedparts stage 8: 1x0.5in bin labels
New public print view at /print/printedparts-labels following the
plugin-owned USB label precedent: multi-select with per-item copies,
CODE128 of the item code via JsBarcode (a QR at this size is at the
edge of scanner tolerance), one label per page on 1in x 0.5in roll
stock via a new @page size. The Detail page's Bin Label button
preselects its item through ?item=<id>; the list header gains a batch
Print Labels button.
2026-07-17 08:00:35 -04:00

185 lines
6.0 KiB
JavaScript

import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import AppLayout from '../views/AppLayout.vue'
import SettingsLayout from '../views/settings/SettingsLayout.vue'
import { setupComplete, setupSkipped, isSetupLoaded, refreshSetupState } from '../composables/setupState'
import { loadEnabledPlugins, isPluginEnabled } from '../composables/enabledPlugins'
import { useToast } from '../composables/toast'
// Auto-discover all route modules from routes/ directory
const routeModules = import.meta.glob('./routes/*.js', { eager: true })
const rawChildren = Object.values(routeModules).flatMap(m => m.default)
// Gather the settings pages (spread across plugin route files) and nest them
// under a single two-pane shell so the grouped rail stays put while the right
// pane swaps. Slides is a sidebar page that happens to live at /settings/slides;
// keep it full-width (not inside the settings rail).
const SETTINGS_STANDALONE = new Set(['settings/slides'])
const settingsChildren = []
const otherChildren = []
for (const route of rawChildren) {
const path = route.path
if (path === 'settings') {
// Old index becomes the shell's default child (keeps name 'settings').
settingsChildren.unshift({ ...route, path: '' })
} else if (typeof path === 'string' && path.startsWith('settings/') && !SETTINGS_STANDALONE.has(path)) {
settingsChildren.push({ ...route, path: path.replace(/^settings\//, '') })
} else {
otherChildren.push(route)
}
}
const appChildren = [
...otherChildren,
{
path: 'settings',
component: SettingsLayout,
meta: { requiresAuth: true, requiresAdmin: true },
children: settingsChildren,
},
]
const routes = [
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue'),
meta: { guest: true }
},
// Forced/self-service password change (standalone, authenticated)
{
path: '/change-password',
name: 'change-password',
component: () => import('../views/ChangePassword.vue'),
meta: { requiresAuth: true }
},
// First-run setup wizard (standalone, admin-only)
{
path: '/setup',
name: 'setup',
component: () => import('../views/SetupWizard.vue'),
meta: { requiresAuth: true, requiresAdmin: true }
},
// Standalone full-screen dashboards (no sidebar, no auth required)
{
path: '/shopfloor',
name: 'shopfloor',
component: () => import('../views/ShopfloorDashboard.vue')
},
{
// Touch kiosk for taking 3D-printed parts: scan bin, scan badge, keypad.
// Open on purpose - see the decision record in the printedparts proposal.
path: '/parts-kiosk',
name: 'parts-kiosk',
component: () => import('../views/printedparts/PartsKiosk.vue'),
meta: { plugin: 'printedparts' }
},
{
path: '/tv',
name: 'tv',
component: () => import('../views/TVDashboard.vue'),
meta: { plugin: 'slides' }
},
// Print pages (standalone, no sidebar/header)
{
path: '/print/machine-badge/:id',
name: 'print-machine-badge',
component: () => import('../views/print/MachineBadge.vue')
},
// Shared asset label/code generator for any asset type (public, like the
// other /print/* routes). assettype = machine|computer|printer|
// network_device|measuring_tool; id = the asset's plugin id.
{
path: '/print/asset-label/:assettype/:id',
name: 'print-asset-label',
component: () => import('../views/print/AssetLabel.vue')
},
// Batch label sheets (ULINE 6-up / mini 72-up) for one asset type.
{
path: '/print/asset-label-batch/:assettype',
name: 'print-asset-label-batch',
component: () => import('../views/print/AssetLabelBatch.vue')
},
{
path: '/print/printer-qr',
name: 'print-printer-qr-batch',
component: () => import('../views/print/PrinterQRBatch.vue'),
meta: { plugin: 'printers' }
},
{
path: '/print/printer-qr/:id',
name: 'print-printer-qr-single',
component: () => import('../views/print/PrinterQRSingle.vue'),
meta: { plugin: 'printers' }
},
{
path: '/print/usb-labels',
name: 'print-usb-labels',
component: () => import('../views/print/USBLabelBatch.vue'),
meta: { plugin: 'usb' }
},
{
path: '/print/printedparts-labels',
name: 'print-printedparts-labels',
component: () => import('../views/print/PrintedPartsLabels.vue'),
meta: { plugin: 'printedparts' }
},
{
path: '/',
component: AppLayout,
children: appChildren
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// Navigation guard
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
// Remember where they were headed so login can send them back there.
return next({ path: '/login', query: { redirect: to.fullPath } })
}
if (to.meta.requiresAdmin && !authStore.isAdmin) {
return next('/')
}
if (to.meta.guest && authStore.isAuthenticated) {
return next('/')
}
// Forced password change: an admin-set temporary password must be replaced
// before the user reaches the rest of the app. Let them log out.
if (authStore.isAuthenticated && authStore.mustChangePassword
&& to.path !== '/change-password' && to.path !== '/login') {
return next('/change-password')
}
// Plugin gating: a disabled backend plugin's frontend routes are dead ends.
// The enabled list is fetched once and cached; fail-open on error so a blip
// cannot brick navigation. Works unauthenticated (endpoint is jwt-optional).
if (to.meta.plugin) {
await loadEnabledPlugins()
if (!isPluginEnabled(to.meta.plugin)) {
useToast().info(`The ${to.meta.plugin} feature is not enabled.`)
return next('/')
}
}
// First-run: steer a fresh admin into the setup wizard (once, until finished).
if (authStore.isAuthenticated && authStore.isAdmin && to.path !== '/setup') {
if (!isSetupLoaded()) {
await refreshSetupState()
}
if (!setupComplete.value && !setupSkipped.value) {
return next('/setup')
}
}
next()
})
export default router