feat: role-based sidebar menu visibility — each role sees only permitted sections

This commit is contained in:
2026-03-26 00:52:29 +03:00
parent a59f9189ed
commit 18358b64b3

View File

@@ -40,10 +40,25 @@ export function Sidebar({ open, onClose }: SidebarProps) {
const navigate = useNavigate()
const isAdmin = user?.role === 'super_admin'
const isHousekeeper = user?.role === 'housekeeper'
const isModuleActive = (id: string) => statuses[id] === 'active' || statuses[id] === 'trial'
// Role-based permission map (which module keys each role can access)
const ROLE_PERMS: Record<string, string[]> = {
hotel_admin: ['*'],
manager: ['*'],
receptionist: ['calendar','bookings','guests','rooms','availability','housekeeping','room_service','rental','pos','reviews','reports'],
housekeeper: ['calendar','housekeeping','maintenance','rooms'],
accountant: ['calendar','reports','pos','discounts','tariffs','pricing','loyalty'],
security: ['calendar','bookings'],
technician: ['calendar','housekeeping','maintenance','rooms'],
}
const can = (permission: string) => {
const role = user?.role ?? 'housekeeper'
const perms = ROLE_PERMS[role] ?? []
return perms.includes('*') || perms.includes(permission)
}
// Core modules with dedicated nav positions (not shown in generic module list)
const CORE_MODULE_IDS = ['channel-manager', 'rental']
@@ -117,35 +132,33 @@ export function Sidebar({ open, onClose }: SidebarProps) {
<p className="px-3 pt-2 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
Основное
</p>
<NavItem to="/calendar" icon={CalendarDays} label="Шахматка" onClick={onClose} />
{!isHousekeeper && (
<>
<NavItem to="/bookings" icon={BookOpen} label="Бронирования" onClick={onClose} />
<NavItem to="/guests" icon={UsersRound} label="Гости" onClick={onClose} />
<NavItem to="/rooms" icon={BedDouble} label="Номера" onClick={onClose} />
<NavItem to="/room-categories" icon={LayoutGrid} label="Категории номеров" onClick={onClose} />
<NavItem to="/availability" icon={CalendarRange} label="Доступность" onClick={onClose} />
{can('calendar') && <NavItem to="/calendar" icon={CalendarDays} label="Шахматка" onClick={onClose} />}
{can('bookings') && <NavItem to="/bookings" icon={BookOpen} label="Бронирования" onClick={onClose} />}
{can('guests') && <NavItem to="/guests" icon={UsersRound} label="Гости" onClick={onClose} />}
{can('rooms') && <NavItem to="/rooms" icon={BedDouble} label="Номера" onClick={onClose} />}
{can('rooms') && <NavItem to="/room-categories" icon={LayoutGrid} label="Категории номеров" onClick={onClose} />}
{can('availability') && <NavItem to="/availability" icon={CalendarRange} label="Доступность" onClick={onClose} />}
{/* ── Цены ── */}
{(can('tariffs') || can('pricing') || can('discounts') || (can('rental') && isModuleActive('rental'))) && (
<p className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
Цены и тарифы
</p>
<NavItem to="/tariffs" icon={Utensils} label="Тарифы" onClick={onClose} />
<NavItem to="/dynamic-pricing" icon={TrendingUp} label="Динамические цены" onClick={onClose} />
<NavItem to="/discounts" icon={Tag} label="Скидки" onClick={onClose} />
{isModuleActive('rental') && (
<NavItem to="/rental" icon={CalendarClock} label="Аренда объектов" onClick={onClose} />
)}
</>
{can('tariffs') && <NavItem to="/tariffs" icon={Utensils} label="Тарифы" onClick={onClose} />}
{can('pricing') && <NavItem to="/dynamic-pricing" icon={TrendingUp} label="Динамические цены" onClick={onClose} />}
{can('discounts')&& <NavItem to="/discounts" icon={Tag} label="Скидки" onClick={onClose} />}
{can('rental') && isModuleActive('rental') && (
<NavItem to="/rental" icon={CalendarClock} label="Аренда объектов" onClick={onClose} />
)}
{/* ── Модули ── */}
{(isModuleActive('housekeeping') || (!isHousekeeper && activeModuleItems.length > 0)) && (
{(can('housekeeping') && isModuleActive('housekeeping') || (activeModuleItems.some(m => can(m.id)) )) && (
<p className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
Модули
</p>
)}
{isModuleActive('housekeeping') && (
{can('housekeeping') && isModuleActive('housekeeping') && (
<>
<NavItem
to="/housekeeping"
@@ -153,12 +166,10 @@ export function Sidebar({ open, onClose }: SidebarProps) {
label="Уборка"
onClick={onClose}
/>
{!isHousekeeper && (
<NavItem to="/technical" icon={Zap} label="Тех. задачи" onClick={onClose} />
)}
{can('maintenance') && <NavItem to="/technical" icon={Zap} label="Тех. задачи" onClick={onClose} />}
</>
)}
{!isHousekeeper && activeModuleItems.map(m => (
{activeModuleItems.filter(m => can(m.id)).map(m => (
<NavItem
key={m.id}
to={m.sidebarItem!.path}
@@ -169,16 +180,16 @@ export function Sidebar({ open, onClose }: SidebarProps) {
))}
{/* ── Управление ── */}
{!isHousekeeper && (
<>
{(can('users') || can('loyalty') || can('maintenance') || can('rooms') || can('channels') || can('settings')) && (
<p className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
Управление
</p>
<NavItem to="/users" icon={UserCog} label="Сотрудники" onClick={onClose} />
<NavItem to="/loyalty" icon={Award} label="Лояльность" onClick={onClose} />
<NavItem to="/maintenance" icon={Wrench} label="Тех. перерывы" onClick={onClose} />
<NavItem to="/floor-map" icon={Map} label="План этажей" onClick={onClose} />
{isModuleActive('channel-manager') && (
)}
{can('users') && <NavItem to="/users" icon={UserCog} label="Сотрудники" onClick={onClose} />}
{can('loyalty') && <NavItem to="/loyalty" icon={Award} label="Лояльность" onClick={onClose} />}
{can('maintenance')&& <NavItem to="/maintenance" icon={Wrench} label="Тех. перерывы" onClick={onClose} />}
{can('rooms') && <NavItem to="/floor-map" icon={Map} label="План этажей" onClick={onClose} />}
{can('channels') && isModuleActive('channel-manager') && (
<NavItem
to="/channels"
icon={MODULES_DATA.find(m => m.id === 'channel-manager')!.sidebarItem!.icon}
@@ -186,9 +197,10 @@ export function Sidebar({ open, onClose }: SidebarProps) {
onClick={onClose}
/>
)}
<NavItem to="/modules" icon={Puzzle} label="Модули" onClick={onClose} />
<NavItem to="/settings" icon={Settings} label="Настройки" onClick={onClose} />
{can('settings') && <NavItem to="/modules" icon={Puzzle} label="Модули" onClick={onClose} />}
{can('settings') && <NavItem to="/settings" icon={Settings} label="Настройки" onClick={onClose} />}
{can('settings') && (
<>
<p className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
Разработчикам
</p>