Add room categories, documents module, guest settings, and booking improvements

- New pages: RoomCategoriesPage (category CRUD with photos, color, amenities) and DocumentsPage (template constructor with variable substitution, print packages)
- Sidebar: added "Категории номеров" link under rooms, Documents module via modulesData
- App.tsx: routes for /room-categories and /documents
- RoomModal: now accepts categories prop for categoryId select
- BookingModal: added additional services dropdown (breakfast, transfer, parking etc.) accumulating into booking total; summary shows services breakdown
- SettingsPage: new "Гости" section with guest tag CRUD (add/edit/remove, color picker, live preview); booking section toggle "Показывать поле Источник бронирования"
- modulesData: added Documents module entry (free tier, sidebar item)
- RoomsPage: passes MOCK_CATEGORIES to RoomModal
- ReviewsPage: rewritten with auto-redirect logic, threshold slider, platform settings tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 22:17:54 +03:00
parent 56df6711e4
commit 96dfd755ed
11 changed files with 1816 additions and 529 deletions

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Save, Building2, Bell, Shield, Globe, CreditCard, BedDouble, Mail, MessageSquare } from 'lucide-react'
import { Save, Building2, Bell, Shield, Globe, CreditCard, BedDouble, Mail, MessageSquare, Users, Plus, X as XIcon } from 'lucide-react'
import { MOCK_HOTELS } from '../data/mockData'
import { cn, PLAN_COLORS, PLAN_LABELS } from '../lib/utils'
import { Badge } from '../components/ui/Badge'
@@ -8,6 +8,7 @@ import { useTheme } from '../contexts/ThemeContext'
const SECTIONS = [
{ id: 'general', label: 'Основные', icon: Building2 },
{ id: 'booking', label: 'Бронирование', icon: BedDouble },
{ id: 'guests', label: 'Гости', icon: Users },
{ id: 'theme', label: 'Внешний вид', icon: Globe },
{ id: 'notify', label: 'Уведомления', icon: Bell },
{ id: 'security', label: 'Безопасность', icon: Shield },
@@ -42,6 +43,19 @@ export function SettingsPage() {
// Booking / assignment settings
const [assignmentStrategy, setAssignmentStrategy] = useState<'spread' | 'together' | 'sequential' | 'manual'>('spread')
const [showBookingSource, setShowBookingSource] = useState(false)
// 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({
@@ -161,6 +175,15 @@ export function SettingsPage() {
<>
<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>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Стратегия автоматического расселения
@@ -224,6 +247,95 @@ export function SettingsPage() {
</>
)}
{/* ── GUESTS ── */}
{section === 'guests' && (
<>
<h2 className="font-semibold text-slate-900 dark:text-slate-100">Статусы гостей</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
Теги отображаются при создании и редактировании бронирования
</p>
{/* Tag list */}
<div className="space-y-2">
{guestTags.map(tag => (
<div
key={tag.id}
className="flex items-center gap-3 p-3 rounded-lg bg-slate-50 dark:bg-slate-700/40"
>
<input
type="color"
value={tag.color}
onChange={e => setGuestTags(prev => prev.map(t => t.id === tag.id ? { ...t, color: e.target.value } : t))}
className="w-7 h-7 rounded cursor-pointer border-0 bg-transparent"
/>
<input
type="text"
className="input flex-1"
value={tag.label}
onChange={e => setGuestTags(prev => prev.map(t => t.id === tag.id ? { ...t, label: e.target.value } : t))}
/>
<button
onClick={() => setGuestTags(prev => prev.filter(t => t.id !== tag.id))}
className="p-1.5 rounded-lg text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
>
<XIcon size={14} />
</button>
</div>
))}
</div>
{/* Add new tag */}
<div className="flex items-center gap-2 pt-1">
<input
type="color"
value={newTagColor}
onChange={e => setNewTagColor(e.target.value)}
className="w-7 h-7 rounded cursor-pointer border-0 bg-transparent shrink-0"
/>
<input
type="text"
className="input flex-1"
placeholder="Название нового статуса..."
value={newTagLabel}
onChange={e => setNewTagLabel(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && newTagLabel.trim()) {
setGuestTags(prev => [...prev, { id: `tag-${Date.now()}`, label: newTagLabel.trim(), color: newTagColor }])
setNewTagLabel('')
}
}}
/>
<button
onClick={() => {
if (!newTagLabel.trim()) return
setGuestTags(prev => [...prev, { id: `tag-${Date.now()}`, label: newTagLabel.trim(), color: newTagColor }])
setNewTagLabel('')
}}
className="btn-primary"
>
<Plus size={14} />
Добавить
</button>
</div>
{/* Preview */}
<div className="pt-2 border-t border-slate-100 dark:border-slate-700">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-2">Предпросмотр тегов</p>
<div className="flex flex-wrap gap-1.5">
{guestTags.map(tag => (
<span
key={tag.id}
className="px-2.5 py-1 rounded-lg text-xs font-medium text-white"
style={{ backgroundColor: tag.color }}
>
{tag.label}
</span>
))}
</div>
</div>
</>
)}
{/* ── THEME ── */}
{section === 'theme' && (
<>