Add Availability Calendar, Modules system, and route refactoring

- Add AvailabilityPage with price grid (categories × dates), drag-select,
  channel prices, EditPanel, PeriodModal, and rate periods tab
- Add ModulesContext with localStorage persistence and dynamic sidebar items
- Add ModulesPage with WiFi Auth, Payments, TV Welcome, Smart Locks,
  OLAP Reports, Website Builder, Booking Widget modules
- Add ReportsPage (OLAP analytics with KPI cards, charts, tables)
- Add WebsitePage and BookingWidgetPage placeholders
- Remove hotel slug from URL routing; hotel context from JWT
- Add Доступность nav item in Sidebar under Управление
- Fix LoginPage redirects and HotelSync branding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 14:43:18 +03:00
parent 63c9ae574f
commit b277e3e38d
14 changed files with 1869 additions and 78 deletions

View File

@@ -63,6 +63,8 @@ export const MOCK_USERS: User[] = [
name: 'Елена Смирнова',
role: 'hotel_manager',
hotelId: 'hotel-1',
hotelName: 'Grand Palace Hotel',
hotelSlug: 'grand-palace',
},
{
id: 'user-housekeeper-1',
@@ -70,6 +72,8 @@ export const MOCK_USERS: User[] = [
name: 'Мария Иванова',
role: 'housekeeper',
hotelId: 'hotel-1',
hotelName: 'Grand Palace Hotel',
hotelSlug: 'grand-palace',
},
]

205
src/data/modulesData.ts Normal file
View File

