159 lines
5.9 KiB
TypeScript
159 lines
5.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { NavLink } from 'react-router-dom';
|
|
import { useDemoStore } from '../../mock/store';
|
|
import { SYSTEM_MODULES } from '../../core/modules/registry';
|
|
import { SYSTEM_ROLES } from '../../core/permissions/types';
|
|
import {
|
|
LayoutDashboard,
|
|
Workflow,
|
|
Users,
|
|
ShoppingBag,
|
|
Boxes,
|
|
BadgeDollarSign,
|
|
UserCheck,
|
|
MessageSquareText,
|
|
Headset,
|
|
CheckSquare,
|
|
Globe,
|
|
Megaphone,
|
|
BarChart3,
|
|
Cpu,
|
|
Sparkles,
|
|
Grid,
|
|
Settings,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
ShieldAlert,
|
|
} from 'lucide-react';
|
|
|
|
const ICON_MAP: Record<string, React.ReactNode> = {
|
|
LayoutDashboard: <LayoutDashboard className="w-4 h-4 shrink-0" />,
|
|
Workflow: <Workflow className="w-4 h-4 shrink-0 text-krona-gold" />,
|
|
Users: <Users className="w-4 h-4 shrink-0" />,
|
|
ShoppingBag: <ShoppingBag className="w-4 h-4 shrink-0" />,
|
|
Boxes: <Boxes className="w-4 h-4 shrink-0" />,
|
|
BadgeDollarSign: <BadgeDollarSign className="w-4 h-4 shrink-0" />,
|
|
UserCheck: <UserCheck className="w-4 h-4 shrink-0" />,
|
|
MessageSquareText: <MessageSquareText className="w-4 h-4 shrink-0" />,
|
|
Headset: <Headset className="w-4 h-4 shrink-0" />,
|
|
CheckSquare: <CheckSquare className="w-4 h-4 shrink-0" />,
|
|
Globe: <Globe className="w-4 h-4 shrink-0" />,
|
|
Megaphone: <Megaphone className="w-4 h-4 shrink-0" />,
|
|
BarChart3: <BarChart3 className="w-4 h-4 shrink-0" />,
|
|
Cpu: <Cpu className="w-4 h-4 shrink-0" />,
|
|
Sparkles: <Sparkles className="w-4 h-4 shrink-0 text-krona-gold" />,
|
|
Grid: <Grid className="w-4 h-4 shrink-0" />,
|
|
Settings: <Settings className="w-4 h-4 shrink-0" />,
|
|
};
|
|
|
|
export const Sidebar: React.FC = () => {
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
const { currentRoleId, getActiveCapabilities } = useDemoStore();
|
|
const capabilities = getActiveCapabilities();
|
|
const role = SYSTEM_ROLES[currentRoleId];
|
|
|
|
// Filter modules by role permissions and active capabilities
|
|
const visibleModules = SYSTEM_MODULES.filter((m) => {
|
|
// Role check
|
|
if (!role.allowedModules.includes('*') && !role.allowedModules.includes(m.id)) {
|
|
return false;
|
|
}
|
|
// Capability check
|
|
if (m.requiredCapability && !capabilities[m.requiredCapability]) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
return (
|
|
<aside
|
|
className={`relative h-screen bg-[#252722] text-[#D8D8D0] flex flex-col border-r border-[#3A3D35] transition-all duration-200 z-40 select-none ${
|
|
collapsed ? 'w-16' : 'w-56'
|
|
}`}
|
|
>
|
|
{/* Brand Header */}
|
|
<div className="h-14 px-3.5 flex items-center justify-between border-b border-[#3A3D35] bg-[#20221E]">
|
|
<div className="flex items-center gap-2.5 overflow-hidden">
|
|
{/* Logo asset from public/branding/krona-logo.png */}
|
|
<img
|
|
src={collapsed ? '/branding/krona-symbol.png' : '/branding/krona-logo.png'}
|
|
alt="Крона"
|
|
className={`object-contain transition-all duration-200 ${
|
|
collapsed ? 'w-8 h-8' : 'h-8 max-w-[150px]'
|
|
}`}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setCollapsed(!collapsed)}
|
|
className="p-1 rounded text-[#858980] hover:text-[#D8D8D0] hover:bg-[#30332C] transition-colors"
|
|
title={collapsed ? 'Развернуть меню' : 'Свернуть меню'}
|
|
>
|
|
{collapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Navigation List */}
|
|
<div className="flex-1 overflow-y-auto py-2.5 px-2 space-y-0.5">
|
|
{visibleModules.map((item) => (
|
|
<NavLink
|
|
key={item.id}
|
|
to={item.route}
|
|
end={item.route === '/'}
|
|
className={({ isActive }) =>
|
|
`group relative flex items-center gap-2.5 px-2.5 py-2 rounded-md text-xs font-medium transition-all ${
|
|
isActive
|
|
? 'bg-[#34382F] text-[#F5F0E5] font-semibold'
|
|
: 'text-[#BFC1B9] hover:text-[#F5F0E5] hover:bg-[#30332C]'
|
|
}`
|
|
}
|
|
title={collapsed ? item.label : undefined}
|
|
>
|
|
{({ isActive }) => (
|
|
<>
|
|
{/* Active indicator bar */}
|
|
{isActive && (
|
|
<div className="absolute left-0 top-1.5 bottom-1.5 w-1 bg-[#B78335] rounded-r" />
|
|
)}
|
|
|
|
<div className={`shrink-0 ${isActive ? 'text-[#D4B16D]' : 'text-[#BFC1B9]'}`}>
|
|
{ICON_MAP[item.iconName] || <LayoutDashboard className="w-4 h-4 shrink-0" />}
|
|
</div>
|
|
|
|
{!collapsed && (
|
|
<div className="flex-1 flex items-center justify-between truncate">
|
|
<span className="truncate">{item.label}</span>
|
|
{item.badge && (
|
|
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-[#3A3D35] text-[#D8D8D0] font-mono">
|
|
{item.badge}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</NavLink>
|
|
))}
|
|
</div>
|
|
|
|
{/* Footer / Role indicator */}
|
|
<div className="p-2.5 border-t border-[#3A3D35] bg-[#20221E]/60 text-xs">
|
|
{!collapsed ? (
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-2 h-2 rounded-full bg-krona-green animate-pulse" />
|
|
<div className="truncate">
|
|
<div className="text-[11px] font-semibold text-[#F5F0E5] truncate">{role.label}</div>
|
|
<div className="text-[9px] text-[#858980]">RBAC: {role.badge}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex justify-center" title={`Текущая роль: ${role.label}`}>
|
|
<div className="w-2.5 h-2.5 rounded-full bg-krona-green" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
);
|
|
};
|