Refactor POS/Reviews/RoomService/BookingWidget/BookingModal
- POS: complete rewrite — shift management + room payments (not products); ATOL placeholder; room balance sidebar; receipts log - BookingModal: remove "Источник" field; restructure layout (notes+summary as bottom full-width row, eliminating scroll) - Reviews: negative (<2 stars = <4/10) → moderation with private manager reply; positive → redirect to external platforms (Booking.com, Яндекс, Google); new tabs: "Требуют ответа" / "Перенаправить" - RoomService: add orderType (room delivery / restaurant table); service charge % for room delivery; scheduledTime for restaurant; paymentStatus (online_paid / pending); settings tab - BookingWidget: complete redesign — live widget constructor with color picker, language, rooms+rental tabs, payment provider selector; live preview; install code tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,63 +1,584 @@
|
||||
import { CalendarCheck2, Code2, CreditCard, Globe } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Code2, CreditCard, Globe, Palette, CalendarCheck2, Copy, CheckCheck,
|
||||
ChevronLeft, ChevronRight, Star, Users, Dumbbell, Waves,
|
||||
Eye, Settings2, ArrowRight,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '../lib/utils'
|
||||
import { useModules } from '../contexts/ModulesContext'
|
||||
|
||||
export function BookingWidgetPage() {
|
||||
const snippet = `<script src="https://widget.hotelsync.ru/v1/embed.js"
|
||||
data-hotel="grand-palace"
|
||||
data-lang="ru">
|
||||
</script>`
|
||||
// ── Widget settings type ───────────────────────────────────────────────────────
|
||||
|
||||
interface WidgetSettings {
|
||||
hotelName: string
|
||||
primaryColor: string
|
||||
language: 'ru' | 'en'
|
||||
showRooms: boolean
|
||||
showRental: boolean
|
||||
minNights: number
|
||||
paymentProvider: 'yukassa' | 'tinkoff' | 'cloudpayments' | 'none'
|
||||
showPromo: boolean
|
||||
}
|
||||
|
||||
const COLORS = [
|
||||
{ label: 'Индиго', value: '#4F46E5' },
|
||||
{ label: 'Изумруд', value: '#059669' },
|
||||
{ label: 'Синий', value: '#2563EB' },
|
||||
{ label: 'Фиолетовый',value: '#7C3AED' },
|
||||
{ label: 'Красный', value: '#DC2626' },
|
||||
{ label: 'Янтарный', value: '#D97706' },
|
||||
{ label: 'Розовый', value: '#DB2777' },
|
||||
{ label: 'Серый', value: '#475569' },
|
||||
]
|
||||
|
||||
const PAYMENT_PROVIDERS = [
|
||||
{ value: 'yukassa', label: 'ЮKassa' },
|
||||
{ value: 'tinkoff', label: 'Тинькофф Pay' },
|
||||
{ value: 'cloudpayments',label: 'CloudPayments' },
|
||||
{ value: 'none', label: 'Без оплаты' },
|
||||
]
|
||||
|
||||
const MOCK_ROOMS = [
|
||||
{ id: 'r1', name: 'Стандарт', beds: 1, guests: 2, price: 4500, img: '🛏️' },
|
||||
{ id: 'r2', name: 'Комфорт', beds: 1, guests: 2, price: 6500, img: '🛏️' },
|
||||
{ id: 'r3', name: 'Делюкс', beds: 2, guests: 3, price: 8900, img: '🛏️' },
|
||||
{ id: 'r4', name: 'Пентхаус', beds: 2, guests: 4, price: 15000, img: '🌇' },
|
||||
]
|
||||
|
||||
const MOCK_RENTAL = [
|
||||
{ id: 'o1', name: 'Теннисный корт', icon: '🎾', price: 1200, unit: 'ч' },
|
||||
{ id: 'o2', name: 'Сауна', icon: '🧖', price: 2500, unit: 'ч' },
|
||||
{ id: 'o3', name: 'Банкетный зал', icon: '🎪', price: 15000, unit: 'день' },
|
||||
]
|
||||
|
||||
// ── Widget Preview Component ───────────────────────────────────────────────────
|
||||
|
||||
function WidgetPreview({ settings }: { settings: WidgetSettings }) {
|
||||
const [previewTab, setPreviewTab] = useState<'rooms' | 'rental'>('rooms')
|
||||
const [checkIn, setCheckIn] = useState('2026-03-20')
|
||||
const [checkOut, setCheckOut] = useState('2026-03-22')
|
||||
const [guests, setGuests] = useState(2)
|
||||
const [selected, setSelected] = useState<string | null>(null)
|
||||
|
||||
const nights = checkIn && checkOut
|
||||
? Math.max(0, (new Date(checkOut).getTime() - new Date(checkIn).getTime()) / 86400000)
|
||||
: 0
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto space-y-6">
|
||||
<div className="bg-white rounded-2xl shadow-2xl overflow-hidden border border-slate-200" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{/* Widget header */}
|
||||
<div className="px-6 py-4 text-white" style={{ background: settings.primaryColor }}>
|
||||
<p className="font-bold text-lg">{settings.hotelName || 'Название отеля'}</p>
|
||||
<p className="text-sm opacity-80">
|
||||
{settings.language === 'ru' ? 'Бронирование онлайн · Без комиссии' : 'Book directly · No fees'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
{settings.showRooms && settings.showRental && (
|
||||
<div className="flex border-b border-slate-200">
|
||||
{[
|
||||
['rooms', settings.language === 'ru' ? 'Номера' : 'Rooms'],
|
||||
['rental', settings.language === 'ru' ? 'Аренда' : 'Rental'],
|
||||
].map(([key, label]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setPreviewTab(key as any)}
|
||||
className={cn(
|
||||
'flex-1 py-3 text-sm font-medium border-b-2 transition-colors',
|
||||
previewTab === key
|
||||
? 'border-b-2 text-slate-900'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-700',
|
||||
)}
|
||||
style={previewTab === key ? { borderColor: settings.primaryColor, color: settings.primaryColor } : {}}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search bar */}
|
||||
{previewTab === 'rooms' && (
|
||||
<div className="px-5 py-4 bg-slate-50 border-b border-slate-200">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 mb-1">
|
||||
{settings.language === 'ru' ? 'Заезд' : 'Check-in'}
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={checkIn}
|
||||
onChange={e => setCheckIn(e.target.value)}
|
||||
className="w-full text-sm border border-slate-200 rounded-lg px-2.5 py-1.5 bg-white focus:outline-none"
|
||||
style={{ '--focus-color': settings.primaryColor } as any}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 mb-1">
|
||||
{settings.language === 'ru' ? 'Выезд' : 'Check-out'}
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={checkOut}
|
||||
onChange={e => setCheckOut(e.target.value)}
|
||||
className="w-full text-sm border border-slate-200 rounded-lg px-2.5 py-1.5 bg-white focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 mb-1">
|
||||
{settings.language === 'ru' ? 'Гостей' : 'Guests'}
|
||||
</label>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setGuests(g => Math.max(1, g - 1))}
|
||||
className="w-7 h-7 rounded-lg border border-slate-200 flex items-center justify-center hover:bg-slate-100 text-slate-700"
|
||||
>
|
||||
<ChevronLeft size={14} />
|
||||
</button>
|
||||
<span className="flex-1 text-center text-sm font-medium">{guests}</span>
|
||||
<button
|
||||
onClick={() => setGuests(g => Math.min(8, g + 1))}
|
||||
className="w-7 h-7 rounded-lg border border-slate-200 flex items-center justify-center hover:bg-slate-100 text-slate-700"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rooms list */}
|
||||
{previewTab === 'rooms' && (
|
||||
<div className="p-4 space-y-3 max-h-80 overflow-y-auto">
|
||||
{nights > 0 && (
|
||||
<p className="text-xs text-slate-500">
|
||||
{nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'} · {guests} {settings.language === 'ru' ? 'гостя' : 'guests'}
|
||||
</p>
|
||||
)}
|
||||
{MOCK_ROOMS.map(room => (
|
||||
<div
|
||||
key={room.id}
|
||||
onClick={() => setSelected(room.id)}
|
||||
className={cn(
|
||||
'flex items-center gap-4 p-3 rounded-xl border-2 cursor-pointer transition-all',
|
||||
selected === room.id ? 'border-2' : 'border-slate-200 hover:border-slate-300',
|
||||
)}
|
||||
style={selected === room.id ? { borderColor: settings.primaryColor, background: settings.primaryColor + '08' } : {}}
|
||||
>
|
||||
<div className="w-14 h-14 rounded-xl bg-slate-100 flex items-center justify-center text-2xl shrink-0">
|
||||
{room.img}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold text-slate-900">{room.name}</p>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-xs text-slate-500 flex items-center gap-0.5">
|
||||
<Users size={10} />{room.guests}
|
||||
</span>
|
||||
<span className="text-xs text-slate-500 flex items-center gap-0.5">
|
||||
<Star size={10} className="fill-amber-400 text-amber-400" />4.8
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<p className="font-bold text-slate-900">
|
||||
{nights > 0
|
||||
? (room.price * nights).toLocaleString('ru-RU')
|
||||
: room.price.toLocaleString('ru-RU')
|
||||
} ₽
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">
|
||||
{nights > 0 ? `за ${nights} ноч.` : 'за ночь'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rental list */}
|
||||
{previewTab === 'rental' && (
|
||||
<div className="p-4 space-y-3">
|
||||
<p className="text-xs text-slate-500">Выберите объект и удобное время</p>
|
||||
{MOCK_RENTAL.map(obj => (
|
||||
<div
|
||||
key={obj.id}
|
||||
onClick={() => setSelected(obj.id)}
|
||||
className={cn(
|
||||
'flex items-center gap-4 p-3 rounded-xl border-2 cursor-pointer transition-all',
|
||||
selected === obj.id ? '' : 'border-slate-200 hover:border-slate-300',
|
||||
)}
|
||||
style={selected === obj.id ? { borderColor: settings.primaryColor, background: settings.primaryColor + '08' } : {}}
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center text-2xl shrink-0">
|
||||
{obj.icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-slate-900">{obj.name}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-bold text-slate-900">{obj.price.toLocaleString('ru-RU')} ₽</p>
|
||||
<p className="text-xs text-slate-400">/{obj.unit}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CTA */}
|
||||
<div className="px-5 pb-5">
|
||||
<button
|
||||
className="w-full py-3 rounded-xl text-white font-semibold text-sm transition-opacity hover:opacity-90"
|
||||
style={{ background: settings.primaryColor }}
|
||||
>
|
||||
{settings.language === 'ru' ? 'Забронировать' : 'Book now'}
|
||||
{selected && nights > 0 && previewTab === 'rooms' && (() => {
|
||||
const r = MOCK_ROOMS.find(r => r.id === selected)
|
||||
return r ? ` · ${(r.price * nights).toLocaleString('ru-RU')} ₽` : ''
|
||||
})()}
|
||||
</button>
|
||||
{settings.showPromo && (
|
||||
<p className="text-center text-xs text-slate-400 mt-2">
|
||||
{settings.language === 'ru' ? 'Лучшая цена гарантирована' : 'Best rate guaranteed'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main Page ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function BookingWidgetPage() {
|
||||
const { statuses } = useModules()
|
||||
const rentalActive = statuses['rental'] === 'active' || statuses['rental'] === 'trial'
|
||||
|
||||
const [settings, setSettings] = useState<WidgetSettings>({
|
||||
hotelName: 'Grand Palace Hotel',
|
||||
primaryColor: '#4F46E5',
|
||||
language: 'ru',
|
||||
showRooms: true,
|
||||
showRental: rentalActive,
|
||||
minNights: 1,
|
||||
paymentProvider: 'yukassa',
|
||||
showPromo: true,
|
||||
})
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [pageTab, setPageTab] = useState<'constructor' | 'code' | 'stats'>('constructor')
|
||||
|
||||
const set = <K extends keyof WidgetSettings>(k: K, v: WidgetSettings[K]) =>
|
||||
setSettings(prev => ({ ...prev, [k]: v }))
|
||||
|
||||
const snippet = `<script src="https://widget.hotelsync.ru/v1/embed.js"
|
||||
data-hotel="grand-palace"
|
||||
data-color="${settings.primaryColor}"
|
||||
data-lang="${settings.language}"
|
||||
data-rental="${settings.showRental}"
|
||||
data-min-nights="${settings.minNights}"
|
||||
data-payment="${settings.paymentProvider}">
|
||||
</script>`
|
||||
|
||||
const copyCode = () => {
|
||||
navigator.clipboard.writeText(snippet).catch(() => {})
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6 space-y-5">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-600 flex items-center justify-center shrink-0">
|
||||
<CalendarCheck2 size={20} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<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">Виджет прямых продаж для сайта отеля</p>
|
||||
<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">Конструктор виджета для размещения на сайте отеля</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ label: 'Бронирований через виджет', value: '—', sub: 'нет данных' },
|
||||
{ label: 'Конверсия', value: '—', sub: 'нет данных' },
|
||||
{ label: 'Прямая выручка', value: '—', sub: 'нет данных' },
|
||||
].map(s => (
|
||||
<div key={s.label} className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4 text-center">
|
||||
<p className="text-2xl font-bold text-slate-400">{s.value}</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1 leading-tight">{s.label}</p>
|
||||
</div>
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 p-1 bg-slate-100 dark:bg-slate-700/50 rounded-xl w-fit">
|
||||
{([
|
||||
['constructor', <Settings2 size={13} />, 'Конструктор'],
|
||||
['code', <Code2 size={13} />, 'Код установки'],
|
||||
['stats', <Eye size={13} />, 'Статистика'],
|
||||
] as const).map(([key, icon, label]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setPageTab(key)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-4 py-1.5 rounded-lg text-sm font-medium transition-colors',
|
||||
pageTab === key
|
||||
? 'bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200',
|
||||
)}
|
||||
>
|
||||
{icon}{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Embed code */}
|
||||
<div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code2 size={16} className="text-indigo-500" />
|
||||
<h3 className="font-semibold text-slate-900 dark:text-slate-100 text-sm">Код для вставки на сайт</h3>
|
||||
{/* ── Constructor ── */}
|
||||
{pageTab === 'constructor' && (
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
{/* Settings panel */}
|
||||
<div className="space-y-4">
|
||||
<div className="card p-5 space-y-4">
|
||||
<h3 className="font-semibold text-slate-900 dark:text-slate-100">Внешний вид</h3>
|
||||
|
||||
<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={settings.hotelName}
|
||||
onChange={e => set('hotelName', e.target.value)}
|
||||
placeholder="Grand Palace Hotel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Основной цвет
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{COLORS.map(c => (
|
||||
<button
|
||||
key={c.value}
|
||||
onClick={() => set('primaryColor', c.value)}
|
||||
title={c.label}
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-full border-2 transition-transform',
|
||||
settings.primaryColor === c.value ? 'border-slate-900 dark:border-slate-100 scale-110' : 'border-transparent',
|
||||
)}
|
||||
style={{ background: c.value }}
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={settings.primaryColor}
|
||||
onChange={e => set('primaryColor', e.target.value)}
|
||||
className="w-8 h-8 rounded-full cursor-pointer border-0 p-0 bg-transparent"
|
||||
/>
|
||||
<span className="text-xs text-slate-500 font-mono">{settings.primaryColor}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Язык</label>
|
||||
<div className="flex gap-2">
|
||||
{([['ru', '🇷🇺 Русский'], ['en', '🇬🇧 English']] as const).map(([k, l]) => (
|
||||
<button
|
||||
key={k}
|
||||
onClick={() => set('language', k)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors',
|
||||
settings.language === k
|
||||
? 'bg-brand-600 text-white border-brand-600'
|
||||
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-700 dark:text-slate-300',
|
||||
)}
|
||||
>
|
||||
{l}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-5 space-y-4">
|
||||
<h3 className="font-semibold text-slate-900 dark:text-slate-100">Что показывать</h3>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<label className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
|
||||
<CalendarCheck2 size={15} className="text-indigo-500" />
|
||||
Бронирование номеров
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.showRooms}
|
||||
onChange={e => set('showRooms', e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{rentalActive && (
|
||||
<label className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
|
||||
<Dumbbell size={15} className="text-emerald-500" />
|
||||
Аренда объектов (корт, сауна и т.д.)
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.showRental}
|
||||
onChange={e => set('showRental', e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
|
||||
<Star size={15} className="text-amber-500" />
|
||||
Плашка "Лучшая цена гарантирована"
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.showPromo}
|
||||
onChange={e => set('showPromo', e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Минимальное кол-во ночей
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={30}
|
||||
className="input w-24"
|
||||
value={settings.minNights}
|
||||
onChange={e => set('minNights', Math.max(1, parseInt(e.target.value) || 1))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-5 space-y-3">
|
||||
<h3 className="font-semibold text-slate-900 dark:text-slate-100 flex items-center gap-2">
|
||||
<CreditCard size={16} className="text-emerald-600" />
|
||||
Онлайн-оплата
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{PAYMENT_PROVIDERS.map(p => (
|
||||
<button
|
||||
key={p.value}
|
||||
onClick={() => set('paymentProvider', p.value as any)}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-xl text-sm font-medium border transition-colors text-left',
|
||||
settings.paymentProvider === p.value
|
||||
? 'bg-brand-600 text-white border-brand-600'
|
||||
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-700 dark:text-slate-300 hover:border-brand-400',
|
||||
)}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{settings.paymentProvider !== 'none' && (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Настройте ключи API в разделе Настройки → Оплата
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live preview */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Eye size={14} className="text-slate-400" />
|
||||
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">Предпросмотр виджета</p>
|
||||
</div>
|
||||
<div className="p-6 rounded-2xl bg-slate-100 dark:bg-slate-700/50">
|
||||
<WidgetPreview settings={settings} />
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-2 text-center">Так виджет будет выглядеть на сайте отеля</p>
|
||||
</div>
|
||||
</div>
|
||||
<pre className="bg-slate-900 text-emerald-400 text-xs rounded-lg p-4 overflow-x-auto font-mono leading-relaxed">
|
||||
{snippet}
|
||||
</pre>
|
||||
<button className="btn-secondary text-sm">Скопировать код</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Features */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
{[
|
||||
{ icon: CalendarCheck2, title: 'Мгновенное подтверждение', desc: 'Email + SMS гостю' },
|
||||
{ icon: CreditCard, title: 'Онлайн-оплата', desc: 'Тинькофф, СБП, ЮKassa' },
|
||||
{ icon: Globe, title: 'Без комиссии OTA', desc: 'Только платёжная комиссия' },
|
||||
].map(f => (
|
||||
<div key={f.title} className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4 text-center">
|
||||
<f.icon size={22} className="text-indigo-500 mx-auto mb-2" />
|
||||
<p className="font-medium text-slate-900 dark:text-slate-100 text-sm">{f.title}</p>
|
||||
<p className="text-xs text-slate-400 mt-0.5">{f.desc}</p>
|
||||
{/* ── Code ── */}
|
||||
{pageTab === 'code' && (
|
||||
<div className="max-w-2xl space-y-5">
|
||||
<div className="card p-5 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code2 size={16} className="text-indigo-500" />
|
||||
<h3 className="font-semibold text-slate-900 dark:text-slate-100 text-sm">Код для вставки на сайт</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={copyCode}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-slate-200 dark:border-slate-600 text-xs font-medium text-slate-700 dark:text-slate-300 hover:border-brand-400 transition-colors"
|
||||
>
|
||||
{copied ? <CheckCheck size={12} className="text-emerald-600" /> : <Copy size={12} />}
|
||||
{copied ? 'Скопировано' : 'Копировать'}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="bg-slate-900 text-emerald-400 text-xs rounded-xl p-4 overflow-x-auto font-mono leading-relaxed">
|
||||
{snippet}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="card p-5 space-y-3">
|
||||
<h3 className="font-semibold text-slate-900 dark:text-slate-100 text-sm">Инструкция по установке</h3>
|
||||
{[
|
||||
{ step: '1', text: 'Скопируйте код выше' },
|
||||
{ step: '2', text: 'Вставьте перед закрывающим тегом </body> на страницах, где хотите показать виджет' },
|
||||
{ step: '3', text: 'Виджет автоматически добавит кнопку "Забронировать" или встроится в нужный блок' },
|
||||
{ step: '4', text: 'Настройте цвета и параметры в конструкторе — изменения применяются сразу' },
|
||||
].map(s => (
|
||||
<div key={s.step} className="flex items-start gap-3">
|
||||
<span className="w-6 h-6 rounded-full bg-indigo-600 text-white text-xs font-bold flex items-center justify-center shrink-0">{s.step}</span>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400 pt-0.5">{s.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ icon: CalendarCheck2, title: 'Мгновенное подтверждение', desc: 'Email + SMS гостю' },
|
||||
{ icon: CreditCard, title: 'Онлайн-оплата', desc: 'ЮKassa, Тинькофф, СБП' },
|
||||
{ icon: Globe, title: 'Без комиссии OTA', desc: 'Только платёжная комиссия ~1.5%' },
|
||||
].map(f => (
|
||||
<div key={f.title} className="card p-4 text-center">
|
||||
<f.icon size={22} className="text-indigo-500 mx-auto mb-2" />
|
||||
<p className="font-medium text-slate-900 dark:text-slate-100 text-sm">{f.title}</p>
|
||||
<p className="text-xs text-slate-400 mt-0.5">{f.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Stats ── */}
|
||||
{pageTab === 'stats' && (
|
||||
<div className="max-w-2xl">
|
||||
<div className="grid grid-cols-3 gap-3 mb-5">
|
||||
{[
|
||||
{ label: 'Бронирований через виджет', value: '—', sub: 'нет данных' },
|
||||
{ label: 'Конверсия посетителей', value: '—', sub: 'нет данных' },
|
||||
{ label: 'Прямая выручка', value: '—', sub: 'нет данных' },
|
||||
].map(s => (
|
||||
<div key={s.label} className="card p-4 text-center">
|
||||
<p className="text-2xl font-bold text-slate-400">{s.value}</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1 leading-tight">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="card p-6 text-center">
|
||||
<Waves size={40} className="mx-auto mb-3 text-slate-300" />
|
||||
<p className="font-semibold text-slate-700 dark:text-slate-300">Статистика появится после установки виджета</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||||
Разместите виджет на сайте и подключите к нему первые бронирования
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setPageTab('code')}
|
||||
className="btn-primary mt-4"
|
||||
>
|
||||
<ArrowRight size={14} />
|
||||
Получить код установки
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user