import { useState, useEffect } from 'react' import { Save, Building2, Bell, Shield, Globe, CreditCard, BedDouble, Mail, MessageSquare, Plus, X as XIcon, Send, CheckCircle2, Copy } from 'lucide-react' import { api } from '../lib/api' import { useAuth } from '../contexts/AuthContext' import type { Hotel } from '../types' import { cn, PLAN_COLORS, PLAN_LABELS } from '../lib/utils' import { Badge } from '../components/ui/Badge' import { useTheme } from '../contexts/ThemeContext' const SECTIONS = [ { id: 'general', label: 'Основные', icon: Building2 }, { id: 'booking', label: 'Бронирование', icon: BedDouble }, { id: 'theme', label: 'Внешний вид', icon: Globe }, { id: 'notify', label: 'Уведомления', icon: Bell }, { id: 'security', label: 'Безопасность', icon: Shield }, { id: 'billing', label: 'Тарифный план', icon: CreditCard }, ] function Toggle({ on, onChange }: { on: boolean; onChange: () => void }) { return ( ) } export function SettingsPage() { const { user } = useAuth() const slug = user?.hotelSlug ?? '' const [section, setSection] = useState('general') const [saved, setSaved] = useState(false) const [hotel, setHotel] = useState(null) const { mode, theme, setMode } = useTheme() useEffect(() => { if (!slug) return api.hotels.get(slug).then(setHotel).catch(console.error) }, [slug]) const [calendarCompact, setCalendarCompact] = useState( () => localStorage.getItem('calendarCompact') === 'true', ) const toggleCalendarCompact = () => { setCalendarCompact(v => { const next = !v localStorage.setItem('calendarCompact', String(next)) return next }) } const [form, setForm] = useState({ name: '', address: '', timezone: 'Europe/Moscow', currency: 'RUB', checkInTime: '14:00', checkOutTime: '12:00', }) useEffect(() => { if (!hotel) return setForm({ name: hotel.name ?? '', address: hotel.address ?? '', timezone: hotel.timezone ?? 'Europe/Moscow', currency: hotel.currency ?? 'RUB', checkInTime: hotel.checkInTime ?? '14:00', checkOutTime: hotel.checkOutTime ?? '12:00', }) }, [hotel]) // Booking / assignment settings const [assignmentStrategy, setAssignmentStrategy] = useState<'spread' | 'together' | 'sequential' | 'manual'>('spread') const [showBookingSource, setShowBookingSource] = useState(false) const [requireGuestDocs, setRequireGuestDocs] = useState(false) const [earlyCheckinEnabled, setEarlyCheckinEnabled] = useState(false) const [lateCheckoutEnabled, setLateCheckoutEnabled] = useState(false) const [hotelSettingsLoaded, setHotelSettingsLoaded] = useState(false) useEffect(() => { if (!slug || hotelSettingsLoaded) return api.hotelSettings.get(slug) .then(s => { setRequireGuestDocs(Boolean(s.require_guest_docs)) setEarlyCheckinEnabled(Boolean(s.early_checkin_enabled)) setLateCheckoutEnabled(Boolean(s.late_checkout_enabled)) setHotelSettingsLoaded(true) }) .catch(console.error) }, [slug, hotelSettingsLoaded]) const toggleRequireGuestDocs = async () => { const next = !requireGuestDocs setRequireGuestDocs(next) try { await api.hotelSettings.update(slug, { require_guest_docs: next }) } catch { setRequireGuestDocs(!next) } } const toggleEarlyCheckin = async () => { const next = !earlyCheckinEnabled setEarlyCheckinEnabled(next) try { await api.hotelSettings.update(slug, { early_checkin_enabled: next }) } catch { setEarlyCheckinEnabled(!next) } } const toggleLateCheckout = async () => { const next = !lateCheckoutEnabled setLateCheckoutEnabled(next) try { await api.hotelSettings.update(slug, { late_checkout_enabled: next }) } catch { setLateCheckoutEnabled(!next) } } // Guest settings const [guestTags, setGuestTags] = useState([ { id: 'vip', label: 'VIP', color: '#F59E0B' }, { id: 'regular', label: 'Постоянный гость', color: '#3B82F6' }, { id: 'corp', label: 'Корпоративный', color: '#8B5CF6' }, { id: 'honey', label: 'Медовый месяц', color: '#EC4899' }, { id: 'bday', label: 'День рождения', color: '#10B981' }, { id: 'special', label: 'Особые пожелания', color: '#64748B' }, ]) const [newTagLabel, setNewTagLabel] = useState('') const [newTagColor, setNewTagColor] = useState('#4F46E5') // Notification toggles const [notifyToggles, setNotifyToggles] = useState({ newBooking: true, cancellation: true, channelError: true, dailyReport: false, }) // SMTP settings const [smtp, setSmtp] = useState({ host: '', port: '587', user: '', password: '', fromEmail: '', fromName: '', }) // SMS settings const [smsProvider, setSmsProvider] = useState('') const [smsApiKey, setSmsApiKey] = useState('') const [smsSender, setSmsSender] = useState('') // Telegram settings const [tgBotToken, setTgBotToken] = useState('') const [tgManagers, setTgManagers] = useState<{ id: string; name: string; chatId: string }[]>([]) const [newTgName, setNewTgName] = useState('') const [newTgChatId, setNewTgChatId] = useState('') const [tgEvents, setTgEvents] = useState({ newBooking: true, cancellation: true, checkIn: true, checkOut: false, channelError: true, payment: false, }) const [tgTestSent, setTgTestSent] = useState(false) const [copiedToken, setCopiedToken] = useState(false) const tgBotConfigured = tgBotToken.length > 10 const addTgManager = () => { if (!newTgName.trim() || !newTgChatId.trim()) return setTgManagers(prev => [...prev, { id: `tg-${Date.now()}`, name: newTgName.trim(), chatId: newTgChatId.trim() }]) setNewTgName('') setNewTgChatId('') } const sendTgTest = () => { setTgTestSent(true) setTimeout(() => setTgTestSent(false), 3000) } const copyBotLink = () => { navigator.clipboard.writeText('https://t.me/HotelSyncBot?start=connect') setCopiedToken(true) setTimeout(() => setCopiedToken(false), 2000) } const handleSave = async () => { if (section === 'general') { try { const updated = await api.hotels.update(slug, { name: form.name, address: form.address, timezone: form.timezone, currency: form.currency, checkInTime: form.checkInTime, checkOutTime: form.checkOutTime, }) setHotel(updated) } catch (err) { console.error('Failed to save hotel settings', err) } } setSaved(true) setTimeout(() => setSaved(false), 2000) } return (

Настройки

{hotel?.name ?? '...'}

{/* Sidebar nav */} {/* Mobile nav */}
{/* Content */}
{/* ── GENERAL ── */} {section === 'general' && ( <>

Основная информация

setForm(p => ({ ...p, name: e.target.value }))} />
setForm(p => ({ ...p, address: e.target.value }))} />
setForm(p => ({ ...p, checkInTime: e.target.value }))} />
setForm(p => ({ ...p, checkOutTime: e.target.value }))} />
)} {/* ── BOOKING ── */} {section === 'booking' && ( <>

Настройки бронирования

{/* Source toggle */}

Показывать поле «Источник бронирования»

В форме создания/редактирования бронирования

setShowBookingSource(p => !p)} />
{/* Require guest docs toggle */}

Обязательное заполнение документов гостей при заселении

Показывает предупреждение, если у совершеннолетних гостей не заполнены паспортные данные

{/* Early check-in fee */}

Платный ранний заезд

Предлагать доп. оплату при заселении до официального времени заезда. Сумма задаётся в настройках каждого номера.

{/* Late checkout fee */}

Платный поздний выезд

Предлагать доп. оплату при выезде после официального времени выезда. Сумма задаётся в настройках каждого номера.

Как система выбирает номер при автоматическом бронировании (кнопка «Забронировать» без выбора номера вручную)

{([ { id: 'spread', label: 'Разброс (шахматный порядок)', desc: 'Максимальное расстояние между гостями — номера заполняются через один. Рекомендуется для большинства отелей.', }, { id: 'together', label: 'Рядом', desc: 'Соседние номера подряд. Удобно для семей и групп, которые хотят быть близко.', }, { id: 'sequential', label: 'Последовательно', desc: 'Следующий свободный номер в порядке нумерации. Упрощает навигацию персонала.', }, { id: 'manual', label: 'Вручную', desc: 'Менеджер всегда сам выбирает номер при создании брони. Автоподбора нет.', }, ] as const).map(opt => ( ))}
)} {/* ── THEME ── */} {section === 'theme' && ( <>

Внешний вид

Тема интерфейса

{([ { id: 'light', label: 'Светлая', desc: null }, { id: 'dark', label: 'Тёмная', desc: null }, { id: 'auto', label: 'Авто', desc: 'Следует системной теме устройства' }, ] as const).map(t => ( ))}
{/* Calendar compact mode */}

Настройки календаря

Компактный режим

Уменьшенные строки в сетке бронирований

)} {/* ── NOTIFICATIONS ── */} {section === 'notify' && ( <>

Уведомления

{/* Toggles */}
{([ { key: 'newBooking' as const, label: 'Новые бронирования', sub: 'При создании нового бронирования' }, { key: 'cancellation' as const, label: 'Отмены', sub: 'При отмене бронирования' }, { key: 'channelError' as const, label: 'Ошибки синхронизации каналов', sub: 'При сбое синхронизации с OTA' }, { key: 'dailyReport' as const, label: 'Ежедневный отчёт', sub: 'Сводка загрузки на день в 8:00' }, ] as const).map(n => (

{n.label}

{n.sub}

setNotifyToggles(p => ({ ...p, [n.key]: !p[n.key] }))} />
))}
{/* SMTP */}

Email-уведомления (SMTP)

Если не заполнено — письма отправляются с ящика HotelSync (noreply@hotelsync.ru)

setSmtp(p => ({ ...p, host: e.target.value }))} />
setSmtp(p => ({ ...p, port: e.target.value }))} />
setSmtp(p => ({ ...p, user: e.target.value }))} />
setSmtp(p => ({ ...p, password: e.target.value }))} />
setSmtp(p => ({ ...p, fromEmail: e.target.value }))} />
setSmtp(p => ({ ...p, fromName: e.target.value }))} />
{/* Telegram */}

