feat: full role permissions system — new modules, API persistence, sidebar context
- Backend: migration 080_role_permissions table (hotel-scoped, upsert) - Backend: routes GET/PUT/DELETE /api/hotels/:slug/role-permissions - Frontend: RolePermissionsContext — loads saved perms from API, provides can() - Frontend: Sidebar uses context can() instead of hardcoded ROLE_PERMS - Frontend: fixed module ID→permKey mapping (room-service, olap-reports, website-builder) - Frontend: Documents page added to Управление nav (was missing) - Frontend: equipment/wifi/ttlock get own permission keys (not bundled under 'settings') - Frontend: floor_map gets own permission key (not bundled under 'rooms') - Frontend: new module group 'Технологии': wifi, equipment, ttlock, floor_map - Frontend: 'schedule' added to Администрирование module group - Updated INITIAL_ROLE_PERMISSIONS: receptionist+availability+reports+documents+website, accountant+documents, technician+floor_map+equipment+ttlock Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
import { useModules } from '../../contexts/ModulesContext'
|
||||
import { useRolePermissions } from '../../contexts/RolePermissionsContext'
|
||||
import { MODULES_DATA } from '../../data/modulesData'
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
@@ -121,6 +122,7 @@ function GroupHeader({
|
||||
export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
const { user } = useAuth()
|
||||
const { statuses } = useModules()
|
||||
const { can } = useRolePermissions()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
@@ -128,21 +130,14 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
|
||||
const isModuleActive = (id: string) => statuses[id] === 'active' || statuses[id] === 'trial'
|
||||
|
||||
// Role-based permission map
|
||||
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)
|
||||
// Maps module IDs whose IDs differ from permission keys
|
||||
const MODULE_PERM_KEY: Record<string, string> = {
|
||||
'room-service': 'room_service',
|
||||
'olap-reports': 'reports',
|
||||
'website-builder':'website',
|
||||
'channel-manager':'channels',
|
||||
}
|
||||
const modulePermKey = (moduleId: string) => MODULE_PERM_KEY[moduleId] ?? moduleId
|
||||
|
||||
// Core modules with dedicated nav positions (not shown in generic module list)
|
||||
const CORE_MODULE_IDS = ['channel-manager', 'rental']
|
||||
@@ -185,7 +180,7 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
basics: ['/calendar', '/bookings', '/guests', '/rooms', '/room-categories', '/availability'],
|
||||
prices: ['/tariffs', '/dynamic-pricing', '/discounts', '/rental'],
|
||||
service: ['/housekeeping', '/technical', ...activeModuleItems.map(m => m.sidebarItem!.path)],
|
||||
management: ['/users', '/schedule', '/loyalty', '/maintenance', '/floor-map', '/channels'],
|
||||
management: ['/users', '/schedule', '/documents', '/loyalty', '/maintenance', '/floor-map', '/channels'],
|
||||
settingsGroup:['/modules', '/settings', '/billing', '/equipment', '/wifi', '/ttlock', '/settings/checklists', '/settings/payments', '/settings/minibar', '/settings/minibar/stock', '/settings/deposit'],
|
||||
devGroup: ['/api-docs'],
|
||||
}
|
||||
@@ -370,7 +365,7 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
{can('maintenance') && <NavItem to="/technical" icon={Zap} label="Тех. задачи" {...navItemProps} />}
|
||||
</>
|
||||
)}
|
||||
{activeModuleItems.filter(m => can(m.id)).map(m => (
|
||||
{activeModuleItems.filter(m => can(modulePermKey(m.id))).map(m => (
|
||||
<NavItem
|
||||
key={m.id}
|
||||
to={m.sidebarItem!.path}
|
||||
@@ -385,16 +380,17 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
)}
|
||||
|
||||
{/* ── Управление ── */}
|
||||
{(can('users') || can('loyalty') || can('maintenance') || can('rooms') || (can('channels') && isModuleActive('channel-manager'))) && (
|
||||
{(can('users') || can('documents') || can('loyalty') || can('maintenance') || can('floor_map') || (can('channels') && isModuleActive('channel-manager'))) && (
|
||||
<>
|
||||
<GroupHeader label="Управление" isOpen={groups.management} onToggle={() => toggleGroup('management')} />
|
||||
{groups.management && (
|
||||
<div className="space-y-0.5">
|
||||
{can('users') && <NavItem to="/users" icon={UserCog} label="Сотрудники" {...navItemProps} />}
|
||||
{can('users') && <NavItem to="/schedule" icon={CalendarClock} label="График работы" {...navItemProps} />}
|
||||
{can('documents') && <NavItem to="/documents" icon={FileText} label="Документы" {...navItemProps} />}
|
||||
{can('loyalty') && <NavItem to="/loyalty" icon={Award} label="Лояльность" {...navItemProps} />}
|
||||
{can('maintenance') && <NavItem to="/maintenance" icon={Wrench} label="Тех. перерывы" {...navItemProps} />}
|
||||
{can('rooms') && <NavItem to="/floor-map" icon={Map} label="План этажей" {...navItemProps} />}
|
||||
{can('floor_map') && <NavItem to="/floor-map" icon={Map} label="План этажей" {...navItemProps} />}
|
||||
{can('channels') && isModuleActive('channel-manager') && (
|
||||
<NavItem
|
||||
to="/channels"
|
||||
@@ -409,21 +405,21 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
||||
)}
|
||||
|
||||
{/* ── Настройки ── */}
|
||||
{can('settings') && (
|
||||
{(can('settings') || can('equipment') || can('wifi') || can('ttlock')) && (
|
||||
<>
|
||||
<GroupHeader label="Настройки" isOpen={groups.settingsGroup} onToggle={() => toggleGroup('settingsGroup')} />
|
||||
{groups.settingsGroup && (
|
||||
<div className="space-y-0.5">
|
||||
<NavItem to="/modules" icon={Puzzle} label="Модули" {...navItemProps} />
|
||||
<NavItem to="/settings" icon={Settings} label="Настройки" {...navItemProps} />
|
||||
<NavItem to="/billing" icon={CreditCard} label="Тарифы и оплата" {...navItemProps} />
|
||||
<NavItem to="/equipment" icon={Monitor} label="Оборудование" {...navItemProps} />
|
||||
<NavItem to="/wifi" icon={Wifi} label="WiFi авторизация" {...navItemProps} />
|
||||
<NavItem to="/ttlock" icon={KeyRound} label="Эл. замки TTLock" {...navItemProps} />
|
||||
<NavItem to="/settings/checklists" icon={ListChecks} label="Чек-листы уборки" {...navItemProps} />
|
||||
<NavItem to="/settings/payments" icon={CreditCard} label="Способы оплаты" {...navItemProps} />
|
||||
<NavItem to="/settings/minibar" icon={ShoppingCart} label="Минибар" {...navItemProps} />
|
||||
<NavItem to="/settings/deposit" icon={ShieldCheck} label="Депозит" {...navItemProps} />
|
||||
{can('settings') && <NavItem to="/modules" icon={Puzzle} label="Модули" {...navItemProps} />}
|
||||
{can('settings') && <NavItem to="/settings" icon={Settings} label="Настройки" {...navItemProps} />}
|
||||
{can('settings') && <NavItem to="/billing" icon={CreditCard} label="Тарифы и оплата" {...navItemProps} />}
|
||||
{can('equipment') && <NavItem to="/equipment" icon={Monitor} label="Оборудование" {...navItemProps} />}
|
||||
{can('wifi') && <NavItem to="/wifi" icon={Wifi} label="WiFi авторизация" {...navItemProps} />}
|
||||
{can('ttlock') && <NavItem to="/ttlock" icon={KeyRound} label="Эл. замки TTLock" {...navItemProps} />}
|
||||
{can('settings') && <NavItem to="/settings/checklists" icon={ListChecks} label="Чек-листы уборки" {...navItemProps} />}
|
||||
{can('settings') && <NavItem to="/settings/payments" icon={CreditCard} label="Способы оплаты" {...navItemProps} />}
|
||||
{can('settings') && <NavItem to="/settings/minibar" icon={ShoppingCart} label="Минибар" {...navItemProps} />}
|
||||
{can('settings') && <NavItem to="/settings/deposit" icon={ShieldCheck} label="Депозит" {...navItemProps} />}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user