@@ -0,0 +1,205 @@
import {
Wifi, CreditCard, Tv2, KeyRound,
BarChart3, Globe, CalendarCheck2,
} from 'lucide-react'
import type { ElementType } from 'react'
export type ModuleStatus = 'active' | 'inactive' | 'trial'
export interface ModuleSidebarItem {
path: string
label: string
icon: ElementType
}
export interface ModuleDef {
id: string
name: string
tagline: string
description: string
icon: ElementType
iconBg: string
iconColor: string
accentColor: string
price: number
trialDays?: number
features: string[]
stats?: { label: string; value: string }[]
badge?: string
/** Если задан — при активации модуля этот пункт появляется в сайдбаре */
sidebarItem?: ModuleSidebarItem
}
export const MODULES_DATA: ModuleDef[] = [
{
id: 'wifi-auth',
name: 'Wi-Fi Авторизация',
tagline: 'Captive portal для гостей',
description:
'Персонализированная страница входа в Wi-Fi с брендингом отеля. Сбор контактов гостей, автовыдача паролей, статистика подключений.',
icon: Wifi,
iconBg: 'bg-blue-100 dark:bg-blue-900/40',
iconColor: 'text-blue-600 dark:text-blue-400',
accentColor: 'bg-blue-500',
price: 2900,
features: [
'Брендированная страница входа',
'Авторизация по номеру телефона / коду бронирования',
'Сбор email для маркетинга',
'Ограничение скорости по тарифу номера',
'Статистика: устройства, время онлайн, трафик',
],
stats: [
{ label: 'Подключений сегодня', value: '47' },
{ label: 'Активных устройств', value: '31' },
{ label: 'Конверсия email', value: '68%' },
],
},
{
id: 'payments',
name: 'Депозиты и страховки',
tagline: 'Онлайн-оплата при бронировании',
description:
'Автоматический сбор страхового депозита и предоплаты. Интеграция с Тинькофф, СБП, ЮKassa. Умное удержание и автовозврат при выезде.',
icon: CreditCard,
iconBg: 'bg-emerald-100 dark:bg-emerald-900/40',
iconColor: 'text-emerald-600 dark:text-emerald-400',
accentColor: 'bg-emerald-500',
price: 4900,
trialDays: 12,
badge: 'Популярный',
features: [
'Приём депозита онлайн при заезде',
'Интеграция: Тинькофф, ЮKassa, СБП',
'Автоматический возврат при выезде',
'Удержание с подтверждением менеджера',
'Отчёты и история транзакций',
'Уведомления гостю и администратору',
],
stats: [
{ label: 'Собрано за месяц', value: '₽ 184 000' },
{ label: 'Активных депозитов', value: '14' },
{ label: 'Возвратов сегодня', value: '3' },
],
},
{
id: 'tv-welcome',
name: 'Приветствие на TV',
tagline: 'Welcome screen в номере',
description:
'Персональное приветствие на телевизоре при заезде. Расписание ресторана, прогноз погоды, QR-код для заказа услуг. Поддержка Samsung, LG, Android TV.',
icon: Tv2,
iconBg: 'bg-violet-100 dark:bg-violet-900/40',
iconColor: 'text-violet-600 dark:text-violet-400',
accentColor: 'bg-violet-500',
price: 3500,
badge: 'Новинка',
features: [
'Персональное приветствие по имени гостя',
'Расписание: ресторан, спа, трансфер',
'Прогноз погоды и местные новости',
'Заказ услуг и room service с TV',
'Поддержка Samsung SSSP, LG webOS, Android TV',
'Брендирование под стиль отеля',
],
},
{
id: 'smart-locks',
name: 'Электронные замки',
tagline: 'Интеграция СКУД и смарт-замков',
description:
'Управление доступом в номера через PMS. Автовыдача кода при заезде, блокировка при выезде. Поддержка Salto, Dormakaba, TTLock и других систем.',
icon: KeyRound,
iconBg: 'bg-amber-100 dark:bg-amber-900/40',
iconColor: 'text-amber-600 dark:text-amber-400',
accentColor: 'bg-amber-500',
price: 5900,
features: [
'Автовыдача PIN-кода / мобильного ключа при заезде',
'Автоблокировка при выезде или отмене',
'История открытий с временными метками',
'Поддержка: Salto KS, Dormakaba, TTLock, ASSA ABLOY',
'Экстренный доступ для персонала',
'Интеграция с мобильным приложением гостя',
],
},
{
id: 'olap-reports',
name: 'OLAP Отчёты',
tagline: 'Аналитика и бизнес-отчёты',
description:
'Глубокая аналитика по загрузке, выручке, каналам продаж и гостям. Конструктор отчётов, экспорт в Excel/PDF, автоотправка руководству.',
icon: BarChart3,
iconBg: 'bg-cyan-100 dark:bg-cyan-900/40',
iconColor: 'text-cyan-600 dark:text-cyan-400',
accentColor: 'bg-cyan-500',
price: 3900,
badge: 'Популярный',
features: [
'Загрузка (OCC), ADR, RevPAR в динамике',
'Анализ по источникам бронирования',
'Портрет гостя: география, повторные визиты',
'Конструктор произвольных отчётов',
'Экспорт в Excel, PDF, Google Sheets',
'Автоотправка отчётов по расписанию на email',
'Сравнение периодов и план/факт',
],
sidebarItem: {
path: '/reports',
label: 'Аналитика',
icon: BarChart3,
},
},
{
id: 'website-builder',
name: 'Конструктор сайта',
tagline: 'Сайт отеля без разработчика',
description:
'Drag & drop конструктор сайта с готовыми шаблонами для отелей. SEO-оптимизация, мультиязычность, интеграция с модулем онлайн-бронирования.',
icon: Globe,
iconBg: 'bg-rose-100 dark:bg-rose-900/40',
iconColor: 'text-rose-600 dark:text-rose-400',
accentColor: 'bg-rose-500',
price: 4500,
features: [
'Готовые шаблоны для отелей и апартаментов',
'Drag & drop редактор страниц',
'Галерея номеров с виртуальными турами',
'SEO-оптимизация и sitemap',
'Мультиязычность (RU, EN, DE, ZH)',
'Интеграция с модулем онлайн-бронирования',
'Кастомный домен и SSL',
],
sidebarItem: {
path: '/website',
label: 'Сайт отеля',
icon: Globe,
},
},
{
id: 'booking-widget',
name: 'Онлайн-бронирование',
tagline: 'Виджет прямых продаж',
description:
'Виджет бронирования для сайта отеля с оплатой картой. Прямые продажи без комиссии OTA. Подбор номеров, тарифы, спецпредложения.',
icon: CalendarCheck2,
iconBg: 'bg-indigo-100 dark:bg-indigo-900/40',
iconColor: 'text-indigo-600 dark:text-indigo-400',
accentColor: 'bg-indigo-500',
price: 3200,
features: [
'Виджет бронирования на сайт отеля',
'Оплата картой, СБП, ЮKassa',
'Тарифы и спецпредложения',
'Промокоды и программа лояльности',
'Подтверждение на email и SMS',
'Минимизация комиссий OTA',
'Аналитика конверсии',
],
sidebarItem: {
path: '/booking-widget',
label: 'Онлайн-бронирование',
icon: CalendarCheck2,
},
},
]

117
src/data/ratesData.ts Normal file
View File

