import { useState } from 'react' import { Award, Star, Gift, Percent, CreditCard, Edit2, Save, Info, Crown, Shield, User, ChevronRight, Smartphone, Mail, QrCode, TrendingUp, } from 'lucide-react' import { cn } from '../lib/utils' // ─── Types ──────────────────────────────────────────────────────────────────── type LevelId = 'bronze' | 'silver' | 'gold' | 'platinum' interface LoyaltyLevel { id: LevelId name: string minPoints: number maxPoints: number | null discount: number // % discount on stay bonusPercent: number // % of spent amount returned as points perks: string[] color: string icon: React.ElementType } interface ProgramSettings { pointsPerRuble: number // points per 1 ₽ spent pointValue: number // 1 point = N rubles expiryMonths: number // points expire after N months isActive: boolean } interface GuestPoints { id: string name: string email: string points: number totalSpent: number level: LevelId } // ─── Mock Data ──────────────────────────────────────────────────────────────── const DEFAULT_LEVELS: LoyaltyLevel[] = [ { id: 'bronze', name: 'Бронза', minPoints: 0, maxPoints: 999, discount: 0, bonusPercent: 3, perks: ['Накопление бонусных баллов 3%', 'Приоритет на бесплатный Wi-Fi', 'Поздний выезд по запросу'], color: 'text-amber-700 dark:text-amber-500', icon: User, }, { id: 'silver', name: 'Серебро', minPoints: 1000, maxPoints: 4999, discount: 5, bonusPercent: 5, perks: ['Скидка 5% на проживание', 'Накопление 5%', 'Бесплатный ранний заезд', 'Комплимент от отеля'], color: 'text-slate-500 dark:text-slate-400', icon: Shield, }, { id: 'gold', name: 'Золото', minPoints: 5000, maxPoints: 14999, discount: 10, bonusPercent: 8, perks: ['Скидка 10% на проживание', 'Накопление 8%', 'Гарантированный апгрейд номера', 'Приветственный напиток'], color: 'text-yellow-500 dark:text-yellow-400', icon: Award, }, { id: 'platinum', name: 'Платина', minPoints: 15000, maxPoints: null, discount: 15, bonusPercent: 12, perks: ['Скидка 15% на проживание', 'Накопление 12%', 'VIP-заезд/выезд', 'Персональный менеджер', 'Ежегодный бонус 500 баллов'], color: 'text-violet-500 dark:text-violet-400', icon: Crown, }, ] const DEFAULT_SETTINGS: ProgramSettings = { pointsPerRuble: 1, pointValue: 0.5, expiryMonths: 24, isActive: true, } const MOCK_GUESTS: GuestPoints[] = [ { id: 'g-1', name: 'Александр Петров', email: 'a.petrov@gmail.com', points: 16250, totalSpent: 287400, level: 'platinum' }, { id: 'g-4', name: 'Елена Новикова', email: 'e.novikova@gmail.com', points: 8100, totalSpent: 76500, level: 'gold' }, { id: 'g-6', name: 'Анна Лебедева', email: 'anna.l@mail.ru', points: 3950, totalSpent: 132000, level: 'silver' }, { id: 'g-2', name: 'Мария Сидорова', email: 'm.sidorova@mail.ru', points: 2200, totalSpent: 98200, level: 'silver' }, { id: 'g-5', name: 'Игорь Волков', email: 'i.volkov@corp.ru', points: 780, totalSpent: 44000, level: 'bronze' }, { id: 'g-3', name: 'Дмитрий Козлов', email: 'dkozlov@yandex.ru', points: 340, totalSpent: 18600, level: 'bronze' }, { id: 'g-7', name: 'Сергей Морозов', email: '', points: 120, totalSpent: 8400, level: 'bronze' }, ] function levelBadge(levelId: LevelId) { const level = DEFAULT_LEVELS.find(l => l.id === levelId)! const bgs: Record = { bronze: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', silver: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300', gold: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400', platinum: 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400', } return ( {level.name} ) } // ─── Level Editor ───────────────────────────────────────────────────────────── function LevelCard({ level, onEdit }: { level: LoyaltyLevel; onEdit: (l: LoyaltyLevel) => void }) { const [editing, setEditing] = useState(false) const [form, setForm] = useState(level) const save = () => { onEdit(form); setEditing(false) } const levelBg: Record = { bronze: 'from-amber-50 to-orange-50 border-amber-200 dark:from-amber-900/10 dark:to-orange-900/10 dark:border-amber-800', silver: 'from-slate-50 to-slate-100 border-slate-200 dark:from-slate-800/50 dark:to-slate-700/50 dark:border-slate-600', gold: 'from-yellow-50 to-amber-50 border-yellow-200 dark:from-yellow-900/10 dark:to-amber-900/10 dark:border-yellow-800', platinum: 'from-violet-50 to-purple-50 border-violet-200 dark:from-violet-900/10 dark:to-purple-900/10 dark:border-violet-800', } return (
{level.name}
{editing ? (
setForm(p => ({ ...p, minPoints: parseInt(e.target.value) || 0 }))} />
setForm(p => ({ ...p, discount: parseInt(e.target.value) || 0 }))} />
setForm(p => ({ ...p, bonusPercent: parseInt(e.target.value) || 0 }))} />
) : (
Порог баллов {level.minPoints.toLocaleString('ru-RU')} {level.maxPoints ? ` — ${level.maxPoints.toLocaleString('ru-RU')}` : '+'}
Скидка на проживание {level.discount}%
Начисление баллов {level.bonusPercent}% от суммы
{level.perks.map((perk, i) => (
{perk}
))}
)}
) } // ─── Guest Portal Preview ───────────────────────────────────────────────────── function GuestPortalPreview({ settings }: { settings: ProgramSettings }) { const guest = MOCK_GUESTS[0] const level = DEFAULT_LEVELS.find(l => l.id === guest.level)! const nextLevel = DEFAULT_LEVELS.find(l => l.minPoints > guest.points) const progressToNext = nextLevel ? ((guest.points - level.minPoints) / (nextLevel.minPoints - level.minPoints)) * 100 : 100 return (

