Files
hotelsync/src/components/rooms/RoomModal.tsx

673 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef } from 'react'
import { Modal } from '../ui/Modal'
import { cn } from '../../lib/utils'
import type { Room, RoomStatus, HousekeepingStatus, BedType, BedItem, BedItemType, ExtraPlace, ChildPolicy } from '../../types'
import { ImagePlus, X as XIcon, ChevronLeft, ChevronRight, Plus, Minus, Baby, Trash2 } from 'lucide-react'
import { useAmenities } from '../../contexts/AmenitiesContext'
const ROOM_TYPES = ['Стандарт', 'Делюкс', 'Полулюкс', 'Люкс', 'Пентхаус', 'Апартаменты', 'Другой']
const BED_TYPES: { value: BedType; label: string }[] = [
{ value: 'single', label: 'Одна кровать' },
{ value: 'double', label: 'Двуспальная' },
{ value: 'queen', label: 'Queen' },
{ value: 'king', label: 'King' },
{ value: 'twin', label: 'Две кровати' },
]
const ROOM_STATUSES: { value: RoomStatus; label: string }[] = [
{ value: 'available', label: 'Свободен' },
{ value: 'occupied', label: 'Занят' },
{ value: 'maintenance', label: 'Ремонт' },
{ value: 'blocked', label: 'Закрыт' },
]
const HK_STATUSES: { value: HousekeepingStatus; label: string }[] = [
{ value: 'clean', label: 'Чистый' },
{ value: 'dirty', label: 'Грязный' },
{ value: 'cleaning', label: 'Убирается' },
{ value: 'inspect', label: 'Проверка' },
]
const BED_ITEM_TYPES: { value: BedItemType; label: string; icon: string }[] = [
{ value: 'single', label: 'Кровать 1-сп.', icon: '🛏' },
{ value: 'double', label: 'Кровать 2-сп.', icon: '🛏' },
{ value: 'queen', label: 'Queen-кровать', icon: '🛏' },
{ value: 'king', label: 'King-кровать', icon: '🛏' },
{ value: 'twin', label: 'Две кровати', icon: '🛏' },
{ value: 'sofa', label: 'Диван', icon: '🛋' },
{ value: 'bunk', label: 'Двухъярусная', icon: '🛏' },
{ value: 'cot', label: 'Раскладушка', icon: '🪑' },
]
// ── Tab type ───────────────────────────────────────────────────────────────────
type ModalTab = 'main' | 'places' | 'description' | 'photos'
interface RoomModalProps {
open: boolean
room?: Room
categories?: { id: string; name: string }[]
onClose: () => void
onSave: (room: Room) => void
onDelete?: () => void
error?: string | null
}
export function RoomModal({ open, room, categories = [], onClose, onSave, onDelete, error }: RoomModalProps) {
const isEdit = !!room
const [tab, setTab] = useState<ModalTab>('main')
const [form, setForm] = useState({
number: room?.number ?? '',
name: room?.name ?? '',
floor: room?.floor ?? 1,
type: room?.type ?? (categories[0]?.name ?? 'Стандарт'),
bedType: (room?.bedType ?? 'double') as BedType,
maxGuests: room?.maxGuests ?? 2,
baseRate: room?.baseRate ?? 5000,
status: (room?.status ?? 'available') as RoomStatus,
housekeepingStatus: (room?.housekeepingStatus ?? 'clean') as HousekeepingStatus,
sortOrder: room?.sortOrder ?? 99,
allowHourly: room?.allowHourly ?? false,
hourlyRate: room?.hourlyRate ?? 1000,
categoryId: room?.categoryId ?? (categories.find(c => c.name === room?.type)?.id ?? categories[0]?.id ?? ''),
description: room?.description ?? '',
earlyCheckinFee: room?.earlyCheckinFee ?? '',
lateCheckoutFee: room?.lateCheckoutFee ?? '',
})
const { amenities: allAmenities } = useAmenities()
const [amenities, setAmenities] = useState<string[]>(room?.amenities ?? ['Wi-Fi', 'TV', 'AC'])
const [photos, setPhotos] = useState<string[]>(room?.photos ?? [])
const [photoIdx, setPhotoIdx] = useState(0)
const fileInputRef = useRef<HTMLInputElement>(null)
// ── Beds constructor ──────────────────────────────────────────────────────
const [beds, setBeds] = useState<BedItem[]>(
room?.beds ?? [{ type: 'double', count: 1 }]
)
const [extraPlace, setExtraPlace] = useState<ExtraPlace>(
room?.extraPlace ?? { enabled: false, count: 1, price: 1500 }
)
const [childPolicy, setChildPolicy] = useState<ChildPolicy>(
room?.childPolicy ?? { enabled: false, freeUnderAge: 12, chargeFromAge: 12 }
)
const addBed = () => setBeds(prev => [...prev, { type: 'single', count: 1 }])
const removeBed = (i: number) => setBeds(prev => prev.filter((_, idx) => idx !== i))
const updateBed = (i: number, patch: Partial<BedItem>) =>
setBeds(prev => prev.map((b, idx) => idx === i ? { ...b, ...patch } : b))
const set = <K extends keyof typeof form>(k: K, v: typeof form[K]) =>
setForm(prev => ({ ...prev, [k]: v }))
const toggleAmenity = (a: string) =>
setAmenities(prev => prev.includes(a) ? prev.filter(x => x !== a) : [...prev, a])
const handlePhotoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files ?? [])
files.forEach(file => {
const reader = new FileReader()
reader.onload = ev => {
const url = ev.target?.result as string
setPhotos(prev => {
const next = [...prev, url]
setPhotoIdx(next.length - 1)
return next
})
}
reader.readAsDataURL(file)
})
e.target.value = ''
}
const removePhoto = (idx: number) => {
setPhotos(prev => prev.filter((_, i) => i !== idx))
setPhotoIdx(p => Math.max(0, p - 1))
}
const handleSave = () => {
if (!form.number) return
onSave({
id: room?.id ?? `r-${Date.now()}`,
hotelId: room?.hotelId ?? 'hotel-1',
number: form.number,
name: form.name || undefined,
floor: form.floor,
type: form.type,
bedType: form.bedType,
maxGuests: form.maxGuests,
baseRate: form.baseRate,
status: form.status,
housekeepingStatus: form.housekeepingStatus,
amenities,
sortOrder: form.sortOrder,
allowHourly: form.allowHourly || undefined,
hourlyRate: form.allowHourly ? form.hourlyRate : undefined,
categoryId: form.categoryId || undefined,
description: form.description || undefined,
photos: photos.length > 0 ? photos : undefined,
beds: beds.length > 0 ? beds : undefined,
extraPlace: extraPlace.enabled ? extraPlace : undefined,
childPolicy: childPolicy.enabled ? childPolicy : undefined,
earlyCheckinFee: form.earlyCheckinFee !== '' ? Number(form.earlyCheckinFee) : undefined,
lateCheckoutFee: form.lateCheckoutFee !== '' ? Number(form.lateCheckoutFee) : undefined,
})
}
const TABS: { key: ModalTab; label: string }[] = [
{ key: 'main', label: 'Основное' },
{ key: 'places', label: 'Места' },
{ key: 'description', label: 'Описание' },
{ key: 'photos', label: `Фото${photos.length > 0 ? ` (${photos.length})` : ''}` },
]
return (
<Modal
open={open}
onClose={onClose}
title={isEdit ? `Редактировать номер ${room.number}` : 'Добавить номер'}
size="xl"
footer={
<>
{isEdit && onDelete && (
<button
onClick={onDelete}
className="btn-ghost text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 mr-auto"
>
<Trash2 size={14} />
Удалить номер
</button>
)}
{error && (
<p className="text-sm text-red-600 dark:text-red-400 flex-1 text-left">{error}</p>
)}
<button onClick={onClose} className="btn-secondary">Отмена</button>
<button onClick={handleSave} className="btn-primary" disabled={!form.number}>
{isEdit ? 'Сохранить' : 'Добавить номер'}
</button>
</>
}
>
{/* Tab bar */}
<div className="flex gap-1 p-1 bg-slate-100 dark:bg-slate-700/50 rounded-xl mb-4">
{TABS.map(t => (
<button
key={t.key}
onClick={() => setTab(t.key)}
className={cn(
'flex-1 py-1.5 rounded-lg text-sm font-medium transition-colors',
tab === t.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',
)}
>
{t.label}
</button>
))}
</div>
{/* ── Main tab ── */}
{tab === 'main' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Номер *</label>
<input type="text" className="input" placeholder="101" value={form.number} onChange={e => set('number', e.target.value)} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Название</label>
<input type="text" className="input" placeholder="Пентхаус" value={form.name} onChange={e => set('name', e.target.value)} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<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={20} className="input" value={form.floor} onChange={e => set('floor', parseInt(e.target.value) || 1)} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Тип номера</label>
{categories.length > 0 ? (
<select
className="input"
value={form.categoryId}
onChange={e => {
const cat = categories.find(c => c.id === e.target.value)
set('categoryId', e.target.value)
if (cat) set('type', cat.name)
}}
>
<option value=""> Выберите категорию </option>
{categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
) : (
<select className="input" value={form.type} onChange={e => set('type', e.target.value)}>
{ROOM_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
</select>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Тип кровати</label>
<select className="input" value={form.bedType} onChange={e => set('bedType', e.target.value as BedType)}>
{BED_TYPES.map(b => <option key={b.value} value={b.value}>{b.label}</option>)}
</select>
</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={10} className="input" value={form.maxGuests} onChange={e => set('maxGuests', parseInt(e.target.value) || 1)} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Цена /ночь</label>
<input type="number" min={0} className="input" value={form.baseRate} onChange={e => set('baseRate', parseInt(e.target.value) || 0)} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Статус номера</label>
<select className="input" value={form.status} onChange={e => set('status', e.target.value as RoomStatus)}>
{ROOM_STATUSES.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Статус уборки</label>
<select className="input" value={form.housekeepingStatus} onChange={e => set('housekeepingStatus', e.target.value as HousekeepingStatus)}>
{HK_STATUSES.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
</select>
</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} className="input" value={form.sortOrder} onChange={e => set('sortOrder', parseInt(e.target.value) || 1)} />
</div>
</div>
{/* Hourly section */}
<div className="rounded-xl border border-slate-200 dark:border-slate-600 overflow-hidden">
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-700/40 border-b border-slate-200 dark:border-slate-600 flex items-center justify-between">
<p className="text-sm font-semibold text-slate-700 dark:text-slate-300">Почасовая аренда</p>
<button
type="button"
onClick={() => set('allowHourly', !form.allowHourly)}
className={cn('relative w-11 h-6 rounded-full transition-colors', form.allowHourly ? 'bg-brand-600' : 'bg-slate-300 dark:bg-slate-600')}
>
<div className={cn('absolute top-0.5 w-5 h-5 rounded-full bg-white shadow-sm transition-transform', form.allowHourly ? 'left-[22px]' : 'left-0.5')} />
</button>
</div>
{form.allowHourly ? (
<div className="p-4">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Цена /час</label>
<input type="number" min={0} className="input w-40" value={form.hourlyRate} onChange={e => set('hourlyRate', parseInt(e.target.value) || 0)} />
</div>
) : (
<div className="px-4 py-3">
<p className="text-sm text-slate-500 dark:text-slate-400">Разрешить бронирование номера на несколько часов</p>
</div>
)}
</div>
{/* Early check-in / late checkout fees */}
<div className="rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-700/40 border-b border-slate-200 dark:border-slate-700">
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">Тарифы за ранний/поздний заезд</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Применяются если включено в настройках отеля</p>
</div>
<div className="p-4 grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Ранний заезд </label>
<input
type="number" min={0} className="input"
placeholder="0 = бесплатно"
value={form.earlyCheckinFee}
onChange={e => set('earlyCheckinFee', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Поздний выезд </label>
<input
type="number" min={0} className="input"
placeholder="0 = бесплатно"
value={form.lateCheckoutFee}
onChange={e => set('lateCheckoutFee', e.target.value)}
/>
</div>
</div>
</div>
{/* Amenities */}
<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">
{allAmenities.map(a => {
const selected = amenities.includes(a)
return (
<button
key={a}
type="button"
onClick={() => toggleAmenity(a)}
className={cn(
'px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors',
selected
? 'bg-brand-600 text-white border-brand-600'
: 'bg-white dark:bg-slate-700 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-600 hover:border-brand-400',
)}
>
{a}
</button>
)
})}
</div>
</div>
</div>
)}
{/* ── Places tab ── */}
{tab === 'places' && (
<div className="space-y-5">
{/* Beds constructor */}
<div className="rounded-xl border border-slate-200 dark:border-slate-600 overflow-hidden">
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-700/40 border-b border-slate-200 dark:border-slate-600">
<p className="text-sm font-semibold text-slate-700 dark:text-slate-300">Спальные места</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Кровати, диваны и другая мебель для сна</p>
</div>
<div className="p-4 space-y-3">
{beds.map((bed, i) => (
<div key={i} className="flex items-center gap-2">
<span className="text-lg w-7 text-center shrink-0">
{BED_ITEM_TYPES.find(b => b.value === bed.type)?.icon ?? '🛏'}
</span>
<select
className="input flex-1"
value={bed.type}
onChange={e => updateBed(i, { type: e.target.value as BedItemType })}
>
{BED_ITEM_TYPES.map(b => (
<option key={b.value} value={b.value}>{b.label}</option>
))}
</select>
{/* Count stepper */}
<div className="flex items-center gap-1 shrink-0">
<button
type="button"
onClick={() => updateBed(i, { count: Math.max(1, bed.count - 1) })}
className="w-7 h-7 rounded-lg border border-slate-200 dark:border-slate-600 flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700"
>
<Minus size={12} />
</button>
<span className="w-6 text-center text-sm font-medium text-slate-700 dark:text-slate-300">{bed.count}</span>
<button
type="button"
onClick={() => updateBed(i, { count: Math.min(6, bed.count + 1) })}
className="w-7 h-7 rounded-lg border border-slate-200 dark:border-slate-600 flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700"
>
<Plus size={12} />
</button>
</div>
<button
type="button"
onClick={() => removeBed(i)}
disabled={beds.length === 1}
className="w-7 h-7 rounded-lg flex items-center justify-center text-slate-400 hover:text-red-500 disabled:opacity-30 disabled:cursor-not-allowed"
>
<XIcon size={14} />
</button>
</div>
))}
<button
type="button"
onClick={addBed}
className="btn-secondary w-full justify-center text-xs py-1.5"
>
<Plus size={13} />
Добавить спальное место
</button>
</div>
</div>
{/* Extra places */}
<div className="rounded-xl border border-slate-200 dark:border-slate-600 overflow-hidden">
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-700/40 border-b border-slate-200 dark:border-slate-600 flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-slate-700 dark:text-slate-300">Дополнительные места</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Раскладные кровати, кушетки за доп. плату</p>
</div>
<button
type="button"
onClick={() => setExtraPlace(p => ({ ...p, enabled: !p.enabled }))}
className={cn('relative w-11 h-6 rounded-full transition-colors shrink-0', extraPlace.enabled ? 'bg-brand-600' : 'bg-slate-300 dark:bg-slate-600')}
>
<div className={cn('absolute top-0.5 w-5 h-5 rounded-full bg-white shadow-sm transition-transform', extraPlace.enabled ? 'left-[22px]' : 'left-0.5')} />
</button>
</div>
{extraPlace.enabled && (
<div className="p-4 grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1.5">Кол-во мест</label>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setExtraPlace(p => ({ ...p, count: Math.max(1, p.count - 1) }))}
className="w-8 h-8 rounded-lg border border-slate-200 dark:border-slate-600 flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700">
<Minus size={13} />
</button>
<span className="w-8 text-center font-semibold text-slate-800 dark:text-slate-200">{extraPlace.count}</span>
<button type="button" onClick={() => setExtraPlace(p => ({ ...p, count: Math.min(5, p.count + 1) }))}
className="w-8 h-8 rounded-lg border border-slate-200 dark:border-slate-600 flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700">
<Plus size={13} />
</button>
</div>
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1.5">Цена /ночь</label>
<input
type="number" min={0} className="input"
value={extraPlace.price}
onChange={e => setExtraPlace(p => ({ ...p, price: parseInt(e.target.value) || 0 }))}
/>
</div>
</div>
)}
</div>
{/* Child policy */}
<div className="rounded-xl border border-slate-200 dark:border-slate-600 overflow-hidden">
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-700/40 border-b border-slate-200 dark:border-slate-600 flex items-center justify-between">
<div className="flex items-center gap-2">
<Baby size={16} className="text-slate-500" />
<div>
<p className="text-sm font-semibold text-slate-700 dark:text-slate-300">Политика для детей</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Возраст бесплатного проживания</p>
</div>
</div>
<button
type="button"
onClick={() => setChildPolicy(p => ({ ...p, enabled: !p.enabled }))}
className={cn('relative w-11 h-6 rounded-full transition-colors shrink-0', childPolicy.enabled ? 'bg-brand-600' : 'bg-slate-300 dark:bg-slate-600')}
>
<div className={cn('absolute top-0.5 w-5 h-5 rounded-full bg-white shadow-sm transition-transform', childPolicy.enabled ? 'left-[22px]' : 'left-0.5')} />
</button>
</div>
{childPolicy.enabled ? (
<div className="p-4 space-y-3">
<div className="flex items-center gap-3 p-3 rounded-lg bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800">
<span className="text-xl">🆓</span>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-emerald-800 dark:text-emerald-300">Бесплатно до</p>
</div>
<div className="flex items-center gap-1.5 shrink-0">
<button type="button" onClick={() => setChildPolicy(p => ({ ...p, freeUnderAge: Math.max(1, p.freeUnderAge - 1) }))}
className="w-7 h-7 rounded-lg border border-emerald-200 dark:border-emerald-700 flex items-center justify-center text-emerald-600 hover:bg-emerald-100 dark:hover:bg-emerald-900/40">
<Minus size={12} />
</button>
<span className="w-16 text-center text-sm font-semibold text-emerald-800 dark:text-emerald-300">
до {childPolicy.freeUnderAge} лет
</span>
<button type="button" onClick={() => setChildPolicy(p => ({ ...p, freeUnderAge: Math.min(17, p.freeUnderAge + 1) }))}
className="w-7 h-7 rounded-lg border border-emerald-200 dark:border-emerald-700 flex items-center justify-center text-emerald-600 hover:bg-emerald-100 dark:hover:bg-emerald-900/40">
<Plus size={12} />
</button>
</div>
</div>
<div className="flex items-center gap-3 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800">
<span className="text-xl">💰</span>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-amber-800 dark:text-amber-300">Доп. место от</p>
{extraPlace.enabled && (
<p className="text-[10px] text-amber-600 dark:text-amber-400">{extraPlace.price} /ночь</p>
)}
{!extraPlace.enabled && (
<p className="text-[10px] text-amber-600 dark:text-amber-400">Включите «Доп. места» и укажите цену</p>
)}
</div>
<div className="flex items-center gap-1.5 shrink-0">
<button type="button" onClick={() => setChildPolicy(p => ({ ...p, chargeFromAge: Math.max(1, p.chargeFromAge - 1) }))}
className="w-7 h-7 rounded-lg border border-amber-200 dark:border-amber-700 flex items-center justify-center text-amber-600 hover:bg-amber-100 dark:hover:bg-amber-900/40">
<Minus size={12} />
</button>
<span className="w-16 text-center text-sm font-semibold text-amber-800 dark:text-amber-300">
от {childPolicy.chargeFromAge} лет
</span>
<button type="button" onClick={() => setChildPolicy(p => ({ ...p, chargeFromAge: Math.min(17, p.chargeFromAge + 1) }))}
className="w-7 h-7 rounded-lg border border-amber-200 dark:border-amber-700 flex items-center justify-center text-amber-600 hover:bg-amber-100 dark:hover:bg-amber-900/40">
<Plus size={12} />
</button>
</div>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 px-1">
Дети до {childPolicy.freeUnderAge} лет бесплатно без доп. места.
Дети от {childPolicy.chargeFromAge} лет оплачивают дополнительное место.
</p>
</div>
) : (
<div className="px-4 py-3">
<p className="text-sm text-slate-500 dark:text-slate-400">Стандартные условия, все гости оплачиваются</p>
</div>
)}
</div>
</div>
)}
{/* ── Description tab ── */}
{tab === 'description' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
Описание номера
</label>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-2">
Будет отображаться в виджете онлайн-бронирования для гостей.
</p>
<textarea
className="input resize-none w-full"
rows={8}
placeholder="Опишите номер: интерьер, вид из окна, особенности, что включено..."
value={form.description}
onChange={e => set('description', e.target.value)}
/>
<p className="text-xs text-slate-400 mt-1 text-right">{form.description.length} символов</p>
</div>
{form.description && (
<div className="rounded-xl border border-slate-200 dark:border-slate-600 p-4">
<p className="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-2">Предпросмотр</p>
<p className="text-sm text-slate-700 dark:text-slate-300 leading-relaxed whitespace-pre-wrap">{form.description}</p>
</div>
)}
</div>
)}
{/* ── Photos tab ── */}
{tab === 'photos' && (
<div className="space-y-4">
{/* Main photo viewer */}
{photos.length > 0 ? (
<div className="space-y-3">
<div className="relative rounded-xl overflow-hidden bg-slate-100 dark:bg-slate-700" style={{ height: 240 }}>
<img
src={photos[photoIdx]}
alt={`Фото ${photoIdx + 1}`}
className="w-full h-full object-cover"
/>
{photos.length > 1 && (
<>
<button
onClick={() => setPhotoIdx(p => (p - 1 + photos.length) % photos.length)}
className="absolute left-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-black/50 text-white flex items-center justify-center hover:bg-black/70"
>
<ChevronLeft size={16} />
</button>
<button
onClick={() => setPhotoIdx(p => (p + 1) % photos.length)}
className="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-black/50 text-white flex items-center justify-center hover:bg-black/70"
>
<ChevronRight size={16} />
</button>
</>
)}
<button
onClick={() => removePhoto(photoIdx)}
className="absolute top-2 right-2 w-7 h-7 rounded-full bg-red-600 text-white flex items-center justify-center hover:bg-red-700"
>
<XIcon size={13} />
</button>
<div className="absolute bottom-2 right-2 bg-black/50 text-white text-xs px-2 py-0.5 rounded-full">
{photoIdx + 1} / {photos.length}
</div>
</div>
{/* Thumbnails */}
<div className="flex gap-2 overflow-x-auto pb-1">
{photos.map((p, i) => (
<button
key={i}
onClick={() => setPhotoIdx(i)}
className={cn(
'w-16 h-16 rounded-lg overflow-hidden border-2 shrink-0 transition-colors',
i === photoIdx ? 'border-brand-500' : 'border-transparent',
)}
>
<img src={p} alt="" className="w-full h-full object-cover" />
</button>
))}
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center h-48 rounded-xl border-2 border-dashed border-slate-300 dark:border-slate-600 text-slate-400">
<ImagePlus size={32} className="mb-2 opacity-40" />
<p className="text-sm">Фотографий пока нет</p>
</div>
)}
{/* Upload button */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={handlePhotoUpload}
/>
<button
onClick={() => fileInputRef.current?.click()}
className="btn-secondary w-full justify-center"
>
<ImagePlus size={15} />
{photos.length > 0 ? 'Добавить ещё фото' : 'Загрузить фотографии'}
</button>
<p className="text-xs text-slate-400 text-center">
Поддерживаются JPG, PNG, WEBP. Первое фото обложка в виджете.
</p>
</div>
)}
</Modal>
)
}