/** * 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: 'Website', component: () => import('@/views/website/LandingPage.vue'), meta: { guestLanding: true }, }, { path: '/app', name: 'Application', 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: '/forbidden', name: 'Forbidden', component: () => import('@/views/pages/Page403.vue'), meta: { publicWithinApp: true }, }, { path: '/my-applications', name: 'Pengajuan Saya', component: () => import('@/views/access-applications/MyApplications.vue'), meta: { publicWithinApp: true }, }, { 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: '/customers/orders', name: 'Customer List Order', component: () => import('@/views/customers/Customers.vue'), meta: { customerScope: 'orders' }, }, { path: '/customers/orders/create', name: 'Registrasi Customer', component: () => import('@/views/customers/CustomerFormPage.vue'), meta: { activeMenu: '/customers/orders' }, }, { path: '/customers/orders/:id/edit', name: 'Edit Customer', component: () => import('@/views/customers/CustomerFormPage.vue'), meta: { activeMenu: '/customers/orders' }, }, { path: '/customers/:scope/:id/detail', name: 'Detail Customer', component: () => import('@/views/customers/CustomerDetailPage.vue'), meta: { activeMenuBase: '/customers' }, }, { path: '/customers/active', name: 'Customer Aktif', component: () => import('@/views/customers/Customers.vue'), meta: { customerScope: 'active' }, }, { path: '/customers/inactive', name: 'Customer Tidak Aktif', component: () => import('@/views/customers/Customers.vue'), meta: { customerScope: 'inactive' }, }, { path: '/customers/unmanaged', name: 'Customer Unmanage', component: () => import('@/views/customers/Customers.vue'), meta: { customerScope: 'unmanaged' }, }, { path: '/customers/trash', name: 'Sampah Customer', component: () => import('@/views/customers/Customers.vue'), meta: { customerScope: 'trash' }, }, { path: '/billing/profiles', name: 'Profile Tagihan', component: () => import('@/views/billing/BillingProfiles.vue'), }, { path: '/billing/running', name: 'Tagihan Berjalan', component: () => import('@/views/billing/Invoices.vue'), meta: { invoiceScope: 'running' }, }, { path: '/billing/overdue', name: 'Tunggakan', component: () => import('@/views/billing/Invoices.vue'), meta: { invoiceScope: 'overdue' }, }, { path: '/billing/paid', name: 'Tagihan Lunas', component: () => import('@/views/billing/Invoices.vue'), meta: { invoiceScope: 'paid' }, }, { path: '/topology/map', name: 'Peta Jaringan', component: () => import('@/views/topology/TopologyMap.vue'), }, { path: '/topology/devices', name: 'Perangkat Topologi', component: () => import('@/views/topology/TopologyResourceView.vue'), meta: { topologyResource: 'node' }, }, { path: '/topology/links', name: 'Jalur Kabel', component: () => import('@/views/topology/TopologyResourceView.vue'), meta: { topologyResource: 'link' }, }, { path: '/topology/network', name: 'Topologi Jaringan', component: () => import('@/views/topology/TopologyNetwork.vue'), }, { path: '/topology/device-types', name: 'Kategori Perangkat', component: () => import('@/views/topology/TopologyResourceView.vue'), meta: { topologyResource: 'device-type' }, }, { path: '/notifications-service/broadcasts', name: 'Pesan Siaran', component: () => import('@/views/notifications-service/NotificationBroadcasts.vue'), }, { path: '/notifications-service/system', name: 'Notifikasi Aplikasi', component: () => import('@/views/notifications-service/NotificationChannelDashboard.vue'), meta: { notificationChannel: 'system' }, }, { path: '/notifications-service/whatsapp-official', name: 'WhatsApp Official', component: () => import('@/views/notifications-service/NotificationChannelDashboard.vue'), meta: { notificationChannel: 'whatsapp_official' }, }, { path: '/notifications-service/whatsapp-unofficial', name: 'WhatsApp Unofficial', component: () => import('@/views/notifications-service/NotificationChannelDashboard.vue'), meta: { notificationChannel: 'whatsapp_unofficial' }, }, { path: '/notifications-service/email', name: 'Email Notifikasi', component: () => import('@/views/notifications-service/NotificationChannelDashboard.vue'), meta: { notificationChannel: 'email' }, }, { path: '/notifications-service/telegram', name: 'Telegram Notifikasi', component: () => import('@/views/notifications-service/NotificationChannelDashboard.vue'), meta: { notificationChannel: 'telegram' }, }, { path: '/payment-gateway/settings', name: 'Setting Payment Gateway', component: () => import('@/views/payment-gateway/PaymentGatewaySettings.vue'), }, { path: '/payment-gateway/logs', name: 'Log Pembayaran', component: () => import('@/views/payment-gateway/PaymentLogs.vue'), }, { path: '/deposit-balance', alias: '/wallet', name: 'Saldo Deposit', component: () => import('@/views/wallet/WalletDashboard.vue'), meta: { activeMenu: '/deposit-balance' }, }, { path: '/voucher-hotspot/sales', name: 'Jual Voucher Hotspot', component: () => import('@/views/voucher-hotspot/VoucherSales.vue'), }, { path: '/voucher-hotspot/agents', name: 'Agen Voucher', component: () => import('@/views/voucher-hotspot/VoucherAgents.vue'), }, { path: '/voucher-hotspot/login-page', name: 'Voucher LoginPage', component: () => import('@/views/voucher-hotspot/VoucherLoginPage.vue'), }, { path: '/access-applications/tenants', name: 'Pengajuan Tenant', component: () => import('@/views/access-applications/ApplicationReviews.vue'), meta: { applicationType: 'tenant' }, }, { path: '/access-applications/agents', name: 'Pengajuan Agen', component: () => import('@/views/access-applications/ApplicationReviews.vue'), meta: { applicationType: 'agent' }, }, { path: '/access-applications/staff', name: 'Pengajuan Staff', component: () => import('@/views/access-applications/ApplicationReviews.vue'), meta: { applicationType: 'staff' }, }, { 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 { const menuStore = useMenuStore() await menuStore.fetchMenus() const publicWithinApp = to.matched.some((record) => record.meta.publicWithinApp) const alwaysAllowed = ['/dashboard', '/profile', '/forbidden', '/my-applications'] if (!publicWithinApp && !alwaysAllowed.includes(to.path)) { let accessUrl = to.meta.activeMenu || to.path if (to.meta.activeMenuBase && to.params.scope) { accessUrl = `${to.meta.activeMenuBase}/${to.params.scope}` } if (!menuStore.canAccessUrl(accessUrl)) { return { name: 'Forbidden', query: { from: to.fullPath }, replace: true, } } } } catch { useAuthStore().logout() return { name: 'Login', query: { redirect: to.fullPath, }, } } } return true }) export default router