import { useState } from 'react' import { Star, ThumbsDown, Copy, CheckCheck, MessageSquare, QrCode, ArrowUpRight, Plus, Trash2, ExternalLink, Settings2, Send, Mail, Smartphone, Clock, Eye, Palette, Globe, } from 'lucide-react' import { format } from 'date-fns' import { ru } from 'date-fns/locale' import { cn } from '../lib/utils' import { Badge } from '../components/ui/Badge' // ── Types ───────────────────────────────────────────────────────────────────── type ReviewStatus = 'pending' | 'published' | 'rejected' type BookingSource = 'booking' | 'yandex' | 'google' | 'direct' | 'widget' interface Review { id: string guestName: string roomNumber: string rating: number text: string status: ReviewStatus source: 'qr' | 'email' | 'sms' bookingSource?: BookingSource createdAt: Date reply?: string autoRedirectedTo?: string[] } interface ReviewPlatform { id: string name: string url: string enabled: boolean } // ── Default settings ────────────────────────────────────────────────────────── const DEFAULT_PLATFORMS: ReviewPlatform[] = [ { id: 'p1', name: 'Booking.com', url: 'https://www.booking.com/hotel/ru/', enabled: true }, { id: 'p2', name: 'Яндекс Путешествия', url: 'https://travel.yandex.ru/hotels/', enabled: true }, { id: 'p3', name: 'Google', url: 'https://g.page/r/', enabled: true }, { id: 'p4', name: '2ГИС', url: 'https://2gis.ru/moscow/firm/', enabled: false }, ] // ── Mock data ───────────────────────────────────────────────────────────────── const MOCK_REVIEWS: Review[] = [ { id: 'rv1', guestName: 'Дмитрий Волков', roomNumber: '101', rating: 5, source: 'email', bookingSource: 'booking', createdAt: new Date(Date.now() - 3600000 * 5), text: 'Замечательный отель! Персонал очень вежливый. Номер чистый, завтрак вкусный. Обязательно вернёмся.', status: 'published', autoRedirectedTo: ['Booking.com'], }, { id: 'rv2', guestName: 'Анна Козлова', roomNumber: '202', rating: 1, source: 'qr', bookingSource: 'direct', createdAt: new Date(Date.now() - 3600000 * 12), text: 'Шум из соседнего номера мешал спать. Кондиционер плохо работал. Разочарована.', status: 'pending', }, { id: 'rv3', guestName: 'Наталья Александрова', roomNumber: '301', rating: 5, source: 'sms', bookingSource: 'widget', createdAt: new Date(Date.now() - 3600000 * 36), text: 'Лучший отель! Вид из окна потрясающий, кровать удобная, всё стильно.', status: 'published', autoRedirectedTo: ['Booking.com', 'Яндекс Путешествия', 'Google'], }, { id: 'rv4', guestName: 'Игорь Соколов', roomNumber: '201', rating: 4, source: 'email', bookingSource: 'yandex', createdAt: new Date(Date.now() - 3600000 * 48), text: 'В целом хорошо. Немного дорого для такого номера. Расположение отличное.', status: 'published', autoRedirectedTo: ['Яндекс Путешествия'], }, { id: 'rv5', guestName: 'Виктор Громов', roomNumber: '402', rating: 2, source: 'qr', bookingSource: 'direct', createdAt: new Date(Date.now() - 3600000 * 60), text: 'Долго ждали заселения. Номер убран с опозданием. Разочарованы.', status: 'pending', }, { id: 'rv6', guestName: 'Михаил Орлов', roomNumber: '401', rating: 4, source: 'email', bookingSource: 'google', createdAt: new Date(Date.now() - 3600000 * 100), text: 'Очень доволен. Персонал внимательный, номер чистый.', status: 'published', reply: 'Михаил, спасибо за тёплые слова! Ждём вас снова.', autoRedirectedTo: ['Google'], }, ] const REVIEW_LINK = 'https://guest.hotelsync.ru/review/grand-palace' const SOURCE_LABEL: Record = { qr: 'QR-код', email: 'Email', sms: 'SMS' } const BOOKING_SOURCE_LABEL: Record = { booking: 'Booking.com', yandex: 'Яндекс Путешествия', google: 'Google', direct: 'Прямое', widget: 'Виджет', } const PREVIEW_COLORS = ['#2563eb', '#16a34a', '#9333ea', '#dc2626'] // ── Helper components ───────────────────────────────────────────────────────── function StarRating({ rating, size = 14 }: { rating: number; size?: number }) { return (
{Array.from({ length: 5 }, (_, i) => ( ))} {rating}/5
) } function ratingColor(r: number, threshold: number): string { if (r >= 5) return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300' if (r >= 4) return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300' if (r >= threshold) return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300' return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300' } type Tab = 'pending' | 'published' | 'all' | 'preview' | 'settings' // ── Component ───────────────────────────────────────────────────────────────── export function ReviewsPage() { const [reviews, setReviews] = useState(MOCK_REVIEWS) const [tab, setTab] = useState('pending') const [replyId, setReplyId] = useState(null) const [replyText, setReplyText] = useState('') const [copied, setCopied] = useState(false) // Settings const [threshold, setThreshold] = useState(4) const [platforms, setPlatforms] = useState(DEFAULT_PLATFORMS) const [newPlatformName, setNewPlatformName] = useState('') const [newPlatformUrl, setNewPlatformUrl] = useState('') // Settings — sending const [sendHours, setSendHours] = useState(2) const [sendChannel, setSendChannel] = useState<'email' | 'sms' | 'both'>('email') const [sendRetry, setSendRetry] = useState(true) const [useSourceRedirect, setUseSourceRedirect] = useState(true) // Preview state const [previewRating, setPreviewRating] = useState(0) const [previewHover, setPreviewHover] = useState(0) const [previewSubmitted, setPreviewSubmitted] = useState(false) const [previewHotelName, setPreviewHotelName] = useState('Гранд Палас') const [previewColor, setPreviewColor] = useState(PREVIEW_COLORS[0]) const [previewWelcome, setPreviewWelcome] = useState('Как вам у нас?') const [previewShowText, setPreviewShowText] = useState(true) const pending = reviews.filter(r => r.status === 'pending') const published = reviews.filter(r => r.status === 'published') const avgRating = published.length ? (published.reduce((s, r) => s + r.rating, 0) / published.length).toFixed(1) : '—' const nps = published.length ? Math.round( (published.filter(r => r.rating >= 5).length / published.length * 100) - (published.filter(r => r.rating <= 2).length / published.length * 100), ) : 0 const filtered = tab === 'all' ? reviews : tab === 'settings' || tab === 'preview' ? [] : reviews.filter(r => r.status === tab) const sendReply = (id: string) => { if (!replyText.trim()) return setReviews(prev => prev.map(r => r.id === id ? { ...r, reply: replyText.trim(), status: 'published' } : r, )) setReplyId(null) setReplyText('') } const reject = (id: string) => setReviews(prev => prev.map(r => r.id === id ? { ...r, status: 'rejected' } : r)) const addPlatform = () => { if (!newPlatformName.trim()) return setPlatforms(prev => [...prev, { id: `p-${Date.now()}`, name: newPlatformName.trim(), url: newPlatformUrl.trim(), enabled: true, }]) setNewPlatformName('') setNewPlatformUrl('') } const copyLink = () => { navigator.clipboard.writeText(REVIEW_LINK).catch(() => {}) setCopied(true) setTimeout(() => setCopied(false), 2000) } const enabledPlatforms = platforms.filter(p => p.enabled) // Determine redirect platform based on booking source const getRedirectPlatform = (source?: BookingSource): string | null => { if (!useSourceRedirect) return null if (source === 'booking') return 'Booking.com' if (source === 'yandex') return 'Яндекс Путешествия' if (source === 'google') return 'Google' return null } const TABS: { key: Tab; label: string; count?: number; colorCls?: string; Icon?: React.ElementType }[] = [ { key: 'pending', label: 'Требуют ответа', count: pending.length, colorCls: 'bg-red-500 text-white' }, { key: 'published', label: 'Архив', count: published.length }, { key: 'all', label: 'Все' }, { key: 'preview', label: 'Превью', Icon: Eye }, { key: 'settings', label: 'Настройки', Icon: Settings2 }, ] return (
{/* Header */}

