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

234 lines
9.1 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 { useSessionStore } from '../../core/session/sessionStore';
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,
Truck,
ChevronLeft,
ChevronRight,
} 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" />,
Truck: <Truck 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 interface SidebarProps {
isTabletCompact?: boolean;
}
export const Sidebar: React.FC<SidebarProps> = ({ isTabletCompact = false }) => {
const [userCollapsed, setUserCollapsed] = useState<boolean | null>(null);
const collapsed = userCollapsed !== null ? userCollapsed : isTabletCompact;
const { currentRoleId, hasModulePermission } = useSessionStore();
const { entities } = useMetadata();
const customEntities = entities.filter((e) => !e.isSystem);
const role = SYSTEM_ROLES[currentRoleId];
// Filter modules by role permissions (strict deny-by-default via sessionStore)
const visibleModules = SYSTEM_MODULES.filter((m) => hasModulePermission(m.id));
return (
<aside
data-testid="sidebar"
className={`relative h-screen bg-[#252722] text-[#D8D8D0] hidden md:flex flex-col border-r border-[#3A3D35] transition-all duration-200 z-20 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">
Бизнес-платформа
</span>
</div>
)}
</div>
<button
onClick={() => setUserCollapsed(!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>
);
};