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:
@@ -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' && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user