@@ -0,0 +1,117 @@
import { addDays, format, getDay } from 'date-fns'
export interface RoomCategory {
id: string
name: string
color: string // tailwind bg class
textColor: string // tailwind text class
basePrice: number
extraPersonPrice: number
maxPersons: number
}
export interface ChannelRate {
channelId: string
price: number // цена для этого канала
}
export interface PriceCell {
price: number
extraPerson: number
minNights: number
channelPrices: Record<string, number> // channelId -> price
closed: boolean // закрыто для продажи
}
export interface RatePeriod {
id: string
name: string
startDate: string
endDate: string
notes?: string
categoryPrices: Record<string, number> // categoryId -> price
channelMarkup: Record<string, number> // channelId -> multiplier (1.15 = +15%)
extraPersonPrice: number
minNights: number
daysOfWeek?: number[] // если задано — применяется только для этих дней (0=вс,1=пн...)
}
export const RATE_CHANNELS = [
{ id: 'direct', name: 'Прямые', icon: '🏨', color: 'text-brand-600' },
{ id: 'booking_com', name: 'Booking.com', icon: '🔵', color: 'text-blue-600' },
{ id: 'airbnb', name: 'Airbnb', icon: '🔴', color: 'text-rose-600' },
{ id: 'expedia', name: 'Expedia', icon: '🟡', color: 'text-amber-600' },
]
export const ROOM_CATEGORIES: RoomCategory[] = [
{ id: 'standard', name: 'Стандарт', color: 'bg-slate-500', textColor: 'text-slate-600', basePrice: 3500, extraPersonPrice: 800, maxPersons: 2 },
{ id: 'deluxe', name: 'Делюкс', color: 'bg-brand-500', textColor: 'text-brand-600', basePrice: 5200, extraPersonPrice: 1000, maxPersons: 3 },
{ id: 'suite', name: 'Сюит', color: 'bg-violet-500', textColor: 'text-violet-600', basePrice: 8900, extraPersonPrice: 1500, maxPersons: 4 },
{ id: 'junior_suite', name: 'Джуниор Сюит', color: 'bg-cyan-500', textColor: 'text-cyan-600', basePrice: 6800, extraPersonPrice: 1200, maxPersons: 4 },
{ id: 'penthouse', name: 'Пентхаус', color: 'bg-amber-500', textColor: 'text-amber-600', basePrice: 18000, extraPersonPrice: 2500, maxPersons: 6 },
]
// Дефолтные мультипликаторы каналов (markup к базовой цене)
export const DEFAULT_CHANNEL_MARKUP: Record<string, number> = {
direct: 1.00,
booking_com: 1.00, // net price — Booking забирает свою комиссию сверху
airbnb: 1.05,
expedia: 1.03,
}
// Генерируем начальную сетку цен на 60 дней
export function buildInitialPriceGrid(): Record<string, Record<string, PriceCell>> {
const result: Record<string, Record<string, PriceCell>> = {}
const today = new Date()
for (const cat of ROOM_CATEGORIES) {
result[cat.id] = {}
for (let i = 0; i < 60; i++) {
const d = addDays(today, i)
const dateStr = format(d, 'yyyy-MM-dd')
const dow = getDay(d)
const isWeekend = dow === 5 || dow === 6
const basePrice = Math.round(cat.basePrice * (isWeekend ? 1.25 : 1.0))
const channelPrices: Record<string, number> = {}
for (const ch of RATE_CHANNELS) {
channelPrices[ch.id] = Math.round(basePrice * DEFAULT_CHANNEL_MARKUP[ch.id])
}
result[cat.id][dateStr] = {
price: basePrice,
extraPerson: cat.extraPersonPrice,
minNights: 1,
channelPrices,
closed: false,
}
}
}
return result
}
// Демо-периоды
export const DEMO_PERIODS: RatePeriod[] = [
{
id: 'p1',
name: 'Праздники 8 марта',
startDate: '2026-03-06',
endDate: '2026-03-10',
categoryPrices: { standard: 4500, deluxe: 6500, suite: 11000, junior_suite: 8500, penthouse: 22000 },
channelMarkup: { direct: 1.0, booking_com: 1.0, airbnb: 1.05, expedia: 1.03 },
extraPersonPrice: 1200,
minNights: 2,
notes: 'Повышенный спрос на праздники',
},
{
id: 'p2',
name: 'Майские праздники',
startDate: '2026-04-30',
endDate: '2026-05-10',
categoryPrices: { standard: 5000, deluxe: 7200, suite: 12500, junior_suite: 9800, penthouse: 25000 },
channelMarkup: { direct: 1.0, booking_com: 1.0, airbnb: 1.08, expedia: 1.05 },
extraPersonPrice: 1500,
minNights: 3,
notes: 'Минимальный заезд 3 ночи',
},
]