Telegram-уведомления для менеджеров

Менеджеры получают уведомления о бронированиях прямо в Telegram. Каждый менеджер должен написать боту команду /start и сообщить свой Chat ID.

{/* How to connect — step instructions */}

Как подключить менеджера:

  1. Менеджер открывает Telegram и переходит в бота
  2. Пишет /start — бот ответит его Chat ID
  3. Менеджер сообщает Chat ID администратору — тот добавляет его ниже
{/* Bot token */}
setTgBotToken(e.target.value)} />
{/* Events to notify */} {tgBotConfigured && ( <>

Уведомлять о событиях:

{([ { key: 'newBooking' as const, label: 'Новое бронирование' }, { key: 'cancellation' as const, label: 'Отмена брони' }, { key: 'checkIn' as const, label: 'Заезд гостя' }, { key: 'checkOut' as const, label: 'Выезд гостя' }, { key: 'channelError' as const, label: 'Ошибка канала' }, { key: 'payment' as const, label: 'Платёж / оплата' }, ]).map(ev => ( ))}
{/* Manager list */}

Менеджеры — получатели уведомлений {tgManagers.length > 0 && ({tgManagers.length})}

{tgManagers.length === 0 && (

Менеджеры ещё не добавлены

)} {tgManagers.map(m => (
{m.name[0]?.toUpperCase()}

{m.name}

Chat ID: {m.chatId}

))}
{/* Add manager */}
setNewTgName(e.target.value)} /> setNewTgChatId(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') addTgManager() }} />
{/* Test button */} {tgManagers.length > 0 && ( )} )}
{/* SMS */}