Личный кабинет гостя (превью)

Добро пожаловать,

{guest.name.split(' ')[0]}

Уровень

{level.name}

{/* Points balance */}

Накопленные баллы

{guest.points.toLocaleString('ru-RU')}

≈ {Math.round(guest.points * settings.pointValue).toLocaleString('ru-RU')} ₽ к оплате

{/* Progress to next level */} {nextLevel && (
До {nextLevel.name} {(nextLevel.minPoints - guest.points).toLocaleString('ru-RU')} баллов
)} {/* Quick perks */}
{[ { icon: Percent, label: `${level.discount}% скидка` }, { icon: Gift, label: 'Комплимент' }, { icon: CreditCard, label: 'Оплата баллами' }, ].map(p => (

{p.label}

))}

Гость получает ссылку на личный кабинет по email после первого бронирования. Доступ — без пароля, по magic link.

) } // ─── Main Page ──────────────────────────────────────────────────────────────── export function LoyaltyPage() { const [levels, setLevels] = useState(DEFAULT_LEVELS) const [settings, setSettings] = useState(DEFAULT_SETTINGS) const [editSettings, setEditSettings] = useState(false) const updateLevel = (updated: LoyaltyLevel) => setLevels(prev => prev.map(l => l.id === updated.id ? updated : l)) const totalGuests = MOCK_GUESTS.length const activeGuests = MOCK_GUESTS.filter(g => g.points > 0).length const totalPoints = MOCK_GUESTS.reduce((s, g) => s + g.points, 0) return (
{/* Header */}

Программа лояльности

Уровни, бонусные баллы, личный кабинет гостя

{settings.isActive ? '● Программа активна' : '○ Программа отключена'}
{/* Stats */}
{[ { label: 'Участников программы', value: activeGuests, icon: User, color: 'text-brand-600 dark:text-brand-400', bg: 'bg-brand-50 dark:bg-brand-900/20' }, { label: 'Всего баллов в обороте', value: totalPoints.toLocaleString('ru-RU'), icon: TrendingUp, color: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-900/20' }, { label: 'Платиновых участников', value: MOCK_GUESTS.filter(g => g.level === 'platinum').length, icon: Crown, color: 'text-violet-600 dark:text-violet-400', bg: 'bg-violet-50 dark:bg-violet-900/20' }, { label: 'Средний балс/гость', value: Math.round(totalPoints / totalGuests).toLocaleString('ru-RU'), icon: Star, color: 'text-yellow-600 dark:text-yellow-400', bg: 'bg-yellow-50 dark:bg-yellow-900/20' }, ].map(s => (

{s.label}

{s.value}

))}
{/* Program settings */}

Настройки программы

{[ { label: 'Начисление', desc: `${settings.pointsPerRuble} балл за 1 ₽`, field: 'pointsPerRuble' as const, unit: 'балл/₽', }, { label: 'Ценность балла', desc: `1 балл = ${settings.pointValue} ₽`, field: 'pointValue' as const, unit: '₽', }, { label: 'Срок действия', desc: `${settings.expiryMonths} месяцев`, field: 'expiryMonths' as const, unit: 'мес.', }, ].map(item => (

{item.label}

{editSettings ? (
setSettings(prev => ({ ...prev, [item.field]: parseFloat(e.target.value) || 0, }))} /> {item.unit}
) : (

{item.desc}

)}
))}
{/* Level cards */}

Уровни программы

{levels.map(level => ( ))}
{/* Top guests */}

Топ участников

{MOCK_GUESTS.sort((a, b) => b.points - a.points).map(g => ( ))}
Гость Уровень Баллы Потрачено
{g.name.split(' ').map(p => p[0]).join('').slice(0, 2)}
{g.name}
{levelBadge(g.level)} {g.points.toLocaleString('ru-RU')} {g.totalSpent.toLocaleString('ru-RU')} ₽
{/* Right column */}
{/* Guest portal preview */} {/* Redemption info */}

Как работает программа

{[ { icon: CreditCard, text: 'При каждом бронировании гостю начисляются баллы пропорционально потраченной сумме' }, { icon: Percent, text: 'Накопленные баллы можно списать при следующем бронировании' }, { icon: Mail, text: 'После первого визита гость получает email с ссылкой на личный кабинет' }, { icon: Award, text: 'При достижении нового уровня приходит уведомление с перечнем привилегий' }, ].map((item, i) => (

{item.text}

))}

В будущих версиях баллы будут работать во всех отелях сети HotelSync — единая программа для всего бренда.

) }