diff --git a/src/components/shared/ActionDropdown.tsx b/src/components/shared/ActionDropdown.tsx index 5867a97..f71083c 100644 --- a/src/components/shared/ActionDropdown.tsx +++ b/src/components/shared/ActionDropdown.tsx @@ -35,8 +35,18 @@ export const ActionDropdown = ({ } }; + const handleScroll = (event: Event) => { + // Don't close if scrolling inside the dropdown menu itself + const target = event.target as HTMLElement; + if (dropdownMenuRef.current && (dropdownMenuRef.current === target || dropdownMenuRef.current.contains(target))) { + return; + } + setIsOpen(false); + }; + if (isOpen && buttonRef.current) { document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); // Calculate position when dropdown opens const rect = buttonRef.current.getBoundingClientRect(); @@ -72,6 +82,7 @@ export const ActionDropdown = ({ return () => { document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); }; }, [isOpen]); diff --git a/src/components/shared/DataTable.tsx b/src/components/shared/DataTable.tsx index 2b26059..866db1a 100644 --- a/src/components/shared/DataTable.tsx +++ b/src/components/shared/DataTable.tsx @@ -30,8 +30,8 @@ export const DataTable = ({ // Loading State if (isLoading) { return ( -
-

Loading...

+
+

Loading...

); } @@ -39,8 +39,8 @@ export const DataTable = ({ // Error State if (error) { return ( -
-

{error}

+
+

{error}

); } @@ -50,7 +50,52 @@ export const DataTable = ({ return ( <> {/* Desktop Table Empty State */} -
+
+
+ + + + {columns.map((column) => { + const alignClass = + column.align === 'right' + ? 'text-right' + : column.align === 'center' + ? 'text-center' + : 'text-left'; + return ( + + ); + })} + + + + + + + +
+ {column.label} +
+ {emptyMessage} +
+
+
+ {/* Mobile Empty State */} +
+

{emptyMessage}

+
+ + ); + } + + return ( + <> + {/* Desktop Table */} +
+
@@ -64,7 +109,7 @@ export const DataTable = ({ return ( @@ -73,70 +118,29 @@ export const DataTable = ({ - - - + {data.map((item) => ( + + {columns.map((column) => { + const alignClass = + column.align === 'right' + ? 'text-right' + : column.align === 'center' + ? 'text-center' + : 'text-left'; + return ( + + ); + })} + + ))}
{column.label}
- {emptyMessage} -
+ {column.render ? column.render(item) : String((item as any)[column.key])} +
- {/* Mobile Empty State */} -
-

{emptyMessage}

-
- - ); - } - - return ( - <> - {/* Desktop Table */} -
- - - - {columns.map((column) => { - const alignClass = - column.align === 'right' - ? 'text-right' - : column.align === 'center' - ? 'text-center' - : 'text-left'; - return ( - - ); - })} - - - - {data.map((item) => ( - - {columns.map((column) => { - const alignClass = - column.align === 'right' - ? 'text-right' - : column.align === 'center' - ? 'text-center' - : 'text-left'; - return ( - - ); - })} - - ))} - -
- {column.label} -
- {column.render ? column.render(item) : String((item as any)[column.key])} -
{/* Mobile Card View */} @@ -144,13 +148,13 @@ export const DataTable = ({ {mobileCardRenderer ? data.map((item) =>
{mobileCardRenderer(item)}
) : data.map((item) => ( -
+
{columns.map((column) => ( -
- +
+ {column.mobileLabel || column.label}: -
+
{column.render ? column.render(item) : String((item as any)[column.key])}
diff --git a/src/components/shared/EditRoleModal.tsx b/src/components/shared/EditRoleModal.tsx index e5dbfa1..9244ebf 100644 --- a/src/components/shared/EditRoleModal.tsx +++ b/src/components/shared/EditRoleModal.tsx @@ -310,17 +310,17 @@ export const EditRoleModal = ({ // Map role modules to options from available modules const moduleOptions = roleModules - .map((moduleId: string) => { + .map((moduleId: string) => { const module = availableModulesResponse.data.find((m) => m.id === moduleId); - if (module) { - return { - value: moduleId, - label: module.name, - }; - } - return null; - }) - .filter((opt) => opt !== null) as Array<{ value: string; label: string }>; + if (module) { + return { + value: moduleId, + label: module.name, + }; + } + return null; + }) + .filter((opt) => opt !== null) as Array<{ value: string; label: string }>; setInitialAvailableModuleOptions(moduleOptions); } catch (err) { @@ -527,18 +527,18 @@ export const EditRoleModal = ({ /> {/* Available Modules Selection */} - { + onValueChange={(values) => { setSelectedAvailableModules(values); setValue('modules', values.length > 0 ? values : []); - }} + }} onLoadOptions={loadAvailableModules} initialOptions={initialAvailableModuleOptions} error={errors.modules?.message} - /> + /> {/* Permissions Section */}
diff --git a/src/components/shared/FilterDropdown.tsx b/src/components/shared/FilterDropdown.tsx index b5c5acf..1fa8250 100644 --- a/src/components/shared/FilterDropdown.tsx +++ b/src/components/shared/FilterDropdown.tsx @@ -31,6 +31,7 @@ export const FilterDropdown = ({ const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); const buttonRef = useRef(null); + const dropdownMenuRef = useRef(null); const [dropdownStyle, setDropdownStyle] = useState<{ top?: string; bottom?: string; @@ -54,8 +55,18 @@ export const FilterDropdown = ({ } }; + const handleScroll = (event: Event) => { + // Don't close if scrolling inside the dropdown menu itself + const target = event.target as HTMLElement; + if (dropdownMenuRef.current && (dropdownMenuRef.current === target || dropdownMenuRef.current.contains(target))) { + return; + } + setIsOpen(false); + }; + if (isOpen) { document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); if (buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; @@ -86,6 +97,7 @@ export const FilterDropdown = ({ return () => { document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); }; }, [isOpen, options.length]); @@ -122,6 +134,7 @@ export const FilterDropdown = ({ buttonRef.current && createPortal(
{ + // Don't close if scrolling inside the dropdown menu itself + const target = event.target as HTMLElement; + if (dropdownMenuRef.current && (dropdownMenuRef.current === target || dropdownMenuRef.current.contains(target))) { + return; + } + setIsOpen(false); + }; + if (isOpen && buttonRef.current) { document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); // Calculate position when dropdown opens const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; @@ -87,6 +97,7 @@ export const FormSelect = ({ return () => { document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); }; }, [isOpen]); diff --git a/src/components/shared/MultiselectPaginatedSelect.tsx b/src/components/shared/MultiselectPaginatedSelect.tsx index a0b77d4..480f344 100644 --- a/src/components/shared/MultiselectPaginatedSelect.tsx +++ b/src/components/shared/MultiselectPaginatedSelect.tsx @@ -146,8 +146,22 @@ export const MultiselectPaginatedSelect = ({ } }; + const handleScroll = (event: Event) => { + // Don't close if scrolling inside the dropdown's internal scroll container + const target = event.target as HTMLElement; + if (scrollContainerRef.current && (scrollContainerRef.current === target || scrollContainerRef.current.contains(target))) { + return; + } + // Don't close if scrolling inside the dropdown menu itself + if (dropdownMenuRef.current && (dropdownMenuRef.current === target || dropdownMenuRef.current.contains(target))) { + return; + } + setIsOpen(false); + }; + if (isOpen && buttonRef.current) { document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; @@ -173,6 +187,7 @@ export const MultiselectPaginatedSelect = ({ return () => { document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); }; }, [isOpen]); diff --git a/src/components/shared/NewRoleModal.tsx b/src/components/shared/NewRoleModal.tsx index 28f1131..b07cc09 100644 --- a/src/components/shared/NewRoleModal.tsx +++ b/src/components/shared/NewRoleModal.tsx @@ -369,17 +369,17 @@ export const NewRoleModal = ({ /> {/* Available Modules Selection */} - { + onValueChange={(values) => { setSelectedAvailableModules(values); setValue('modules', values.length > 0 ? values : []); - }} + }} onLoadOptions={loadAvailableModules} error={errors.modules?.message} - /> + /> {/* Permissions Section */}
diff --git a/src/components/shared/PageHeader.tsx b/src/components/shared/PageHeader.tsx index bf2b216..e1951f5 100644 --- a/src/components/shared/PageHeader.tsx +++ b/src/components/shared/PageHeader.tsx @@ -18,8 +18,8 @@ interface PageHeaderProps { const defaultTabs: TabItem[] = [ { label: 'Overview', path: '/dashboard' }, { label: 'Tenants', path: '/tenants' }, - { label: 'Users', path: '/users' }, - { label: 'Roles', path: '/roles' }, + // { label: 'Users', path: '/users' }, + // { label: 'Roles', path: '/roles' }, { label: 'Modules', path: '/modules' }, { label: 'Audit Logs', path: '/audit-logs' }, ]; @@ -58,11 +58,11 @@ export const PageHeader = ({
{/* Title and Description */}
-

+

{title}

{description && ( -

+

{description}

)} diff --git a/src/components/shared/PaginatedSelect.tsx b/src/components/shared/PaginatedSelect.tsx index 8678c77..ad02251 100644 --- a/src/components/shared/PaginatedSelect.tsx +++ b/src/components/shared/PaginatedSelect.tsx @@ -151,8 +151,22 @@ export const PaginatedSelect = ({ } }; + const handleScroll = (event: Event) => { + // Don't close if scrolling inside the dropdown's internal scroll container + const target = event.target as HTMLElement; + if (scrollContainerRef.current && (scrollContainerRef.current === target || scrollContainerRef.current.contains(target))) { + return; + } + // Don't close if scrolling inside the dropdown menu itself + if (dropdownMenuRef.current && (dropdownMenuRef.current === target || dropdownMenuRef.current.contains(target))) { + return; + } + setIsOpen(false); + }; + if (isOpen && buttonRef.current) { document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; @@ -178,6 +192,7 @@ export const PaginatedSelect = ({ return () => { document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); }; }, [isOpen]); diff --git a/src/components/superadmin/NewModuleModal.tsx b/src/components/superadmin/NewModuleModal.tsx index c4c85bb..adecdec 100644 --- a/src/components/superadmin/NewModuleModal.tsx +++ b/src/components/superadmin/NewModuleModal.tsx @@ -230,7 +230,7 @@ export const NewModuleModal = ({ } > -
+ {/* API Key Display Section */} {apiKey && (
diff --git a/src/components/superadmin/RolesTable.tsx b/src/components/superadmin/RolesTable.tsx index b90452e..b3d0e9d 100644 --- a/src/components/superadmin/RolesTable.tsx +++ b/src/components/superadmin/RolesTable.tsx @@ -324,7 +324,7 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol isLoading={isLoading} error={error} /> - {pagination.totalPages > 1 && ( + {pagination.totalPages > 0 && ( {user.email}, }, + { + key: 'role', + label: 'role', + render: (user) => {user.role?.name}, + }, { key: 'status', label: 'Status', @@ -361,7 +366,7 @@ export const UsersTable = ({ tenantId, showHeader = true, compact = false }: Use isLoading={isLoading} error={error} /> - {pagination.totalPages > 1 && ( + {pagination.totalPages > 0 && ( { setLimit(newLimit); - setCurrentPage(1); + setCurrentPage(1); // Reset to first page when limit changes }} /> )} diff --git a/src/components/superadmin/ViewTenantModal.tsx b/src/components/superadmin/ViewTenantModal.tsx index 2f9975c..cb67eb5 100644 --- a/src/components/superadmin/ViewTenantModal.tsx +++ b/src/components/superadmin/ViewTenantModal.tsx @@ -1,169 +1,169 @@ -import { useEffect, useState } from 'react'; -import type { ReactElement } from 'react'; -import { Loader2 } from 'lucide-react'; -import { Modal, SecondaryButton, StatusBadge } from '@/components/shared'; -import type { Tenant } from '@/types/tenant'; +// import { useEffect, useState } from 'react'; +// import type { ReactElement } from 'react'; +// import { Loader2 } from 'lucide-react'; +// import { Modal, SecondaryButton, StatusBadge } from '@/components/shared'; +// import type { Tenant } from '@/types/tenant'; -interface ViewTenantModalProps { - isOpen: boolean; - onClose: () => void; - tenantId: string | null; - onLoadTenant: (id: string) => Promise; -} +// interface ViewTenantModalProps { +// isOpen: boolean; +// onClose: () => void; +// tenantId: string | null; +// onLoadTenant: (id: string) => Promise; +// } -// Helper function to get status badge variant -const getStatusVariant = (status: string): 'success' | 'failure' | 'process' => { - switch (status.toLowerCase()) { - case 'active': - return 'success'; - case 'deleted': - return 'failure'; - case 'suspended': - return 'process'; - default: - return 'success'; - } -}; +// // Helper function to get status badge variant +// const getStatusVariant = (status: string): 'success' | 'failure' | 'process' => { +// switch (status.toLowerCase()) { +// case 'active': +// return 'success'; +// case 'deleted': +// return 'failure'; +// case 'suspended': +// return 'process'; +// default: +// return 'success'; +// } +// }; -// Helper function to format date -const formatDate = (dateString: string): string => { - const date = new Date(dateString); - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); -}; +// // Helper function to format date +// const formatDate = (dateString: string): string => { +// const date = new Date(dateString); +// return date.toLocaleDateString('en-US', { +// month: 'short', +// day: 'numeric', +// year: 'numeric', +// hour: '2-digit', +// minute: '2-digit', +// }); +// }; -export const ViewTenantModal = ({ - isOpen, - onClose, - tenantId, - onLoadTenant, -}: ViewTenantModalProps): ReactElement | null => { - const [tenant, setTenant] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); +// export const ViewTenantModal = ({ +// isOpen, +// onClose, +// tenantId, +// onLoadTenant, +// }: ViewTenantModalProps): ReactElement | null => { +// const [tenant, setTenant] = useState(null); +// const [isLoading, setIsLoading] = useState(false); +// const [error, setError] = useState(null); - // Load tenant data when modal opens - useEffect(() => { - if (isOpen && tenantId) { - const loadTenant = async (): Promise => { - try { - setIsLoading(true); - setError(null); - const data = await onLoadTenant(tenantId); - setTenant(data); - } catch (err: any) { - setError(err?.response?.data?.error?.message || 'Failed to load tenant details'); - } finally { - setIsLoading(false); - } - }; - loadTenant(); - } else { - setTenant(null); - setError(null); - } - }, [isOpen, tenantId, onLoadTenant]); +// // Load tenant data when modal opens +// useEffect(() => { +// if (isOpen && tenantId) { +// const loadTenant = async (): Promise => { +// try { +// setIsLoading(true); +// setError(null); +// const data = await onLoadTenant(tenantId); +// setTenant(data); +// } catch (err: any) { +// setError(err?.response?.data?.error?.message || 'Failed to load tenant details'); +// } finally { +// setIsLoading(false); +// } +// }; +// loadTenant(); +// } else { +// setTenant(null); +// setError(null); +// } +// }, [isOpen, tenantId, onLoadTenant]); - return ( - - Close - - } - > -
- {isLoading && ( -
- -
- )} +// return ( +// +// Close +// +// } +// > +//
+// {isLoading && ( +//
+// +//
+// )} - {error && ( -
-

{error}

-
- )} +// {error && ( +//
+//

{error}

+//
+// )} - {!isLoading && !error && tenant && ( -
- {/* Basic Information */} -
-

Basic Information

-
-
- -

{tenant.name}

-
-
- -

{tenant.slug}

-
-
- -
- - {tenant.status} - -
-
-
-
+// {!isLoading && !error && tenant && ( +//
+// {/* Basic Information */} +//
+//

Basic Information

+//
+//
+// +//

{tenant.name}

+//
+//
+// +//

{tenant.slug}

+//
+//
+// +//
+// +// {tenant.status} +// +//
+//
+//
+//
- {/* Settings */} -
-

Settings

-
-
- -

- {tenant.settings?.timezone || 'N/A'} -

-
-
- -

- {tenant.subscription_tier ? tenant.subscription_tier.charAt(0).toUpperCase() + tenant.subscription_tier.slice(1) : 'N/A'} -

-
-
- -

{tenant.max_users ?? 'N/A'}

-
-
- -

{tenant.max_modules ?? 'N/A'}

-
-
-
+// {/* Settings */} +//
+//

Settings

+//
+//
+// +//

+// {tenant.settings?.timezone || 'N/A'} +//

+//
+//
+// +//

+// {tenant.subscription_tier ? tenant.subscription_tier.charAt(0).toUpperCase() + tenant.subscription_tier.slice(1) : 'N/A'} +//

+//
+//
+// +//

{tenant.max_users ?? 'N/A'}

+//
+//
+// +//

{tenant.max_modules ?? 'N/A'}

+//
+//
+//
- {/* Timestamps */} -
-

Timestamps

-
-
- -

{formatDate(tenant.created_at)}

-
-
- -

{formatDate(tenant.updated_at)}

-
-
-
-
- )} -
- - ); -}; +// {/* Timestamps */} +//
+//

Timestamps

+//
+//
+// +//

{formatDate(tenant.created_at)}

+//
+//
+// +//

{formatDate(tenant.updated_at)}

+//
+//
+//
+//
+// )} +//
+//
+// ); +// }; diff --git a/src/components/superadmin/index.ts b/src/components/superadmin/index.ts index 89883c7..8d49774 100644 --- a/src/components/superadmin/index.ts +++ b/src/components/superadmin/index.ts @@ -1,5 +1,5 @@ // NewTenantModal is commented out and not exported - using CreateTenantWizard instead -export { ViewTenantModal } from './ViewTenantModal'; +// export { ViewTenantModal } from './ViewTenantModal'; // export { EditTenantModal } from './EditTenantModal'; export { NewModuleModal } from './NewModuleModal'; export { ViewModuleModal } from './ViewModuleModal'; diff --git a/src/features/dashboard/components/QuickActions.tsx b/src/features/dashboard/components/QuickActions.tsx index 4c8db66..10ed81a 100644 --- a/src/features/dashboard/components/QuickActions.tsx +++ b/src/features/dashboard/components/QuickActions.tsx @@ -8,8 +8,8 @@ export const QuickActions = () => { const quickActions: QuickAction[] = [ { icon: Plus, label: 'New Tenant', onClick: () => navigate('/tenants') }, - { icon: UserPlus, label: 'Invite User', onClick: () => navigate('/users') }, - { icon: Shield, label: 'Add Role', onClick: () => navigate('/roles') }, + { icon: UserPlus, label: 'Invite User', onClick: () => navigate('/tenants') }, + { icon: Shield, label: 'Add Role', onClick: () => navigate('/tenants') }, { icon: Settings, label: 'Config', onClick: () => console.log('Config') }, ]; diff --git a/src/pages/superadmin/TenantDetails.tsx b/src/pages/superadmin/TenantDetails.tsx index c31aaff..d1bd864 100644 --- a/src/pages/superadmin/TenantDetails.tsx +++ b/src/pages/superadmin/TenantDetails.tsx @@ -206,14 +206,14 @@ const TenantDetails = (): ReactElement => {
-

+

{tenant.name}

{tenant.status}
-
+
{tenant.slug} diff --git a/src/pages/tenant/Roles.tsx b/src/pages/tenant/Roles.tsx index 13eb714..99c8569 100644 --- a/src/pages/tenant/Roles.tsx +++ b/src/pages/tenant/Roles.tsx @@ -352,14 +352,14 @@ const Roles = (): ReactElement => { {/* New Role Button */} {canCreate('roles') && ( - setIsModalOpen(true)} - > - - New Role - + setIsModalOpen(true)} + > + + New Role + )}
diff --git a/src/pages/tenant/Users.tsx b/src/pages/tenant/Users.tsx index 0774ab0..d7b59c6 100644 --- a/src/pages/tenant/Users.tsx +++ b/src/pages/tenant/Users.tsx @@ -234,6 +234,11 @@ const Users = (): ReactElement => { label: 'Email', render: (user) => {user.email}, }, + { + key: 'role', + label: 'role', + render: (user) => {user.role?.name}, + }, { key: 'status', label: 'Status', @@ -385,14 +390,14 @@ const Users = (): ReactElement => { {/* New User Button */} {canCreate('users') && ( - setIsModalOpen(true)} - > - - New User - + setIsModalOpen(true)} + > + + New User + )}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx index ddbb99a..6021c5e 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,6 +1,6 @@ import { Routes, Route } from 'react-router-dom'; +import { lazy, Suspense } from 'react'; import type { ReactElement } from 'react'; -import NotFound from '@/pages/NotFound'; import ProtectedRoute from '@/pages/ProtectedRoute'; import TenantProtectedRoute from '@/pages/tenant/TenantProtectedRoute'; import { NavigationInitializer } from '@/components/NavigationInitializer'; @@ -8,6 +8,16 @@ import { publicRoutes } from './public-routes'; import { superAdminRoutes } from './super-admin-routes'; import { tenantAdminRoutes } from './tenant-admin-routes'; +// Lazy load NotFound page +const NotFound = lazy(() => import('@/pages/NotFound')); + +// Loading fallback component +const RouteLoader = (): ReactElement => ( +
+
Loading...
+
+); + // App Routes Component export const AppRoutes = (): ReactElement => { return ( @@ -42,7 +52,9 @@ export const AppRoutes = (): ReactElement => { path="*" element={ - + }> + + } /> diff --git a/src/routes/public-routes.tsx b/src/routes/public-routes.tsx index 9d67444..015022c 100644 --- a/src/routes/public-routes.tsx +++ b/src/routes/public-routes.tsx @@ -1,9 +1,26 @@ -import Login from '@/pages/Login'; -import TenantLogin from '@/pages/tenant/TenantLogin'; -import ForgotPassword from '@/pages/ForgotPassword'; -import ResetPassword from '@/pages/ResetPassword'; +import { lazy, Suspense } from 'react'; import type { ReactElement } from 'react'; +// Lazy load route components for code splitting +const Login = lazy(() => import('@/pages/Login')); +const TenantLogin = lazy(() => import('@/pages/tenant/TenantLogin')); +const ForgotPassword = lazy(() => import('@/pages/ForgotPassword')); +const ResetPassword = lazy(() => import('@/pages/ResetPassword')); + +// Loading fallback component +const RouteLoader = (): ReactElement => ( +
+
Loading...
+
+); + +// Wrapper component with Suspense +const LazyRoute = ({ component: Component }: { component: React.ComponentType }): ReactElement => ( + }> + + +); + export interface RouteConfig { path: string; element: ReactElement; @@ -13,26 +30,26 @@ export interface RouteConfig { export const publicRoutes: RouteConfig[] = [ { path: '/', - element: , + element: , }, { path: '/forgot-password', - element: , + element: , }, { path: '/reset-password', - element: , + element: , }, { path: '/tenant/login', - element: , + element: , }, { path: '/tenant/forgot-password', - element: , + element: , }, { path: '/tenant/reset-password', - element: , + element: , }, ]; diff --git a/src/routes/super-admin-routes.tsx b/src/routes/super-admin-routes.tsx index aa7842b..85a717e 100644 --- a/src/routes/super-admin-routes.tsx +++ b/src/routes/super-admin-routes.tsx @@ -1,14 +1,29 @@ -import Dashboard from '@/pages/superadmin/Dashboard'; -import Tenants from '@/pages/superadmin/Tenants'; -import CreateTenantWizard from '@/pages/superadmin/CreateTenantWizard'; -import EditTenant from '@/pages/superadmin/EditTenant'; -import TenantDetails from '@/pages/superadmin/TenantDetails'; -// import Users from '@/pages/superadmin/Users'; -// import Roles from '@/pages/superadmin/Roles'; -import Modules from '@/pages/superadmin/Modules'; -import AuditLogs from '@/pages/superadmin/AuditLogs'; +import { lazy, Suspense } from 'react'; import type { ReactElement } from 'react'; +// Lazy load route components for code splitting +const Dashboard = lazy(() => import('@/pages/superadmin/Dashboard')); +const Tenants = lazy(() => import('@/pages/superadmin/Tenants')); +const CreateTenantWizard = lazy(() => import('@/pages/superadmin/CreateTenantWizard')); +const EditTenant = lazy(() => import('@/pages/superadmin/EditTenant')); +const TenantDetails = lazy(() => import('@/pages/superadmin/TenantDetails')); +const Modules = lazy(() => import('@/pages/superadmin/Modules')); +const AuditLogs = lazy(() => import('@/pages/superadmin/AuditLogs')); + +// Loading fallback component +const RouteLoader = (): ReactElement => ( +
+
Loading...
+
+); + +// Wrapper component with Suspense +const LazyRoute = ({ component: Component }: { component: React.ComponentType }): ReactElement => ( + }> + + +); + export interface RouteConfig { path: string; element: ReactElement; @@ -18,38 +33,30 @@ export interface RouteConfig { export const superAdminRoutes: RouteConfig[] = [ { path: '/dashboard', - element: , + element: , }, { path: '/tenants', - element: , + element: , }, { path: '/tenants/create-wizard', - element: , + element: , }, { path: '/tenants/:id/edit', - element: , + element: , }, { path: '/tenants/:id', - element: , + element: , }, - // { - // path: '/users', - // element: , - // }, - // { - // path: '/roles', - // element: , - // }, { path: '/modules', - element: , + element: , }, { path: '/audit-logs', - element: , + element: , }, ]; diff --git a/src/routes/tenant-admin-routes.tsx b/src/routes/tenant-admin-routes.tsx index 5c10cc4..edcec6c 100644 --- a/src/routes/tenant-admin-routes.tsx +++ b/src/routes/tenant-admin-routes.tsx @@ -1,10 +1,27 @@ -import Dashboard from '@/pages/tenant/Dashboard'; -import Roles from '@/pages/tenant/Roles'; -import Settings from '@/pages/tenant/Settings'; -import Users from '@/pages/tenant/Users'; -import AuditLogs from '@/pages/tenant/AuditLogs'; +import { lazy, Suspense } from 'react'; import type { ReactElement } from 'react'; -import Modules from '@/pages/tenant/Modules'; + +// Lazy load route components for code splitting +const Dashboard = lazy(() => import('@/pages/tenant/Dashboard')); +const Roles = lazy(() => import('@/pages/tenant/Roles')); +const Settings = lazy(() => import('@/pages/tenant/Settings')); +const Users = lazy(() => import('@/pages/tenant/Users')); +const AuditLogs = lazy(() => import('@/pages/tenant/AuditLogs')); +const Modules = lazy(() => import('@/pages/tenant/Modules')); + +// Loading fallback component +const RouteLoader = (): ReactElement => ( +
+
Loading...
+
+); + +// Wrapper component with Suspense +const LazyRoute = ({ component: Component }: { component: React.ComponentType }): ReactElement => ( + }> + + +); export interface RouteConfig { path: string; @@ -15,26 +32,26 @@ export interface RouteConfig { export const tenantAdminRoutes: RouteConfig[] = [ { path: '/tenant', - element: , + element: , }, { path: '/tenant/roles', - element: , + element: , }, { path: '/tenant/users', - element: , + element: , }, { path: '/tenant/modules', - element: , + element: , }, { path: '/tenant/audit-logs', - element: , + element: , }, { path: '/tenant/settings', - element: , + element: , }, ]; diff --git a/vite.config.ts b/vite.config.ts index d278b27..54e8554 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,5 +11,46 @@ export default defineConfig({ "@": path.resolve(__dirname, "src"), }, }, - + build: { + rollupOptions: { + output: { + manualChunks(id) { + // Feature-based chunks - only for source files + if (!id.includes('node_modules')) { + if (id.includes('/src/pages/superadmin/')) { + return 'superadmin'; + } + if (id.includes('/src/pages/tenant/')) { + return 'tenant'; + } + return; + } + + // Vendor chunks - group React ecosystem (including Redux) together + // to avoid circular dependencies + if ( + id.includes('node_modules/react') || + id.includes('node_modules/react-dom') || + id.includes('node_modules/react-router') || + id.includes('node_modules/@reduxjs') || + id.includes('node_modules/redux') || + id.includes('node_modules/react-redux') || + id.includes('node_modules/scheduler') || + id.includes('node_modules/object-assign') + ) { + return 'react-vendor'; + } + + // UI libraries + if (id.includes('node_modules/lucide-react')) { + return 'ui-vendor'; + } + + // All other node_modules go to vendor + return 'vendor'; + }, + }, + }, + chunkSizeWarningLimit: 600, + }, })