krona-front/src/app/shell/Sidebar.tsx

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>
);
};