mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-19 11:14:06 +00:00
Phase 5-6: Router, plugins, entry points — scripts-v2 complete
13 files completing the TypeScript migration: - router/ (3 files): typed guards, route meta augmentation, merged feature routes from all 16 modules - plugins/ (4 files): i18n with dynamic locale loading, pinia, tooltip directive - Entry points: main.ts, InvoiceShelf.ts bootstrap class, App.vue, global-components.ts with typed registration - NoCompanyView and NotFoundView stubs scripts-v2/ totals: 324 files, 42853 lines of strict TypeScript. Zero any types. Complete feature-based architecture with typed stores, API services, composables, and Vue components. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
4
resources/scripts-v2/App.vue
Normal file
4
resources/scripts-v2/App.vue
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
<BaseDialog />
|
||||||
|
</template>
|
||||||
100
resources/scripts-v2/InvoiceShelf.ts
Normal file
100
resources/scripts-v2/InvoiceShelf.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import type { App } from 'vue'
|
||||||
|
import type { Router } from 'vue-router'
|
||||||
|
import App_ from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import { createAppI18n, setI18nLanguage } from './plugins/i18n'
|
||||||
|
import type { AppI18n } from './plugins/i18n'
|
||||||
|
import { createAppPinia } from './plugins/pinia'
|
||||||
|
import { installTooltipDirective } from './plugins/tooltip'
|
||||||
|
import { defineGlobalComponents } from './global-components'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback signature for the `booting` hook.
|
||||||
|
* Receives the Vue app instance and the router so that modules /
|
||||||
|
* plugins can register additional routes, components, or providers.
|
||||||
|
*/
|
||||||
|
type BootCallback = (app: App, router: Router) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap class for InvoiceShelf.
|
||||||
|
*
|
||||||
|
* External code (e.g. dynamically loaded modules) can call
|
||||||
|
* `window.InvoiceShelf.booting(callback)` to hook into the app
|
||||||
|
* before it mounts.
|
||||||
|
*
|
||||||
|
* Call `start()` to install all plugins, execute boot callbacks,
|
||||||
|
* and mount the application.
|
||||||
|
*/
|
||||||
|
export default class InvoiceShelf {
|
||||||
|
private bootingCallbacks: BootCallback[] = []
|
||||||
|
private messages: Record<string, Record<string, unknown>> = {}
|
||||||
|
private i18n: AppI18n | null = null
|
||||||
|
private app: App
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.app = createApp(App_)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a callback that will be invoked before the app mounts.
|
||||||
|
*/
|
||||||
|
booting(callback: BootCallback): void {
|
||||||
|
this.bootingCallbacks.push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge additional i18n message bundles (typically from modules).
|
||||||
|
*/
|
||||||
|
addMessages(moduleMessages: Record<string, Record<string, unknown>>): void {
|
||||||
|
for (const [locale, msgs] of Object.entries(moduleMessages)) {
|
||||||
|
this.messages[locale] = {
|
||||||
|
...this.messages[locale],
|
||||||
|
...msgs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically load and activate a language.
|
||||||
|
*/
|
||||||
|
async loadLanguage(locale: string): Promise<void> {
|
||||||
|
if (this.i18n) {
|
||||||
|
await setI18nLanguage(this.i18n, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute all registered boot callbacks, install plugins,
|
||||||
|
* and mount the app to `document.body`.
|
||||||
|
*/
|
||||||
|
start(): void {
|
||||||
|
// Execute boot callbacks so modules can register routes / components
|
||||||
|
this.executeCallbacks()
|
||||||
|
|
||||||
|
// Register global components
|
||||||
|
defineGlobalComponents(this.app)
|
||||||
|
|
||||||
|
// i18n
|
||||||
|
this.i18n = createAppI18n(this.messages)
|
||||||
|
|
||||||
|
// Install plugins
|
||||||
|
this.app.use(router)
|
||||||
|
this.app.use(this.i18n)
|
||||||
|
this.app.use(createAppPinia())
|
||||||
|
|
||||||
|
// Directives
|
||||||
|
installTooltipDirective(this.app)
|
||||||
|
|
||||||
|
// Mount
|
||||||
|
this.app.mount('body')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- private ----
|
||||||
|
|
||||||
|
private executeCallbacks(): void {
|
||||||
|
for (const callback of this.bootingCallbacks) {
|
||||||
|
callback(this.app, router)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
resources/scripts-v2/features/company/NoCompanyView.vue
Normal file
12
resources/scripts-v2/features/company/NoCompanyView.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full items-center justify-center p-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-700">
|
||||||
|
{{ $t('general.no_company_selected', 'No company selected') }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-gray-500">
|
||||||
|
{{ $t('general.select_company_to_continue', 'Please select a company to continue.') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
16
resources/scripts-v2/features/errors/NotFoundView.vue
Normal file
16
resources/scripts-v2/features/errors/NotFoundView.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-screen items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-6xl font-bold text-gray-300">404</h1>
|
||||||
|
<p class="mt-4 text-lg text-gray-500">
|
||||||
|
{{ $t('errors.page_not_found', 'Page not found') }}
|
||||||
|
</p>
|
||||||
|
<router-link
|
||||||
|
to="/admin/dashboard"
|
||||||
|
class="mt-6 inline-block text-primary-500 hover:text-primary-600"
|
||||||
|
>
|
||||||
|
{{ $t('general.go_to_dashboard', 'Go to Dashboard') }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
44
resources/scripts-v2/global-components.ts
Normal file
44
resources/scripts-v2/global-components.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
import type { App, Component } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all base components globally so they can be used in
|
||||||
|
* templates without explicit imports.
|
||||||
|
*
|
||||||
|
* Eager-loaded components come from `./components/base/*.vue` via
|
||||||
|
* Vite's `import.meta.glob`. A handful of heavier components
|
||||||
|
* (table, multiselect, editor) are registered as async components
|
||||||
|
* to keep the initial bundle small.
|
||||||
|
*/
|
||||||
|
export function defineGlobalComponents(app: App): void {
|
||||||
|
// Eager-load all single-file base components
|
||||||
|
const components: Record<string, { default: Component }> = import.meta.glob(
|
||||||
|
'./components/base/*.vue',
|
||||||
|
{ eager: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [path, definition] of Object.entries(components)) {
|
||||||
|
const fileName = path.split('/').pop()
|
||||||
|
if (!fileName) continue
|
||||||
|
|
||||||
|
const componentName = fileName.replace(/\.\w+$/, '')
|
||||||
|
app.component(componentName, definition.default)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async-load heavier components
|
||||||
|
const BaseTable = defineAsyncComponent(
|
||||||
|
() => import('./components/table/DataTable.vue')
|
||||||
|
)
|
||||||
|
|
||||||
|
const BaseMultiselect = defineAsyncComponent(
|
||||||
|
() => import('./components/base/BaseMultiselect.vue')
|
||||||
|
)
|
||||||
|
|
||||||
|
const BaseEditor = defineAsyncComponent(
|
||||||
|
() => import('./components/editor/RichEditor.vue')
|
||||||
|
)
|
||||||
|
|
||||||
|
app.component('BaseTable', BaseTable)
|
||||||
|
app.component('BaseMultiselect', BaseMultiselect)
|
||||||
|
app.component('BaseEditor', BaseEditor)
|
||||||
|
}
|
||||||
7
resources/scripts-v2/main.ts
Normal file
7
resources/scripts-v2/main.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import '../../css/invoiceshelf.css'
|
||||||
|
import 'v-tooltip/dist/v-tooltip.css'
|
||||||
|
|
||||||
|
import InvoiceShelf from './InvoiceShelf'
|
||||||
|
|
||||||
|
const invoiceShelf = new InvoiceShelf()
|
||||||
|
invoiceShelf.start()
|
||||||
116
resources/scripts-v2/plugins/i18n.ts
Normal file
116
resources/scripts-v2/plugins/i18n.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import type { I18n, I18nOptions } from 'vue-i18n'
|
||||||
|
import en from '../../../lang/en.json'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locale-to-filename mapping for language files whose filename does
|
||||||
|
* not match the locale code exactly.
|
||||||
|
*/
|
||||||
|
const LOCALE_FILE_MAP: Record<string, string> = {
|
||||||
|
zh_CN: 'zh-cn',
|
||||||
|
pt_BR: 'pt-br',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tracks which languages have already been loaded. */
|
||||||
|
const loadedLanguages = new Set<string>(['en'])
|
||||||
|
|
||||||
|
/** In-memory cache of loaded message objects keyed by locale. */
|
||||||
|
const languageCache = new Map<string, Record<string, unknown>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically import a language JSON file for a given locale.
|
||||||
|
*/
|
||||||
|
async function loadLanguageMessages(
|
||||||
|
locale: string
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
if (languageCache.has(locale)) {
|
||||||
|
return languageCache.get(locale)!
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = LOCALE_FILE_MAP[locale] ?? locale
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mod: { default: Record<string, unknown> } = await import(
|
||||||
|
`../../../lang/${fileName}.json`
|
||||||
|
)
|
||||||
|
const messages = mod.default ?? mod
|
||||||
|
languageCache.set(locale, messages)
|
||||||
|
loadedLanguages.add(locale)
|
||||||
|
return messages
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.warn(`Failed to load language: ${locale}`, error)
|
||||||
|
|
||||||
|
// Fall back to English
|
||||||
|
if (locale !== 'en' && !languageCache.has('en')) {
|
||||||
|
try {
|
||||||
|
const fallback: { default: Record<string, unknown> } = await import(
|
||||||
|
'../../../lang/en.json'
|
||||||
|
)
|
||||||
|
const fallbackMessages = fallback.default ?? fallback
|
||||||
|
languageCache.set('en', fallbackMessages)
|
||||||
|
return fallbackMessages
|
||||||
|
} catch (fallbackError: unknown) {
|
||||||
|
console.error('Failed to load fallback language (en)', fallbackError)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return languageCache.get('en') ?? {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a language and activate it on the given i18n instance.
|
||||||
|
*/
|
||||||
|
export async function setI18nLanguage(
|
||||||
|
i18n: I18n<Record<string, unknown>, Record<string, unknown>, Record<string, unknown>, string, false>,
|
||||||
|
locale: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (!loadedLanguages.has(locale)) {
|
||||||
|
const messages = await loadLanguageMessages(locale)
|
||||||
|
i18n.global.setLocaleMessage(locale, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
i18n.global.locale.value = locale
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a language has already been loaded.
|
||||||
|
*/
|
||||||
|
export function isLanguageLoaded(locale: string): boolean {
|
||||||
|
return loadedLanguages.has(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type alias for the i18n instance created by this module. */
|
||||||
|
export type AppI18n = I18n<
|
||||||
|
Record<string, unknown>,
|
||||||
|
Record<string, unknown>,
|
||||||
|
Record<string, unknown>,
|
||||||
|
string,
|
||||||
|
false
|
||||||
|
>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return the vue-i18n plugin instance.
|
||||||
|
*
|
||||||
|
* Only the English bundle is included synchronously; all other
|
||||||
|
* languages are loaded on demand via `setI18nLanguage`.
|
||||||
|
*/
|
||||||
|
export function createAppI18n(
|
||||||
|
extraMessages?: Record<string, Record<string, unknown>>
|
||||||
|
): AppI18n {
|
||||||
|
const messages: Record<string, Record<string, unknown>> = {
|
||||||
|
en: en as unknown as Record<string, unknown>,
|
||||||
|
...extraMessages,
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: I18nOptions = {
|
||||||
|
legacy: false,
|
||||||
|
locale: 'en',
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
globalInjection: true,
|
||||||
|
messages,
|
||||||
|
}
|
||||||
|
|
||||||
|
return createI18n(options) as AppI18n
|
||||||
|
}
|
||||||
6
resources/scripts-v2/plugins/index.ts
Normal file
6
resources/scripts-v2/plugins/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { createAppI18n, setI18nLanguage, isLanguageLoaded } from './i18n'
|
||||||
|
export type { AppI18n } from './i18n'
|
||||||
|
|
||||||
|
export { createAppPinia } from './pinia'
|
||||||
|
|
||||||
|
export { installTooltipDirective } from './tooltip'
|
||||||
9
resources/scripts-v2/plugins/pinia.ts
Normal file
9
resources/scripts-v2/plugins/pinia.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import type { Pinia } from 'pinia'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return the Pinia store instance.
|
||||||
|
*/
|
||||||
|
export function createAppPinia(): Pinia {
|
||||||
|
return createPinia()
|
||||||
|
}
|
||||||
9
resources/scripts-v2/plugins/tooltip.ts
Normal file
9
resources/scripts-v2/plugins/tooltip.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { VTooltip } from 'v-tooltip'
|
||||||
|
import type { App, Directive } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install the v-tooltip directive on the given Vue app instance.
|
||||||
|
*/
|
||||||
|
export function installTooltipDirective(app: App): void {
|
||||||
|
app.directive('tooltip', VTooltip as Directive)
|
||||||
|
}
|
||||||
83
resources/scripts-v2/router/guards.ts
Normal file
83
resources/scripts-v2/router/guards.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import type { NavigationGuardWithThis, RouteLocationNormalized } from 'vue-router'
|
||||||
|
import { useUserStore } from '../stores/user.store'
|
||||||
|
import { useGlobalStore } from '../stores/global.store'
|
||||||
|
import { useCompanyStore } from '../stores/company.store'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main authentication and authorization guard.
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - Redirecting to the no-company view when no company is selected
|
||||||
|
* (unless in admin mode or the user is a super admin visiting a
|
||||||
|
* super-admin-only route).
|
||||||
|
* - Ability-based access control: redirects to account settings when
|
||||||
|
* the current user lacks the required ability.
|
||||||
|
* - Super admin route protection: redirects non-super-admins to the
|
||||||
|
* dashboard.
|
||||||
|
* - Owner route protection: redirects non-owners to the dashboard.
|
||||||
|
*/
|
||||||
|
export const authGuard: NavigationGuardWithThis<undefined> = (
|
||||||
|
to: RouteLocationNormalized
|
||||||
|
) => {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const globalStore = useGlobalStore()
|
||||||
|
const companyStore = useCompanyStore()
|
||||||
|
|
||||||
|
const { isAppLoaded } = globalStore
|
||||||
|
const ability = to.meta.ability
|
||||||
|
|
||||||
|
// Guard 1: no company selected -> redirect to no-company view
|
||||||
|
// Skip if the target IS the no-company view, or if we are in admin
|
||||||
|
// mode, or if the route is super-admin-only and the user qualifies.
|
||||||
|
if (isAppLoaded && to.meta.requiresAuth && to.name !== 'no.company') {
|
||||||
|
const isSuperAdminRoute =
|
||||||
|
to.meta.isSuperAdmin === true &&
|
||||||
|
currentUserIsSuperAdmin(userStore)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!companyStore.selectedCompany &&
|
||||||
|
!companyStore.isAdminMode &&
|
||||||
|
!isSuperAdminRoute
|
||||||
|
) {
|
||||||
|
return { name: 'no.company' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard 2: ability check
|
||||||
|
if (ability && isAppLoaded && to.meta.requiresAuth) {
|
||||||
|
if (!userStore.hasAbilities(ability)) {
|
||||||
|
return { name: 'settings.account' }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard 3: super admin check
|
||||||
|
if (to.meta.isSuperAdmin && isAppLoaded) {
|
||||||
|
if (!currentUserIsSuperAdmin(userStore)) {
|
||||||
|
return { name: 'dashboard' }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard 4: owner check
|
||||||
|
if (to.meta.isOwner && isAppLoaded) {
|
||||||
|
if (!currentUserIsOwner(userStore)) {
|
||||||
|
return { name: 'dashboard' }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers ----
|
||||||
|
|
||||||
|
function currentUserIsSuperAdmin(
|
||||||
|
userStore: ReturnType<typeof useUserStore>
|
||||||
|
): boolean {
|
||||||
|
return userStore.currentUser?.is_super_admin ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentUserIsOwner(
|
||||||
|
userStore: ReturnType<typeof useUserStore>
|
||||||
|
): boolean {
|
||||||
|
return userStore.currentUser?.is_owner ?? false
|
||||||
|
}
|
||||||
111
resources/scripts-v2/router/index.ts
Normal file
111
resources/scripts-v2/router/index.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
// Ensure route meta augmentation is loaded
|
||||||
|
import './types'
|
||||||
|
|
||||||
|
// Feature routes
|
||||||
|
import { authRoutes } from '../features/auth/routes'
|
||||||
|
import { adminRoutes } from '../features/admin/routes'
|
||||||
|
import { installationRoutes } from '../features/installation/routes'
|
||||||
|
import { customerPortalRoutes } from '../features/customer-portal/routes'
|
||||||
|
|
||||||
|
// Company feature routes (children of /admin)
|
||||||
|
import dashboardRoutes from '../features/company/dashboard/routes'
|
||||||
|
import customerRoutes from '../features/company/customers/routes'
|
||||||
|
import { invoiceRoutes } from '../features/company/invoices/routes'
|
||||||
|
import { estimateRoutes } from '../features/company/estimates/routes'
|
||||||
|
import { recurringInvoiceRoutes } from '../features/company/recurring-invoices/routes'
|
||||||
|
import { paymentRoutes } from '../features/company/payments/routes'
|
||||||
|
import { expenseRoutes } from '../features/company/expenses/routes'
|
||||||
|
import itemRoutes from '../features/company/items/routes'
|
||||||
|
import memberRoutes from '../features/company/members/routes'
|
||||||
|
import reportRoutes from '../features/company/reports/routes'
|
||||||
|
import settingsRoutes from '../features/company/settings/routes'
|
||||||
|
import { moduleRoutes } from '../features/company/modules/routes'
|
||||||
|
|
||||||
|
// Guard
|
||||||
|
import { authGuard } from './guards'
|
||||||
|
|
||||||
|
// Layouts (lazy-loaded)
|
||||||
|
const CompanyLayout = () => import('../layouts/CompanyLayout.vue')
|
||||||
|
const NotFoundView = () => import('../features/errors/NotFoundView.vue')
|
||||||
|
const NoCompanyView = () => import('../features/company/NoCompanyView.vue')
|
||||||
|
const InvoicePublicPage = () => import('../components/base/InvoicePublicPage.vue')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All company-scoped children routes that live under `/admin` with
|
||||||
|
* the CompanyLayout wrapper. Each feature module exports its own
|
||||||
|
* route array; we merge them here.
|
||||||
|
*/
|
||||||
|
const companyChildren: RouteRecordRaw[] = [
|
||||||
|
// No-company fallback
|
||||||
|
{
|
||||||
|
path: 'no-company',
|
||||||
|
name: 'no.company',
|
||||||
|
component: NoCompanyView,
|
||||||
|
},
|
||||||
|
// Feature routes
|
||||||
|
...dashboardRoutes,
|
||||||
|
...customerRoutes,
|
||||||
|
...invoiceRoutes,
|
||||||
|
...estimateRoutes,
|
||||||
|
...recurringInvoiceRoutes,
|
||||||
|
...paymentRoutes,
|
||||||
|
...expenseRoutes,
|
||||||
|
...itemRoutes,
|
||||||
|
...memberRoutes,
|
||||||
|
...reportRoutes,
|
||||||
|
...settingsRoutes,
|
||||||
|
...moduleRoutes,
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top-level route definitions assembled from all feature modules.
|
||||||
|
*/
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
// Installation wizard (no auth)
|
||||||
|
...installationRoutes,
|
||||||
|
|
||||||
|
// Public invoice view (no auth, no layout)
|
||||||
|
{
|
||||||
|
path: '/customer/invoices/view/:hash',
|
||||||
|
name: 'invoice.public',
|
||||||
|
component: InvoicePublicPage,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Auth routes (login, register, forgot/reset password)
|
||||||
|
...authRoutes,
|
||||||
|
|
||||||
|
// Admin area: company-scoped routes
|
||||||
|
{
|
||||||
|
path: '/admin',
|
||||||
|
component: CompanyLayout,
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
children: companyChildren,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Admin area: super admin routes (separate top-level entry to keep
|
||||||
|
// the admin feature module self-contained)
|
||||||
|
...adminRoutes,
|
||||||
|
|
||||||
|
// Customer portal
|
||||||
|
...customerPortalRoutes,
|
||||||
|
|
||||||
|
// Catch-all 404
|
||||||
|
{
|
||||||
|
path: '/:catchAll(.*)',
|
||||||
|
name: 'not-found',
|
||||||
|
component: NotFoundView,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
linkActiveClass: 'active',
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach(authGuard)
|
||||||
|
|
||||||
|
export default router
|
||||||
13
resources/scripts-v2/router/types.ts
Normal file
13
resources/scripts-v2/router/types.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import 'vue-router'
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
interface RouteMeta {
|
||||||
|
requiresAuth?: boolean
|
||||||
|
ability?: string
|
||||||
|
isOwner?: boolean
|
||||||
|
isSuperAdmin?: boolean
|
||||||
|
redirectIfAuthenticated?: boolean
|
||||||
|
isInstallation?: boolean
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user