SMS-уведомления

SMS отправляются гостям при подтверждении брони, заезде и выезде

{smsProvider && ( <>
setSmsApiKey(e.target.value)} />
setSmsSender(e.target.value)} />

Латиницей, до 11 символов. Требует регистрации у провайдера.

)}
)} {/* ── SECURITY ── */} {section === 'security' && ( <>

Безопасность

)} {/* ── BILLING ── */} {section === 'billing' && ( <>

Тарифный план

Текущий план

{PLAN_LABELS[hotel?.plan ?? 'starter']}
{([ { plan: 'starter', price: '990 ₽/мес', rooms: '10 номеров', channels: '1 канал', support: 'Email' }, { plan: 'pro', price: '3 490 ₽/мес', rooms: 'До 50 номеров', channels: '5 каналов', support: 'Чат + Email' }, { plan: 'enterprise', price: 'Договорная', rooms: 'Неограничено', channels: 'Все каналы', support: 'Выделенный менеджер' }, ] as const).map(p => (
{PLAN_LABELS[p.plan]} {hotel?.plan === p.plan && Текущий}

{p.price}

  • ✓ {p.rooms}
  • ✓ {p.channels}
  • ✓ {p.support}
))}
)} {/* Save button */} {section !== 'billing' && section !== 'theme' && (
)}
) }