diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 4509113..767178f 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,8 +1,9 @@ -import { NavLink, useNavigate } from 'react-router-dom' +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, + TrendingUp, Tag, Award, Wrench, Utensils, CalendarClock, Zap, ChevronRight, Star, } from 'lucide-react' import { useAuth } from '../../contexts/AuthContext' import { useModules } from '../../contexts/ModulesContext' @@ -14,36 +15,119 @@ interface SidebarProps { onClose: () => void } +// ── Favorites helpers ────────────────────────────────────────────────────── +function loadFavorites(): string[] { + 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' + +const DEFAULT_GROUPS: Record = { + basics: true, + prices: true, + service: true, + management: true, + settingsGroup: true, + devGroup: true, +} + +function loadGroups(): Record { + try { + const stored = JSON.parse(localStorage.getItem('sidebarGroups') || '{}') + return { ...DEFAULT_GROUPS, ...stored } + } catch { + return { ...DEFAULT_GROUPS } + } +} + +function saveGroups(groups: Record) { + localStorage.setItem('sidebarGroups', JSON.stringify(groups)) +} + +// ── NavItem with star ────────────────────────────────────────────────────── function NavItem({ - to, icon: Icon, label, onClick, + to, + icon: Icon, + label, + onClick, + favorites, + onToggleFavorite, }: { to: string icon: React.ElementType label: string onClick?: () => void + favorites: string[] + onToggleFavorite: (path: string) => void }) { + const isFav = favorites.includes(to) + return ( - cn('sidebar-link', isActive && 'active')} - > - - {label} - +
+ cn('sidebar-link', isActive && 'active')} + > + + {label} + + +
) } +// ── Group header ─────────────────────────────────────────────────────────── +function GroupHeader({ + label, + isOpen, + onToggle, +}: { + label: string + isOpen: boolean + onToggle: () => void +}) { + return ( + + ) +} + +// ── Main Sidebar ─────────────────────────────────────────────────────────── export function Sidebar({ open, onClose }: SidebarProps) { const { user } = useAuth() const { statuses } = useModules() const navigate = useNavigate() + const location = useLocation() const isAdmin = user?.role === 'super_admin' const isModuleActive = (id: string) => statuses[id] === 'active' || statuses[id] === 'trial' - // Role-based permission map (which module keys each role can access) + // Role-based permission map const ROLE_PERMS: Record = { hotel_admin: ['*'], manager: ['*'], @@ -62,7 +146,7 @@ export function Sidebar({ open, onClose }: SidebarProps) { // 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 + // Module items for the "Сервис" sidebar section const activeModuleItems = MODULES_DATA.filter( m => m.sidebarItem && !m.sidebarItem.showForHousekeeper && @@ -70,6 +154,90 @@ export function Sidebar({ open, onClose }: SidebarProps) { isModuleActive(m.id), ) + // ── Favorites state ────────────────────────────────────────────────────── + const [favorites, setFavorites] = useState(loadFavorites) + + const toggleFavorite = (path: string) => { + setFavorites(prev => { + const next = prev.includes(path) ? prev.filter(p => p !== path) : [...prev, path] + saveFavorites(next) + return next + }) + } + + // ── Group open/close state ─────────────────────────────────────────────── + const [groups, setGroups] = useState>(loadGroups) + + const toggleGroup = (key: GroupKey) => { + setGroups(prev => { + const next = { ...prev, [key]: !prev[key] } + saveGroups(next) + return next + }) + } + + // Auto-open the group containing the currently active route + useEffect(() => { + const path = location.pathname + + const groupPaths: Record = { + 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'], + settingsGroup:['/modules', '/settings'], + devGroup: ['/api-docs'], + } + + const matchedGroup = (Object.keys(groupPaths) as GroupKey[]).find( + key => groupPaths[key].some(p => path === p || path.startsWith(p + '/')), + ) + + if (matchedGroup) { + setGroups(prev => { + if (prev[matchedGroup]) return prev + const next = { ...prev, [matchedGroup]: true } + saveGroups(next) + return next + }) + } + // 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: 'Настройки' }, + // dev + { to: '/api-docs', icon: FileText, label: 'API & Документация' }, + ] + + const navItemProps = { favorites, onToggleFavorite: toggleFavorite, onClick: onClose } + return ( <> {open && ( @@ -116,96 +284,148 @@ export function Sidebar({ open, onClose }: SidebarProps) { )} {/* Nav */} -