Files
hotelsync/src/pages/SettingsPage.tsx
HotelSync edd493e24e feat: early check-in / late checkout fees with room-level configuration
- DB migration 016: add early_checkin_fee, late_checkout_fee to rooms table
- Backend rooms: support new fee columns in POST/PATCH
- Backend hotel-settings GET: also returns check_in_time, check_out_time
- Room type + RoomPayload: add earlyCheckinFee, lateCheckoutFee fields
- RoomModal: add fee amount inputs in pricing section
- SettingsPage: add toggles for early_checkin_enabled / late_checkout_enabled
- BookingDetailPanel: on check-in shows fee warning + adds payment record if
  current time is before checkInTime and fee is set on the room; same logic
  for check-out after checkOutTime

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 19:24:36 +03:00

791 lines
41 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<button
onClick={onChange}
className={cn('relative w-11 h-6 rounded-full transition-colors shrink-0', on ? 'bg-brand-600' : 'bg-slate-200 dark:bg-slate-600')}
>
<div className={cn('absolute top-0.5 w-5 h-5 rounded-full bg-white shadow-sm transition-transform', on ? 'left-[22px]' : 'left-0.5')} />
</button>
)
}
export function SettingsPage() {
const { user } = useAuth()
const slug = user?.hotelSlug ?? ''
const [section, setSection] = useState('general')
const [saved, setSaved] = useState(false)
const [hotel, setHotel] = useState<Hotel | null>(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 (
<div className="p-4 md:p-6 max-w-4xl mx-auto">
<div className="mb-5">
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">Настройки</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">{hotel?.name ?? '...'}</p>
</div>
<div className="flex gap-5">
{/* Sidebar nav */}
<nav className="hidden md:flex flex-col gap-0.5 w-44 shrink-0">
{SECTIONS.map(s => {
const Icon = s.icon
return (
<button
key={s.id}
onClick={() => setSection(s.id)}
className={cn(
'flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm font-medium text-left transition-colors',
section === s.id
? 'bg-brand-50 dark:bg-brand-900/30 text-brand-700 dark:text-brand-300'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800',
)}
>
<Icon size={15} />
{s.label}
</button>
)
})}
</nav>
{/* Mobile nav */}
<div className="md:hidden w-full mb-4">
<select value={section} onChange={e => setSection(e.target.value)} className="input">
{SECTIONS.map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
</select>
</div>
{/* Content */}
<div className="flex-1 card p-5 space-y-5">
{/* ── GENERAL ── */}
{section === 'general' && (
<>
<h2 className="font-semibold text-slate-900 dark:text-slate-100">Основная информация</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Название отеля</label>
<input type="text" className="input" value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Адрес</label>
<input type="text" className="input" value={form.address} onChange={e => setForm(p => ({ ...p, address: e.target.value }))} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Часовой пояс</label>
<select className="input" value={form.timezone} onChange={e => setForm(p => ({ ...p, timezone: e.target.value }))}>
<option value="Europe/Moscow">Москва (UTC+3)</option>
<option value="Asia/Yekaterinburg">Екатеринбург (UTC+5)</option>
<option value="Asia/Novosibirsk">Новосибирск (UTC+7)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Валюта</label>
<select className="input" value={form.currency} onChange={e => setForm(p => ({ ...p, currency: e.target.value }))}>
<option value="RUB"> RUB</option>
<option value="USD">$ USD</option>
<option value="EUR"> EUR</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Время заезда</label>
<input type="time" className="input" value={form.checkInTime} onChange={e => setForm(p => ({ ...p, checkInTime: e.target.value }))} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Время выезда</label>
<input type="time" className="input" value={form.checkOutTime} onChange={e => setForm(p => ({ ...p, checkOutTime: e.target.value }))} />
</div>
</div>
</div>
</>
)}
{/* ── BOOKING ── */}
{section === 'booking' && (
<>
<h2 className="font-semibold text-slate-900 dark:text-slate-100">Настройки бронирования</h2>
{/* Source toggle */}
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-50 dark:bg-slate-700/40">
<div>
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">Показывать поле «Источник бронирования»</p>
<p className="text-xs text-slate-500 dark:text-slate-400">В форме создания/редактирования бронирования</p>
</div>
<Toggle on={showBookingSource} onChange={() => setShowBookingSource(p => !p)} />
</div>
{/* Require guest docs toggle */}
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-50 dark:bg-slate-700/40">
<div>
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">Обязательное заполнение документов гостей при заселении</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
Показывает предупреждение, если у совершеннолетних гостей не заполнены паспортные данные
</p>
</div>
<Toggle on={requireGuestDocs} onChange={toggleRequireGuestDocs} />
</div>
{/* Early check-in fee */}
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-50 dark:bg-slate-700/40">
<div>
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">Платный ранний заезд</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
Предлагать доп. оплату при заселении до официального времени заезда. Сумма задаётся в настройках каждого номера.
</p>
</div>
<Toggle on={earlyCheckinEnabled} onChange={toggleEarlyCheckin} />
</div>
{/* Late checkout fee */}
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-50 dark:bg-slate-700/40">
<div>
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">Платный поздний выезд</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
Предлагать доп. оплату при выезде после официального времени выезда. Сумма задаётся в настройках каждого номера.
</p>
</div>
<Toggle on={lateCheckoutEnabled} onChange={toggleLateCheckout} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Стратегия автоматического расселения
</label>
<p className="text-xs text-slate-400 mb-3">
Как система выбирает номер при автоматическом бронировании (кнопка «Забронировать» без выбора номера вручную)
</p>
<div className="space-y-2">
{([
{
id: 'spread',
label: 'Разброс (шахматный порядок)',
desc: 'Максимальное расстояние между гостями — номера заполняются через один. Рекомендуется для большинства отелей.',
},
{
id: 'together',
label: 'Рядом',
desc: 'Соседние номера подряд. Удобно для семей и групп, которые хотят быть близко.',
},
{
id: 'sequential',
label: 'Последовательно',
desc: 'Следующий свободный номер в порядке нумерации. Упрощает навигацию персонала.',
},
{
id: 'manual',
label: 'Вручную',
desc: 'Менеджер всегда сам выбирает номер при создании брони. Автоподбора нет.',
},
] as const).map(opt => (
<label
key={opt.id}
className={cn(
'flex items-start gap-3 p-3.5 rounded-xl border-2 cursor-pointer transition-all',
assignmentStrategy === opt.id
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20'
: 'border-slate-200 dark:border-slate-600 hover:border-slate-300 dark:hover:border-slate-500',
)}
>
<input
type="radio"
name="assignment"
value={opt.id}
checked={assignmentStrategy === opt.id}
onChange={() => setAssignmentStrategy(opt.id)}
className="mt-0.5 accent-brand-600"
/>
<div>
<p className={cn(
'text-sm font-medium',
assignmentStrategy === opt.id ? 'text-brand-700 dark:text-brand-300' : 'text-slate-900 dark:text-slate-100',
)}>
{opt.label}
</p>
<p className="text-xs text-slate-400 mt-0.5">{opt.desc}</p>
</div>
</label>
))}
</div>
</div>
</>
)}
{/* ── THEME ── */}
{section === 'theme' && (
<>
<h2 className="font-semibold text-slate-900 dark:text-slate-100">Внешний вид</h2>
<div className="space-y-4">
<div>
<p className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">Тема интерфейса</p>
<div className="grid grid-cols-3 gap-3">
{([
{ id: 'light', label: 'Светлая', desc: null },
{ id: 'dark', label: 'Тёмная', desc: null },
{ id: 'auto', label: 'Авто', desc: 'Следует системной теме устройства' },
] as const).map(t => (
<button
key={t.id}
onClick={() => setMode(t.id)}
className={cn(
'p-4 rounded-xl border-2 transition-all text-left',
mode === t.id ? 'border-brand-500' : 'border-slate-200 dark:border-slate-600 hover:border-slate-300',
)}
>
{t.id === 'auto' ? (
<div className="w-full h-14 rounded-lg mb-2 overflow-hidden border border-slate-200 flex">
<div className="flex-1 bg-white" />
<div className="flex-1 bg-slate-800" />
</div>
) : (
<div className={cn('w-full h-14 rounded-lg mb-2', t.id === 'light' ? 'bg-white border border-slate-200' : 'bg-slate-800')}>
<div className={cn('h-3 w-3/4 rounded mx-2 mt-2', t.id === 'light' ? 'bg-slate-200' : 'bg-slate-600')} />
<div className={cn('h-2 w-1/2 rounded mx-2 mt-1', t.id === 'light' ? 'bg-slate-100' : 'bg-slate-700')} />
</div>
)}
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">{t.label}</p>
{mode === t.id
? <p className="text-xs text-brand-600 dark:text-brand-400">Активна</p>
: t.desc && <p className="text-xs text-slate-400 dark:text-slate-500 leading-tight">{t.desc}</p>
}
</button>
))}
</div>
</div>
{/* Calendar compact mode */}
<div className="pt-3 border-t border-slate-100 dark:border-slate-700">
<p className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Настройки календаря</p>
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-50 dark:bg-slate-700/40">
<div>
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">Компактный режим</p>
<p className="text-xs text-slate-500 dark:text-slate-400">Уменьшенные строки в сетке бронирований</p>
</div>
<Toggle on={calendarCompact} onChange={toggleCalendarCompact} />
</div>
</div>
</div>
</>
)}
{/* ── NOTIFICATIONS ── */}
{section === 'notify' && (
<>
<h2 className="font-semibold text-slate-900 dark:text-slate-100">Уведомления</h2>
{/* Toggles */}
<div className="space-y-2">
{([
{ 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 => (
<div key={n.key} className="flex items-center justify-between p-3 rounded-lg bg-slate-50 dark:bg-slate-700/40">
<div>
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">{n.label}</p>
<p className="text-xs text-slate-500 dark:text-slate-400">{n.sub}</p>
</div>
<Toggle on={notifyToggles[n.key]} onChange={() => setNotifyToggles(p => ({ ...p, [n.key]: !p[n.key] }))} />
</div>
))}
</div>
{/* SMTP */}
<div className="pt-2 border-t border-slate-100 dark:border-slate-700">
<div className="flex items-center gap-2 mb-1">
<Mail size={15} className="text-slate-500" />
<h3 className="text-sm font-semibold text-slate-800 dark:text-slate-200">Email-уведомления (SMTP)</h3>
</div>
<p className="text-xs text-slate-400 mb-3">
Если не заполнено письма отправляются с ящика HotelSync (<code className="bg-slate-100 dark:bg-slate-700 px-1 rounded">noreply@hotelsync.ru</code>)
</p>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">SMTP-хост</label>
<input type="text" className="input text-sm" placeholder="smtp.gmail.com" value={smtp.host} onChange={e => setSmtp(p => ({ ...p, host: e.target.value }))} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Порт</label>
<input type="number" className="input text-sm" placeholder="587" value={smtp.port} onChange={e => setSmtp(p => ({ ...p, port: e.target.value }))} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Пользователь</label>
<input type="text" className="input text-sm" placeholder="user@gmail.com" value={smtp.user} onChange={e => setSmtp(p => ({ ...p, user: e.target.value }))} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Пароль</label>
<input type="password" className="input text-sm" placeholder="••••••••" value={smtp.password} onChange={e => setSmtp(p => ({ ...p, password: e.target.value }))} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Email отправителя</label>
<input type="email" className="input text-sm" placeholder="hotel@myhotel.ru" value={smtp.fromEmail} onChange={e => setSmtp(p => ({ ...p, fromEmail: e.target.value }))} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Имя отправителя</label>
<input type="text" className="input text-sm" placeholder="Grand Palace Hotel" value={smtp.fromName} onChange={e => setSmtp(p => ({ ...p, fromName: e.target.value }))} />
</div>
</div>
</div>
</div>
{/* Telegram */}
<div className="pt-2 border-t border-slate-100 dark:border-slate-700">
<div className="flex items-center gap-2 mb-1">
<Send size={15} className="text-sky-500" />
<h3 className="text-sm font-semibold text-slate-800 dark:text-slate-200">Telegram-уведомления для менеджеров</h3>
</div>
<p className="text-xs text-slate-400 mb-3">
Менеджеры получают уведомления о бронированиях прямо в Telegram. Каждый менеджер должен написать боту команду <code className="bg-slate-100 dark:bg-slate-700 px-1 rounded">/start</code> и сообщить свой Chat ID.
</p>
{/* How to connect — step instructions */}
<div className="rounded-xl bg-sky-50 dark:bg-sky-900/20 border border-sky-200 dark:border-sky-800 p-3.5 mb-3 space-y-2">
<p className="text-xs font-semibold text-sky-700 dark:text-sky-300">Как подключить менеджера:</p>
<ol className="text-xs text-sky-700 dark:text-sky-300 space-y-1 list-decimal list-inside">
<li>Менеджер открывает Telegram и переходит в бота</li>
<li>Пишет <code className="bg-sky-100 dark:bg-sky-900 px-1 rounded">/start</code> бот ответит его Chat ID</li>
<li>Менеджер сообщает Chat ID администратору тот добавляет его ниже</li>
</ol>
<button
onClick={copyBotLink}
className="flex items-center gap-1.5 text-xs font-medium text-sky-600 dark:text-sky-400 hover:underline"
>
{copiedToken ? <CheckCircle2 size={13} className="text-emerald-500" /> : <Copy size={13} />}
{copiedToken ? 'Скопировано!' : 'Скопировать ссылку на бота'}
</button>
</div>
{/* Bot token */}
<div className="mb-3">
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">
Токен бота
<span className="ml-1 text-slate-400 font-normal">(получить у @BotFather в Telegram)</span>
</label>
<input
type="password"
className="input text-sm font-mono"
placeholder="123456789:AAExx..."
value={tgBotToken}
onChange={e => setTgBotToken(e.target.value)}
/>
</div>
{/* Events to notify */}
{tgBotConfigured && (
<>
<div className="mb-3">
<p className="text-xs font-medium text-slate-600 dark:text-slate-400 mb-2">Уведомлять о событиях:</p>
<div className="grid grid-cols-2 gap-1.5">
{([
{ 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 => (
<label key={ev.key} className="flex items-center gap-2 text-xs cursor-pointer">
<input
type="checkbox"
className="accent-brand-600"
checked={tgEvents[ev.key]}
onChange={() => setTgEvents(p => ({ ...p, [ev.key]: !p[ev.key] }))}
/>
<span className="text-slate-700 dark:text-slate-300">{ev.label}</span>
</label>
))}
</div>
</div>
{/* Manager list */}
<div className="mb-3">
<p className="text-xs font-medium text-slate-600 dark:text-slate-400 mb-2">
Менеджеры получатели уведомлений
{tgManagers.length > 0 && <span className="ml-1 text-slate-400">({tgManagers.length})</span>}
</p>
<div className="space-y-1.5 mb-2">
{tgManagers.length === 0 && (
<p className="text-xs text-slate-400 py-2 text-center rounded-lg bg-slate-50 dark:bg-slate-700/30">
Менеджеры ещё не добавлены
</p>
)}
{tgManagers.map(m => (
<div key={m.id} className="flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-50 dark:bg-slate-700/40 text-sm">
<div className="w-7 h-7 rounded-full bg-sky-100 dark:bg-sky-900/50 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-sky-600 dark:text-sky-400">{m.name[0]?.toUpperCase()}</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">{m.name}</p>
<p className="text-xs text-slate-500 dark:text-slate-400 font-mono">Chat ID: {m.chatId}</p>
</div>
<button
onClick={() => setTgManagers(prev => prev.filter(x => x.id !== m.id))}
className="p-1 rounded text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
>
<XIcon size={13} />
</button>
</div>
))}
</div>
{/* Add manager */}
<div className="flex gap-2">
<input
type="text"
className="input text-sm flex-1"
placeholder="Имя менеджера"
value={newTgName}
onChange={e => setNewTgName(e.target.value)}
/>
<input
type="text"
className="input text-sm w-32 font-mono"
placeholder="Chat ID"
value={newTgChatId}
onChange={e => setNewTgChatId(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') addTgManager() }}
/>
<button onClick={addTgManager} className="btn-secondary px-3">
<Plus size={14} />
</button>
</div>
</div>
{/* Test button */}
{tgManagers.length > 0 && (
<button
onClick={sendTgTest}
className={cn(
'flex items-center gap-1.5 text-sm font-medium px-3 py-2 rounded-lg border transition-colors',
tgTestSent
? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-300 dark:border-emerald-700 text-emerald-700 dark:text-emerald-400'
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-700 dark:text-slate-300 hover:border-sky-400 hover:text-sky-600',
)}
>
{tgTestSent
? <><CheckCircle2 size={14} /> Тестовое сообщение отправлено!</>
: <><Send size={14} /> Отправить тестовое сообщение</>
}
</button>
)}
</>
)}
</div>
{/* SMS */}
<div className="pt-2 border-t border-slate-100 dark:border-slate-700">
<div className="flex items-center gap-2 mb-1">
<MessageSquare size={15} className="text-slate-500" />
<h3 className="text-sm font-semibold text-slate-800 dark:text-slate-200">SMS-уведомления</h3>
</div>
<p className="text-xs text-slate-400 mb-3">
SMS отправляются гостям при подтверждении брони, заезде и выезде
</p>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">SMS-провайдер</label>
<select className="input text-sm" value={smsProvider} onChange={e => setSmsProvider(e.target.value)}>
<option value="">Не настроено</option>
<option value="smsc">SMSC.ru</option>
<option value="smsru">SMS.ru</option>
<option value="mts">МТС Коммуникатор</option>
<option value="beeline">Beeline Business</option>
<option value="smsaero">SMS Aero</option>
</select>
</div>
{smsProvider && (
<>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">API-ключ</label>
<input type="password" className="input text-sm" placeholder="Ваш API-ключ" value={smsApiKey} onChange={e => setSmsApiKey(e.target.value)} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Имя отправителя</label>
<input type="text" className="input text-sm" placeholder="MYHOTEL" maxLength={11} value={smsSender} onChange={e => setSmsSender(e.target.value)} />
<p className="text-xs text-slate-400 mt-1">Латиницей, до 11 символов. Требует регистрации у провайдера.</p>
</div>
</>
)}
</div>
</div>
</>
)}
{/* ── SECURITY ── */}
{section === 'security' && (
<>
<h2 className="font-semibold text-slate-900 dark:text-slate-100">Безопасность</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Текущий пароль</label>
<input type="password" className="input" placeholder="••••••••" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Новый пароль</label>
<input type="password" className="input" placeholder="••••••••" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Подтверждение</label>
<input type="password" className="input" placeholder="••••••••" />
</div>
</div>
</>
)}
{/* ── BILLING ── */}
{section === 'billing' && (
<>
<h2 className="font-semibold text-slate-900 dark:text-slate-100">Тарифный план</h2>
<div className="p-4 rounded-xl bg-slate-50 dark:bg-slate-700/40 flex items-center justify-between mb-4">
<div>
<p className="text-sm text-slate-500 dark:text-slate-400">Текущий план</p>
<div className="flex items-center gap-2 mt-1">
<Badge className={PLAN_COLORS[hotel?.plan ?? 'starter']}>{PLAN_LABELS[hotel?.plan ?? 'starter']}</Badge>
</div>
</div>
<button className="btn-primary">Улучшить план</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{([
{ 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 => (
<div key={p.plan} className={cn(
'p-4 rounded-xl border-2 transition-all',
hotel?.plan === p.plan ? 'border-brand-500 bg-brand-50/50 dark:bg-brand-900/10' : 'border-slate-200 dark:border-slate-600',
)}>
<div className="flex items-center justify-between mb-2">
<Badge className={PLAN_COLORS[p.plan]}>{PLAN_LABELS[p.plan]}</Badge>
{hotel?.plan === p.plan && <span className="text-xs text-brand-600 dark:text-brand-400 font-medium">Текущий</span>}
</div>
<p className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-3">{p.price}</p>
<ul className="space-y-1 text-xs text-slate-600 dark:text-slate-400">
<li> {p.rooms}</li>
<li> {p.channels}</li>
<li> {p.support}</li>
</ul>
</div>
))}
</div>
</>
)}
{/* Save button */}
{section !== 'billing' && section !== 'theme' && (
<div className="pt-2 flex justify-end">
<button onClick={handleSave} className="btn-primary">
<Save size={14} />
{saved ? 'Сохранено!' : 'Сохранить'}
</button>
</div>
)}
</div>
</div>
</div>
)
}