Enhance Vite configuration for optimized chunking and build performance. Implement manual chunking for feature-based and vendor dependencies to improve loading efficiency. Update ActionDropdown, FilterDropdown, FormSelect, and MultiselectPaginatedSelect components to prevent closing on internal scroll events. Refactor DataTable and other components for improved styling and responsiveness. Introduce lazy loading for route components to enhance application performance.

This commit is contained in:
Yashwin 2026-02-03 12:31:50 +05:30
parent 55b0d9c8c1
commit 41565c4c53
23 changed files with 499 additions and 326 deletions

View File

@ -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]);

View File

@ -30,8 +30,8 @@ export const DataTable = <T,>({
// Loading State
if (isLoading) {
return (
<div className="p-8 text-center">
<p className="text-sm text-[#6b7280]">Loading...</p>
<div className="p-4 md:p-6 lg:p-8 text-center">
<p className="text-xs md:text-sm text-[#6b7280]">Loading...</p>
</div>
);
}
@ -39,8 +39,8 @@ export const DataTable = <T,>({
// Error State
if (error) {
return (
<div className="p-8 text-center">
<p className="text-sm text-[#ef4444]">{error}</p>
<div className="p-4 md:p-6 lg:p-8 text-center">
<p className="text-xs md:text-sm text-[#ef4444]">{error}</p>
</div>
);
}
@ -50,7 +50,52 @@ export const DataTable = <T,>({
return (
<>
{/* Desktop Table Empty State */}
<div className="hidden md:block overflow-x-auto">
<div className="hidden md:block overflow-x-auto -mx-2 md:mx-0">
<div className="inline-block min-w-full align-middle">
<table className="w-full">
<thead>
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]">
{columns.map((column) => {
const alignClass =
column.align === 'right'
? 'text-right'
: column.align === 'center'
? 'text-center'
: 'text-left';
return (
<th
key={column.key}
className={`px-3 md:px-4 lg:px-5 py-2 md:py-2.5 lg:py-3 ${alignClass} text-[10px] md:text-xs font-medium text-[#9aa6b2] uppercase`}
>
{column.label}
</th>
);
})}
</tr>
</thead>
<tbody>
<tr>
<td colSpan={columns.length} className="px-3 md:px-4 lg:px-5 py-6 md:py-7 lg:py-8 text-center text-xs md:text-sm text-[#6b7280]">
{emptyMessage}
</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Mobile Empty State */}
<div className="md:hidden p-4 text-center">
<p className="text-xs text-[#6b7280]">{emptyMessage}</p>
</div>
</>
);
}
return (
<>
{/* Desktop Table */}
<div className="hidden md:block overflow-x-auto -mx-2 md:mx-0">
<div className="inline-block min-w-full align-middle">
<table className="w-full">
<thead>
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]">
@ -64,7 +109,7 @@ export const DataTable = <T,>({
return (
<th
key={column.key}
className={`px-5 py-3 ${alignClass} text-xs font-medium text-[#9aa6b2] uppercase`}
className={`px-3 md:px-4 lg:px-5 py-2 md:py-2.5 lg:py-3 ${alignClass} text-[10px] md:text-xs font-medium text-[#9aa6b2] uppercase`}
>
{column.label}
</th>
@ -73,70 +118,29 @@ export const DataTable = <T,>({
</tr>
</thead>
<tbody>
<tr>
<td colSpan={columns.length} className="px-5 py-8 text-center text-sm text-[#6b7280]">
{emptyMessage}
</td>
</tr>
{data.map((item) => (
<tr
key={keyExtractor(item)}
className="border-b border-[rgba(0,0,0,0.08)] hover:bg-gray-50 transition-colors"
>
{columns.map((column) => {
const alignClass =
column.align === 'right'
? 'text-right'
: column.align === 'center'
? 'text-center'
: 'text-left';
return (
<td key={column.key} className={`px-3 md:px-4 lg:px-5 py-2.5 md:py-3 lg:py-4 ${alignClass} text-xs md:text-sm`}>
{column.render ? column.render(item) : String((item as any)[column.key])}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Empty State */}
<div className="md:hidden p-8 text-center">
<p className="text-sm text-[#6b7280]">{emptyMessage}</p>
</div>
</>
);
}
return (
<>
{/* Desktop Table */}
<div className="hidden md:block overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]">
{columns.map((column) => {
const alignClass =
column.align === 'right'
? 'text-right'
: column.align === 'center'
? 'text-center'
: 'text-left';
return (
<th
key={column.key}
className={`px-5 py-3 ${alignClass} text-xs font-medium text-[#9aa6b2] uppercase`}
>
{column.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{data.map((item) => (
<tr
key={keyExtractor(item)}
className="border-b border-[rgba(0,0,0,0.08)] hover:bg-gray-50 transition-colors"
>
{columns.map((column) => {
const alignClass =
column.align === 'right'
? 'text-right'
: column.align === 'center'
? 'text-center'
: 'text-left';
return (
<td key={column.key} className={`px-5 py-4 ${alignClass}`}>
{column.render ? column.render(item) : String((item as any)[column.key])}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Card View */}
@ -144,13 +148,13 @@ export const DataTable = <T,>({
{mobileCardRenderer
? data.map((item) => <div key={keyExtractor(item)}>{mobileCardRenderer(item)}</div>)
: data.map((item) => (
<div key={keyExtractor(item)} className="p-4">
<div key={keyExtractor(item)} className="p-3 sm:p-4">
{columns.map((column) => (
<div key={column.key} className="mb-3 last:mb-0">
<span className="text-xs text-[#9aa6b2] mb-1 block">
<div key={column.key} className="mb-2.5 sm:mb-3 last:mb-0">
<span className="text-[10px] sm:text-xs text-[#9aa6b2] mb-0.5 sm:mb-1 block">
{column.mobileLabel || column.label}:
</span>
<div className="text-sm text-[#0f1724]">
<div className="text-xs sm:text-sm text-[#0f1724]">
{column.render ? column.render(item) : String((item as any)[column.key])}
</div>
</div>

View File

@ -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 */}
<MultiselectPaginatedSelect
<MultiselectPaginatedSelect
label="Available Modules"
placeholder="Select available modules"
value={selectedAvailableModules}
onValueChange={(values) => {
onValueChange={(values) => {
setSelectedAvailableModules(values);
setValue('modules', values.length > 0 ? values : []);
}}
}}
onLoadOptions={loadAvailableModules}
initialOptions={initialAvailableModuleOptions}
error={errors.modules?.message}
/>
/>
{/* Permissions Section */}
<div className="pb-4">

View File

@ -31,6 +31,7 @@ export const FilterDropdown = ({
const [isOpen, setIsOpen] = useState<boolean>(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const dropdownMenuRef = useRef<HTMLDivElement>(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(
<div
ref={dropdownMenuRef}
data-filter-dropdown="true"
className="fixed bg-white border border-[rgba(0,0,0,0.2)] rounded-md shadow-lg z-[250] max-h-60 overflow-y-auto"
style={dropdownStyle}

View File

@ -51,8 +51,18 @@ export const FormSelect = ({
}
};
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();
const spaceBelow = window.innerHeight - rect.bottom;
@ -87,6 +97,7 @@ export const FormSelect = ({
return () => {
document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('scroll', handleScroll, true);
};
}, [isOpen]);

View File

@ -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]);

View File

@ -369,17 +369,17 @@ export const NewRoleModal = ({
/>
{/* Available Modules Selection */}
<MultiselectPaginatedSelect
<MultiselectPaginatedSelect
label="Available Modules"
placeholder="Select available modules"
value={selectedAvailableModules}
onValueChange={(values) => {
onValueChange={(values) => {
setSelectedAvailableModules(values);
setValue('modules', values.length > 0 ? values : []);
}}
}}
onLoadOptions={loadAvailableModules}
error={errors.modules?.message}
/>
/>
{/* Permissions Section */}
<div className="pb-4">

View File

@ -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 = ({
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4 md:gap-6 mb-6">
{/* Title and Description */}
<div className="flex flex-col gap-1 max-w-full md:max-w-[434px]">
<h1 className="text-2xl md:text-3xl font-bold text-[#0f1724] tracking-[-0.48px]">
<h1 className="text-xl md:text-2xl font-bold text-[#0f1724] tracking-[-0.48px]">
{title}
</h1>
{description && (
<p className="text-sm md:text-base font-normal text-[#6b7280]">
<p className="text-sm font-normal text-[#6b7280]">
{description}
</p>
)}

View File

@ -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]);

View File

@ -230,7 +230,7 @@ export const NewModuleModal = ({
</>
}
>
<form onSubmit={handleSubmit(handleFormSubmit)} className="p-5 max-h-[70vh] overflow-y-auto">
<form onSubmit={handleSubmit(handleFormSubmit)} className="p-5">
{/* API Key Display Section */}
{apiKey && (
<div className="mb-6 p-4 bg-[rgba(34,197,94,0.1)] border border-[#22c55e] rounded-md">

View File

@ -324,7 +324,7 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol
isLoading={isLoading}
error={error}
/>
{pagination.totalPages > 1 && (
{pagination.totalPages > 0 && (
<Pagination
currentPage={currentPage}
totalPages={pagination.totalPages}

View File

@ -239,6 +239,11 @@ export const UsersTable = ({ tenantId, showHeader = true, compact = false }: Use
label: 'Email',
render: (user) => <span className="text-sm font-normal text-[#0f1724]">{user.email}</span>,
},
{
key: 'role',
label: 'role',
render: (user) => <span className="text-sm font-normal text-[#0f1724]">{user.role?.name}</span>,
},
{
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 && (
<Pagination
currentPage={currentPage}
totalPages={pagination.totalPages}
@ -529,7 +534,7 @@ export const UsersTable = ({ tenantId, showHeader = true, compact = false }: Use
}}
onLimitChange={(newLimit: number) => {
setLimit(newLimit);
setCurrentPage(1);
setCurrentPage(1); // Reset to first page when limit changes
}}
/>
)}

View File

@ -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<Tenant>;
}
// interface ViewTenantModalProps {
// isOpen: boolean;
// onClose: () => void;
// tenantId: string | null;
// onLoadTenant: (id: string) => Promise<Tenant>;
// }
// 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<Tenant | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
// export const ViewTenantModal = ({
// isOpen,
// onClose,
// tenantId,
// onLoadTenant,
// }: ViewTenantModalProps): ReactElement | null => {
// const [tenant, setTenant] = useState<Tenant | null>(null);
// const [isLoading, setIsLoading] = useState<boolean>(false);
// const [error, setError] = useState<string | null>(null);
// Load tenant data when modal opens
useEffect(() => {
if (isOpen && tenantId) {
const loadTenant = async (): Promise<void> => {
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<void> => {
// 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 (
<Modal
isOpen={isOpen}
onClose={onClose}
title="View Tenant Details"
description="View tenant information"
maxWidth="lg"
footer={
<SecondaryButton type="button" onClick={onClose} className="px-4 py-2.5 text-sm">
Close
</SecondaryButton>
}
>
<div className="p-5">
{isLoading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
</div>
)}
// return (
// <Modal
// isOpen={isOpen}
// onClose={onClose}
// title="View Tenant Details"
// description="View tenant information"
// maxWidth="lg"
// footer={
// <SecondaryButton type="button" onClick={onClose} className="px-4 py-2.5 text-sm">
// Close
// </SecondaryButton>
// }
// >
// <div className="p-5">
// {isLoading && (
// <div className="flex items-center justify-center py-12">
// <Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
// </div>
// )}
{error && (
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
<p className="text-sm text-[#ef4444]">{error}</p>
</div>
)}
// {error && (
// <div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
// <p className="text-sm text-[#ef4444]">{error}</p>
// </div>
// )}
{!isLoading && !error && tenant && (
<div className="flex flex-col gap-6">
{/* Basic Information */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-[#0e1b2a]">Basic Information</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Tenant Name</label>
<p className="text-sm text-[#0e1b2a]">{tenant.name}</p>
</div>
<div>
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Slug</label>
<p className="text-sm text-[#0e1b2a]">{tenant.slug}</p>
</div>
<div>
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Status</label>
<div className="mt-1">
<StatusBadge variant={getStatusVariant(tenant.status)}>
{tenant.status}
</StatusBadge>
</div>
</div>
</div>
</div>
// {!isLoading && !error && tenant && (
// <div className="flex flex-col gap-6">
// {/* Basic Information */}
// <div className="flex flex-col gap-4">
// <h3 className="text-sm font-semibold text-[#0e1b2a]">Basic Information</h3>
// <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
// <div>
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Tenant Name</label>
// <p className="text-sm text-[#0e1b2a]">{tenant.name}</p>
// </div>
// <div>
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Slug</label>
// <p className="text-sm text-[#0e1b2a]">{tenant.slug}</p>
// </div>
// <div>
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Status</label>
// <div className="mt-1">
// <StatusBadge variant={getStatusVariant(tenant.status)}>
// {tenant.status}
// </StatusBadge>
// </div>
// </div>
// </div>
// </div>
{/* Settings */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-[#0e1b2a]">Settings</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Timezone</label>
<p className="text-sm text-[#0e1b2a]">
{tenant.settings?.timezone || 'N/A'}
</p>
</div>
<div>
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Subscription Tier</label>
<p className="text-sm text-[#0e1b2a]">
{tenant.subscription_tier ? tenant.subscription_tier.charAt(0).toUpperCase() + tenant.subscription_tier.slice(1) : 'N/A'}
</p>
</div>
<div>
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Max Users</label>
<p className="text-sm text-[#0e1b2a]">{tenant.max_users ?? 'N/A'}</p>
</div>
<div>
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Max Modules</label>
<p className="text-sm text-[#0e1b2a]">{tenant.max_modules ?? 'N/A'}</p>
</div>
</div>
</div>
// {/* Settings */}
// <div className="flex flex-col gap-4">
// <h3 className="text-sm font-semibold text-[#0e1b2a]">Settings</h3>
// <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
// <div>
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Timezone</label>
// <p className="text-sm text-[#0e1b2a]">
// {tenant.settings?.timezone || 'N/A'}
// </p>
// </div>
// <div>
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Subscription Tier</label>
// <p className="text-sm text-[#0e1b2a]">
// {tenant.subscription_tier ? tenant.subscription_tier.charAt(0).toUpperCase() + tenant.subscription_tier.slice(1) : 'N/A'}
// </p>
// </div>
// <div>
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Max Users</label>
// <p className="text-sm text-[#0e1b2a]">{tenant.max_users ?? 'N/A'}</p>
// </div>
// <div>
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Max Modules</label>
// <p className="text-sm text-[#0e1b2a]">{tenant.max_modules ?? 'N/A'}</p>
// </div>
// </div>
// </div>
{/* Timestamps */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-[#0e1b2a]">Timestamps</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Created At</label>
<p className="text-sm text-[#0e1b2a]">{formatDate(tenant.created_at)}</p>
</div>
<div>
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Updated At</label>
<p className="text-sm text-[#0e1b2a]">{formatDate(tenant.updated_at)}</p>
</div>
</div>
</div>
</div>
)}
</div>
</Modal>
);
};
// {/* Timestamps */}
// <div className="flex flex-col gap-4">
// <h3 className="text-sm font-semibold text-[#0e1b2a]">Timestamps</h3>
// <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
// <div>
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Created At</label>
// <p className="text-sm text-[#0e1b2a]">{formatDate(tenant.created_at)}</p>
// </div>
// <div>
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Updated At</label>
// <p className="text-sm text-[#0e1b2a]">{formatDate(tenant.updated_at)}</p>
// </div>
// </div>
// </div>
// </div>
// )}
// </div>
// </Modal>
// );
// };

View File

@ -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';

View File

@ -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') },
];

View File

@ -206,14 +206,14 @@ const TenantDetails = (): ReactElement => {
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<h1 className="text-xl md:text-2xl font-bold text-[#0f1724] truncate">
<h1 className="text-xl md:text-2xl font-bold text-[#0f1724] tracking-[-0.48px] truncate">
{tenant.name}
</h1>
<StatusBadge variant={getStatusVariant(tenant.status)}>
{tenant.status}
</StatusBadge>
</div>
<div className="flex flex-wrap items-center gap-4 md:gap-6 text-sm text-[#6b7280]">
<div className="flex flex-wrap items-center gap-4 md:gap-6 text-sm font-normal text-[#6b7280]">
<div className="flex items-center gap-1.5">
<Hash className="w-4 h-4" />
<span className="truncate">{tenant.slug}</span>

View File

@ -352,14 +352,14 @@ const Roles = (): ReactElement => {
{/* New Role Button */}
{canCreate('roles') && (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => setIsModalOpen(true)}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New Role</span>
</PrimaryButton>
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => setIsModalOpen(true)}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New Role</span>
</PrimaryButton>
)}
</div>
</div>

View File

@ -234,6 +234,11 @@ const Users = (): ReactElement => {
label: 'Email',
render: (user) => <span className="text-sm font-normal text-[#0f1724]">{user.email}</span>,
},
{
key: 'role',
label: 'role',
render: (user) => <span className="text-sm font-normal text-[#0f1724]">{user.role?.name}</span>,
},
{
key: 'status',
label: 'Status',
@ -385,14 +390,14 @@ const Users = (): ReactElement => {
{/* New User Button */}
{canCreate('users') && (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => setIsModalOpen(true)}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New User</span>
</PrimaryButton>
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => setIsModalOpen(true)}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New User</span>
</PrimaryButton>
)}
</div>
</div>

View File

@ -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 => (
<div className="flex items-center justify-center min-h-screen">
<div className="text-sm text-[#6b7280]">Loading...</div>
</div>
);
// App Routes Component
export const AppRoutes = (): ReactElement => {
return (
@ -42,7 +52,9 @@ export const AppRoutes = (): ReactElement => {
path="*"
element={
<ProtectedRoute>
<NotFound />
<Suspense fallback={<RouteLoader />}>
<NotFound />
</Suspense>
</ProtectedRoute>
}
/>

View File

@ -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 => (
<div className="flex items-center justify-center min-h-screen">
<div className="text-sm text-[#6b7280]">Loading...</div>
</div>
);
// Wrapper component with Suspense
const LazyRoute = ({ component: Component }: { component: React.ComponentType }): ReactElement => (
<Suspense fallback={<RouteLoader />}>
<Component />
</Suspense>
);
export interface RouteConfig {
path: string;
element: ReactElement;
@ -13,26 +30,26 @@ export interface RouteConfig {
export const publicRoutes: RouteConfig[] = [
{
path: '/',
element: <Login />,
element: <LazyRoute component={Login} />,
},
{
path: '/forgot-password',
element: <ForgotPassword />,
element: <LazyRoute component={ForgotPassword} />,
},
{
path: '/reset-password',
element: <ResetPassword />,
element: <LazyRoute component={ResetPassword} />,
},
{
path: '/tenant/login',
element: <TenantLogin />,
element: <LazyRoute component={TenantLogin} />,
},
{
path: '/tenant/forgot-password',
element: <ForgotPassword />,
element: <LazyRoute component={ForgotPassword} />,
},
{
path: '/tenant/reset-password',
element: <ResetPassword />,
element: <LazyRoute component={ResetPassword} />,
},
];

View File

@ -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 => (
<div className="flex items-center justify-center min-h-screen">
<div className="text-sm text-[#6b7280]">Loading...</div>
</div>
);
// Wrapper component with Suspense
const LazyRoute = ({ component: Component }: { component: React.ComponentType }): ReactElement => (
<Suspense fallback={<RouteLoader />}>
<Component />
</Suspense>
);
export interface RouteConfig {
path: string;
element: ReactElement;
@ -18,38 +33,30 @@ export interface RouteConfig {
export const superAdminRoutes: RouteConfig[] = [
{
path: '/dashboard',
element: <Dashboard />,
element: <LazyRoute component={Dashboard} />,
},
{
path: '/tenants',
element: <Tenants />,
element: <LazyRoute component={Tenants} />,
},
{
path: '/tenants/create-wizard',
element: <CreateTenantWizard />,
element: <LazyRoute component={CreateTenantWizard} />,
},
{
path: '/tenants/:id/edit',
element: <EditTenant />,
element: <LazyRoute component={EditTenant} />,
},
{
path: '/tenants/:id',
element: <TenantDetails />,
element: <LazyRoute component={TenantDetails} />,
},
// {
// path: '/users',
// element: <Users />,
// },
// {
// path: '/roles',
// element: <Roles />,
// },
{
path: '/modules',
element: <Modules />,
element: <LazyRoute component={Modules} />,
},
{
path: '/audit-logs',
element: <AuditLogs />,
element: <LazyRoute component={AuditLogs} />,
},
];

View File

@ -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 => (
<div className="flex items-center justify-center min-h-screen">
<div className="text-sm text-[#6b7280]">Loading...</div>
</div>
);
// Wrapper component with Suspense
const LazyRoute = ({ component: Component }: { component: React.ComponentType }): ReactElement => (
<Suspense fallback={<RouteLoader />}>
<Component />
</Suspense>
);
export interface RouteConfig {
path: string;
@ -15,26 +32,26 @@ export interface RouteConfig {
export const tenantAdminRoutes: RouteConfig[] = [
{
path: '/tenant',
element: <Dashboard />,
element: <LazyRoute component={Dashboard} />,
},
{
path: '/tenant/roles',
element: <Roles />,
element: <LazyRoute component={Roles} />,
},
{
path: '/tenant/users',
element: <Users />,
element: <LazyRoute component={Users} />,
},
{
path: '/tenant/modules',
element: <Modules />,
element: <LazyRoute component={Modules} />,
},
{
path: '/tenant/audit-logs',
element: <AuditLogs />,
element: <LazyRoute component={AuditLogs} />,
},
{
path: '/tenant/settings',
element: <Settings />,
element: <LazyRoute component={Settings} />,
},
];

View File

@ -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,
},
})