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

238 lines
9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import { NavLink } from 'react-router-dom';
import { useDemoStore } from '../../mock/store';
import { useMetadata } from '../../services/hooks/useMetadata';
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 { entities } = useMetadata();
const customEntities = entities.filter((e) => !e.isSystem);
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-symbol.png */}
<img
src="/branding/krona-symbol.png"
alt="Крона"
className="w-8 h-8 object-contain shrink-0"
/>
{!collapsed && (
<div className="flex flex-col min-w-0 leading-tight select-none">
<span className="text-sm font-bold tracking-wider text-[#E5B842] uppercase truncate">
Крона
</span>
<span className="text-[9px] uppercase tracking-widest text-[#B78335] font-semibold">
Business OS
</span>
</div>
)}
</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>
))}
{/* Custom Entities Section */}
{customEntities.length > 0 && (
<div className="pt-2 mt-2 border-t border-[#3A3D35]/80">
{!collapsed && (
<div className="px-2 pb-1 text-[10px] font-semibold uppercase tracking-wider text-[#858980]">
Пользовательские модули
</div>
)}
{customEntities.map((ent) => (
<NavLink
key={ent.code}
to={`/entities/${ent.code}`}
className={({ isActive }) =>
`group relative flex items-center gap-2.5 px-2.5 py-1.5 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 ? ent.name : undefined}
>
{({ isActive }) => (
<>
{isActive && (
<div className="absolute left-0 top-1.5 bottom-1.5 w-1 bg-[#B78335] rounded-r" />
)}
<div className="shrink-0 text-krona-gold">
<Sparkles className="w-3.5 h-3.5" />
</div>
{!collapsed && (
<span className="truncate text-xs">{ent.name}</span>
)}
</>
)}
</NavLink>
))}
</div>
)}
{/* Configuration Engine Quick Link */}
<div className="pt-1">
<NavLink
to="/settings/entities"
className={({ isActive }) =>
`group relative flex items-center gap-2.5 px-2.5 py-1.5 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 ? 'Конструктор сущностей' : undefined}
>
{({ isActive }) => (
<>
{isActive && (
<div className="absolute left-0 top-1.5 bottom-1.5 w-1 bg-[#B78335] rounded-r" />
)}
<div className="shrink-0 text-krona-green">
<Workflow className="w-3.5 h-3.5" />
</div>
{!collapsed && (
<span className="truncate text-xs">Конструктор сущностей</span>
)}
</>
)}
</NavLink>
</div>
</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>
);
};