BookingModal: redesign — dates first, category cards, specific room toggle
This commit is contained in:
@@ -51,6 +51,17 @@ function availableRooms(rooms: Room[], bookings: Booking[], checkIn: string, che
|
||||
return rooms.filter(r => !isConflict(bookings, r.id, checkIn, checkOut, excludeId))
|
||||
}
|
||||
|
||||
// Возвращает уникальные категории (типы) номеров с количеством свободных
|
||||
function getRoomCategories(rooms: Room[], bookings: Booking[], checkIn: string, checkOut: string, excludeId?: string) {
|
||||
const types = Array.from(new Set(rooms.map(r => r.type)))
|
||||
return types.map(type => {
|
||||
const roomsOfType = rooms.filter(r => r.type === type)
|
||||
const freeRooms = roomsOfType.filter(r => !isConflict(bookings, r.id, checkIn, checkOut, excludeId))
|
||||
const minRate = Math.min(...roomsOfType.map(r => r.baseRate))
|
||||
return { type, total: roomsOfType.length, free: freeRooms.length, minRate, rooms: freeRooms, allRooms: roomsOfType }
|
||||
})
|
||||
}
|
||||
|
||||
export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSave, existing }: BookingModalProps) {
|
||||
const { statuses } = useModules()
|
||||
const { user } = useAuth()
|
||||
@@ -80,8 +91,16 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
}
|
||||
|
||||
const _nameParts = (existing?.guestName ?? '').split(' ')
|
||||
|
||||
// Определяем начальную категорию из существующего бронирования или первого доступного номера
|
||||
const _initRoomId = existing?.roomId ?? draft.roomId
|
||||
const _initCategory = rooms.find(r => r.id === _initRoomId)?.type ?? rooms[0]?.type ?? ''
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useState(_initCategory)
|
||||
const [pickSpecificRoom, setPickSpecificRoom] = useState(!!existing?.roomId)
|
||||
|
||||
const [form, setForm] = useState({
|
||||
roomId: existing?.roomId ?? draft.roomId,
|
||||
roomId: _initRoomId,
|
||||
lastName: _nameParts[0] ?? '',
|
||||
firstName: _nameParts[1] ?? '',
|
||||
middleName: _nameParts.slice(2).join(' '),
|
||||
@@ -115,6 +134,34 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
const EXTRA_BED_PRICE = 1500
|
||||
|
||||
const room = rooms.find(r => r.id === form.roomId)
|
||||
|
||||
// Категории и доступность
|
||||
const categories = getRoomCategories(rooms, bookings, form.checkIn, form.checkOut, existing?.id)
|
||||
const currentCategory = categories.find(c => c.type === selectedCategory)
|
||||
const categoryAvailableRooms = currentCategory
|
||||
? (pickSpecificRoom ? currentCategory.rooms : currentCategory.allRooms)
|
||||
: []
|
||||
|
||||
// При смене категории или дат (если не выбран конкретный номер) — авто-выбираем первый свободный
|
||||
const handleCategorySelect = (type: string) => {
|
||||
setSelectedCategory(type)
|
||||
if (!pickSpecificRoom) {
|
||||
const cat = getRoomCategories(rooms, bookings, form.checkIn, form.checkOut, existing?.id).find(c => c.type === type)
|
||||
const first = cat?.rooms[0] ?? cat?.allRooms[0]
|
||||
if (first) set('roomId', first.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePickSpecificToggle = (on: boolean) => {
|
||||
setPickSpecificRoom(on)
|
||||
if (!on) {
|
||||
// Возвращаемся к первому свободному в категории
|
||||
const cat = getRoomCategories(rooms, bookings, form.checkIn, form.checkOut, existing?.id).find(c => c.type === selectedCategory)
|
||||
const first = cat?.rooms[0] ?? cat?.allRooms[0]
|
||||
if (first) set('roomId', first.id)
|
||||
}
|
||||
}
|
||||
|
||||
const nights = form.checkIn && form.checkOut
|
||||
? Math.max(0, (new Date(form.checkOut).getTime() - new Date(form.checkIn).getTime()) / 86400000)
|
||||
: 0
|
||||
@@ -214,136 +261,167 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||
{/* ── LEFT COLUMN ── */}
|
||||
<div className="flex-1 space-y-3 min-w-0">
|
||||
{/* Room */}
|
||||
<div className="flex-1 space-y-4 min-w-0">
|
||||
|
||||
{/* 1. Даты заезда / выезда */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
Номер
|
||||
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
|
||||
Даты проживания
|
||||
</label>
|
||||
{isHourly && room?.allowHourly ? (
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="date"
|
||||
className="input"
|
||||
value={hourlyDate}
|
||||
onChange={e => setHourlyDate(e.target.value)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Начало</label>
|
||||
<select className="input" value={startHour} onChange={e => setStartHour(parseInt(e.target.value))}>
|
||||
{HOURS.map(h => <option key={h} value={h}>{String(h).padStart(2, '0')}:00</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Конец</label>
|
||||
<select className="input" value={endHour} onChange={e => setEndHour(parseInt(e.target.value))}>
|
||||
{HOURS.filter(h => h > startHour).map(h => <option key={h} value={h}>{String(h).padStart(2, '0')}:00</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Заезд *</label>
|
||||
<input
|
||||
type="date" className="input"
|
||||
value={form.checkIn}
|
||||
onChange={e => set('checkIn', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Выезд *</label>
|
||||
<input
|
||||
type="date" className="input"
|
||||
value={form.checkOut}
|
||||
onChange={e => set('checkOut', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 2. Категория номера */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300">
|
||||
Категория номера
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFloorMap(true)}
|
||||
className="flex items-center gap-1 text-xs text-brand-600 dark:text-brand-400 hover:underline"
|
||||
>
|
||||
<Map size={12} />
|
||||
Поэтажный план
|
||||
<Map size={12} /> Поэтажный план
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{categories.map(cat => {
|
||||
const isSelected = selectedCategory === cat.type
|
||||
const hasAvailable = cat.free > 0
|
||||
const datesSet = !!(form.checkIn && form.checkOut && form.checkIn < form.checkOut)
|
||||
return (
|
||||
<button
|
||||
key={cat.type}
|
||||
type="button"
|
||||
onClick={() => handleCategorySelect(cat.type)}
|
||||
className={cn(
|
||||
'relative text-left px-3 py-2.5 rounded-xl border-2 transition-all',
|
||||
isSelected
|
||||
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20'
|
||||
: hasAvailable || !datesSet
|
||||
? 'border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700/40 hover:border-brand-300 dark:hover:border-brand-600'
|
||||
: 'border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/40 opacity-60 cursor-default',
|
||||
)}
|
||||
>
|
||||
<p className={cn(
|
||||
'text-sm font-medium leading-tight',
|
||||
isSelected ? 'text-brand-700 dark:text-brand-300' : 'text-slate-700 dark:text-slate-300',
|
||||
!hasAvailable && datesSet && 'text-slate-400 dark:text-slate-500',
|
||||
)}>
|
||||
{cat.type}
|
||||
</p>
|
||||
<p className="text-[11px] text-slate-400 dark:text-slate-500 mt-0.5">
|
||||
от {cat.minRate.toLocaleString('ru-RU')} ₽/ночь
|
||||
</p>
|
||||
{datesSet && (
|
||||
<span className={cn(
|
||||
'absolute top-2 right-2 text-[10px] font-semibold px-1.5 py-0.5 rounded-full',
|
||||
hasAvailable
|
||||
? 'bg-emerald-100 dark:bg-emerald-900/40 text-emerald-700 dark:text-emerald-400'
|
||||
: 'bg-slate-100 dark:bg-slate-700 text-slate-400',
|
||||
)}>
|
||||
{hasAvailable ? `${cat.free} св.` : 'занято'}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Переключатель конкретного номера */}
|
||||
{selectedCategory && (
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
<div
|
||||
role="switch"
|
||||
aria-checked={pickSpecificRoom}
|
||||
onClick={() => handlePickSpecificToggle(!pickSpecificRoom)}
|
||||
className={cn(
|
||||
'relative w-9 h-5 rounded-full transition-colors',
|
||||
pickSpecificRoom ? 'bg-brand-600' : 'bg-slate-300 dark:bg-slate-600',
|
||||
)}
|
||||
>
|
||||
<span className={cn(
|
||||
'absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform',
|
||||
pickSpecificRoom && 'translate-x-4',
|
||||
)} />
|
||||
</div>
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">Выбрать конкретный номер</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Выпадающий список конкретных номеров */}
|
||||
{pickSpecificRoom && selectedCategory && (
|
||||
<div className="mt-2">
|
||||
{categoryAvailableRooms.length > 0 ? (
|
||||
<select
|
||||
value={form.roomId}
|
||||
onChange={e => set('roomId', e.target.value)}
|
||||
className="input"
|
||||
>
|
||||
{rooms.map(r => (
|
||||
{categoryAvailableRooms.map(r => (
|
||||
<option key={r.id} value={r.id}>
|
||||
{r.number} — {r.type} ({r.baseRate.toLocaleString('ru-RU')} ₽/ночь{r.allowHourly ? `, ${(r.hourlyRate ?? 0).toLocaleString('ru-RU')} ₽/ч` : ''})
|
||||
№{r.number}{r.floor ? `, ${r.floor} эт.` : ''} — {r.baseRate.toLocaleString('ru-RU')} ₽/ночь
|
||||
{r.allowHourly ? ` · ${(r.hourlyRate ?? 0).toLocaleString('ru-RU')} ₽/ч` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/* Conflict warning */}
|
||||
{!isHourly && form.roomId && form.checkIn && form.checkOut && form.checkIn < form.checkOut &&
|
||||
isConflict(bookings, form.roomId, form.checkIn, form.checkOut, existing?.id) && (() => {
|
||||
const free = availableRooms(rooms, bookings, form.checkIn, form.checkOut, existing?.id)
|
||||
return (
|
||||
<div className="mt-2 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700">
|
||||
<div className="flex items-start gap-2 text-amber-800 dark:text-amber-300 text-xs mb-2">
|
||||
<AlertCircle size={13} className="shrink-0 mt-0.5" />
|
||||
<span>Номер занят на эти даты.</span>
|
||||
</div>
|
||||
{free.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<span className="text-xs text-amber-700 dark:text-amber-400 self-center">Свободны:</span>
|
||||
{free.map(r => (
|
||||
<button
|
||||
key={r.id}
|
||||
type="button"
|
||||
onClick={() => set('roomId', r.id)}
|
||||
className="text-xs px-2 py-0.5 rounded-md bg-white dark:bg-slate-700 border border-amber-300 dark:border-amber-600 text-amber-800 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/30 font-medium transition-colors"
|
||||
>
|
||||
№{r.number} ({r.type})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-amber-700 dark:text-amber-400">Нет свободных номеров на эти даты.</p>
|
||||
<div className="flex items-center gap-2 p-2.5 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 text-xs text-amber-700 dark:text-amber-300">
|
||||
<AlertCircle size={13} className="shrink-0" />
|
||||
Нет свободных номеров в этой категории на выбранные даты
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ФИО гостя */}
|
||||
<div className="grid grid-cols-3 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="Иванов"
|
||||
value={form.lastName}
|
||||
onChange={e => set('lastName', 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.firstName}
|
||||
onChange={e => set('firstName', 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.middleName}
|
||||
onChange={e => set('middleName', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Телефон + Email */}
|
||||
<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="tel"
|
||||
className="input"
|
||||
placeholder="+7 (999) 000-00-00"
|
||||
value={form.phone}
|
||||
onChange={e => set('phone', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className="input"
|
||||
placeholder="guest@example.com"
|
||||
value={form.guestEmail}
|
||||
onChange={e => set('guestEmail', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hourly / Daily toggle */}
|
||||
{/* Почасово / посуточно — только если у выбранного номера есть почасовая ставка */}
|
||||
{showHourlyTab && (
|
||||
<div className="flex rounded-lg overflow-hidden border border-slate-200 dark:border-slate-600">
|
||||
<div className="mt-3 flex rounded-lg overflow-hidden border border-slate-200 dark:border-slate-600">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsHourly(false)}
|
||||
@@ -370,107 +448,79 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dates */}
|
||||
{isHourly && room?.allowHourly ? (
|
||||
<div className="space-y-3">
|
||||
{/* 3. ФИО гостя */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Дата *
|
||||
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
|
||||
Гость
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
className="input"
|
||||
value={hourlyDate}
|
||||
onChange={e => setHourlyDate(e.target.value)}
|
||||
/>
|
||||
</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={startHour}
|
||||
onChange={e => setStartHour(parseInt(e.target.value))}
|
||||
>
|
||||
{HOURS.map(h => (
|
||||
<option key={h} value={h}>{String(h).padStart(2, '0')}:00</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Конец
|
||||
</label>
|
||||
<select
|
||||
className="input"
|
||||
value={endHour}
|
||||
onChange={e => setEndHour(parseInt(e.target.value))}
|
||||
>
|
||||
{HOURS.filter(h => h > startHour).map(h => (
|
||||
<option key={h} value={h}>{String(h).padStart(2, '0')}:00</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</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="date"
|
||||
className="input"
|
||||
value={form.checkIn}
|
||||
onChange={e => set('checkIn', 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="date"
|
||||
className="input"
|
||||
value={form.checkOut}
|
||||
onChange={e => set('checkOut', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Adults + Children + Status */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
Взрослых
|
||||
</label>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Фамилия *</label>
|
||||
<input
|
||||
type="number" min={1} max={room?.maxGuests ?? 6}
|
||||
className="input"
|
||||
type="text" className="input" placeholder="Иванов"
|
||||
value={form.lastName}
|
||||
onChange={e => set('lastName', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Имя *</label>
|
||||
<input
|
||||
type="text" className="input" placeholder="Иван"
|
||||
value={form.firstName}
|
||||
onChange={e => set('firstName', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Отчество</label>
|
||||
<input
|
||||
type="text" className="input" placeholder="Иванович"
|
||||
value={form.middleName}
|
||||
onChange={e => set('middleName', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Телефон</label>
|
||||
<input
|
||||
type="tel" className="input" placeholder="+7 (999) 000-00-00"
|
||||
value={form.phone}
|
||||
onChange={e => set('phone', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Email</label>
|
||||
<input
|
||||
type="email" className="input" placeholder="guest@example.com"
|
||||
value={form.guestEmail}
|
||||
onChange={e => set('guestEmail', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4. Взрослые / Дети / Статус */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Взрослых</label>
|
||||
<input
|
||||
type="number" min={1} max={room?.maxGuests ?? 6} className="input"
|
||||
value={form.adults}
|
||||
onChange={e => set('adults', 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>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Детей</label>
|
||||
<input
|
||||
type="number" min={0} max={4}
|
||||
className="input"
|
||||
type="number" min={0} max={4} className="input"
|
||||
value={form.children}
|
||||
onChange={e => set('children', 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>
|
||||
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1">Статус</label>
|
||||
<select
|
||||
className="input"
|
||||
value={form.status}
|
||||
@@ -506,9 +556,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
disabled={tvSending || !tvMsg.trim()}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-violet-600 hover:bg-violet-700 disabled:opacity-50 text-white text-xs font-medium transition-colors"
|
||||
>
|
||||
{tvSending
|
||||
? <Loader2 size={12} className="animate-spin" />
|
||||
: <Send size={12} />}
|
||||
{tvSending ? <Loader2 size={12} className="animate-spin" /> : <Send size={12} />}
|
||||
Отправить на TV
|
||||
</button>
|
||||
{tvSent && (
|
||||
@@ -516,15 +564,13 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
<CheckCircle2 size={12} /> Отправлено
|
||||
</span>
|
||||
)}
|
||||
{tvError && (
|
||||
<span className="text-xs text-red-500">{tvError}</span>
|
||||
)}
|
||||
{tvError && <span className="text-xs text-red-500">{tvError}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Extra beds */}
|
||||
<div className="flex items-center justify-between px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-700/40 border border-slate-200 dark:border-slate-600">
|
||||
{/* Доп. места */}
|
||||
<div className="flex items-center justify-between px-3 py-2.5 rounded-xl bg-slate-50 dark:bg-slate-700/40 border border-slate-200 dark:border-slate-600">
|
||||
<div className="flex items-center gap-2">
|
||||
<BedDouble size={15} className="text-slate-400 shrink-0" />
|
||||
<div>
|
||||
@@ -541,22 +587,18 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
<button type="button" onClick={() => setExtraBeds(e => Math.max(0, e - 1))}
|
||||
className="w-6 h-6 rounded-md bg-slate-200 dark:bg-slate-600 flex items-center justify-center hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors disabled:opacity-40"
|
||||
disabled={extraBeds === 0}
|
||||
>
|
||||
<Minus size={11} />
|
||||
</button>
|
||||
><Minus size={11} /></button>
|
||||
<span className="w-5 text-center text-sm font-semibold text-slate-900 dark:text-slate-100">{extraBeds}</span>
|
||||
<button type="button" onClick={() => setExtraBeds(e => Math.min(4, e + 1))}
|
||||
className="w-6 h-6 rounded-md bg-slate-200 dark:bg-slate-600 flex items-center justify-center hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors disabled:opacity-40"
|
||||
disabled={extraBeds >= 4}
|
||||
>
|
||||
<Plus size={11} />
|
||||
</button>
|
||||
><Plus size={11} /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Guest tags */}
|
||||
{/* Статус гостя (теги) */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
|
||||
Статус гостя
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
|
||||
Reference in New Issue
Block a user