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:
parent
55b0d9c8c1
commit
41565c4c53
@ -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) {
|
if (isOpen && buttonRef.current) {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
window.addEventListener('scroll', handleScroll, true);
|
||||||
|
|
||||||
// Calculate position when dropdown opens
|
// Calculate position when dropdown opens
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
@ -72,6 +82,7 @@ export const ActionDropdown = ({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
window.removeEventListener('scroll', handleScroll, true);
|
||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
|||||||
@ -30,8 +30,8 @@ export const DataTable = <T,>({
|
|||||||
// Loading State
|
// Loading State
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8 text-center">
|
<div className="p-4 md:p-6 lg:p-8 text-center">
|
||||||
<p className="text-sm text-[#6b7280]">Loading...</p>
|
<p className="text-xs md:text-sm text-[#6b7280]">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -39,8 +39,8 @@ export const DataTable = <T,>({
|
|||||||
// Error State
|
// Error State
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8 text-center">
|
<div className="p-4 md:p-6 lg:p-8 text-center">
|
||||||
<p className="text-sm text-[#ef4444]">{error}</p>
|
<p className="text-xs md:text-sm text-[#ef4444]">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -50,7 +50,52 @@ export const DataTable = <T,>({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Desktop Table Empty State */}
|
{/* 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">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]">
|
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]">
|
||||||
@ -64,7 +109,7 @@ export const DataTable = <T,>({
|
|||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
key={column.key}
|
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}
|
{column.label}
|
||||||
</th>
|
</th>
|
||||||
@ -73,70 +118,29 @@ export const DataTable = <T,>({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
{data.map((item) => (
|
||||||
<td colSpan={columns.length} className="px-5 py-8 text-center text-sm text-[#6b7280]">
|
<tr
|
||||||
{emptyMessage}
|
key={keyExtractor(item)}
|
||||||
</td>
|
className="border-b border-[rgba(0,0,0,0.08)] hover:bg-gray-50 transition-colors"
|
||||||
</tr>
|
>
|
||||||
|
{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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Card View */}
|
{/* Mobile Card View */}
|
||||||
@ -144,13 +148,13 @@ export const DataTable = <T,>({
|
|||||||
{mobileCardRenderer
|
{mobileCardRenderer
|
||||||
? data.map((item) => <div key={keyExtractor(item)}>{mobileCardRenderer(item)}</div>)
|
? data.map((item) => <div key={keyExtractor(item)}>{mobileCardRenderer(item)}</div>)
|
||||||
: data.map((item) => (
|
: data.map((item) => (
|
||||||
<div key={keyExtractor(item)} className="p-4">
|
<div key={keyExtractor(item)} className="p-3 sm:p-4">
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<div key={column.key} className="mb-3 last:mb-0">
|
<div key={column.key} className="mb-2.5 sm:mb-3 last:mb-0">
|
||||||
<span className="text-xs text-[#9aa6b2] mb-1 block">
|
<span className="text-[10px] sm:text-xs text-[#9aa6b2] mb-0.5 sm:mb-1 block">
|
||||||
{column.mobileLabel || column.label}:
|
{column.mobileLabel || column.label}:
|
||||||
</span>
|
</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])}
|
{column.render ? column.render(item) : String((item as any)[column.key])}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -310,17 +310,17 @@ export const EditRoleModal = ({
|
|||||||
|
|
||||||
// Map role modules to options from available modules
|
// Map role modules to options from available modules
|
||||||
const moduleOptions = roleModules
|
const moduleOptions = roleModules
|
||||||
.map((moduleId: string) => {
|
.map((moduleId: string) => {
|
||||||
const module = availableModulesResponse.data.find((m) => m.id === moduleId);
|
const module = availableModulesResponse.data.find((m) => m.id === moduleId);
|
||||||
if (module) {
|
if (module) {
|
||||||
return {
|
return {
|
||||||
value: moduleId,
|
value: moduleId,
|
||||||
label: module.name,
|
label: module.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.filter((opt) => opt !== null) as Array<{ value: string; label: string }>;
|
.filter((opt) => opt !== null) as Array<{ value: string; label: string }>;
|
||||||
|
|
||||||
setInitialAvailableModuleOptions(moduleOptions);
|
setInitialAvailableModuleOptions(moduleOptions);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -527,18 +527,18 @@ export const EditRoleModal = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Available Modules Selection */}
|
{/* Available Modules Selection */}
|
||||||
<MultiselectPaginatedSelect
|
<MultiselectPaginatedSelect
|
||||||
label="Available Modules"
|
label="Available Modules"
|
||||||
placeholder="Select available modules"
|
placeholder="Select available modules"
|
||||||
value={selectedAvailableModules}
|
value={selectedAvailableModules}
|
||||||
onValueChange={(values) => {
|
onValueChange={(values) => {
|
||||||
setSelectedAvailableModules(values);
|
setSelectedAvailableModules(values);
|
||||||
setValue('modules', values.length > 0 ? values : []);
|
setValue('modules', values.length > 0 ? values : []);
|
||||||
}}
|
}}
|
||||||
onLoadOptions={loadAvailableModules}
|
onLoadOptions={loadAvailableModules}
|
||||||
initialOptions={initialAvailableModuleOptions}
|
initialOptions={initialAvailableModuleOptions}
|
||||||
error={errors.modules?.message}
|
error={errors.modules?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Permissions Section */}
|
{/* Permissions Section */}
|
||||||
<div className="pb-4">
|
<div className="pb-4">
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export const FilterDropdown = ({
|
|||||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const dropdownMenuRef = useRef<HTMLDivElement>(null);
|
||||||
const [dropdownStyle, setDropdownStyle] = useState<{
|
const [dropdownStyle, setDropdownStyle] = useState<{
|
||||||
top?: string;
|
top?: string;
|
||||||
bottom?: 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) {
|
if (isOpen) {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
window.addEventListener('scroll', handleScroll, true);
|
||||||
if (buttonRef.current) {
|
if (buttonRef.current) {
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
const spaceBelow = window.innerHeight - rect.bottom;
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
@ -86,6 +97,7 @@ export const FilterDropdown = ({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
window.removeEventListener('scroll', handleScroll, true);
|
||||||
};
|
};
|
||||||
}, [isOpen, options.length]);
|
}, [isOpen, options.length]);
|
||||||
|
|
||||||
@ -122,6 +134,7 @@ export const FilterDropdown = ({
|
|||||||
buttonRef.current &&
|
buttonRef.current &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<div
|
||||||
|
ref={dropdownMenuRef}
|
||||||
data-filter-dropdown="true"
|
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"
|
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}
|
style={dropdownStyle}
|
||||||
|
|||||||
@ -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) {
|
if (isOpen && buttonRef.current) {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
window.addEventListener('scroll', handleScroll, true);
|
||||||
// Calculate position when dropdown opens
|
// Calculate position when dropdown opens
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
const spaceBelow = window.innerHeight - rect.bottom;
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
@ -87,6 +97,7 @@ export const FormSelect = ({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
window.removeEventListener('scroll', handleScroll, true);
|
||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
if (isOpen && buttonRef.current) {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
window.addEventListener('scroll', handleScroll, true);
|
||||||
|
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
const spaceBelow = window.innerHeight - rect.bottom;
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
@ -173,6 +187,7 @@ export const MultiselectPaginatedSelect = ({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
window.removeEventListener('scroll', handleScroll, true);
|
||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
|||||||
@ -369,17 +369,17 @@ export const NewRoleModal = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Available Modules Selection */}
|
{/* Available Modules Selection */}
|
||||||
<MultiselectPaginatedSelect
|
<MultiselectPaginatedSelect
|
||||||
label="Available Modules"
|
label="Available Modules"
|
||||||
placeholder="Select available modules"
|
placeholder="Select available modules"
|
||||||
value={selectedAvailableModules}
|
value={selectedAvailableModules}
|
||||||
onValueChange={(values) => {
|
onValueChange={(values) => {
|
||||||
setSelectedAvailableModules(values);
|
setSelectedAvailableModules(values);
|
||||||
setValue('modules', values.length > 0 ? values : []);
|
setValue('modules', values.length > 0 ? values : []);
|
||||||
}}
|
}}
|
||||||
onLoadOptions={loadAvailableModules}
|
onLoadOptions={loadAvailableModules}
|
||||||
error={errors.modules?.message}
|
error={errors.modules?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Permissions Section */}
|
{/* Permissions Section */}
|
||||||
<div className="pb-4">
|
<div className="pb-4">
|
||||||
|
|||||||
@ -18,8 +18,8 @@ interface PageHeaderProps {
|
|||||||
const defaultTabs: TabItem[] = [
|
const defaultTabs: TabItem[] = [
|
||||||
{ label: 'Overview', path: '/dashboard' },
|
{ label: 'Overview', path: '/dashboard' },
|
||||||
{ label: 'Tenants', path: '/tenants' },
|
{ label: 'Tenants', path: '/tenants' },
|
||||||
{ label: 'Users', path: '/users' },
|
// { label: 'Users', path: '/users' },
|
||||||
{ label: 'Roles', path: '/roles' },
|
// { label: 'Roles', path: '/roles' },
|
||||||
{ label: 'Modules', path: '/modules' },
|
{ label: 'Modules', path: '/modules' },
|
||||||
{ label: 'Audit Logs', path: '/audit-logs' },
|
{ 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">
|
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4 md:gap-6 mb-6">
|
||||||
{/* Title and Description */}
|
{/* Title and Description */}
|
||||||
<div className="flex flex-col gap-1 max-w-full md:max-w-[434px]">
|
<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}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
{description && (
|
{description && (
|
||||||
<p className="text-sm md:text-base font-normal text-[#6b7280]">
|
<p className="text-sm font-normal text-[#6b7280]">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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) {
|
if (isOpen && buttonRef.current) {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
window.addEventListener('scroll', handleScroll, true);
|
||||||
|
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
const spaceBelow = window.innerHeight - rect.bottom;
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
@ -178,6 +192,7 @@ export const PaginatedSelect = ({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
window.removeEventListener('scroll', handleScroll, true);
|
||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
|||||||
@ -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 */}
|
{/* API Key Display Section */}
|
||||||
{apiKey && (
|
{apiKey && (
|
||||||
<div className="mb-6 p-4 bg-[rgba(34,197,94,0.1)] border border-[#22c55e] rounded-md">
|
<div className="mb-6 p-4 bg-[rgba(34,197,94,0.1)] border border-[#22c55e] rounded-md">
|
||||||
|
|||||||
@ -324,7 +324,7 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
{pagination.totalPages > 1 && (
|
{pagination.totalPages > 0 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={pagination.totalPages}
|
totalPages={pagination.totalPages}
|
||||||
|
|||||||
@ -239,6 +239,11 @@ export const UsersTable = ({ tenantId, showHeader = true, compact = false }: Use
|
|||||||
label: 'Email',
|
label: 'Email',
|
||||||
render: (user) => <span className="text-sm font-normal text-[#0f1724]">{user.email}</span>,
|
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',
|
key: 'status',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
@ -361,7 +366,7 @@ export const UsersTable = ({ tenantId, showHeader = true, compact = false }: Use
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
{pagination.totalPages > 1 && (
|
{pagination.totalPages > 0 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={pagination.totalPages}
|
totalPages={pagination.totalPages}
|
||||||
@ -529,7 +534,7 @@ export const UsersTable = ({ tenantId, showHeader = true, compact = false }: Use
|
|||||||
}}
|
}}
|
||||||
onLimitChange={(newLimit: number) => {
|
onLimitChange={(newLimit: number) => {
|
||||||
setLimit(newLimit);
|
setLimit(newLimit);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1); // Reset to first page when limit changes
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,169 +1,169 @@
|
|||||||
import { useEffect, useState } from 'react';
|
// import { useEffect, useState } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
// import type { ReactElement } from 'react';
|
||||||
import { Loader2 } from 'lucide-react';
|
// import { Loader2 } from 'lucide-react';
|
||||||
import { Modal, SecondaryButton, StatusBadge } from '@/components/shared';
|
// import { Modal, SecondaryButton, StatusBadge } from '@/components/shared';
|
||||||
import type { Tenant } from '@/types/tenant';
|
// import type { Tenant } from '@/types/tenant';
|
||||||
|
|
||||||
interface ViewTenantModalProps {
|
// interface ViewTenantModalProps {
|
||||||
isOpen: boolean;
|
// isOpen: boolean;
|
||||||
onClose: () => void;
|
// onClose: () => void;
|
||||||
tenantId: string | null;
|
// tenantId: string | null;
|
||||||
onLoadTenant: (id: string) => Promise<Tenant>;
|
// onLoadTenant: (id: string) => Promise<Tenant>;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Helper function to get status badge variant
|
// // Helper function to get status badge variant
|
||||||
const getStatusVariant = (status: string): 'success' | 'failure' | 'process' => {
|
// const getStatusVariant = (status: string): 'success' | 'failure' | 'process' => {
|
||||||
switch (status.toLowerCase()) {
|
// switch (status.toLowerCase()) {
|
||||||
case 'active':
|
// case 'active':
|
||||||
return 'success';
|
// return 'success';
|
||||||
case 'deleted':
|
// case 'deleted':
|
||||||
return 'failure';
|
// return 'failure';
|
||||||
case 'suspended':
|
// case 'suspended':
|
||||||
return 'process';
|
// return 'process';
|
||||||
default:
|
// default:
|
||||||
return 'success';
|
// return 'success';
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
// Helper function to format date
|
// // Helper function to format date
|
||||||
const formatDate = (dateString: string): string => {
|
// const formatDate = (dateString: string): string => {
|
||||||
const date = new Date(dateString);
|
// const date = new Date(dateString);
|
||||||
return date.toLocaleDateString('en-US', {
|
// return date.toLocaleDateString('en-US', {
|
||||||
month: 'short',
|
// month: 'short',
|
||||||
day: 'numeric',
|
// day: 'numeric',
|
||||||
year: 'numeric',
|
// year: 'numeric',
|
||||||
hour: '2-digit',
|
// hour: '2-digit',
|
||||||
minute: '2-digit',
|
// minute: '2-digit',
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
export const ViewTenantModal = ({
|
// export const ViewTenantModal = ({
|
||||||
isOpen,
|
// isOpen,
|
||||||
onClose,
|
// onClose,
|
||||||
tenantId,
|
// tenantId,
|
||||||
onLoadTenant,
|
// onLoadTenant,
|
||||||
}: ViewTenantModalProps): ReactElement | null => {
|
// }: ViewTenantModalProps): ReactElement | null => {
|
||||||
const [tenant, setTenant] = useState<Tenant | null>(null);
|
// const [tenant, setTenant] = useState<Tenant | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
// const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
// const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Load tenant data when modal opens
|
// // Load tenant data when modal opens
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (isOpen && tenantId) {
|
// if (isOpen && tenantId) {
|
||||||
const loadTenant = async (): Promise<void> => {
|
// const loadTenant = async (): Promise<void> => {
|
||||||
try {
|
// try {
|
||||||
setIsLoading(true);
|
// setIsLoading(true);
|
||||||
setError(null);
|
// setError(null);
|
||||||
const data = await onLoadTenant(tenantId);
|
// const data = await onLoadTenant(tenantId);
|
||||||
setTenant(data);
|
// setTenant(data);
|
||||||
} catch (err: any) {
|
// } catch (err: any) {
|
||||||
setError(err?.response?.data?.error?.message || 'Failed to load tenant details');
|
// setError(err?.response?.data?.error?.message || 'Failed to load tenant details');
|
||||||
} finally {
|
// } finally {
|
||||||
setIsLoading(false);
|
// setIsLoading(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
loadTenant();
|
// loadTenant();
|
||||||
} else {
|
// } else {
|
||||||
setTenant(null);
|
// setTenant(null);
|
||||||
setError(null);
|
// setError(null);
|
||||||
}
|
// }
|
||||||
}, [isOpen, tenantId, onLoadTenant]);
|
// }, [isOpen, tenantId, onLoadTenant]);
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<Modal
|
// <Modal
|
||||||
isOpen={isOpen}
|
// isOpen={isOpen}
|
||||||
onClose={onClose}
|
// onClose={onClose}
|
||||||
title="View Tenant Details"
|
// title="View Tenant Details"
|
||||||
description="View tenant information"
|
// description="View tenant information"
|
||||||
maxWidth="lg"
|
// maxWidth="lg"
|
||||||
footer={
|
// footer={
|
||||||
<SecondaryButton type="button" onClick={onClose} className="px-4 py-2.5 text-sm">
|
// <SecondaryButton type="button" onClick={onClose} className="px-4 py-2.5 text-sm">
|
||||||
Close
|
// Close
|
||||||
</SecondaryButton>
|
// </SecondaryButton>
|
||||||
}
|
// }
|
||||||
>
|
// >
|
||||||
<div className="p-5">
|
// <div className="p-5">
|
||||||
{isLoading && (
|
// {isLoading && (
|
||||||
<div className="flex items-center justify-center py-12">
|
// <div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
// <Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
|
|
||||||
{error && (
|
// {error && (
|
||||||
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
|
// <div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
|
||||||
<p className="text-sm text-[#ef4444]">{error}</p>
|
// <p className="text-sm text-[#ef4444]">{error}</p>
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
|
|
||||||
{!isLoading && !error && tenant && (
|
// {!isLoading && !error && tenant && (
|
||||||
<div className="flex flex-col gap-6">
|
// <div className="flex flex-col gap-6">
|
||||||
{/* Basic Information */}
|
// {/* Basic Information */}
|
||||||
<div className="flex flex-col gap-4">
|
// <div className="flex flex-col gap-4">
|
||||||
<h3 className="text-sm font-semibold text-[#0e1b2a]">Basic Information</h3>
|
// <h3 className="text-sm font-semibold text-[#0e1b2a]">Basic Information</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
// <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
// <div>
|
||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Tenant Name</label>
|
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Tenant Name</label>
|
||||||
<p className="text-sm text-[#0e1b2a]">{tenant.name}</p>
|
// <p className="text-sm text-[#0e1b2a]">{tenant.name}</p>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Slug</label>
|
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Slug</label>
|
||||||
<p className="text-sm text-[#0e1b2a]">{tenant.slug}</p>
|
// <p className="text-sm text-[#0e1b2a]">{tenant.slug}</p>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Status</label>
|
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Status</label>
|
||||||
<div className="mt-1">
|
// <div className="mt-1">
|
||||||
<StatusBadge variant={getStatusVariant(tenant.status)}>
|
// <StatusBadge variant={getStatusVariant(tenant.status)}>
|
||||||
{tenant.status}
|
// {tenant.status}
|
||||||
</StatusBadge>
|
// </StatusBadge>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
|
|
||||||
{/* Settings */}
|
// {/* Settings */}
|
||||||
<div className="flex flex-col gap-4">
|
// <div className="flex flex-col gap-4">
|
||||||
<h3 className="text-sm font-semibold text-[#0e1b2a]">Settings</h3>
|
// <h3 className="text-sm font-semibold text-[#0e1b2a]">Settings</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
// <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
// <div>
|
||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Timezone</label>
|
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Timezone</label>
|
||||||
<p className="text-sm text-[#0e1b2a]">
|
// <p className="text-sm text-[#0e1b2a]">
|
||||||
{tenant.settings?.timezone || 'N/A'}
|
// {tenant.settings?.timezone || 'N/A'}
|
||||||
</p>
|
// </p>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Subscription Tier</label>
|
// <label className="text-xs font-medium text-[#6b7280] mb-1 block">Subscription Tier</label>
|
||||||
<p className="text-sm text-[#0e1b2a]">
|
// <p className="text-sm text-[#0e1b2a]">
|
||||||
{tenant.subscription_tier ? tenant.subscription_tier.charAt(0).toUpperCase() + tenant.subscription_tier.slice(1) : 'N/A'}
|
// {tenant.subscription_tier ? tenant.subscription_tier.charAt(0).toUpperCase() + tenant.subscription_tier.slice(1) : 'N/A'}
|
||||||
</p>
|
// </p>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Max Users</label>
|
// <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>
|
// <p className="text-sm text-[#0e1b2a]">{tenant.max_users ?? 'N/A'}</p>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Max Modules</label>
|
// <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>
|
// <p className="text-sm text-[#0e1b2a]">{tenant.max_modules ?? 'N/A'}</p>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
|
|
||||||
{/* Timestamps */}
|
// {/* Timestamps */}
|
||||||
<div className="flex flex-col gap-4">
|
// <div className="flex flex-col gap-4">
|
||||||
<h3 className="text-sm font-semibold text-[#0e1b2a]">Timestamps</h3>
|
// <h3 className="text-sm font-semibold text-[#0e1b2a]">Timestamps</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
// <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
// <div>
|
||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Created At</label>
|
// <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>
|
// <p className="text-sm text-[#0e1b2a]">{formatDate(tenant.created_at)}</p>
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Updated At</label>
|
// <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>
|
// <p className="text-sm text-[#0e1b2a]">{formatDate(tenant.updated_at)}</p>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
</Modal>
|
// </Modal>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// NewTenantModal is commented out and not exported - using CreateTenantWizard instead
|
// NewTenantModal is commented out and not exported - using CreateTenantWizard instead
|
||||||
export { ViewTenantModal } from './ViewTenantModal';
|
// export { ViewTenantModal } from './ViewTenantModal';
|
||||||
// export { EditTenantModal } from './EditTenantModal';
|
// export { EditTenantModal } from './EditTenantModal';
|
||||||
export { NewModuleModal } from './NewModuleModal';
|
export { NewModuleModal } from './NewModuleModal';
|
||||||
export { ViewModuleModal } from './ViewModuleModal';
|
export { ViewModuleModal } from './ViewModuleModal';
|
||||||
|
|||||||
@ -8,8 +8,8 @@ export const QuickActions = () => {
|
|||||||
|
|
||||||
const quickActions: QuickAction[] = [
|
const quickActions: QuickAction[] = [
|
||||||
{ icon: Plus, label: 'New Tenant', onClick: () => navigate('/tenants') },
|
{ icon: Plus, label: 'New Tenant', onClick: () => navigate('/tenants') },
|
||||||
{ icon: UserPlus, label: 'Invite User', onClick: () => navigate('/users') },
|
{ icon: UserPlus, label: 'Invite User', onClick: () => navigate('/tenants') },
|
||||||
{ icon: Shield, label: 'Add Role', onClick: () => navigate('/roles') },
|
{ icon: Shield, label: 'Add Role', onClick: () => navigate('/tenants') },
|
||||||
{ icon: Settings, label: 'Config', onClick: () => console.log('Config') },
|
{ icon: Settings, label: 'Config', onClick: () => console.log('Config') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -206,14 +206,14 @@ const TenantDetails = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<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}
|
{tenant.name}
|
||||||
</h1>
|
</h1>
|
||||||
<StatusBadge variant={getStatusVariant(tenant.status)}>
|
<StatusBadge variant={getStatusVariant(tenant.status)}>
|
||||||
{tenant.status}
|
{tenant.status}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
</div>
|
</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">
|
<div className="flex items-center gap-1.5">
|
||||||
<Hash className="w-4 h-4" />
|
<Hash className="w-4 h-4" />
|
||||||
<span className="truncate">{tenant.slug}</span>
|
<span className="truncate">{tenant.slug}</span>
|
||||||
|
|||||||
@ -352,14 +352,14 @@ const Roles = (): ReactElement => {
|
|||||||
|
|
||||||
{/* New Role Button */}
|
{/* New Role Button */}
|
||||||
{canCreate('roles') && (
|
{canCreate('roles') && (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
size="default"
|
size="default"
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => setIsModalOpen(true)}
|
onClick={() => setIsModalOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus className="w-3.5 h-3.5" />
|
<Plus className="w-3.5 h-3.5" />
|
||||||
<span className="text-xs">New Role</span>
|
<span className="text-xs">New Role</span>
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -234,6 +234,11 @@ const Users = (): ReactElement => {
|
|||||||
label: 'Email',
|
label: 'Email',
|
||||||
render: (user) => <span className="text-sm font-normal text-[#0f1724]">{user.email}</span>,
|
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',
|
key: 'status',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
@ -385,14 +390,14 @@ const Users = (): ReactElement => {
|
|||||||
|
|
||||||
{/* New User Button */}
|
{/* New User Button */}
|
||||||
{canCreate('users') && (
|
{canCreate('users') && (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
size="default"
|
size="default"
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={() => setIsModalOpen(true)}
|
onClick={() => setIsModalOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus className="w-3.5 h-3.5" />
|
<Plus className="w-3.5 h-3.5" />
|
||||||
<span className="text-xs">New User</span>
|
<span className="text-xs">New User</span>
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
|
import { lazy, Suspense } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import NotFound from '@/pages/NotFound';
|
|
||||||
import ProtectedRoute from '@/pages/ProtectedRoute';
|
import ProtectedRoute from '@/pages/ProtectedRoute';
|
||||||
import TenantProtectedRoute from '@/pages/tenant/TenantProtectedRoute';
|
import TenantProtectedRoute from '@/pages/tenant/TenantProtectedRoute';
|
||||||
import { NavigationInitializer } from '@/components/NavigationInitializer';
|
import { NavigationInitializer } from '@/components/NavigationInitializer';
|
||||||
@ -8,6 +8,16 @@ import { publicRoutes } from './public-routes';
|
|||||||
import { superAdminRoutes } from './super-admin-routes';
|
import { superAdminRoutes } from './super-admin-routes';
|
||||||
import { tenantAdminRoutes } from './tenant-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
|
// App Routes Component
|
||||||
export const AppRoutes = (): ReactElement => {
|
export const AppRoutes = (): ReactElement => {
|
||||||
return (
|
return (
|
||||||
@ -42,7 +52,9 @@ export const AppRoutes = (): ReactElement => {
|
|||||||
path="*"
|
path="*"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<NotFound />
|
<Suspense fallback={<RouteLoader />}>
|
||||||
|
<NotFound />
|
||||||
|
</Suspense>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,9 +1,26 @@
|
|||||||
import Login from '@/pages/Login';
|
import { lazy, Suspense } from 'react';
|
||||||
import TenantLogin from '@/pages/tenant/TenantLogin';
|
|
||||||
import ForgotPassword from '@/pages/ForgotPassword';
|
|
||||||
import ResetPassword from '@/pages/ResetPassword';
|
|
||||||
import type { ReactElement } 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 {
|
export interface RouteConfig {
|
||||||
path: string;
|
path: string;
|
||||||
element: ReactElement;
|
element: ReactElement;
|
||||||
@ -13,26 +30,26 @@ export interface RouteConfig {
|
|||||||
export const publicRoutes: RouteConfig[] = [
|
export const publicRoutes: RouteConfig[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <Login />,
|
element: <LazyRoute component={Login} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/forgot-password',
|
path: '/forgot-password',
|
||||||
element: <ForgotPassword />,
|
element: <LazyRoute component={ForgotPassword} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/reset-password',
|
path: '/reset-password',
|
||||||
element: <ResetPassword />,
|
element: <LazyRoute component={ResetPassword} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenant/login',
|
path: '/tenant/login',
|
||||||
element: <TenantLogin />,
|
element: <LazyRoute component={TenantLogin} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenant/forgot-password',
|
path: '/tenant/forgot-password',
|
||||||
element: <ForgotPassword />,
|
element: <LazyRoute component={ForgotPassword} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenant/reset-password',
|
path: '/tenant/reset-password',
|
||||||
element: <ResetPassword />,
|
element: <LazyRoute component={ResetPassword} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,14 +1,29 @@
|
|||||||
import Dashboard from '@/pages/superadmin/Dashboard';
|
import { lazy, Suspense } from 'react';
|
||||||
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 type { ReactElement } 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 {
|
export interface RouteConfig {
|
||||||
path: string;
|
path: string;
|
||||||
element: ReactElement;
|
element: ReactElement;
|
||||||
@ -18,38 +33,30 @@ export interface RouteConfig {
|
|||||||
export const superAdminRoutes: RouteConfig[] = [
|
export const superAdminRoutes: RouteConfig[] = [
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
element: <Dashboard />,
|
element: <LazyRoute component={Dashboard} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenants',
|
path: '/tenants',
|
||||||
element: <Tenants />,
|
element: <LazyRoute component={Tenants} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenants/create-wizard',
|
path: '/tenants/create-wizard',
|
||||||
element: <CreateTenantWizard />,
|
element: <LazyRoute component={CreateTenantWizard} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenants/:id/edit',
|
path: '/tenants/:id/edit',
|
||||||
element: <EditTenant />,
|
element: <LazyRoute component={EditTenant} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenants/:id',
|
path: '/tenants/:id',
|
||||||
element: <TenantDetails />,
|
element: <LazyRoute component={TenantDetails} />,
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// path: '/users',
|
|
||||||
// element: <Users />,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: '/roles',
|
|
||||||
// element: <Roles />,
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
path: '/modules',
|
path: '/modules',
|
||||||
element: <Modules />,
|
element: <LazyRoute component={Modules} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/audit-logs',
|
path: '/audit-logs',
|
||||||
element: <AuditLogs />,
|
element: <LazyRoute component={AuditLogs} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,10 +1,27 @@
|
|||||||
import Dashboard from '@/pages/tenant/Dashboard';
|
import { lazy, Suspense } from 'react';
|
||||||
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 type { ReactElement } 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 {
|
export interface RouteConfig {
|
||||||
path: string;
|
path: string;
|
||||||
@ -15,26 +32,26 @@ export interface RouteConfig {
|
|||||||
export const tenantAdminRoutes: RouteConfig[] = [
|
export const tenantAdminRoutes: RouteConfig[] = [
|
||||||
{
|
{
|
||||||
path: '/tenant',
|
path: '/tenant',
|
||||||
element: <Dashboard />,
|
element: <LazyRoute component={Dashboard} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenant/roles',
|
path: '/tenant/roles',
|
||||||
element: <Roles />,
|
element: <LazyRoute component={Roles} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenant/users',
|
path: '/tenant/users',
|
||||||
element: <Users />,
|
element: <LazyRoute component={Users} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenant/modules',
|
path: '/tenant/modules',
|
||||||
element: <Modules />,
|
element: <LazyRoute component={Modules} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenant/audit-logs',
|
path: '/tenant/audit-logs',
|
||||||
element: <AuditLogs />,
|
element: <LazyRoute component={AuditLogs} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tenant/settings',
|
path: '/tenant/settings',
|
||||||
element: <Settings />,
|
element: <LazyRoute component={Settings} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -11,5 +11,46 @@ export default defineConfig({
|
|||||||
"@": path.resolve(__dirname, "src"),
|
"@": 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,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user