From d26ad30433bee6d86c7f4b051f71b78a2afe42bf Mon Sep 17 00:00:00 2001
From: HotelSync
Date: Fri, 17 Apr 2026 12:26:03 +0300
Subject: [PATCH] =?UTF-8?q?fix:=20edit=20user=20modal=20empty=20fields=20(?=
=?UTF-8?q?key=20prop);=20refactor:=20sidebar=20concept=20A=20=E2=80=94=20?=
=?UTF-8?q?by=20work=20role?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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
---
src/components/layout/Sidebar.tsx | 385 +++++++++++++-----------------
src/pages/UsersPage.tsx | 1 +
2 files changed, 164 insertions(+), 222 deletions(-)
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index bb51e06..8f347ed 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -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 = {
- 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 {
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) {
- 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 (
{ e.preventDefault(); e.stopPropagation(); onToggleFavorite(to) }}
title={isFav ? 'Убрать из избранного' : 'Добавить в избранное'}
>
-
+
)
}
// ── 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 (
-
- {user.hotelName}
-
+ {user.hotelName}
)}
@@ -289,9 +210,9 @@ export function Sidebar({ open, onClose }: SidebarProps) {
Администрирование
-
-
-
+
+
+
>
) : (
<>
@@ -303,135 +224,117 @@ export function Sidebar({ open, onClose }: SidebarProps) {
Избранное
{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 (
-
- )
+ return
})}
)}
- {/* ── Основное ── */}
- toggleGroup('basics')} />
- {groups.basics && (
-
- {can('calendar') && }
- {can('bookings') && }
- {can('guests') && }
- {can('rooms') && }
- {can('rooms') && }
- {can('availability') && }
-
- )}
-
- {/* ── Цены ── */}
- {(can('tariffs') || can('pricing') || can('discounts') || (can('rental') && isModuleActive('rental'))) && (
+ {/* ══ ФРОНТ-ДЕСК ══ */}
+ {(can('calendar') || can('bookings') || can('guests') || can('pos') || can('room_service') || can('reviews')) && (
<>
- toggleGroup('prices')} />
- {groups.prices && (
+ toggleGroup('frontdesk')} />
+ {groups.frontdesk && (
- {can('tariffs') && }
- {can('pricing') && }
- {can('discounts') && }
- {can('rental') && isModuleActive('rental') && (
-
- )}
+ {can('calendar') && }
+ {can('bookings') && }
+ {can('guests') && }
+ {can('pos') && isActive('pos') && }
+ {can('room_service')&& isActive('room-service')&& }
+ {can('reviews') && isActive('reviews') && }
)}
>
)}
- {/* ── Сервис ── */}
- {(can('housekeeping') && isModuleActive('housekeeping') || activeModuleItems.some(m => can(m.id))) && (
+ {/* ══ НОМЕРНОЙ ФОНД ══ */}
+ {(can('rooms') || can('availability') || can('housekeeping') || can('maintenance') || can('floor_map')) && (
<>
- toggleGroup('service')} />
- {groups.service && (
+ toggleGroup('rooms')} />
+ {groups.rooms && (
- {can('housekeeping') && isModuleActive('housekeeping') && (
+ {can('rooms') && }
+ {can('rooms') && }
+ {can('availability') && }
+ {can('floor_map') && }
+ {can('housekeeping') && isActive('housekeeping') && (
<>
- m.id === 'housekeeping')!.sidebarItem!.icon}
- label="Уборка"
- {...navItemProps}
- />
- {can('maintenance') && }
+
+ {can('maintenance') && }
>
)}
- {activeModuleItems.filter(m => can(modulePermKey(m.id))).map(m => (
-
- ))}
+ {can('maintenance') && }
)}
>
)}
- {/* ── Управление ── */}
- {(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')) && (
<>
- toggleGroup('management')} />
- {groups.management && (
+ toggleGroup('sales')} />
+ {groups.sales && (
- {can('users') && }
- {can('users') && }
- {can('documents') && }
- {can('loyalty') && }
- {can('maintenance') && }
- {can('floor_map') && }
- {can('channels') && isModuleActive('channel-manager') && (
- m.id === 'channel-manager')!.sidebarItem!.icon}
- label="Каналы"
- {...navItemProps}
- />
- )}
+ {can('tariffs') && }
+ {can('pricing') && }
+ {can('discounts') && }
+ {can('rental') && isActive('rental') && }
+ {can('channels') && isActive('channel-manager') && }
+ {can('website') && isActive('website-builder') && }
+ {can('website') && isActive('booking-widget') && }
)}
>
)}
- {/* ── Настройки ── */}
+ {/* ══ АНАЛИТИКА ══ */}
+ {(can('reports') || can('loyalty')) && (
+ <>
+ toggleGroup('analytics')} />
+ {groups.analytics && (
+
+ {can('reports') && isActive('olap-reports') && }
+ {can('loyalty') && }
+
+ )}
+ >
+ )}
+
+ {/* ══ ПЕРСОНАЛ ══ */}
+ {(can('users') || can('documents')) && (
+ <>
+ toggleGroup('staff')} />
+ {groups.staff && (
+
+ {can('users') && }
+ {can('users') && }
+ {can('documents') && isActive('documents')
+ && }
+
+ )}
+ >
+ )}
+
+ {/* ══ НАСТРОЙКИ ══ */}
{(can('settings') || can('equipment') || can('wifi') || can('ttlock')) && (
<>
toggleGroup('settingsGroup')} />
{groups.settingsGroup && (
- {can('settings') && }
- {can('settings') && }
- {can('settings') && }
- {can('equipment') && }
- {can('wifi') && }
- {can('ttlock') && }
- {can('settings') && }
- {can('settings') && }
- {can('settings') && }
- {can('settings') && }
-
- )}
- >
- )}
-
- {/* ── Разработчикам ── */}
- {can('settings') && (
- <>
- toggleGroup('devGroup')} />
- {groups.devGroup && (
-
-
+ {can('settings') && }
+ {can('settings') && }
+ {can('settings') && }
+ {can('settings') && }
+ {can('settings') && }
+ {can('settings') && }
+ {can('settings') && }
+ {can('equipment')&& }
+ {can('wifi') && }
+ {can('ttlock') && }
+ {can('settings') && isActive('migration')
+ && }
+ {can('settings') && }
)}
>
@@ -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 документация' },
+]
diff --git a/src/pages/UsersPage.tsx b/src/pages/UsersPage.tsx
index 6bf75b5..5f07441 100644
--- a/src/pages/UsersPage.tsx
+++ b/src/pages/UsersPage.tsx
@@ -1107,6 +1107,7 @@ export function UsersPage() {
{/* User modal */}
setModalOpen(false)}