Major UX improvements across multiple pages

Шахматка:
- Date/period picker dropdown on navigation button (choose start date + days window)
- Cancelled bookings fade out with animation after 1 second

Бронирования:
- Clickable column headers with sort asc/desc (Гость, Заезд, Выезд, Статус, Источник, Сумма)

Страница входа:
- Removed role-based account selector — just email + password
- System auto-detects role/hotel from credentials

Настройки:
- New "Бронирование" section with room assignment strategy (spread/together/sequential/manual)
- Notifications: SMTP email config + SMS provider config (SMSC, SMS.ru, МТС, etc.)

Модули:
- Added Housekeeping and Channel Manager as proper modules
- Channel Manager lists: Booking.com, Airbnb, Яндекс Путешествия, Островок, Суточно.ру, OneTwoTrip
- Housekeeping visible to all roles (including housekeeper) via module status
- Sidebar now uses module status to show/hide Уборка and Каналы

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 15:20:05 +03:00
parent 4e615359eb
commit c233590c4b
29 changed files with 2171 additions and 149 deletions

View File

@@ -1,6 +1,6 @@
import { NavLink, useNavigate } from 'react-router-dom'
import {
CalendarDays, BookOpen, BedDouble, Sparkles, Globe, Settings,
CalendarDays, BookOpen, BedDouble, Globe, Settings,
FileText, LayoutDashboard, Building2, Users, X, Hotel, Puzzle, CalendarRange,
} from 'lucide-react'
import { useAuth } from '../../contexts/AuthContext'
@@ -41,9 +41,17 @@ export function Sidebar({ open, onClose }: SidebarProps) {
const isAdmin = user?.role === 'super_admin'
const isHousekeeper = user?.role === 'housekeeper'
// Активные модули у которых есть sidebarItem
const isModuleActive = (id: string) => statuses[id] === 'active' || statuses[id] === 'trial'
// Core modules with dedicated nav positions (not shown in generic module list)
const CORE_MODULE_IDS = ['housekeeping', 'channel-manager']
// Extra module items for the "Модули" sidebar section (excluding core ones)
const activeModuleItems = MODULES_DATA.filter(
m => m.sidebarItem && (statuses[m.id] === 'active' || statuses[m.id] === 'trial'),
m => m.sidebarItem &&
!m.sidebarItem.showForHousekeeper &&
!CORE_MODULE_IDS.includes(m.id) &&
isModuleActive(m.id),
)
return (
@@ -98,33 +106,48 @@ 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="Дашборд" onClick={onClose} />
<NavItem to="/admin/hotels" icon={Building2} label="Отели" onClick={onClose} />
<NavItem to="/admin/users" icon={Users} label="Пользователи" onClick={onClose} />
<NavItem to="/admin" icon={LayoutDashboard} label="Дашборд" onClick={onClose} />
<NavItem to="/admin/hotels" icon={Building2} label="Отели" onClick={onClose} />
<NavItem to="/admin/users" icon={Users} label="Пользователи" onClick={onClose} />
</>
) : (
<>
<p className="px-3 pt-2 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
Основное
</p>
<NavItem to="/calendar" icon={CalendarDays} label="Шахматка" onClick={onClose} />
<NavItem to="/calendar" icon={CalendarDays} label="Шахматка" onClick={onClose} />
{!isHousekeeper && (
<NavItem to="/bookings" icon={BookOpen} label="Бронирования" onClick={onClose} />
<NavItem to="/bookings" icon={BookOpen} label="Бронирования" onClick={onClose} />
)}
{/* Housekeeping — controlled by module status, visible to all roles */}
{isModuleActive('housekeeping') && (
<NavItem
to="/housekeeping"
icon={MODULES_DATA.find(m => m.id === 'housekeeping')!.sidebarItem!.icon}
label="Уборка"
onClick={onClose}
/>
)}
<NavItem to="/housekeeping" icon={Sparkles} label="Уборка" onClick={onClose} />
{!isHousekeeper && (
<>
<p className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">
Управление
</p>
<NavItem to="/rooms" icon={BedDouble} label="Номера" onClick={onClose} />
<NavItem to="/availability" icon={CalendarRange} label="Доступность" onClick={onClose} />
<NavItem to="/channels" icon={Globe} label="Каналы" onClick={onClose} />
<NavItem to="/modules" icon={Puzzle} label="Модули" onClick={onClose} />
<NavItem to="/settings" icon={Settings} label="Настройки" onClick={onClose} />
<NavItem to="/rooms" icon={BedDouble} label="Номера" onClick={onClose} />
<NavItem to="/availability" icon={CalendarRange} label="Доступность" onClick={onClose} />
{isModuleActive('channel-manager') && (
<NavItem
to="/channels"
icon={MODULES_DATA.find(m => m.id === 'channel-manager')!.sidebarItem!.icon}
label="Каналы"
onClick={onClose}
/>
)}
<NavItem to="/modules" icon={Puzzle} label="Модули" onClick={onClose} />
<NavItem to="/settings" icon={Settings} label="Настройки" onClick={onClose} />
{/* Активные модули с разделами */}
{/* Active addon modules */}
{activeModuleItems.length > 0 && (
<>
<p className="px-3 pt-3 pb-1 text-xs font-semibold text-slate-400 uppercase tracking-wider">