diff --git a/client/src/components/common/RoutePageFallback.tsx b/client/src/components/common/RoutePageFallback.tsx
new file mode 100644
index 00000000..9e6f82fc
--- /dev/null
+++ b/client/src/components/common/RoutePageFallback.tsx
@@ -0,0 +1,12 @@
+import { Spinner } from "../ui/spinner";
+
+export function RoutePageFallback() {
+ return (
+
+ );
+}
diff --git a/client/src/router/index.tsx b/client/src/router/index.tsx
index 50d2f9fd..92f697b9 100644
--- a/client/src/router/index.tsx
+++ b/client/src/router/index.tsx
@@ -1,38 +1,46 @@
+import { Suspense, type ReactNode } from "react";
import { Navigate, Route, Routes } from "react-router-dom";
import { AppShellLayout } from "../layouts/AppShellLayout";
import { AuthLayout } from "../layouts/AuthLayout";
-import { AuditLogsPage } from "../pages/AuditLogsPage";
-import { ApprovalRulesPage } from "../pages/ApprovalRulesPage";
-import { FeatureFlagsPage } from "../pages/FeatureFlagsPage";
-import { ChangePasswordPage } from "../pages/ChangePasswordPage";
import { BudgetsPage } from "../pages/BudgetsPage";
+import { ChangePasswordPage } from "../pages/ChangePasswordPage";
import { DashboardPage } from "../pages/DashboardPage";
-import { DepartmentsPage } from "../pages/DepartmentsPage";
-import { MasterDataPage } from "../pages/MasterDataPage";
-import { DelegationsPage } from "../pages/DelegationsPage";
import { ForbiddenPage } from "../pages/ForbiddenPage";
-import { MobileApprovalsPage } from "../pages/MobileApprovalsPage";
-import { MobileAssetScanPage } from "../pages/MobileAssetScanPage";
-import { MobileCapturePage } from "../pages/MobileCapturePage";
-import { InvoicePoolPage } from "../pages/InvoicePoolPage";
import { LoginPage } from "../pages/LoginPage";
-import { LeaveRequestPage } from "../pages/LeaveRequestPage";
-import { NotificationsPage } from "../pages/NotificationsPage";
-import { NotificationChannelsPage } from "../pages/NotificationChannelsPage";
-import { ErpIntegrationPage } from "../pages/ErpIntegrationPage";
-import { PurchaseRequestsPage } from "../pages/PurchaseRequestsPage";
-import { TravelRequestsPage } from "../pages/TravelRequestsPage";
-import { AssetRegistryPage } from "../pages/AssetRegistryPage";
-import { InventoryChecksPage } from "../pages/InventoryChecksPage";
import { NotFoundPage } from "../pages/NotFoundPage";
import { ProfilePage } from "../pages/ProfilePage";
-import { ReimbursementDetailPage } from "../pages/ReimbursementDetailPage";
-import { ReimbursementsPage } from "../pages/ReimbursementsPage";
-import { UserManagementPage } from "../pages/UserManagementPage";
-import { WorkflowTaskDetailPage } from "../pages/WorkflowTaskDetailPage";
-import { WorkflowTasksPage } from "../pages/WorkflowTasksPage";
+import { RoutePageFallback } from "../components/common/RoutePageFallback";
+import {
+ ApprovalRulesPage,
+ AssetRegistryPage,
+ AuditLogsPage,
+ DelegationsPage,
+ DepartmentsPage,
+ ErpIntegrationPage,
+ FeatureFlagsPage,
+ InventoryChecksPage,
+ InvoicePoolPage,
+ LeaveRequestPage,
+ MasterDataPage,
+ MobileApprovalsPage,
+ MobileAssetScanPage,
+ MobileCapturePage,
+ NotificationChannelsPage,
+ NotificationsPage,
+ PurchaseRequestsPage,
+ ReimbursementDetailPage,
+ ReimbursementsPage,
+ TravelRequestsPage,
+ UserManagementPage,
+ WorkflowTaskDetailPage,
+ WorkflowTasksPage,
+} from "./lazy";
import { GuestOnly, RequireAuth, RequirePermission } from "./guards";
+function withRouteFallback(node: ReactNode) {
+ return }>{node};
+}
+
export function AppRouter() {
return (
@@ -82,7 +90,7 @@ export function AppRouter() {
path="reimbursements"
element={
-
+ {withRouteFallback()}
}
/>
@@ -91,7 +99,7 @@ export function AppRouter() {
path="reimbursements/:reimbursementId"
element={
-
+ {withRouteFallback()}
}
/>
@@ -100,7 +108,7 @@ export function AppRouter() {
path="leave-requests"
element={
-
+ {withRouteFallback()}
}
/>
@@ -109,7 +117,7 @@ export function AppRouter() {
path="purchase-requests"
element={
-
+ {withRouteFallback()}
}
/>
@@ -118,7 +126,7 @@ export function AppRouter() {
path="travel-requests"
element={
-
+ {withRouteFallback()}
}
/>
@@ -127,7 +135,7 @@ export function AppRouter() {
path="assets"
element={
-
+ {withRouteFallback()}
}
/>
@@ -136,7 +144,7 @@ export function AppRouter() {
path="inventory-checks"
element={
-
+ {withRouteFallback()}
}
/>
@@ -145,7 +153,7 @@ export function AppRouter() {
path="workflow/tasks"
element={
-
+ {withRouteFallback()}
}
/>
@@ -154,7 +162,7 @@ export function AppRouter() {
path="workflow/tasks/:taskId"
element={
-
+ {withRouteFallback()}
}
/>
@@ -163,7 +171,7 @@ export function AppRouter() {
path="invoice-pool"
element={
-
+ {withRouteFallback()}
}
/>
@@ -172,7 +180,7 @@ export function AppRouter() {
path="mobile/capture"
element={
-
+ {withRouteFallback()}
}
/>
@@ -181,7 +189,7 @@ export function AppRouter() {
path="mobile/assets/scan"
element={
-
+ {withRouteFallback()}
}
/>
@@ -190,18 +198,18 @@ export function AppRouter() {
path="mobile/approvals"
element={
-
+ {withRouteFallback()}
}
/>
- } />
+ )} />
-
+ {withRouteFallback()}
}
/>
@@ -210,7 +218,7 @@ export function AppRouter() {
path="delegations"
element={
-
+ {withRouteFallback()}
}
/>
@@ -219,7 +227,7 @@ export function AppRouter() {
path="system/users"
element={
-
+ {withRouteFallback()}
}
/>
@@ -228,7 +236,7 @@ export function AppRouter() {
path="system/departments"
element={
-
+ {withRouteFallback()}
}
/>
@@ -237,7 +245,7 @@ export function AppRouter() {
path="system/master-data"
element={
-
+ {withRouteFallback()}
}
/>
@@ -246,7 +254,7 @@ export function AppRouter() {
path="system/notification-channels"
element={
-
+ {withRouteFallback()}
}
/>
@@ -255,7 +263,7 @@ export function AppRouter() {
path="system/integrations/erp"
element={
-
+ {withRouteFallback()}
}
/>
@@ -264,7 +272,7 @@ export function AppRouter() {
path="system/approval-rules"
element={
-
+ {withRouteFallback()}
}
/>
@@ -273,7 +281,7 @@ export function AppRouter() {
path="system/feature-flags"
element={
-
+ {withRouteFallback()}
}
/>
diff --git a/client/src/router/lazy/ApprovalRulesPage.ts b/client/src/router/lazy/ApprovalRulesPage.ts
new file mode 100644
index 00000000..6b1efb0e
--- /dev/null
+++ b/client/src/router/lazy/ApprovalRulesPage.ts
@@ -0,0 +1 @@
+export { ApprovalRulesPage as default } from "../../pages/ApprovalRulesPage";
diff --git a/client/src/router/lazy/AssetRegistryPage.ts b/client/src/router/lazy/AssetRegistryPage.ts
new file mode 100644
index 00000000..07d90d57
--- /dev/null
+++ b/client/src/router/lazy/AssetRegistryPage.ts
@@ -0,0 +1 @@
+export { AssetRegistryPage as default } from "../../pages/AssetRegistryPage";
diff --git a/client/src/router/lazy/AuditLogsPage.ts b/client/src/router/lazy/AuditLogsPage.ts
new file mode 100644
index 00000000..d9b731e0
--- /dev/null
+++ b/client/src/router/lazy/AuditLogsPage.ts
@@ -0,0 +1 @@
+export { AuditLogsPage as default } from "../../pages/AuditLogsPage";
diff --git a/client/src/router/lazy/DelegationsPage.ts b/client/src/router/lazy/DelegationsPage.ts
new file mode 100644
index 00000000..f4c9efa5
--- /dev/null
+++ b/client/src/router/lazy/DelegationsPage.ts
@@ -0,0 +1 @@
+export { DelegationsPage as default } from "../../pages/DelegationsPage";
diff --git a/client/src/router/lazy/DepartmentsPage.ts b/client/src/router/lazy/DepartmentsPage.ts
new file mode 100644
index 00000000..2bbf3468
--- /dev/null
+++ b/client/src/router/lazy/DepartmentsPage.ts
@@ -0,0 +1 @@
+export { DepartmentsPage as default } from "../../pages/DepartmentsPage";
diff --git a/client/src/router/lazy/ErpIntegrationPage.ts b/client/src/router/lazy/ErpIntegrationPage.ts
new file mode 100644
index 00000000..fb3891ce
--- /dev/null
+++ b/client/src/router/lazy/ErpIntegrationPage.ts
@@ -0,0 +1 @@
+export { ErpIntegrationPage as default } from "../../pages/ErpIntegrationPage";
diff --git a/client/src/router/lazy/FeatureFlagsPage.ts b/client/src/router/lazy/FeatureFlagsPage.ts
new file mode 100644
index 00000000..2b0ab4a9
--- /dev/null
+++ b/client/src/router/lazy/FeatureFlagsPage.ts
@@ -0,0 +1 @@
+export { FeatureFlagsPage as default } from "../../pages/FeatureFlagsPage";
diff --git a/client/src/router/lazy/InventoryChecksPage.ts b/client/src/router/lazy/InventoryChecksPage.ts
new file mode 100644
index 00000000..0be2157f
--- /dev/null
+++ b/client/src/router/lazy/InventoryChecksPage.ts
@@ -0,0 +1 @@
+export { InventoryChecksPage as default } from "../../pages/InventoryChecksPage";
diff --git a/client/src/router/lazy/InvoicePoolPage.ts b/client/src/router/lazy/InvoicePoolPage.ts
new file mode 100644
index 00000000..ee8b0167
--- /dev/null
+++ b/client/src/router/lazy/InvoicePoolPage.ts
@@ -0,0 +1 @@
+export { InvoicePoolPage as default } from "../../pages/InvoicePoolPage";
diff --git a/client/src/router/lazy/LeaveRequestPage.ts b/client/src/router/lazy/LeaveRequestPage.ts
new file mode 100644
index 00000000..975e6db1
--- /dev/null
+++ b/client/src/router/lazy/LeaveRequestPage.ts
@@ -0,0 +1 @@
+export { LeaveRequestPage as default } from "../../pages/LeaveRequestPage";
diff --git a/client/src/router/lazy/MasterDataPage.ts b/client/src/router/lazy/MasterDataPage.ts
new file mode 100644
index 00000000..9d0f3d0d
--- /dev/null
+++ b/client/src/router/lazy/MasterDataPage.ts
@@ -0,0 +1 @@
+export { MasterDataPage as default } from "../../pages/MasterDataPage";
diff --git a/client/src/router/lazy/MobileApprovalsPage.ts b/client/src/router/lazy/MobileApprovalsPage.ts
new file mode 100644
index 00000000..bd70cf7f
--- /dev/null
+++ b/client/src/router/lazy/MobileApprovalsPage.ts
@@ -0,0 +1 @@
+export { MobileApprovalsPage as default } from "../../pages/MobileApprovalsPage";
diff --git a/client/src/router/lazy/MobileAssetScanPage.ts b/client/src/router/lazy/MobileAssetScanPage.ts
new file mode 100644
index 00000000..59bb81df
--- /dev/null
+++ b/client/src/router/lazy/MobileAssetScanPage.ts
@@ -0,0 +1 @@
+export { MobileAssetScanPage as default } from "../../pages/MobileAssetScanPage";
diff --git a/client/src/router/lazy/MobileCapturePage.ts b/client/src/router/lazy/MobileCapturePage.ts
new file mode 100644
index 00000000..c8cb1e20
--- /dev/null
+++ b/client/src/router/lazy/MobileCapturePage.ts
@@ -0,0 +1 @@
+export { MobileCapturePage as default } from "../../pages/MobileCapturePage";
diff --git a/client/src/router/lazy/NotificationChannelsPage.ts b/client/src/router/lazy/NotificationChannelsPage.ts
new file mode 100644
index 00000000..fdf9c78d
--- /dev/null
+++ b/client/src/router/lazy/NotificationChannelsPage.ts
@@ -0,0 +1 @@
+export { NotificationChannelsPage as default } from "../../pages/NotificationChannelsPage";
diff --git a/client/src/router/lazy/NotificationsPage.ts b/client/src/router/lazy/NotificationsPage.ts
new file mode 100644
index 00000000..d1e0d67c
--- /dev/null
+++ b/client/src/router/lazy/NotificationsPage.ts
@@ -0,0 +1 @@
+export { NotificationsPage as default } from "../../pages/NotificationsPage";
diff --git a/client/src/router/lazy/PurchaseRequestsPage.ts b/client/src/router/lazy/PurchaseRequestsPage.ts
new file mode 100644
index 00000000..e691ff7f
--- /dev/null
+++ b/client/src/router/lazy/PurchaseRequestsPage.ts
@@ -0,0 +1 @@
+export { PurchaseRequestsPage as default } from "../../pages/PurchaseRequestsPage";
diff --git a/client/src/router/lazy/ReimbursementDetailPage.ts b/client/src/router/lazy/ReimbursementDetailPage.ts
new file mode 100644
index 00000000..1d58f440
--- /dev/null
+++ b/client/src/router/lazy/ReimbursementDetailPage.ts
@@ -0,0 +1 @@
+export { ReimbursementDetailPage as default } from "../../pages/ReimbursementDetailPage";
diff --git a/client/src/router/lazy/ReimbursementsPage.ts b/client/src/router/lazy/ReimbursementsPage.ts
new file mode 100644
index 00000000..927692ba
--- /dev/null
+++ b/client/src/router/lazy/ReimbursementsPage.ts
@@ -0,0 +1 @@
+export { ReimbursementsPage as default } from "../../pages/ReimbursementsPage";
diff --git a/client/src/router/lazy/TravelRequestsPage.ts b/client/src/router/lazy/TravelRequestsPage.ts
new file mode 100644
index 00000000..b4f4ef88
--- /dev/null
+++ b/client/src/router/lazy/TravelRequestsPage.ts
@@ -0,0 +1 @@
+export { TravelRequestsPage as default } from "../../pages/TravelRequestsPage";
diff --git a/client/src/router/lazy/UserManagementPage.ts b/client/src/router/lazy/UserManagementPage.ts
new file mode 100644
index 00000000..10d19259
--- /dev/null
+++ b/client/src/router/lazy/UserManagementPage.ts
@@ -0,0 +1 @@
+export { UserManagementPage as default } from "../../pages/UserManagementPage";
diff --git a/client/src/router/lazy/WorkflowTaskDetailPage.ts b/client/src/router/lazy/WorkflowTaskDetailPage.ts
new file mode 100644
index 00000000..64934ae6
--- /dev/null
+++ b/client/src/router/lazy/WorkflowTaskDetailPage.ts
@@ -0,0 +1 @@
+export { WorkflowTaskDetailPage as default } from "../../pages/WorkflowTaskDetailPage";
diff --git a/client/src/router/lazy/WorkflowTasksPage.ts b/client/src/router/lazy/WorkflowTasksPage.ts
new file mode 100644
index 00000000..68a222db
--- /dev/null
+++ b/client/src/router/lazy/WorkflowTasksPage.ts
@@ -0,0 +1 @@
+export { WorkflowTasksPage as default } from "../../pages/WorkflowTasksPage";
diff --git a/client/src/router/lazy/index.ts b/client/src/router/lazy/index.ts
new file mode 100644
index 00000000..658ff6e3
--- /dev/null
+++ b/client/src/router/lazy/index.ts
@@ -0,0 +1,25 @@
+import { lazy } from "react";
+
+export const AuditLogsPage = lazy(() => import("./AuditLogsPage"));
+export const ApprovalRulesPage = lazy(() => import("./ApprovalRulesPage"));
+export const AssetRegistryPage = lazy(() => import("./AssetRegistryPage"));
+export const DelegationsPage = lazy(() => import("./DelegationsPage"));
+export const DepartmentsPage = lazy(() => import("./DepartmentsPage"));
+export const ErpIntegrationPage = lazy(() => import("./ErpIntegrationPage"));
+export const FeatureFlagsPage = lazy(() => import("./FeatureFlagsPage"));
+export const InventoryChecksPage = lazy(() => import("./InventoryChecksPage"));
+export const InvoicePoolPage = lazy(() => import("./InvoicePoolPage"));
+export const LeaveRequestPage = lazy(() => import("./LeaveRequestPage"));
+export const MasterDataPage = lazy(() => import("./MasterDataPage"));
+export const MobileApprovalsPage = lazy(() => import("./MobileApprovalsPage"));
+export const MobileAssetScanPage = lazy(() => import("./MobileAssetScanPage"));
+export const MobileCapturePage = lazy(() => import("./MobileCapturePage"));
+export const NotificationChannelsPage = lazy(() => import("./NotificationChannelsPage"));
+export const NotificationsPage = lazy(() => import("./NotificationsPage"));
+export const PurchaseRequestsPage = lazy(() => import("./PurchaseRequestsPage"));
+export const ReimbursementDetailPage = lazy(() => import("./ReimbursementDetailPage"));
+export const ReimbursementsPage = lazy(() => import("./ReimbursementsPage"));
+export const TravelRequestsPage = lazy(() => import("./TravelRequestsPage"));
+export const UserManagementPage = lazy(() => import("./UserManagementPage"));
+export const WorkflowTaskDetailPage = lazy(() => import("./WorkflowTaskDetailPage"));
+export const WorkflowTasksPage = lazy(() => import("./WorkflowTasksPage"));
diff --git a/client/vite.config.ts b/client/vite.config.ts
index 8fc716e8..f2382c1d 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -2,6 +2,86 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { VitePWA } from "vite-plugin-pwa";
+const PAGE_CHUNK_PATTERNS = {
+ "page-travel": ["TravelRequestsPage"],
+ "page-assets": ["AssetRegistryPage", "InventoryChecksPage"],
+ "page-procurement": ["PurchaseRequestsPage"],
+ "page-workflow": ["WorkflowTasksPage", "WorkflowTaskDetailPage"],
+ "page-system": [
+ "AuditLogsPage",
+ "ApprovalRulesPage",
+ "DelegationsPage",
+ "DepartmentsPage",
+ "ErpIntegrationPage",
+ "FeatureFlagsPage",
+ "InvoicePoolPage",
+ "LeaveRequestPage",
+ "MasterDataPage",
+ "NotificationChannelsPage",
+ "NotificationsPage",
+ "ReimbursementDetailPage",
+ "ReimbursementsPage",
+ "UserManagementPage",
+ ],
+ "page-mobile": ["MobileCapturePage", "MobileAssetScanPage", "MobileApprovalsPage"],
+} as const;
+
+function resolveManualChunk(id: string) {
+ if (id.includes("node_modules")) {
+ if (["react", "react-dom", "react-router-dom"].some((pkg) => id.includes(`/node_modules/${pkg}/`))) {
+ return "vendor-react";
+ }
+
+ if (id.includes("/node_modules/@tanstack/react-query/")) {
+ return "vendor-query";
+ }
+
+ if (id.includes("/node_modules/@zxing/browser/")) {
+ return "vendor-scan";
+ }
+
+ if (
+ id.includes("/node_modules/@radix-ui/") ||
+ id.includes("/node_modules/class-variance-authority/") ||
+ id.includes("/node_modules/clsx/") ||
+ id.includes("/node_modules/tailwind-merge/") ||
+ id.includes("/node_modules/sonner/")
+ ) {
+ return "vendor-ui";
+ }
+
+ return undefined;
+ }
+
+ if (
+ id.includes("/src/api/http.ts") ||
+ id.includes("/src/api/types.ts") ||
+ id.includes("/src/api/errorMessages.ts") ||
+ id.includes("/src/api/auth.ts") ||
+ id.includes("/src/auth/") ||
+ id.includes("/src/components/common/") ||
+ id.includes("/src/hooks/") ||
+ id.includes("/src/lib/") ||
+ id.includes("/src/pwa.ts")
+ ) {
+ return "app-core";
+ }
+
+ if (id.includes("/src/api/masterData.ts") || id.includes("/src/components/ai/")) {
+ return "app-domain-shared";
+ }
+
+ for (const [chunkName, pageNames] of Object.entries(PAGE_CHUNK_PATTERNS)) {
+ for (const pageName of pageNames) {
+ if (id.includes(`/src/router/lazy/${pageName}.ts`)) {
+ return chunkName;
+ }
+ }
+ }
+
+ return undefined;
+}
+
export default defineConfig({
plugins: [
react(),
@@ -89,6 +169,15 @@ export default defineConfig({
},
}),
],
+ build: {
+ rollupOptions: {
+ output: {
+ manualChunks(id) {
+ return resolveManualChunk(id);
+ },
+ },
+ },
+ },
server: {
proxy: {
"/api": {