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:
@@ -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
205
src/data/modulesData.ts
Normal 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
117
src/data/ratesData.ts
Normal 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 ночи',
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user