373 lines
13 KiB
TypeScript
373 lines
13 KiB
TypeScript
import { Link, useLocation } from 'react-router-dom';
|
|
import {
|
|
LayoutDashboard,
|
|
Building2,
|
|
Users,
|
|
Package,
|
|
FileText,
|
|
Settings,
|
|
HelpCircle,
|
|
X,
|
|
Shield
|
|
} from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { useAppSelector } from '@/hooks/redux-hooks';
|
|
import { useTenantTheme } from '@/hooks/useTenantTheme';
|
|
|
|
interface MenuItem {
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
label: string;
|
|
path: string;
|
|
requiredPermission?: {
|
|
resource: string;
|
|
action?: string; // If not provided, checks for '*' or 'read'
|
|
};
|
|
}
|
|
|
|
interface SidebarProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
// Super Admin menu items
|
|
const superAdminPlatformMenu: MenuItem[] = [
|
|
{ icon: LayoutDashboard, label: 'Dashboard', path: '/dashboard' },
|
|
{ icon: Building2, label: 'Tenants', path: '/tenants' },
|
|
// { icon: Users, label: 'User Management', path: '/users' },
|
|
// { icon: Shield, label: 'Roles', path: '/roles' },
|
|
{ icon: Package, label: 'Modules', path: '/modules' },
|
|
];
|
|
|
|
const superAdminSystemMenu: MenuItem[] = [
|
|
{ icon: FileText, label: 'Audit Logs', path: '/audit-logs' },
|
|
// { icon: Settings, label: 'Settings', path: '/settings' },
|
|
];
|
|
|
|
// Tenant Admin menu items
|
|
const tenantAdminPlatformMenu: MenuItem[] = [
|
|
{ icon: LayoutDashboard, label: 'Dashboard', path: '/tenant' },
|
|
{ icon: Shield, label: 'Roles', path: '/tenant/roles', requiredPermission: { resource: 'roles' } },
|
|
{ icon: Users, label: 'Users', path: '/tenant/users', requiredPermission: { resource: 'users' } },
|
|
{ icon: Package, label: 'Modules', path: '/tenant/modules' },
|
|
];
|
|
|
|
const tenantAdminSystemMenu: MenuItem[] = [
|
|
{ icon: FileText, label: 'Audit Logs', path: '/tenant/audit-logs', requiredPermission: { resource: 'audit_logs' } },
|
|
{ icon: Settings, label: 'Settings', path: '/tenant/settings', requiredPermission: { resource: 'tenants' } },
|
|
];
|
|
|
|
export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|
const location = useLocation();
|
|
const { roles, permissions } = useAppSelector((state) => state.auth);
|
|
const { theme, logoUrl } = useAppSelector((state) => state.theme);
|
|
|
|
// Fetch theme for tenant admin
|
|
const isSuperAdminCheck = () => {
|
|
let rolesArray: string[] = [];
|
|
if (Array.isArray(roles)) {
|
|
rolesArray = roles;
|
|
} else if (typeof roles === 'string') {
|
|
try {
|
|
rolesArray = JSON.parse(roles);
|
|
} catch {
|
|
rolesArray = [];
|
|
}
|
|
}
|
|
return rolesArray.includes('super_admin');
|
|
};
|
|
|
|
const isSuperAdmin = isSuperAdminCheck();
|
|
|
|
// Get role name for display
|
|
const getRoleName = (): string => {
|
|
if (isSuperAdmin) {
|
|
return 'Super Admin';
|
|
}
|
|
let rolesArray: string[] = [];
|
|
if (Array.isArray(roles)) {
|
|
rolesArray = roles;
|
|
} else if (typeof roles === 'string') {
|
|
try {
|
|
rolesArray = JSON.parse(roles);
|
|
} catch {
|
|
rolesArray = [];
|
|
}
|
|
}
|
|
// Get the first role and format it
|
|
if (rolesArray.length > 0) {
|
|
const role = rolesArray[0];
|
|
// Convert snake_case to Title Case
|
|
return role
|
|
.split('_')
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
.join(' ');
|
|
}
|
|
return 'User';
|
|
};
|
|
|
|
const roleName = getRoleName();
|
|
|
|
// Fetch theme if tenant admin
|
|
if (!isSuperAdmin) {
|
|
useTenantTheme();
|
|
}
|
|
|
|
// Helper function to check if user has permission for a resource
|
|
const hasPermission = (resource: string, requiredAction?: string): boolean => {
|
|
if (isSuperAdmin) {
|
|
return true; // Super admin has all permissions
|
|
}
|
|
|
|
const allowedActions = requiredAction ? [requiredAction] : ['*', 'read'];
|
|
|
|
return permissions.some((perm) => {
|
|
// Check if resource matches (exact match or wildcard)
|
|
const resourceMatches = perm.resource === resource || perm.resource === '*';
|
|
|
|
// Check if action matches (exact match or wildcard)
|
|
const actionMatches = allowedActions.some(
|
|
(allowedAction) => perm.action === allowedAction || perm.action === '*'
|
|
);
|
|
|
|
return resourceMatches && actionMatches;
|
|
});
|
|
};
|
|
|
|
// Filter menu items based on permissions for tenant users
|
|
const filterMenuItems = (items: MenuItem[]): MenuItem[] => {
|
|
if (isSuperAdmin) {
|
|
return items; // Show all items for super admin
|
|
}
|
|
|
|
return items.filter((item) => {
|
|
// If no required permission, always show (e.g., Dashboard, Modules, Settings)
|
|
if (!item.requiredPermission) {
|
|
return true;
|
|
}
|
|
|
|
return hasPermission(
|
|
item.requiredPermission.resource,
|
|
item.requiredPermission.action
|
|
);
|
|
});
|
|
};
|
|
|
|
// Select and filter menu items based on role and permissions
|
|
const platformMenu = filterMenuItems(
|
|
isSuperAdmin ? superAdminPlatformMenu : tenantAdminPlatformMenu
|
|
);
|
|
const systemMenu = filterMenuItems(
|
|
isSuperAdmin ? superAdminSystemMenu : tenantAdminSystemMenu
|
|
);
|
|
|
|
const MenuSection = ({ title, items }: { title: string; items: MenuItem[] }) => (
|
|
<div className="w-full">
|
|
<div className="flex flex-col gap-1">
|
|
<div className="pb-1 px-2 md:px-2 lg:px-3">
|
|
<div className="text-[10px] md:text-[10px] lg:text-[11px] font-semibold text-[#6b7280] uppercase tracking-[0.88px]">
|
|
{title}
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-1 mt-1">
|
|
{items.map((item) => {
|
|
const Icon = item.icon;
|
|
const isActive = location.pathname === item.path;
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
onClick={() => {
|
|
// Close sidebar on mobile when navigating
|
|
if (window.innerWidth < 768) {
|
|
onClose();
|
|
}
|
|
}}
|
|
className={cn(
|
|
'flex items-center gap-2 md:gap-2 lg:gap-2.5 px-2 md:px-2 lg:px-3 py-2 rounded-md transition-colors min-h-[44px]',
|
|
isActive
|
|
? 'shadow-[0px_2px_8px_0px_rgba(15,23,42,0.15)]'
|
|
: 'text-[#0f1724] hover:bg-gray-50'
|
|
)}
|
|
style={
|
|
isActive
|
|
? {
|
|
backgroundColor: !isSuperAdmin && theme?.primary_color ? theme.primary_color : '#112868',
|
|
color: !isSuperAdmin && theme?.secondary_color ? theme.secondary_color : '#23dce1',
|
|
}
|
|
: undefined
|
|
}
|
|
>
|
|
<Icon className="w-4 h-4 shrink-0" />
|
|
<span className="text-xs md:text-xs lg:text-[13px] font-medium whitespace-nowrap">{item.label}</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{/* Mobile Sidebar */}
|
|
<aside
|
|
className={cn(
|
|
'fixed top-0 left-0 h-full bg-white border-r border-[rgba(0,0,0,0.08)] z-50 flex flex-col gap-6 p-4 transition-transform duration-300 ease-in-out md:hidden',
|
|
isOpen ? 'translate-x-0' : '-translate-x-full'
|
|
)}
|
|
style={{ width: '280px' }}
|
|
>
|
|
{/* Mobile Header with Close Button */}
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex gap-3 items-center">
|
|
{!isSuperAdmin && logoUrl ? (
|
|
<img
|
|
src={logoUrl}
|
|
alt="Logo"
|
|
className="h-9 w-auto max-w-[180px] object-contain"
|
|
onError={(e) => {
|
|
e.currentTarget.style.display = 'none';
|
|
const fallback = e.currentTarget.nextElementSibling as HTMLElement;
|
|
if (fallback) fallback.style.display = 'flex';
|
|
}}
|
|
/>
|
|
) : null}
|
|
<div
|
|
className="w-9 h-9 rounded-[10px] flex items-center justify-center shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)] shrink-0"
|
|
style={{
|
|
display: (!isSuperAdmin && logoUrl) ? 'none' : 'flex',
|
|
backgroundColor: !isSuperAdmin && theme?.primary_color ? theme.primary_color : '#112868',
|
|
}}
|
|
>
|
|
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<div className="text-[18px] font-bold text-[#0f1724] tracking-[-0.36px]">
|
|
{(!isSuperAdmin && logoUrl) ? '' : 'QAssure'}
|
|
</div>
|
|
{(!isSuperAdmin && logoUrl) ? null : (
|
|
<div
|
|
className="text-[12px] font-semibold capitalize mt-[7px] -translate-y-1/2"
|
|
style={{
|
|
color: !isSuperAdmin && theme?.accent_color ? theme.accent_color : '#084cc8',
|
|
}}
|
|
>
|
|
{roleName}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="w-8 h-8 flex items-center justify-center rounded-md hover:bg-gray-100 transition-colors"
|
|
aria-label="Close menu"
|
|
>
|
|
<X className="w-5 h-5 text-[#0f1724]" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Platform Menu */}
|
|
<MenuSection title="Platform" items={platformMenu} />
|
|
|
|
{/* System Menu */}
|
|
<MenuSection title="System" items={systemMenu} />
|
|
|
|
{/* Support Center */}
|
|
<div className="mt-auto">
|
|
<button className="w-full bg-white border border-[rgba(0,0,0,0.08)] rounded-md px-[13px] py-[9px] flex gap-2.5 items-center hover:bg-gray-50 transition-colors min-h-[44px]">
|
|
<HelpCircle className="w-4 h-4 shrink-0 text-[#0f1724]" />
|
|
<span className="text-[13px] font-medium text-[#0f1724]">Support Center</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Powered by LTTS */}
|
|
<div className="w-full flex flex-col items-center justify-center gap-0 px-2">
|
|
<p className="text-[10px] font-medium text-[#9ca3af] capitalize leading-normal">
|
|
Powered by
|
|
</p>
|
|
<img
|
|
src="/LTTS.svg"
|
|
alt="L&T Technology Services"
|
|
className="h-6 w-auto max-w-[150px] object-contain"
|
|
/>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Desktop Sidebar */}
|
|
<aside className="hidden md:flex bg-white border border-[rgba(0,0,0,0.08)] rounded-xl p-2 md:p-2.5 lg:p-3 xl:p-[17px] w-[160px] md:w-[160px] lg:w-[180px] xl:w-[240px] h-full max-h-screen flex-col gap-3 md:gap-3.5 lg:gap-4 xl:gap-6 shrink-0 overflow-hidden">
|
|
{/* Logo */}
|
|
<div className="w-full md:w-[140px] lg:w-[160px] xl:w-[206px] shrink-0">
|
|
<div className="flex gap-3 items-center px-2">
|
|
{!isSuperAdmin && logoUrl ? (
|
|
<img
|
|
src={logoUrl}
|
|
alt="Logo"
|
|
className="h-9 w-auto max-w-[180px] object-contain"
|
|
onError={(e) => {
|
|
e.currentTarget.style.display = 'none';
|
|
const fallback = e.currentTarget.nextElementSibling as HTMLElement;
|
|
if (fallback) fallback.style.display = 'flex';
|
|
}}
|
|
/>
|
|
) : null}
|
|
<div
|
|
className="w-9 h-9 rounded-[10px] flex items-center justify-center shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)] shrink-0"
|
|
style={{
|
|
display: (!isSuperAdmin && logoUrl) ? 'none' : 'flex',
|
|
backgroundColor: !isSuperAdmin && theme?.primary_color ? theme.primary_color : '#112868',
|
|
}}
|
|
>
|
|
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<div className="text-base md:text-base lg:text-base xl:text-[18px] font-bold text-[#0f1724] tracking-[-0.36px]">
|
|
{(!isSuperAdmin && logoUrl) ? '' : 'QAssure'}
|
|
</div>
|
|
{(!isSuperAdmin && logoUrl) ? null : (
|
|
<div
|
|
className="text-[12px] font-semibold capitalize mt-[7px] -translate-y-1/2"
|
|
style={{
|
|
color: !isSuperAdmin && theme?.accent_color ? theme.accent_color : '#084cc8',
|
|
}}
|
|
>
|
|
{roleName}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Platform Menu */}
|
|
{platformMenu.length > 0 && (
|
|
<MenuSection title="Platform" items={platformMenu} />
|
|
)}
|
|
|
|
{/* System Menu */}
|
|
{systemMenu.length > 0 && (
|
|
<MenuSection title="System" items={systemMenu} />
|
|
)}
|
|
|
|
{/* Support Center */}
|
|
<div className="mt-auto w-full">
|
|
<button className="w-full bg-white border border-[rgba(0,0,0,0.08)] rounded-md px-2 md:px-2.5 lg:px-[13px] py-[9px] flex gap-2 md:gap-2 lg:gap-2.5 items-center hover:bg-gray-50 transition-colors">
|
|
<HelpCircle className="w-4 h-4 shrink-0 text-[#0f1724]" />
|
|
<span className="text-xs md:text-xs lg:text-[13px] font-medium text-[#0f1724]">Support Center</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Powered by LTTS */}
|
|
<div className="w-full flex flex-col items-center justify-center gap-0 px-2">
|
|
<p className="text-[10px] font-medium text-[#9ca3af] capitalize leading-normal">
|
|
Powered by
|
|
</p>
|
|
<img
|
|
src="/LTTS.svg"
|
|
alt="L&T Technology Services"
|
|
className="h-6 w-auto max-w-[150px] object-contain"
|
|
/>
|
|
</div>
|
|
</aside>
|
|
</>
|
|
);
|
|
};
|