/** * router/index.js - Vue Router Configuration * * This file configures the application routing using Vue Router 5. * It defines all routes and navigation structure for the SPA. * * Routing Features: * - Hash-based routing (createWebHashHistory) for static hosting compatibility * - Lazy loading for all route components (code splitting) * - Nested routes for layout-based navigation * - Automatic scroll to top on navigation * * Route Structure: * - Protected routes: Wrapped in DefaultLayout with sidebar and header * - Public routes: Login, Register, 404, 500 pages without layout * * Adding New Routes: * 1. Import component (use dynamic import for code splitting) * 2. Add route object to appropriate section * 3. Update _nav.js for sidebar navigation (if needed) * * @see https://router.vuejs.org/ */ import { h, resolveComponent } from 'vue' import { createRouter, createWebHashHistory } from 'vue-router' import DefaultLayout from '@/layouts/DefaultLayout' import { useAuthStore } from '@/stores/auth' import { useMenuStore } from '@/stores/menu' import { hasToken } from '@/utils/session' /** * Application routes configuration * @type {Array} */ const routes = [ { path: '/', name: 'Home', component: DefaultLayout, redirect: '/dashboard', meta: { requiresAuth: true, }, children: [ { path: '/dashboard', name: 'Dashboard', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "dashboard" */ '@/views/dashboard/Dashboard.vue'), }, { path: '/profile', name: 'Profile', component: () => import('@/views/profile/Profile.vue'), }, { path: '/tickets', name: 'Tickets', component: { render() { return h(resolveComponent('router-view')) }, }, redirect: '/tickets/ticket', children: [ { path: '/tickets/ticket', name: 'Ticket', component: () => import('@/views/tickets/Ticket.vue'), }, { path: '/tickets/ticket-type', alias: '/ticket-types', name: 'Ticket Type', component: () => import('@/views/tickets/TicketType.vue'), }, { path: '/tickets/ticket-materials', alias: '/materials', name: 'Ticket Materials', component: () => import('@/views/tickets/TicketMaterials.vue'), }, { path: '/tickets/ticket-incident', alias: '/ticket-incident-types', name: 'Ticket Incident', component: () => import('@/views/tickets/TicketIncident.vue'), }, { path: '/tickets/approved', alias: '/tickets/approval', name: 'Approved', component: () => import('@/views/tickets/Approved.vue'), }, { path: '/tickets/rejected', name: 'Rejected', component: () => import('@/views/tickets/Rejected.vue'), }, ], }, { path: '/theme', name: 'Theme', redirect: '/theme/typography', }, { path: '/theme/colors', name: 'Colors', component: () => import('@/views/theme/Colors.vue'), }, { path: '/theme/typography', name: 'Typography', component: () => import('@/views/theme/Typography.vue'), }, { path: '/base', name: 'Base', component: { render() { return h(resolveComponent('router-view')) }, }, redirect: '/base/breadcrumbs', children: [ { path: '/base/accordion', name: 'Accordion', component: () => import('@/views/base/Accordion.vue'), }, { path: '/base/breadcrumbs', name: 'Breadcrumbs', component: () => import('@/views/base/Breadcrumbs.vue'), }, { path: '/base/cards', name: 'Cards', component: () => import('@/views/base/Cards.vue'), }, { path: '/base/carousels', name: 'Carousels', component: () => import('@/views/base/Carousels.vue'), }, { path: '/base/chips', name: 'Chips', component: () => import('@/views/base/Chips.vue'), }, { path: '/base/collapses', name: 'Collapses', component: () => import('@/views/base/Collapses.vue'), }, { path: '/base/list-groups', name: 'List Groups', component: () => import('@/views/base/ListGroups.vue'), }, { path: '/base/navs', name: 'Navs', component: () => import('@/views/base/Navs.vue'), }, { path: '/base/paginations', name: 'Paginations', component: () => import('@/views/base/Paginations.vue'), }, { path: '/base/placeholders', name: 'Placeholders', component: () => import('@/views/base/Placeholders.vue'), }, { path: '/base/popovers', name: 'Popovers', component: () => import('@/views/base/Popovers.vue'), }, { path: '/base/progress', name: 'Progress', component: () => import('@/views/base/Progress.vue'), }, { path: '/base/spinners', name: 'Spinners', component: () => import('@/views/base/Spinners.vue'), }, { path: '/base/tables', name: 'Tables', component: () => import('@/views/base/Tables.vue'), }, { path: '/base/tabs', name: 'Tabs', component: () => import('@/views/base/Tabs.vue'), }, { path: '/base/tooltips', name: 'Tooltips', component: () => import('@/views/base/Tooltips.vue'), }, ], }, { path: '/buttons', name: 'Buttons', component: { render() { return h(resolveComponent('router-view')) }, }, redirect: '/buttons/standard-buttons', children: [ { path: '/buttons/standard-buttons', name: 'Button Component', component: () => import('@/views/buttons/Buttons.vue'), }, { path: '/buttons/dropdowns', name: 'Dropdowns', component: () => import('@/views/buttons/Dropdowns.vue'), }, { path: '/buttons/button-groups', name: 'Button Groups', component: () => import('@/views/buttons/ButtonGroups.vue'), }, ], }, { path: '/forms', name: 'Forms', component: { render() { return h(resolveComponent('router-view')) }, }, redirect: '/forms/form-control', children: [ { path: '/forms/form-control', name: 'Form Control', component: () => import('@/views/forms/FormControl.vue'), }, { path: '/forms/select', name: 'Select', component: () => import('@/views/forms/Select.vue'), }, { path: '/forms/checks-radios', name: 'Checks & Radios', component: () => import('@/views/forms/ChecksRadios.vue'), }, { path: '/forms/chip-input', name: 'Chip Input', component: () => import('@/views/forms/ChipInput.vue'), }, { path: '/forms/range', name: 'Range', component: () => import('@/views/forms/Range.vue'), }, { path: '/forms/input-group', name: 'Input Group', component: () => import('@/views/forms/InputGroup.vue'), }, { path: '/forms/floating-labels', name: 'Floating Labels', component: () => import('@/views/forms/FloatingLabels.vue'), }, { path: '/forms/layout', name: 'Layout', component: () => import('@/views/forms/Layout.vue'), }, { path: '/forms/validation', name: 'Validation', component: () => import('@/views/forms/Validation.vue'), }, ], }, { path: '/charts', name: 'Charts', component: () => import('@/views/charts/Charts.vue'), }, { path: '/icons', name: 'Icons', component: { render() { return h(resolveComponent('router-view')) }, }, redirect: '/icons/coreui-icons', children: [ { path: '/icons/coreui-icons', name: 'CoreUI Icons', component: () => import('@/views/icons/CoreUIIcons.vue'), }, { path: '/icons/brands', name: 'Brands', component: () => import('@/views/icons/Brands.vue'), }, { path: '/icons/flags', name: 'Flags', component: () => import('@/views/icons/Flags.vue'), }, ], }, { path: '/notifications', name: 'Notifications', component: { render() { return h(resolveComponent('router-view')) }, }, redirect: '/notifications/alerts', children: [ { path: '/notifications/alerts', name: 'Alerts', component: () => import('@/views/notifications/Alerts.vue'), }, { path: '/notifications/badges', name: 'Badges', component: () => import('@/views/notifications/Badges.vue'), }, { path: '/notifications/modals', name: 'Modals', component: () => import('@/views/notifications/Modals.vue'), }, { path: '/notifications/toasts', name: 'Toasts', component: () => import('@/views/notifications/Toasts.vue'), }, ], }, { path: '/group-menus', name: 'Group Menus', component: () => import('@/views/group-menus/GroupMenus.vue'), }, { path: '/group-menus/:id/menus', name: 'Group Menu Permissions', component: () => import('@/views/group-menus/GroupMenuPermissions.vue'), meta: { activeMenu: '/group-menus' }, }, { path: '/nas/mikrotik', name: 'NAS Mikrotik', component: () => import('@/views/nas/NasResourceView.vue'), meta: { nasResource: 'mikrotik' }, }, { path: '/nas/package-profiles', name: 'NAS Profile Paket', component: () => import('@/views/nas/NasResourceView.vue'), meta: { nasResource: 'package-profile' }, }, { path: '/nas/olt', name: 'NAS OLT', component: () => import('@/views/nas/NasResourceView.vue'), meta: { nasResource: 'olt' }, }, { path: '/nas/webfig', name: 'NAS Webfig', component: () => import('@/views/nas/NasResourceView.vue'), meta: { nasResource: 'webfig' }, }, { path: '/users', name: 'Users', component: () => import('@/views/users/Users.vue'), }, { path: '/tenants', name: 'Tenants', component: () => import('@/views/tenants/Tenants.vue'), }, { path: '/wilayah/desa', name: 'Wilayah Desa', component: () => import('@/views/wilayah/Wilayah.vue'), meta: { tingkat: 'desa' }, }, { path: '/wilayah/kecamatan', name: 'Wilayah Kecamatan', component: () => import('@/views/wilayah/Wilayah.vue'), meta: { tingkat: 'kecamatan' }, }, { path: '/wilayah/kabupaten', name: 'Wilayah Kabupaten', component: () => import('@/views/wilayah/Wilayah.vue'), meta: { tingkat: 'kabupaten' }, }, { path: '/wilayah/provinsi', name: 'Wilayah Provinsi', component: () => import('@/views/wilayah/Wilayah.vue'), meta: { tingkat: 'provinsi' }, }, { path: '/widgets', name: 'Widgets', component: () => import('@/views/widgets/Widgets.vue'), }, ], }, { path: '/pages', redirect: '/pages/404', name: 'Pages', component: { render() { return h(resolveComponent('router-view')) }, }, children: [ { path: '404', name: 'Page404', component: () => import('@/views/pages/Page404'), }, { path: '500', name: 'Page500', component: () => import('@/views/pages/Page500'), }, { path: 'login', name: 'Login', component: () => import('@/views/pages/Login'), meta: { guestOnly: true, }, }, { path: 'register', name: 'Register', component: () => import('@/views/pages/Register'), meta: { guestOnly: true, }, }, { path: 'verify-code', name: 'VerifyCode', component: () => import('@/views/pages/VerifyCode'), meta: { guestOnly: true, }, }, ], }, ] const router = createRouter({ history: createWebHashHistory(import.meta.env.BASE_URL), routes, scrollBehavior() { // always scroll to top return { top: 0 } }, }) router.beforeEach(async (to) => { const requiresAuth = to.matched.some((record) => record.meta.requiresAuth) const guestOnly = to.matched.some((record) => record.meta.guestOnly) const authenticated = hasToken() if (requiresAuth && !authenticated) { return { name: 'Login', query: { redirect: to.fullPath, }, } } if (guestOnly && authenticated) { return { name: 'Dashboard' } } if (requiresAuth) { try { await useMenuStore().fetchMenus() } catch { useAuthStore().logout() return { name: 'Login', query: { redirect: to.fullPath, }, } } } return true }) export default router