Отзывы гостей

Отрицательные (< {threshold} звёзд) → модерация. Положительные → автоматический редирект на площадки.

{/* Stats */}
{[ { label: 'Средняя оценка', value: avgRating, sub: 'из 5 звёзд', color: 'text-amber-600 dark:text-amber-400' }, { label: 'NPS', value: nps >= 0 ? `+${nps}` : `${nps}`, sub: 'лояльность', color: 'text-emerald-600 dark:text-emerald-400' }, { label: 'В архиве', value: published.length, sub: 'опубликованных', color: 'text-brand-600 dark:text-brand-400' }, { label: 'Ждут ответа', value: pending.length, sub: 'негативных', color: pending.length > 0 ? 'text-red-600 dark:text-red-400' : 'text-slate-500' }, ].map(s => (

{s.value}

{s.label}

{s.sub}

))}
{/* Logic banners */}

Отрицательные (< {threshold} звёзд)

Модерация — менеджер пишет личный ответ гостю. Публично не публикуется.

Положительные (≥ {threshold} звёзд)

Авторедирект на площадку источника бронирования или выбор из: {enabledPlatforms.map(p => p.name).join(', ') || '(настройте)'}

{/* Review link */}

Ссылка для сбора отзывов

{REVIEW_LINK}
{/* Tabs */}
{TABS.map(t => ( ))}
{/* ══════════════════════ TAB: preview ══════════════════════ */} {tab === 'preview' && (
{/* Phone frame */}
{/* Phone notch */}
{/* Preview content */}
{/* Hotel logo */}
{previewHotelName[0]}

{previewHotelName}

{previewWelcome}

{/* Star selector */} {!previewSubmitted && (
{Array.from({ length: 5 }, (_, i) => ( ))}
)} {/* Text area (if rating selected and positive) */} {previewRating > 0 && !previewSubmitted && previewShowText && (