fix: edit user modal empty fields (key prop); refactor: sidebar concept A — by work role

Sidebar: 6 groups by daily work role instead of mixed functionality
- Фронт-деск: calendar, bookings, guests, pos, room-service, reviews
- Номерной фонд: rooms, categories, availability, floor-map, housekeeping, tech tasks
- Продажи: tariffs, pricing, discounts, rental, channels, website, widget
- Аналитика: reports, loyalty
- Персонал: users, schedule, documents
- Настройки: settings, modules, billing, equipment, wifi, ttlock, sub-settings
Removed: separate 'Разработчикам' group (API docs moved to Настройки)
Removed: stale localStorage key sidebarGroups_v2 → v3
Fixed: dynamic modules (pos, room-service, etc) placed in logical groups instead of dumped in 'Сервис'
Fixed: UserModal key prop — form now resets correctly when switching between employees

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 12:26:03 +03:00
parent 512e3c6659
commit d26ad30433
2 changed files with 164 additions and 222 deletions

View File

@@ -2,9 +2,13 @@ import { NavLink, useLocation, useNavigate } from 'react-router-dom'
import { useState, useEffect } from 'react'
import {
CalendarDays, BookOpen, BedDouble, Globe, Settings,
FileText, LayoutDashboard, Building2, Users, X, Hotel, Puzzle, CalendarRange, Map, LayoutGrid, UserCog, UsersRound,
TrendingUp, Tag, Award, Wrench, Utensils, CalendarClock, Zap, ChevronRight, Star, CreditCard, Monitor, Wifi, KeyRound,
ListChecks, ShoppingCart, ShieldCheck,
FileText, LayoutDashboard, Building2, Users, X, Hotel, Puzzle,
CalendarRange, Map, LayoutGrid, UserCog, UsersRound,
TrendingUp, Tag, Award, Wrench, CalendarClock, Zap,
ChevronRight, Star, CreditCard, Monitor, Wifi, KeyRound,
ListChecks, ShoppingCart, ShieldCheck, BarChart3, Utensils,
Network, Sparkles, UtensilsCrossed, Star as StarIcon, Globe2,
CalendarCheck2,
} from 'lucide-react'
import { useAuth } from '../../contexts/AuthContext'
import { useModules } from '../../contexts/ModulesContext'
@@ -19,50 +23,38 @@ interface SidebarProps {
// ── Favorites helpers ──────────────────────────────────────────────────────
function loadFavorites(): string[] {
try {
return JSON.parse(localStorage.getItem('sidebarFavorites') || '[]')
} catch {
return []
}
try { return JSON.parse(localStorage.getItem('sidebarFavorites') || '[]') }
catch { return [] }
}
function saveFavorites(favs: string[]) {
localStorage.setItem('sidebarFavorites', JSON.stringify(favs))
}
// ── Group state helpers ────────────────────────────────────────────────────
type GroupKey = 'basics' | 'prices' | 'service' | 'management' | 'settingsGroup' | 'devGroup'
type GroupKey = 'frontdesk' | 'rooms' | 'sales' | 'analytics' | 'staff' | 'settingsGroup'
const DEFAULT_GROUPS: Record<GroupKey, boolean> = {
basics: true,
prices: false,
service: false,
management: false,
settingsGroup: false,
devGroup: false,
frontdesk: true,
rooms: false,
sales: false,
analytics: false,
staff: false,
settingsGroup:false,
}
function loadGroups(): Record<GroupKey, boolean> {
try {
const stored = JSON.parse(localStorage.getItem('sidebarGroups_v2') || '{}')
const stored = JSON.parse(localStorage.getItem('sidebarGroups_v3') || '{}')
return { ...DEFAULT_GROUPS, ...stored }
} catch {
return { ...DEFAULT_GROUPS }
}
} catch { return { ...DEFAULT_GROUPS } }
}
function saveGroups(groups: Record<GroupKey, boolean>) {
localStorage.setItem('sidebarGroups_v2', JSON.stringify(groups))
localStorage.setItem('sidebarGroups_v3', JSON.stringify(groups))
}
// ── NavItem with star ──────────────────────────────────────────────────────
// ── NavItem ────────────────────────────────────────────────────────────────
function NavItem({
to,
icon: Icon,
label,
onClick,
favorites,
onToggleFavorite,
to, icon: Icon, label, onClick, favorites, onToggleFavorite,
}: {
to: string
icon: React.ElementType
@@ -72,7 +64,6 @@ function NavItem({
onToggleFavorite: (path: string) => void
}) {
const isFav = favorites.includes(to)
return (
<div className="relative group/item">
<NavLink
@@ -88,24 +79,15 @@ function NavItem({
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onToggleFavorite(to) }}
title={isFav ? 'Убрать из избранного' : 'Добавить в избранное'}
>
<Star
size={12}
className={isFav ? 'fill-yellow-400 text-yellow-400' : 'text-slate-400'}
/>
<Star size={12} className={isFav ? 'fill-yellow-400 text-yellow-400' : 'text-slate-400'} />
</button>
</div>
)
}
// ── Group header ───────────────────────────────────────────────────────────
function GroupHeader({
label,
isOpen,
onToggle,
}: {
label: string
isOpen: boolean
onToggle: () => void
function GroupHeader({ label, isOpen, onToggle }: {
label: string; isOpen: boolean; onToggle: () => void
}) {
return (
<button
@@ -127,32 +109,10 @@ export function Sidebar({ open, onClose }: SidebarProps) {
const location = useLocation()
const isAdmin = user?.role === 'super_admin'
const isActive = (id: string) => statuses[id] === 'active' || statuses[id] === 'trial'
const isModuleActive = (id: string) => statuses[id] === 'active' || statuses[id] === 'trial'
// 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']
// Module items for the "Сервис" sidebar section
const activeModuleItems = MODULES_DATA.filter(
m => m.sidebarItem &&
!m.sidebarItem.showForHousekeeper &&
!CORE_MODULE_IDS.includes(m.id) &&
isModuleActive(m.id),
)
// ── Favorites state ──────────────────────────────────────────────────────
// ── Favorites ─────────────────────────────────────────────────────────────
const [favorites, setFavorites] = useState<string[]>(loadFavorites)
const toggleFavorite = (path: string) => {
setFavorites(prev => {
const next = prev.includes(path) ? prev.filter(p => p !== path) : [...prev, path]
@@ -161,9 +121,8 @@ export function Sidebar({ open, onClose }: SidebarProps) {
})
}
// ── Group open/close state ───────────────────────────────────────────────
// ── Groups ────────────────────────────────────────────────────────────────
const [groups, setGroups] = useState<Record<GroupKey, boolean>>(loadGroups)
const toggleGroup = (key: GroupKey) => {
setGroups(prev => {
const next = { ...prev, [key]: !prev[key] }
@@ -172,27 +131,26 @@ export function Sidebar({ open, onClose }: SidebarProps) {
})
}
// Auto-open the group containing the currently active route
// Auto-open group for active route
useEffect(() => {
const path = location.pathname
const groupPaths: Record<GroupKey, string[]> = {
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', '/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'],
frontdesk: ['/calendar', '/bookings', '/guests', '/pos', '/room-service', '/reviews'],
rooms: ['/rooms', '/room-categories', '/availability', '/housekeeping', '/technical', '/maintenance', '/floor-map'],
sales: ['/tariffs', '/dynamic-pricing', '/discounts', '/rental', '/channels', '/website', '/booking-widget'],
analytics: ['/reports', '/loyalty'],
staff: ['/users', '/schedule', '/documents'],
settingsGroup:['/modules', '/settings', '/billing', '/equipment', '/wifi', '/ttlock',
'/settings/checklists', '/settings/payments', '/settings/minibar',
'/settings/minibar/stock', '/settings/deposit', '/api-docs', '/migration'],
}
const matchedGroup = (Object.keys(groupPaths) as GroupKey[]).find(
const matched = (Object.keys(groupPaths) as GroupKey[]).find(
key => groupPaths[key].some(p => path === p || path.startsWith(p + '/')),
)
if (matchedGroup) {
if (matched) {
setGroups(prev => {
if (prev[matchedGroup]) return prev
const next = { ...prev, [matchedGroup]: true }
if (prev[matched]) return prev
const next = { ...prev, [matched]: true }
saveGroups(next)
return next
})
@@ -200,50 +158,15 @@ export function Sidebar({ open, onClose }: SidebarProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname])
// All items metadata for favorites lookup
const allItems: { to: string; icon: React.ElementType; label: string }[] = [
// basics
{ to: '/calendar', icon: CalendarDays, label: 'Шахматка' },
{ to: '/bookings', icon: BookOpen, label: 'Бронирования' },
{ to: '/guests', icon: UsersRound, label: 'Гости' },
{ to: '/rooms', icon: BedDouble, label: 'Номера' },
{ to: '/room-categories', icon: LayoutGrid, label: 'Категории номеров' },
{ to: '/availability', icon: CalendarRange, label: 'Доступность' },
// prices
{ to: '/tariffs', icon: Utensils, label: 'Тарифы' },
{ to: '/dynamic-pricing', icon: TrendingUp, label: 'Динамические цены' },
{ to: '/discounts', icon: Tag, label: 'Скидки' },
{ to: '/rental', icon: CalendarClock, label: 'Аренда объектов' },
// service
{ to: '/housekeeping', icon: MODULES_DATA.find(m => m.id === 'housekeeping')?.sidebarItem?.icon ?? Wrench, label: 'Уборка' },
{ to: '/technical', icon: Zap, label: 'Тех. задачи' },
...activeModuleItems.map(m => ({ to: m.sidebarItem!.path, icon: m.sidebarItem!.icon, label: m.sidebarItem!.label })),
// management
{ to: '/users', icon: UserCog, label: 'Сотрудники' },
{ to: '/schedule', icon: CalendarClock, label: 'График работы' },
{ to: '/loyalty', icon: Award, label: 'Лояльность' },
{ to: '/maintenance', icon: Wrench, label: 'Тех. перерывы' },
{ to: '/floor-map', icon: Map, label: 'План этажей' },
{ to: '/channels', icon: Globe, label: 'Каналы' },
// settings
{ to: '/modules', icon: Puzzle, label: 'Модули' },
{ to: '/settings', icon: Settings, label: 'Настройки' },
{ to: '/equipment', icon: Monitor, label: 'Оборудование' },
{ to: '/wifi', icon: Wifi, label: 'WiFi авторизация' },
{ to: '/ttlock', icon: KeyRound, label: 'Электронные замки' },
// dev
{ to: '/api-docs', icon: FileText, label: 'API & Документация' },
]
const np = { favorites, onToggleFavorite: toggleFavorite, onClick: onClose }
const navItemProps = { favorites, onToggleFavorite: toggleFavorite, onClick: onClose }
// Helper: module icon lookup
const modIcon = (id: string) => MODULES_DATA.find(m => m.id === id)?.sidebarItem?.icon ?? Wrench
return (
<>
{open && (
<div
className="fixed inset-0 bg-black/40 z-40 lg:hidden"
onClick={onClose}
/>
<div className="fixed inset-0 bg-black/40 z-40 lg:hidden" onClick={onClose} />
)}
<aside className={cn(
@@ -275,9 +198,7 @@ export function Sidebar({ open, onClose }: SidebarProps) {
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium mb-0.5">Текущий отель</p>
<div className="flex items-center gap-1.5">
<Building2 size={14} className="text-brand-600 shrink-0" />
<span className="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">
{user.hotelName}
</span>
<span className="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">{user.hotelName}</span>
</div>
</div>
)}
@@ -289,9 +210,9 @@ 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="/admin" icon={LayoutDashboard} label="Дашборд" {...navItemProps} />
<NavItem to="/admin/hotels" icon={Building2} label="Отели" {...navItemProps} />
<NavItem to="/admin/users" icon={Users} label="Пользователи" {...navItemProps} />
<NavItem to="/admin" icon={LayoutDashboard} label="Дашборд" {...np} />
<NavItem to="/admin/hotels" icon={Building2} label="Отели" {...np} />
<NavItem to="/admin/users" icon={Users} label="Пользователи" {...np} />
</>
) : (
<>
@@ -303,135 +224,117 @@ export function Sidebar({ open, onClose }: SidebarProps) {
<span>Избранное</span>
</div>
{favorites.map(path => {
const item = allItems.find(i => i.to === path)
const item = ALL_ITEMS.find(i => i.to === path)
if (!item) return null
return (
<NavItem
key={path}
to={item.to}
icon={item.icon}
label={item.label}
{...navItemProps}
/>
)
return <NavItem key={path} to={item.to} icon={item.icon} label={item.label} {...np} />
})}
</div>
)}
{/* ── Основное ── */}
<GroupHeader label="Основное" isOpen={groups.basics} onToggle={() => toggleGroup('basics')} />
{groups.basics && (
<div className="space-y-0.5">
{can('calendar') && <NavItem to="/calendar" icon={CalendarDays} label="Шахматка" {...navItemProps} />}
{can('bookings') && <NavItem to="/bookings" icon={BookOpen} label="Бронирования" {...navItemProps} />}
{can('guests') && <NavItem to="/guests" icon={UsersRound} label="Гости" {...navItemProps} />}
{can('rooms') && <NavItem to="/rooms" icon={BedDouble} label="Номера" {...navItemProps} />}
{can('rooms') && <NavItem to="/room-categories" icon={LayoutGrid} label="Категории номеров" {...navItemProps} />}
{can('availability') && <NavItem to="/availability" icon={CalendarRange} label="Доступность" {...navItemProps} />}
</div>
)}
{/* ── Цены ── */}
{(can('tariffs') || can('pricing') || can('discounts') || (can('rental') && isModuleActive('rental'))) && (
{/* ══ ФРОНТ-ДЕСК ══ */}
{(can('calendar') || can('bookings') || can('guests') || can('pos') || can('room_service') || can('reviews')) && (
<>
<GroupHeader label="Цены" isOpen={groups.prices} onToggle={() => toggleGroup('prices')} />
{groups.prices && (
<GroupHeader label="Фронт-деск" isOpen={groups.frontdesk} onToggle={() => toggleGroup('frontdesk')} />
{groups.frontdesk && (
<div className="space-y-0.5">
{can('tariffs') && <NavItem to="/tariffs" icon={Utensils} label="Тарифы" {...navItemProps} />}
{can('pricing') && <NavItem to="/dynamic-pricing" icon={TrendingUp} label="Динамические цены" {...navItemProps} />}
{can('discounts') && <NavItem to="/discounts" icon={Tag} label="Скидки" {...navItemProps} />}
{can('rental') && isModuleActive('rental') && (
<NavItem to="/rental" icon={CalendarClock} label="Аренда объектов" {...navItemProps} />
)}
{can('calendar') && <NavItem to="/calendar" icon={CalendarDays} label="Шахматка" {...np} />}
{can('bookings') && <NavItem to="/bookings" icon={BookOpen} label="Бронирования" {...np} />}
{can('guests') && <NavItem to="/guests" icon={UsersRound} label="Гости" {...np} />}
{can('pos') && isActive('pos') && <NavItem to="/pos" icon={modIcon('pos')} label="Касса" {...np} />}
{can('room_service')&& isActive('room-service')&& <NavItem to="/room-service" icon={modIcon('room-service')} label="Room Service" {...np} />}
{can('reviews') && isActive('reviews') && <NavItem to="/reviews" icon={modIcon('reviews')} label="Отзывы" {...np} />}
</div>
)}
</>
)}
{/* ── Сервис ── */}
{(can('housekeeping') && isModuleActive('housekeeping') || activeModuleItems.some(m => can(m.id))) && (
{/* ══ НОМЕРНОЙ ФОНД ══ */}
{(can('rooms') || can('availability') || can('housekeeping') || can('maintenance') || can('floor_map')) && (
<>
<GroupHeader label="Сервис" isOpen={groups.service} onToggle={() => toggleGroup('service')} />
{groups.service && (
<GroupHeader label="Номерной фонд" isOpen={groups.rooms} onToggle={() => toggleGroup('rooms')} />
{groups.rooms && (
<div className="space-y-0.5">
{can('housekeeping') && isModuleActive('housekeeping') && (
{can('rooms') && <NavItem to="/rooms" icon={BedDouble} label="Номера" {...np} />}
{can('rooms') && <NavItem to="/room-categories" icon={LayoutGrid} label="Категории номеров" {...np} />}
{can('availability') && <NavItem to="/availability" icon={CalendarRange} label="Доступность" {...np} />}
{can('floor_map') && <NavItem to="/floor-map" icon={Map} label="План этажей" {...np} />}
{can('housekeeping') && isActive('housekeeping') && (
<>
<NavItem
to="/housekeeping"
icon={MODULES_DATA.find(m => m.id === 'housekeeping')!.sidebarItem!.icon}
label="Уборка"
{...navItemProps}
/>
{can('maintenance') && <NavItem to="/technical" icon={Zap} label="Тех. задачи" {...navItemProps} />}
<NavItem to="/housekeeping" icon={modIcon('housekeeping')} label="Уборка" {...np} />
{can('maintenance') && <NavItem to="/technical" icon={Zap} label="Тех. задачи" {...np} />}
</>
)}
{activeModuleItems.filter(m => can(modulePermKey(m.id))).map(m => (
<NavItem
key={m.id}
to={m.sidebarItem!.path}
icon={m.sidebarItem!.icon}
label={m.sidebarItem!.label}
{...navItemProps}
/>
))}
{can('maintenance') && <NavItem to="/maintenance" icon={Wrench} label="Тех. перерывы" {...np} />}
</div>
)}
</>
)}
{/* ── Управление ── */}
{(can('users') || can('documents') || can('loyalty') || can('maintenance') || can('floor_map') || (can('channels') && isModuleActive('channel-manager'))) && (
{/* ══ ПРОДАЖИ ══ */}
{(can('tariffs') || can('pricing') || can('discounts') || can('rental') || can('channels') || can('website')) && (
<>
<GroupHeader label="Управление" isOpen={groups.management} onToggle={() => toggleGroup('management')} />
{groups.management && (
<GroupHeader label="Продажи" isOpen={groups.sales} onToggle={() => toggleGroup('sales')} />
{groups.sales && (
<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('floor_map') && <NavItem to="/floor-map" icon={Map} label="План этажей" {...navItemProps} />}
{can('channels') && isModuleActive('channel-manager') && (
<NavItem
to="/channels"
icon={MODULES_DATA.find(m => m.id === 'channel-manager')!.sidebarItem!.icon}
label="Каналы"
{...navItemProps}
/>
)}
{can('tariffs') && <NavItem to="/tariffs" icon={Utensils} label="Тарифы" {...np} />}
{can('pricing') && <NavItem to="/dynamic-pricing" icon={TrendingUp} label="Динамические цены" {...np} />}
{can('discounts') && <NavItem to="/discounts" icon={Tag} label="Скидки" {...np} />}
{can('rental') && isActive('rental') && <NavItem to="/rental" icon={modIcon('rental')} label="Аренда объектов" {...np} />}
{can('channels') && isActive('channel-manager') && <NavItem to="/channels" icon={modIcon('channel-manager')} label="Каналы продаж" {...np} />}
{can('website') && isActive('website-builder') && <NavItem to="/website" icon={modIcon('website-builder')} label="Сайт отеля" {...np} />}
{can('website') && isActive('booking-widget') && <NavItem to="/booking-widget" icon={modIcon('booking-widget')} label="Онлайн-бронирование" {...np} />}
</div>
)}
</>
)}
{/* ── Настройки ── */}
{/* ══ АНАЛИТИКА ══ */}
{(can('reports') || can('loyalty')) && (
<>
<GroupHeader label="Аналитика" isOpen={groups.analytics} onToggle={() => toggleGroup('analytics')} />
{groups.analytics && (
<div className="space-y-0.5">
{can('reports') && isActive('olap-reports') && <NavItem to="/reports" icon={BarChart3} label="Отчёты" {...np} />}
{can('loyalty') && <NavItem to="/loyalty" icon={Award} label="Лояльность" {...np} />}
</div>
)}
</>
)}
{/* ══ ПЕРСОНАЛ ══ */}
{(can('users') || can('documents')) && (
<>
<GroupHeader label="Персонал" isOpen={groups.staff} onToggle={() => toggleGroup('staff')} />
{groups.staff && (
<div className="space-y-0.5">
{can('users') && <NavItem to="/users" icon={UserCog} label="Сотрудники" {...np} />}
{can('users') && <NavItem to="/schedule" icon={CalendarClock} label="График работы" {...np} />}
{can('documents') && isActive('documents')
&& <NavItem to="/documents" icon={modIcon('documents')} label="Документы" {...np} />}
</div>
)}
</>
)}
{/* ══ НАСТРОЙКИ ══ */}
{(can('settings') || can('equipment') || can('wifi') || can('ttlock')) && (
<>
<GroupHeader label="Настройки" isOpen={groups.settingsGroup} onToggle={() => toggleGroup('settingsGroup')} />
{groups.settingsGroup && (
<div className="space-y-0.5">
{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>
)}
</>
)}
{/* ── Разработчикам ── */}
{can('settings') && (
<>
<GroupHeader label="Разработчикам" isOpen={groups.devGroup} onToggle={() => toggleGroup('devGroup')} />
{groups.devGroup && (
<div className="space-y-0.5">
<NavItem to="/api-docs" icon={FileText} label="API & Документация" {...navItemProps} />
{can('settings') && <NavItem to="/settings" icon={Settings} label="Общие настройки" {...np} />}
{can('settings') && <NavItem to="/modules" icon={Puzzle} label="Модули" {...np} />}
{can('settings') && <NavItem to="/billing" icon={CreditCard} label="Тарифы HotelSync" {...np} />}
{can('settings') && <NavItem to="/settings/checklists" icon={ListChecks} label="Чек-листы уборки" {...np} />}
{can('settings') && <NavItem to="/settings/payments" icon={CreditCard} label="Способы оплаты" {...np} />}
{can('settings') && <NavItem to="/settings/minibar" icon={ShoppingCart} label="Минибар" {...np} />}
{can('settings') && <NavItem to="/settings/deposit" icon={ShieldCheck} label="Депозит" {...np} />}
{can('equipment')&& <NavItem to="/equipment" icon={Monitor} label="Оборудование" {...np} />}
{can('wifi') && <NavItem to="/wifi" icon={Wifi} label="WiFi авторизация" {...np} />}
{can('ttlock') && <NavItem to="/ttlock" icon={KeyRound} label="Эл. замки TTLock" {...np} />}
{can('settings') && isActive('migration')
&& <NavItem to="/migration" icon={modIcon('migration')} label="Миграция данных" {...np} />}
{can('settings') && <NavItem to="/api-docs" icon={FileText} label="API документация" {...np} />}
</div>
)}
</>
@@ -447,3 +350,41 @@ export function Sidebar({ open, onClose }: SidebarProps) {
</>
)
}
// ── All items list for favorites lookup ────────────────────────────────────
const modIcon2 = (id: string) => MODULES_DATA.find(m => m.id === id)?.sidebarItem?.icon ?? Wrench
const ALL_ITEMS: { to: string; icon: React.ElementType; label: string }[] = [
{ to: '/calendar', icon: CalendarDays, label: 'Шахматка' },
{ to: '/bookings', icon: BookOpen, label: 'Бронирования' },
{ to: '/guests', icon: UsersRound, label: 'Гости' },
{ to: '/pos', icon: modIcon2('pos'), label: 'Касса' },
{ to: '/room-service', icon: modIcon2('room-service'), label: 'Room Service' },
{ to: '/reviews', icon: modIcon2('reviews'), label: 'Отзывы' },
{ to: '/rooms', icon: BedDouble, label: 'Номера' },
{ to: '/room-categories', icon: LayoutGrid, label: 'Категории номеров' },
{ to: '/availability', icon: CalendarRange, label: 'Доступность' },
{ to: '/floor-map', icon: Map, label: 'План этажей' },
{ to: '/housekeeping', icon: modIcon2('housekeeping'), label: 'Уборка' },
{ to: '/technical', icon: Zap, label: 'Тех. задачи' },
{ to: '/maintenance', icon: Wrench, label: 'Тех. перерывы' },
{ to: '/tariffs', icon: Utensils, label: 'Тарифы' },
{ to: '/dynamic-pricing', icon: TrendingUp, label: 'Динамические цены' },
{ to: '/discounts', icon: Tag, label: 'Скидки' },
{ to: '/rental', icon: modIcon2('rental'), label: 'Аренда объектов' },
{ to: '/channels', icon: modIcon2('channel-manager'), label: 'Каналы продаж' },
{ to: '/website', icon: modIcon2('website-builder'), label: 'Сайт отеля' },
{ to: '/booking-widget', icon: modIcon2('booking-widget'), label: 'Онлайн-бронирование' },
{ to: '/reports', icon: BarChart3, label: 'Отчёты' },
{ to: '/loyalty', icon: Award, label: 'Лояльность' },
{ to: '/users', icon: UserCog, label: 'Сотрудники' },
{ to: '/schedule', icon: CalendarClock, label: 'График работы' },
{ to: '/documents', icon: modIcon2('documents'), label: 'Документы' },
{ to: '/settings', icon: Settings, label: 'Общие настройки' },
{ to: '/modules', icon: Puzzle, label: 'Модули' },
{ to: '/billing', icon: CreditCard, label: 'Тарифы HotelSync' },
{ to: '/equipment', icon: Monitor, label: 'Оборудование' },
{ to: '/wifi', icon: Wifi, label: 'WiFi авторизация' },
{ to: '/ttlock', icon: KeyRound, label: 'Эл. замки TTLock' },
{ to: '/api-docs', icon: FileText, label: 'API документация' },
]

View File

@@ -1107,6 +1107,7 @@ export function UsersPage() {
{/* User modal */}
<UserModal
key={editing?.id ?? 'new'}
open={modalOpen}
user={editing}
onClose={() => setModalOpen(false)}