feat: role-based sidebar menu visibility — each role sees only permitted sections
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user