- 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>
118 lines
4.5 KiB
TypeScript
118 lines
4.5 KiB
TypeScript
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 ночи',
|
|
},
|
|
]
|