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 navigate = useNavigate()
|
||||||
|
|
||||||
const isAdmin = user?.role === 'super_admin'
|
const isAdmin = user?.role === 'super_admin'
|
||||||
const isHousekeeper = user?.role === 'housekeeper'
|
|
||||||
|
|
||||||
const isModuleActive = (id: string) => statuses[id] === 'active' || statuses[id] === 'trial'
|
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)
|
// Core modules with dedicated nav positions (not shown in generic module list)
|
||||||
const CORE_MODULE_IDS = ['channel-manager', 'rental']
|
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 className="px-3 pt-2 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||||
Основное
|
Основное
|
||||||
</p>
|
</p>
|
||||||
<NavItem to="/calendar" icon={CalendarDays} label="Шахматка" onClick={onClose} />
|
{can('calendar') && <NavItem to="/calendar" icon={CalendarDays} label="Шахматка" onClick={onClose} />}
|
||||||
{!isHousekeeper && (
|
{can('bookings') && <NavItem to="/bookings" icon={BookOpen} label="Бронирования" onClick={onClose} />}
|
||||||
<>
|
{can('guests') && <NavItem to="/guests" icon={UsersRound} label="Гости" onClick={onClose} />}
|
||||||
<NavItem to="/bookings" icon={BookOpen} label="Бронирования" onClick={onClose} />
|
{can('rooms') && <NavItem to="/rooms" icon={BedDouble} label="Номера" onClick={onClose} />}
|
||||||
<NavItem to="/guests" icon={UsersRound} label="Гости" onClick={onClose} />
|
{can('rooms') && <NavItem to="/room-categories" icon={LayoutGrid} label="Категории номеров" onClick={onClose} />}
|
||||||
<NavItem to="/rooms" icon={BedDouble} label="Номера" onClick={onClose} />
|
{can('availability') && <NavItem to="/availability" icon={CalendarRange} label="Доступность" onClick={onClose} />}
|
||||||
<NavItem to="/room-categories" icon={LayoutGrid} label="Категории номеров" onClick={onClose} />
|
|
||||||
<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 className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||||
Цены и тарифы
|
Цены и тарифы
|
||||||
</p>
|
</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 className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||||
Модули
|
Модули
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{isModuleActive('housekeeping') && (
|
{can('housekeeping') && isModuleActive('housekeeping') && (
|
||||||
<>
|
<>
|
||||||
<NavItem
|
<NavItem
|
||||||
to="/housekeeping"
|
to="/housekeeping"
|
||||||
@@ -153,12 +166,10 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
|||||||
label="Уборка"
|
label="Уборка"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
{!isHousekeeper && (
|
{can('maintenance') && <NavItem to="/technical" icon={Zap} label="Тех. задачи" onClick={onClose} />}
|
||||||
<NavItem to="/technical" icon={Zap} label="Тех. задачи" onClick={onClose} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isHousekeeper && activeModuleItems.map(m => (
|
{activeModuleItems.filter(m => can(m.id)).map(m => (
|
||||||
<NavItem
|
<NavItem
|
||||||
key={m.id}
|
key={m.id}
|
||||||
to={m.sidebarItem!.path}
|
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 className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||||
Управление
|
Управление
|
||||||
</p>
|
</p>
|
||||||
<NavItem to="/users" icon={UserCog} label="Сотрудники" onClick={onClose} />
|
)}
|
||||||
<NavItem to="/loyalty" icon={Award} label="Лояльность" onClick={onClose} />
|
{can('users') && <NavItem to="/users" icon={UserCog} label="Сотрудники" onClick={onClose} />}
|
||||||
<NavItem to="/maintenance" icon={Wrench} label="Тех. перерывы" onClick={onClose} />
|
{can('loyalty') && <NavItem to="/loyalty" icon={Award} label="Лояльность" onClick={onClose} />}
|
||||||
<NavItem to="/floor-map" icon={Map} label="План этажей" onClick={onClose} />
|
{can('maintenance')&& <NavItem to="/maintenance" icon={Wrench} label="Тех. перерывы" onClick={onClose} />}
|
||||||
{isModuleActive('channel-manager') && (
|
{can('rooms') && <NavItem to="/floor-map" icon={Map} label="План этажей" onClick={onClose} />}
|
||||||
|
{can('channels') && isModuleActive('channel-manager') && (
|
||||||
<NavItem
|
<NavItem
|
||||||
to="/channels"
|
to="/channels"
|
||||||
icon={MODULES_DATA.find(m => m.id === 'channel-manager')!.sidebarItem!.icon}
|
icon={MODULES_DATA.find(m => m.id === 'channel-manager')!.sidebarItem!.icon}
|
||||||
@@ -186,9 +197,10 @@ export function Sidebar({ open, onClose }: SidebarProps) {
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<NavItem to="/modules" icon={Puzzle} label="Модули" onClick={onClose} />
|
{can('settings') && <NavItem to="/modules" icon={Puzzle} label="Модули" onClick={onClose} />}
|
||||||
<NavItem to="/settings" icon={Settings} 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 className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||||
Разработчикам
|
Разработчикам
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user