Files
hotelsync/src/components/bookings/BookingModal.tsx
HotelSync 684073c4ad Fix booking creation: localStorage token, conflict detection, paid_amount
- Fix api.ts getToken/saveToken to read from localStorage (matching AuthContext) — was reading sessionStorage causing all API requests to fail auth until refresh
- Fix session cleanup on 401 to use localStorage
- Add isConflict/availableRooms helpers to BookingModal; show amber warning when selected room is occupied with clickable free room suggestions
- Pass bookings prop to BookingModal via BookingCalendar
- Add paid_amount to backend POST /bookings INSERT
- Show alert() when booking creation fails so user sees the error
- Pass paidAmount in CalendarPage handleCreate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 19:48:37 +03:00

901 lines
42 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 } from 'react'
import { format } from 'date-fns'
import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2, IdCard, CalendarDays, Tag, ScanLine, Minus, BedDouble } from 'lucide-react'
import { MOCK_DISCOUNTS } from '../../pages/DiscountsPage'
import type { Discount } from '../../pages/DiscountsPage'
import { Modal } from '../ui/Modal'
import { cn, BOOKING_STATUS_LABELS } from '../../lib/utils'
import type { Booking, DraftBooking, Room, BookingStatus } from '../../types'
import { FloorMapModal } from '../floormap/FloorMapModal'
type DocType = 'rf_passport' | 'foreign_passport' | 'other'
interface PassportData {
docType: DocType
lastName: string
firstName: string
patronymic: string
birthDate: string
birthPlace: string
nationality: string
series: string
number: string
issuedBy: string
issuedDate: string
divisionCode: string
registrationAddress: string
}
const EMPTY_PASSPORT: PassportData = {
docType: 'rf_passport',
lastName: '', firstName: '', patronymic: '',
birthDate: '', birthPlace: '', nationality: 'Россия',
series: '', number: '', issuedBy: '', issuedDate: '',
divisionCode: '', registrationAddress: '',
}
const DOC_TYPES: { id: DocType; label: string }[] = [
{ id: 'rf_passport', label: 'Паспорт РФ' },
{ id: 'foreign_passport', label: 'Загранпаспорт' },
{ id: 'other', label: 'Иной документ' },
]
const GUEST_TAGS = ['VIP', 'Постоянный гость', 'Корпоративный', 'Медовый месяц', 'День рождения', 'Особые пожелания']
const ADDITIONAL_SERVICES = [
{ id: 'breakfast', label: 'Завтрак', price: 800 },
{ id: 'transfer', label: 'Трансфер', price: 2500 },
{ id: 'parking', label: 'Парковка', price: 500 },
{ id: 'laundry', label: 'Стирка', price: 600 },
{ id: 'minibar', label: 'Мини-бар', price: 1500 },
{ id: 'excursion', label: 'Экскурсия', price: 3000 },
]
interface AddedService { id: string; label: string; price: number; qty: number }
const HOURS = Array.from({ length: 24 }, (_, i) => i)
interface BookingModalProps {
open: boolean
draft: DraftBooking
rooms: Room[]
bookings?: Booking[]
onClose: () => void
onSave: (data: Partial<Booking>) => void
existing?: Booking
}
function isConflict(bookings: Booking[], roomId: string, checkIn: string, checkOut: string, excludeId?: string) {
if (!checkIn || !checkOut || checkIn >= checkOut) return false
return bookings.some(b =>
b.roomId === roomId &&
b.id !== excludeId &&
b.status !== 'cancelled' && b.status !== 'no_show' &&
b.checkIn < checkOut && b.checkOut > checkIn,
)
}
function availableRooms(rooms: Room[], bookings: Booking[], checkIn: string, checkOut: string, excludeId?: string) {
return rooms.filter(r => !isConflict(bookings, r.id, checkIn, checkOut, excludeId))
}
export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSave, existing }: BookingModalProps) {
const [form, setForm] = useState({
roomId: existing?.roomId ?? draft.roomId,
guestName: existing?.guestName ?? '',
guestEmail:existing?.guestEmail ?? '',
checkIn: existing?.checkIn ?? draft.checkIn,
checkOut: existing?.checkOut ?? draft.checkOut,
adults: existing?.adults ?? 2,
children: existing?.children ?? 0,
status: (existing?.status ?? 'confirmed') as BookingStatus,
notes: existing?.notes ?? '',
})
const [guestTags, setGuestTags] = useState<string[]>([])
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'terminal' | null>(null)
const [paidAmount, setPaidAmount] = useState(existing?.paidAmount ?? 0)
const [showFloorMap, setShowFloorMap] = useState(false)
const [addedServices, setAddedServices] = useState<AddedService[]>([])
const [selectedServiceId, setSelectedServiceId] = useState(ADDITIONAL_SERVICES[0].id)
const [activeTab, setActiveTab] = useState<'booking' | 'passport'>('booking')
const [passport, setPassport] = useState<PassportData>(EMPTY_PASSPORT)
const [selectedDiscountId, setSelectedDiscountId] = useState<string>('')
const activeDiscounts = MOCK_DISCOUNTS.filter(d => d.isActive)
const selectedDiscount: Discount | undefined = activeDiscounts.find(d => d.id === selectedDiscountId)
const setP = <K extends keyof PassportData>(k: K, v: PassportData[K]) =>
setPassport(prev => ({ ...prev, [k]: v }))
const passportFilled = !!(passport.lastName || passport.number)
// Hourly booking state
const [isHourly, setIsHourly] = useState(false)
const [hourlyDate, setHourlyDate] = useState(draft.checkIn)
const [startHour, setStartHour] = useState(10)
const [endHour, setEndHour] = useState(12)
// Extra beds
const [extraBeds, setExtraBeds] = useState(0)
const EXTRA_BED_PRICE = 1500
const room = rooms.find(r => r.id === form.roomId)
const nights = form.checkIn && form.checkOut
? Math.max(0, (new Date(form.checkOut).getTime() - new Date(form.checkIn).getTime()) / 86400000)
: 0
const hourlyHours = Math.max(0, endHour - startHour)
const roomBaseTotal = isHourly && room?.allowHourly
? (room.hourlyRate ?? 0) * hourlyHours
: (room?.baseRate ?? 0) * nights
const discountAmount = selectedDiscount
? selectedDiscount.valueType === 'percent'
? Math.round(roomBaseTotal * Math.min(100, selectedDiscount.value) / 100)
: Math.min(roomBaseTotal, selectedDiscount.value)
: 0
const roomTotal = Math.max(0, roomBaseTotal - discountAmount)
const servicesTotal = addedServices.reduce((s, sv) => s + sv.price * sv.qty, 0)
const extraBedsTotal = extraBeds * EXTRA_BED_PRICE * (isHourly ? 1 : Math.max(1, nights))
const total = roomTotal + servicesTotal + extraBedsTotal
const debt = Math.max(0, total - paidAmount)
const addService = () => {
const svc = ADDITIONAL_SERVICES.find(s => s.id === selectedServiceId)
if (!svc) return
setAddedServices(prev => {
const ex = prev.find(s => s.id === svc.id)
if (ex) return prev.map(s => s.id === svc.id ? { ...s, qty: s.qty + 1 } : s)
return [...prev, { ...svc, qty: 1 }]
})
}
const removeService = (id: string) =>
setAddedServices(prev => prev.filter(s => s.id !== id))
const isFutureBooking = form.checkIn > format(new Date(), 'yyyy-MM-dd')
const set = <K extends keyof typeof form>(k: K, v: typeof form[K]) =>
setForm(prev => ({ ...prev, [k]: v }))
const toggleTag = (tag: string) =>
setGuestTags(prev => prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag])
const handleSave = () => {
if (!form.guestName) return
if (isHourly && room?.allowHourly) {
if (!hourlyDate) return
} else {
if (!form.checkIn || !form.checkOut) return
}
const hourlyPrefix = isHourly && room?.allowHourly
? `[Почасово: ${String(startHour).padStart(2, '0')}:00${String(endHour).padStart(2, '0')}:00] `
: ''
const extraBedsPrefix = extraBeds > 0 ? `[Доп.места: ${extraBeds}] ` : ''
const checkIn = isHourly && room?.allowHourly ? hourlyDate : form.checkIn
const checkOut = isHourly && room?.allowHourly ? hourlyDate : form.checkOut
onSave({
...form,
source: 'direct',
checkIn,
checkOut,
notes: hourlyPrefix + extraBedsPrefix + form.notes,
totalAmount: total,
paidAmount,
id: existing?.id ?? `b-${Date.now()}`,
hotelId: 'hotel-1',
guestId: existing?.guestId ?? `g-${Date.now()}`,
createdAt: existing?.createdAt ?? format(new Date(), 'yyyy-MM-dd'),
})
}
const showHourlyTab = room?.allowHourly === true
return (
<>
<Modal
open={open}
onClose={onClose}
title={existing ? 'Редактировать бронирование' : 'Новое бронирование'}
size="2xl"
footer={
<>
<button onClick={onClose} className="btn-secondary">Отмена</button>
<button onClick={handleSave} className="btn-primary" disabled={!form.guestName}>
{existing ? 'Сохранить' : 'Создать бронирование'}
</button>
</>
}
>
{/* ── Tabs ── */}
<div className="flex border-b border-slate-200 dark:border-slate-700 mb-4 -mt-1">
{([
{ id: 'booking' as const, label: 'Бронирование', icon: CalendarDays },
{ id: 'passport' as const, label: 'Документы гостя', icon: IdCard,
badge: passportFilled },
]).map(t => (
<button
key={t.id}
type="button"
onClick={() => setActiveTab(t.id)}
className={cn(
'flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors',
activeTab === t.id
? 'border-brand-600 text-brand-600 dark:text-brand-400 dark:border-brand-400'
: 'border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300',
)}
>
<t.icon size={14} />
{t.label}
{t.badge && (
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
)}
</button>
))}
</div>
{/* ── BOOKING TAB ── */}
{activeTab === 'booking' && (
<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>
<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>
<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} />
Поэтажный план
</button>
</div>
<select
value={form.roomId}
onChange={e => set('roomId', e.target.value)}
className="input"
>
{rooms.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')} ₽/ч` : ''})
</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>
)
})()
}
</div>
{/* Guest name + 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="text"
className="input"
placeholder="Иван Иванов"
value={form.guestName}
onChange={e => set('guestName', 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">
<button
type="button"
onClick={() => setIsHourly(false)}
className={cn(
'flex-1 py-1.5 text-sm font-medium transition-colors',
!isHourly
? 'bg-brand-600 text-white'
: 'bg-white dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-600',
)}
>
Посуточно
</button>
<button
type="button"
onClick={() => setIsHourly(true)}
className={cn(
'flex-1 py-1.5 text-sm font-medium transition-colors',
isHourly
? 'bg-brand-600 text-white'
: 'bg-white dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-600',
)}
>
Почасово
</button>
</div>
)}
{/* Dates */}
{isHourly && room?.allowHourly ? (
<div className="space-y-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={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>
<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>
<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>
<select
className="input"
value={form.status}
onChange={e => set('status', e.target.value as BookingStatus)}
>
{(Object.entries(BOOKING_STATUS_LABELS) as [BookingStatus, string][]).map(([k, v]) => (
<option key={k} value={k}>{v}</option>
))}
</select>
</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 gap-2">
<BedDouble size={15} className="text-slate-400 shrink-0" />
<div>
<p className="text-sm font-medium text-slate-700 dark:text-slate-300 leading-tight">Доп. места</p>
<p className="text-[10px] text-slate-400">{EXTRA_BED_PRICE.toLocaleString('ru-RU')} /место{!isHourly && nights > 0 ? '/ночь' : ''}</p>
</div>
</div>
<div className="flex items-center gap-2">
{extraBeds > 0 && (
<span className="text-xs font-medium text-amber-600 dark:text-amber-400 mr-1">
+{extraBedsTotal.toLocaleString('ru-RU')}
</span>
)}
<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>
<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>
</div>
</div>
{/* Guest tags */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
Статус гостя
</label>
<div className="flex flex-wrap gap-1.5">
{GUEST_TAGS.map(tag => {
const isVip = tag === 'VIP'
const selected = guestTags.includes(tag)
return (
<button
key={tag}
type="button"
onClick={() => toggleTag(tag)}
className={cn(
'flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-medium border transition-colors',
selected
? isVip
? 'bg-amber-500 border-amber-500 text-white'
: 'bg-brand-600 border-brand-600 text-white'
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-600 dark:text-slate-300 hover:border-brand-400',
)}
>
{isVip && <Star size={10} className={selected ? 'text-white' : 'text-amber-500'} />}
{tag}
</button>
)
})}
</div>
</div>
</div>
{/* ── RIGHT COLUMN ── */}
<div className="flex-1 space-y-3 min-w-0">
{/* Payment */}
{total > 0 && (
<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>
</div>
<div className="p-4 space-y-3">
<div>
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1.5">Способ оплаты</label>
<div className="flex gap-2">
<button
type="button"
onClick={() => setPaymentMethod(p => p === 'cash' ? null : 'cash')}
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors',
paymentMethod === 'cash'
? 'bg-emerald-600 text-white border-emerald-600'
: 'bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-600 hover:border-emerald-400',
)}
>
<Banknote size={14} />
Наличные
</button>
<button
type="button"
onClick={() => setPaymentMethod(p => p === 'terminal' ? null : 'terminal')}
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors',
paymentMethod === 'terminal'
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-600 hover:border-blue-400',
)}
>
<CreditCard size={14} />
Терминал
</button>
</div>
</div>
{/* Discount */}
<div>
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1.5 flex items-center gap-1">
<Tag size={11} /> Скидка
</label>
<select
className="input text-sm"
value={selectedDiscountId}
onChange={e => setSelectedDiscountId(e.target.value)}
>
<option value=""> Без скидки </option>
{activeDiscounts.map(d => (
<option key={d.id} value={d.id}>
{d.name} ({d.value === 100 ? 'Бесплатно' : `${d.value}${d.valueType === 'percent' ? '%' : ' ₽'}`})
</option>
))}
</select>
{selectedDiscount && (
<p className="text-xs text-emerald-600 dark:text-emerald-400 mt-1">
{discountAmount.toLocaleString('ru-RU')}
{selectedDiscount.note ? ` · ${selectedDiscount.note}` : ''}
</p>
)}
</div>
{/* Additional services */}
<div>
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1.5">Дополнительные услуги</label>
<div className="flex gap-2 mb-2">
<select
className="input flex-1 text-sm"
value={selectedServiceId}
onChange={e => setSelectedServiceId(e.target.value)}
>
{ADDITIONAL_SERVICES.map(s => (
<option key={s.id} value={s.id}>{s.label} {s.price.toLocaleString('ru-RU')} </option>
))}
</select>
<button type="button" onClick={addService} className="btn-secondary px-2">
<Plus size={14} />
</button>
</div>
{addedServices.length > 0 && (
<div className="space-y-1">
{addedServices.map(s => (
<div key={s.id} className="flex items-center justify-between text-xs px-2 py-1 rounded bg-slate-100 dark:bg-slate-700">
<span className="text-slate-700 dark:text-slate-300">{s.label} × {s.qty}</span>
<div className="flex items-center gap-2">
<span className="font-medium text-slate-900 dark:text-slate-100">{(s.price * s.qty).toLocaleString('ru-RU')} </span>
<button type="button" onClick={() => removeService(s.id)} className="text-slate-400 hover:text-red-500">
<Trash2 size={11} />
</button>
</div>
</div>
))}
</div>
)}
</div>
<div>
<label className="block text-xs text-slate-500 dark:text-slate-400 mb-1.5">
Оплачено ()
</label>
<input
type="number"
min={0}
max={total}
className="input w-40"
value={paidAmount}
onChange={e => setPaidAmount(Math.min(total, Math.max(0, parseInt(e.target.value) || 0)))}
/>
</div>
</div>
</div>
)}
</div>
</div>
{/* ── BOTTOM ROW: Notes + Summary ── */}
<div className="flex gap-6">
<div className="flex-1">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
Примечания
</label>
<textarea
className="input resize-none"
rows={2}
placeholder="Дополнительные пожелания..."
value={form.notes}
onChange={e => set('notes', e.target.value)}
/>
</div>
{(total > 0 || discountAmount > 0) && room && (
<div className="flex-1 rounded-xl bg-slate-50 dark:bg-slate-700/50 px-4 py-3 space-y-1.5 self-end">
{isHourly && room.allowHourly ? (
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600 dark:text-slate-300">
{hourlyHours} {hourlyHours === 1 ? 'час' : hourlyHours < 5 ? 'часа' : 'часов'} × {(room.hourlyRate ?? 0).toLocaleString('ru-RU')}
</span>
<span className={cn('font-semibold', discountAmount > 0 ? 'line-through text-slate-400' : 'text-slate-900 dark:text-slate-100')}>
{roomBaseTotal.toLocaleString('ru-RU')}
</span>
</div>
) : (
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600 dark:text-slate-300">
{nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'} × {room.baseRate.toLocaleString('ru-RU')}
</span>
<span className={cn('font-semibold', discountAmount > 0 ? 'line-through text-slate-400' : 'text-slate-900 dark:text-slate-100')}>
{roomBaseTotal.toLocaleString('ru-RU')}
</span>
</div>
)}
{discountAmount > 0 && selectedDiscount && (
<div className="flex items-center justify-between text-sm">
<span className="text-emerald-600 dark:text-emerald-400">
{selectedDiscount.value === 100 ? 'Бесплатное проживание' : selectedDiscount.name}
</span>
<span className="font-semibold text-emerald-600 dark:text-emerald-400">
{discountAmount.toLocaleString('ru-RU')}
</span>
</div>
)}
{servicesTotal > 0 && (
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 dark:text-slate-300">Доп. услуги</span>
<span className="font-semibold text-slate-900 dark:text-slate-100">{servicesTotal.toLocaleString('ru-RU')} </span>
</div>
)}
{(servicesTotal > 0 || discountAmount > 0) && (
<div className="flex items-center justify-between border-t border-slate-200 dark:border-slate-600 pt-1.5">
<span className="text-sm font-medium text-slate-700 dark:text-slate-200">Итого</span>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">{total.toLocaleString('ru-RU')} </span>
</div>
)}
{servicesTotal === 0 && discountAmount === 0 && (
<div className="text-xl font-bold text-slate-900 dark:text-slate-100 text-right">{total.toLocaleString('ru-RU')} </div>
)}
{paidAmount > 0 && (
<div className="flex items-center justify-between text-sm">
<span className="text-emerald-600 dark:text-emerald-400">Оплачено</span>
<span className="font-semibold text-emerald-600 dark:text-emerald-400">
{paidAmount.toLocaleString('ru-RU')}
</span>
</div>
)}
{debt > 0 && (
<div className="flex items-center gap-1.5 text-sm text-red-600 dark:text-red-400">
<AlertCircle size={13} />
<span className="flex-1">{isFutureBooking ? 'Задолженность' : 'Долг при заселении'}</span>
<span className="font-semibold">{debt.toLocaleString('ru-RU')} </span>
</div>
)}
</div>
)}
</div>
</div>
)} {/* end booking tab */}
{/* ── PASSPORT TAB ── */}
{activeTab === 'passport' && (
<div className="space-y-4">
{/* Document type + scan */}
<div className="flex items-center justify-between gap-3">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">Тип документа</label>
<button
type="button"
onClick={() => alert('Для сканирования подключите RFID/OCR сканер паспорта')}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-dashed border-brand-300 dark:border-brand-600 text-brand-600 dark:text-brand-400 text-xs font-medium hover:bg-brand-50 dark:hover:bg-brand-900/20 transition-colors shrink-0"
>
<ScanLine size={13} /> Сканировать
</button>
</div>
<div>
<div className="h-0" />
<div className="flex gap-2">
{DOC_TYPES.map(dt => (
<button
key={dt.id}
type="button"
onClick={() => setP('docType', dt.id)}
className={cn(
'flex-1 py-2 rounded-lg text-sm font-medium border transition-colors',
passport.docType === dt.id
? 'bg-brand-600 border-brand-600 text-white'
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-700 dark:text-slate-300 hover:border-brand-400',
)}
>
{dt.label}
</button>
))}
</div>
</div>
{/* Name fields */}
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Фамилия</label>
<input type="text" className="input" placeholder="Иванов"
value={passport.lastName} onChange={e => setP('lastName', e.target.value)} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Имя</label>
<input type="text" className="input" placeholder="Иван"
value={passport.firstName} onChange={e => setP('firstName', e.target.value)} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Отчество</label>
<input type="text" className="input" placeholder="Иванович"
value={passport.patronymic} onChange={e => setP('patronymic', e.target.value)} />
</div>
</div>
{/* Birth + nationality */}
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Дата рождения</label>
<input type="date" className="input"
value={passport.birthDate} onChange={e => setP('birthDate', e.target.value)} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Место рождения</label>
<input type="text" className="input" placeholder="г. Москва"
value={passport.birthPlace} onChange={e => setP('birthPlace', e.target.value)} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Гражданство</label>
<input type="text" className="input" placeholder="Россия"
value={passport.nationality} onChange={e => setP('nationality', e.target.value)} />
</div>
</div>
{/* Document fields */}
<div className="rounded-xl border border-slate-200 dark:border-slate-600 overflow-hidden">
<div className="px-4 py-2.5 bg-slate-50 dark:bg-slate-700/40 border-b border-slate-200 dark:border-slate-600">
<p className="text-xs font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wide">Реквизиты документа</p>
</div>
<div className="p-4 space-y-3">
{passport.docType === 'rf_passport' && (
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Серия</label>
<input
type="text" className="input" placeholder="45 16" maxLength={5}
value={passport.series} onChange={e => setP('series', e.target.value)}
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Номер</label>
<input
type="text" className="input" placeholder="123456" maxLength={6}
value={passport.number} onChange={e => setP('number', e.target.value)}
/>
</div>
</div>
)}
{passport.docType !== 'rf_passport' && (
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Номер документа</label>
<input
type="text" className="input" placeholder="Номер документа"
value={passport.number} onChange={e => setP('number', e.target.value)}
/>
</div>
)}
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Кем выдан</label>
<input
type="text" className="input" placeholder="ОУМВД России по г. Москве"
value={passport.issuedBy} onChange={e => setP('issuedBy', e.target.value)}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Дата выдачи</label>
<input type="date" className="input"
value={passport.issuedDate} onChange={e => setP('issuedDate', e.target.value)} />
</div>
{passport.docType === 'rf_passport' && (
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">Код подразделения</label>
<input
type="text" className="input" placeholder="770-001" maxLength={7}
value={passport.divisionCode} onChange={e => setP('divisionCode', e.target.value)}
/>
</div>
)}
</div>
</div>
</div>
{/* Registration address */}
<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="г. Москва, ул. Примерная, д. 1, кв. 1"
value={passport.registrationAddress}
onChange={e => setP('registrationAddress', e.target.value)}
/>
</div>
{/* Hint */}
<p className="text-xs text-slate-400 dark:text-slate-500">
Данные используются для формирования регистрационной карты и уведомления УМВД (при необходимости)
</p>
</div>
)} {/* end passport tab */}
</Modal>
{showFloorMap && (
<FloorMapModal
rooms={rooms}
selectedRoomId={form.roomId}
onSelectRoom={(id) => { set('roomId', id); setShowFloorMap(false) }}
onClose={() => setShowFloorMap(false)}
/>
)}
</>
)
}