import { useState, useEffect, useRef } from 'react' import { format, differenceInDays } from 'date-fns' import { X, Mail, Calendar, Users, CreditCard, Tag, CheckCircle, XCircle, Printer, ScanLine, Banknote, Building2, Plus, Pencil, FileText, FileCheck, Receipt, IdCard, AlertTriangle, Trash2, Loader2, UserCheck, Baby, Search, LogIn, } from 'lucide-react' import { cn, BOOKING_STATUS_BADGE, BOOKING_STATUS_LABELS, SOURCE_COLORS, SOURCE_LABELS, formatCurrency, nightsCount, } from '../../lib/utils' import type { Booking, Room } from '../../types' import { api, type BookingGuest, type BookingGuestPayload, type GuestApiType } from '../../lib/api' const fmtDate = (iso: string) => new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(new Date(iso)) import { Badge } from '../ui/Badge' import { MOCK_DISCOUNTS } from '../../pages/DiscountsPage' // ─── Local types ────────────────────────────────────────────────────────────── interface PassportData { lastName: string; firstName: string; middleName: string dob: string; series: string; number: string issuedBy: string; issueDate: string; regAddress: string } interface Payment { id: string; date: string; amount: number method: 'cash' | 'card' | 'transfer'; note: string } type Tab = 'booking' | 'guest' | 'payment' | 'docs' const TABS: { id: Tab; label: string }[] = [ { id: 'booking', label: 'Бронь' }, { id: 'guest', label: 'Гости' }, { id: 'payment', label: 'Оплата' }, { id: 'docs', label: 'Документы' }, ] const METHODS: { id: 'cash' | 'card' | 'transfer'; label: string; icon: React.ElementType }[] = [ { id: 'cash', label: 'Наличные', icon: Banknote }, { id: 'card', label: 'Карта', icon: CreditCard }, { id: 'transfer', label: 'Перевод', icon: Building2 }, ] const DOCUMENTS = [ { id: 'reg', label: 'Регистрационная карта', desc: 'Персональные данные гостя', icon: IdCard, always: true }, { id: 'contract', label: 'Договор на проживание', desc: 'Договор между гостем и отелем', icon: FileText, always: true }, { id: 'invoice', label: 'Счёт на оплату', desc: 'Счёт для оплаты проживания', icon: Receipt, always: true }, { id: 'act', label: 'Акт об оказании услуг', desc: 'Закрывающий документ при выезде', icon: FileCheck, always: false }, ] // ─── Component ──────────────────────────────────────────────────────────────── interface BookingDetailPanelProps { booking: Booking room?: Room rooms?: Room[] allBookings?: Booking[] slug?: string onClose: () => void onUpdate: (id: string, data: Partial) => void onBulkUpdate?: (updates: Array<{ id: string; data: Partial }>) => void } export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, onClose, onUpdate, onBulkUpdate }: BookingDetailPanelProps) { const [tab, setTab] = useState('booking') // ── Multi-guest state ──────────────────────────────────────────────────── const [bgGuests, setBgGuests] = useState([]) const [bgLoading, setBgLoading] = useState(false) const [bgLoaded, setBgLoaded] = useState(false) const [requireDocs, setRequireDocs] = useState(false) const [showGuestForm, setShowGuestForm] = useState(false) const [editingBg, setEditingBg] = useState(null) const [bgForm, setBgForm] = useState({}) const [bgSaving, setBgSaving] = useState(false) // Autocomplete const [acQuery, setAcQuery] = useState('') const [acResults, setAcResults] = useState([]) const [acLoading, setAcLoading] = useState(false) const acTimerRef = useRef | null>(null) const acWrapRef = useRef(null) // Hotel timing settings (for early/late fee warnings) const [hotelCheckInTime, setHotelCheckInTime] = useState('14:00') const [hotelCheckOutTime, setHotelCheckOutTime] = useState('12:00') const [earlyCheckinEnabled, setEarlyCheckinEnabled] = useState(false) const [lateCheckoutEnabled, setLateCheckoutEnabled] = useState(false) const [hotelTimingsLoaded, setHotelTimingsLoaded] = useState(false) useEffect(() => { if (!slug || hotelTimingsLoaded) return api.hotelSettings.get(slug) .then(s => { const st = s as Record if (st.check_in_time) setHotelCheckInTime(String(st.check_in_time).slice(0, 5)) if (st.check_out_time) setHotelCheckOutTime(String(st.check_out_time).slice(0, 5)) setEarlyCheckinEnabled(Boolean(st.early_checkin_enabled)) setLateCheckoutEnabled(Boolean(st.late_checkout_enabled)) setHotelTimingsLoaded(true) }) .catch(console.error) }, [slug, hotelTimingsLoaded]) // Load booking guests + settings when tab is activated useEffect(() => { if (tab !== 'guest' || !slug || bgLoaded) return setBgLoading(true) Promise.all([ api.bookingGuests.list(slug, booking.id), api.hotelSettings.get(slug).catch(() => ({})), ]) .then(([guests, settings]) => { setBgGuests(guests) setRequireDocs(Boolean((settings as { require_guest_docs?: boolean }).require_guest_docs)) setBgLoaded(true) // Auto-open add form pre-filled with booking FIO when no guests yet if (guests.length === 0 && booking.status !== 'cancelled') { const parts = (booking.guestName ?? '').split(' ') setBgForm({ last_name: parts[0] ?? '', first_name: parts[1] ?? '', middle_name: parts.slice(2).join(' ') || undefined, is_main: true, }) setShowGuestForm(true) } }) .catch(console.error) .finally(() => setBgLoading(false)) }, [tab, slug, booking.id, bgLoaded]) // Reset when booking changes useEffect(() => { setBgLoaded(false) setBgGuests([]) setShowGuestForm(false) setEditingBg(null) setBgForm({}) }, [booking.id]) // Debounced guest autocomplete const handleAcInput = (val: string) => { setBgForm(f => ({ ...f, last_name: val })) setAcQuery(val) if (acTimerRef.current) clearTimeout(acTimerRef.current) if (val.length < 2 || !slug) { setAcResults([]); return } acTimerRef.current = setTimeout(() => { setAcLoading(true) api.guests.list(slug, val) .then(setAcResults) .catch(() => setAcResults([])) .finally(() => setAcLoading(false)) }, 300) } const selectAcGuest = (g: GuestApiType) => { setBgForm(f => ({ ...f, first_name: g.firstName, last_name: g.lastName, middle_name: g.middleName ?? undefined, birth_date: g.birthDate ?? undefined, passport_series: g.passportSeries ?? undefined, passport_number: g.passportNumber ?? undefined, passport_issued_by: g.passportIssuedBy ?? undefined, passport_issue_date: g.passportIssueDate ?? undefined, nationality: g.nationality ?? undefined, })) setAcQuery(g.lastName) setAcResults([]) } const passportTimerRef = useRef | null>(null) const handlePassportInput = (field: 'passport_series' | 'passport_number', val: string) => { setBgForm(f => ({ ...f, [field]: val })) if (passportTimerRef.current) clearTimeout(passportTimerRef.current) const series = field === 'passport_series' ? val : (bgForm.passport_series ?? '') const number = field === 'passport_number' ? val : (bgForm.passport_number ?? '') const query = (series + number).trim() if (query.length < 4 || !slug) return passportTimerRef.current = setTimeout(() => { api.guests.list(slug, undefined, query) .then(results => { if (results.length === 1) selectAcGuest(results[0]) }) .catch(() => {/* ignore */}) }, 400) } const openAddForm = () => { setEditingBg(null) setBgForm({ is_main: bgGuests.length === 0 }) setAcQuery('') setAcResults([]) setShowGuestForm(true) } const openEditForm = (bg: BookingGuest) => { setEditingBg(bg) setBgForm({ first_name: bg.firstName, last_name: bg.lastName, middle_name: bg.middleName ?? undefined, birth_date: bg.birthDate?.slice(0, 10) ?? undefined, is_child: bg.isChild, is_main: bg.isMain, passport_series: bg.passportSeries ?? undefined, passport_number: bg.passportNumber ?? undefined, passport_issued_by: bg.passportIssuedBy ?? undefined, passport_issue_date: bg.passportIssueDate?.slice(0, 10) ?? undefined, nationality: bg.nationality ?? undefined, }) setAcQuery('') setAcResults([]) setShowGuestForm(true) } const cancelGuestForm = () => { setShowGuestForm(false) setEditingBg(null) setBgForm({}) setAcResults([]) setAcQuery('') } const saveGuest = async () => { if (!slug || !bgForm.first_name?.trim() || !bgForm.last_name?.trim()) { showToast('Введите имя и фамилию') return } setBgSaving(true) try { if (editingBg) { const updated = await api.bookingGuests.update(slug, booking.id, editingBg.id, bgForm) setBgGuests(prev => prev.map(g => g.id === editingBg.id ? updated : g)) } else { const added = await api.bookingGuests.add(slug, booking.id, bgForm) setBgGuests(prev => [...prev, added]) } cancelGuestForm() } catch { showToast('Ошибка при сохранении гостя') } finally { setBgSaving(false) } } const removeGuest = async (id: string) => { if (!slug) return try { await api.bookingGuests.remove(slug, booking.id, id) setBgGuests(prev => prev.filter(g => g.id !== id)) } catch { showToast('Ошибка при удалении гостя') } } const adultsWithoutDocs = bgGuests.filter(g => !g.isChild && (!g.passportSeries || !g.passportNumber)) // ── Date editing ───────────────────────────────────────────────────────── const [editDates, setEditDates] = useState(false) const [newCheckIn, setNewCheckIn] = useState(booking.checkIn) const [newCheckOut, setNewCheckOut] = useState(booking.checkOut) const [newRoomId, setNewRoomId] = useState(booking.roomId) const [conflictBooking, setConflictBooking] = useState(null) const [conflictResolveRoomId, setConflictResolveRoomId] = useState('') const canEditDates = booking.status === 'confirmed' || booking.status === 'inquiry' const canRelocate = booking.status === 'checked_in' const today = format(new Date(), 'yyyy-MM-dd') const canEarlyCheckIn = booking.status === 'confirmed' && booking.checkIn > today const [relocating, setRelocating] = useState(false) const [relocateRoomId, setRelocateRoomId] = useState(booking.roomId) const [earlyOpen, setEarlyOpen] = useState(false) const [earlyNewCheckOut, setEarlyNewCheckOut] = useState(booking.checkOut) const [earlyRoomId, setEarlyRoomId] = useState(booking.roomId) const [earlySaving, setEarlySaving] = useState(false) const [earlyOutOpen, setEarlyOutOpen] = useState(false) const [earlyOutDate, setEarlyOutDate] = useState(today) const [earlyOutMethod, setEarlyOutMethod] = useState<'cash' | 'card' | 'transfer'>('cash') const [earlyOutSaving, setEarlyOutSaving] = useState(false) const freeRoomsForRelocate = (rooms ?? []).filter(r => r.id !== booking.roomId && !(allBookings ?? []).some(b => b.id !== booking.id && b.roomId === r.id && b.status !== 'cancelled' && b.status !== 'no_show' && b.status !== 'checked_out' && b.checkIn < booking.checkOut && b.checkOut > booking.checkIn, ), ) const handleRelocate = () => { if (!relocateRoomId || relocateRoomId === booking.roomId) return onUpdate(booking.id, { roomId: relocateRoomId }) setRelocating(false) } const handleEarlyCheckIn = async () => { if (earlyNewCheckOut <= today) { showToast('Дата выезда должна быть позже сегодня'); return } setEarlySaving(true) try { await onUpdate(booking.id, { checkIn: today, checkOut: earlyNewCheckOut, roomId: earlyRoomId, status: 'checked_in', }) setEarlyOpen(false) showToast('Гость заселён досрочно') } finally { setEarlySaving(false) } } const handleEarlyCheckOut = async () => { if (earlyOutDate > booking.checkOut) { showToast('Дата не может быть позже плановой'); return } setEarlyOutSaving(true) try { const origNights = differenceInDays(new Date(booking.checkOut), new Date(booking.checkIn)) const actualNights = differenceInDays(new Date(earlyOutDate), new Date(booking.checkIn)) const refundNights = origNights - actualNights if (refundNights > 0) { const refundAmt = Math.round(refundNights * booking.totalAmount / origNights) setPayments(prev => [...prev, { id: `refund-${Date.now()}`, date: new Date().toLocaleDateString('ru-RU'), amount: -refundAmt, method: earlyOutMethod, note: `Возврат за ${refundNights} неиспользованных ${refundNights === 1 ? 'ночь' : refundNights < 5 ? 'ночи' : 'ночей'}`, }]) } await onUpdate(booking.id, { checkOut: earlyOutDate, status: 'checked_out' }) setEarlyOutOpen(false) showToast('Гость выселен') } finally { setEarlyOutSaving(false) } } const findConflict = (ci: string, co: string, rid: string): Booking | null => (allBookings ?? []).find(b => b.id !== booking.id && b.roomId === rid && b.status !== 'cancelled' && b.status !== 'no_show' && b.status !== 'checked_out' && b.checkIn < co && b.checkOut > ci, ) ?? null const handleSaveDates = () => { if (newCheckIn >= newCheckOut) { showToast('Дата выезда должна быть позже заезда'); return } const conflict = findConflict(newCheckIn, newCheckOut, newRoomId) if (conflict) { setConflictBooking(conflict); setConflictResolveRoomId(''); return } onUpdate(booking.id, { checkIn: newCheckIn, checkOut: newCheckOut, roomId: newRoomId }) } const handleForceUpdate = () => onUpdate(booking.id, { checkIn: newCheckIn, checkOut: newCheckOut, roomId: newRoomId }) const handleResolveWithRoomSwap = () => { if (!conflictBooking || !onBulkUpdate || !conflictResolveRoomId) return if (findConflict(conflictBooking.checkIn, conflictBooking.checkOut, conflictResolveRoomId)) { showToast('Выбранный номер тоже занят на эти даты') return } onBulkUpdate([ { id: booking.id, data: { checkIn: newCheckIn, checkOut: newCheckOut, roomId: newRoomId } }, { id: conflictBooking.id, data: { roomId: conflictResolveRoomId } }, ]) } // Passport data const nameParts = booking.guestName.split(' ') const [passport, setPassport] = useState({ lastName: nameParts[0] ?? '', firstName: nameParts[1] ?? '', middleName: nameParts[2] ?? '', dob: '', series: '', number: '', issuedBy: '', issueDate: '', regAddress: '', }) const setP = (k: K, v: PassportData[K]) => setPassport(prev => ({ ...prev, [k]: v })) // Payments const [payments, setPayments] = useState(() => booking.paidAmount > 0 ? [{ id: 'init', date: booking.createdAt, amount: booking.paidAmount, method: 'card', note: 'Предоплата при бронировании' }] : [] ) const [showPayForm, setShowPayForm] = useState(false) const [payAmount, setPayAmount] = useState('') const [payMethod, setPayMethod] = useState<'cash' | 'card' | 'transfer'>('cash') const [payNote, setPayNote] = useState('') // Discount const [discountId, setDiscountId] = useState('') // Toast const [toast, setToast] = useState('') const showToast = (msg: string) => { setToast(msg); setTimeout(() => setToast(''), 2500) } // Calculations const nights = nightsCount(booking.checkIn, booking.checkOut) const baseTotal = booking.totalAmount const activeDiscs = MOCK_DISCOUNTS.filter(d => d.isActive) const selDiscount = activeDiscs.find(d => d.id === discountId) const discountAmt = selDiscount ? selDiscount.valueType === 'percent' ? Math.round(baseTotal * Math.min(100, selDiscount.value) / 100) : Math.min(baseTotal, selDiscount.value) : 0 const finalTotal = baseTotal - discountAmt const totalPaid = payments.reduce((s, p) => s + p.amount, 0) const balance = finalTotal - totalPaid const setStatus = (status: typeof booking.status) => onUpdate(booking.id, { status }) const addPayment = () => { const amt = parseFloat(payAmount) if (!amt || amt <= 0) return setPayments(prev => [...prev, { id: `p-${Date.now()}`, date: new Date().toLocaleDateString('ru-RU'), amount: amt, method: payMethod, note: payNote, }]) setPayAmount(''); setPayNote('') setShowPayForm(false) showToast(`✓ Оплата ${formatCurrency(amt)} принята`) } const printDoc = (label: string) => showToast(`🖨 «${label}» отправлен на печать`) const fld = 'input text-sm' const lbl = 'block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1' return ( <>
{/* Toast notification */} {toast && (
{toast}
)} {/* Header */}

{booking.guestName}

{BOOKING_STATUS_LABELS[booking.status]} {SOURCE_LABELS[booking.source]}
{/* Tab bar */}
{TABS.map(t => ( ))}
{/* Tab content */}
{/* ── Бронь ────────────────────────────────────────────────────────── */} {tab === 'booking' && (
{room && (

Номер

{canRelocate && !relocating && ( )}

№{room.number} — {room.type}

Этаж {room.floor} · {room.bedType} bed

{/* Форма переселения */} {relocating && (

Переселить гостя в другой номер

{freeRoomsForRelocate.length > 0 ? ( <>
) : (
Нет свободных номеров на эти даты
)}
)}
)} {/* Early check-in banner */} {canEarlyCheckIn && !earlyOpen && (

Гость приехал раньше

Бронь начинается {fmtDate(booking.checkIn)}, но можно заселить сейчас

)} {/* Early check-in form */} {canEarlyCheckIn && earlyOpen && (() => { const currentRoomFree = !((allBookings ?? []).some(b => b.id !== booking.id && b.roomId === booking.roomId && b.status !== 'cancelled' && b.status !== 'no_show' && b.status !== 'checked_out' && b.checkIn < earlyNewCheckOut && b.checkOut > today, )) const availableRooms = (rooms ?? []).filter(r => !((allBookings ?? []).some(b => b.id !== booking.id && b.roomId === r.id && b.status !== 'cancelled' && b.status !== 'no_show' && b.status !== 'checked_out' && b.checkIn < earlyNewCheckOut && b.checkOut > today, )), ) const extraNights = differenceInDays(new Date(booking.checkIn), new Date(today)) const selectedRoom = (rooms ?? []).find(r => r.id === earlyRoomId) const origRoom = (rooms ?? []).find(r => r.id === booking.roomId) const priceDiff = selectedRoom && origRoom ? selectedRoom.baseRate - origRoom.baseRate : 0 return (

Ранний заезд

{/* Room selector */} {currentRoomFree ? (
Текущий номер свободен — можно заселить без переноса
) : (

Текущий номер занят. Выберите свободный:

{availableRooms.length > 0 ? ( ) : (

Нет свободных номеров

)}
)} {/* Checkout date */}
setEarlyNewCheckOut(e.target.value)} />
{/* Extra nights info */} {earlyNewCheckOut > today && (

Заезд сегодня → доп. {extraNights} {extraNights === 1 ? 'ночь' : extraNights < 5 ? 'ночи' : 'ночей'}

{priceDiff !== 0 && selectedRoom && (

0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}> Разница в стоимости: {priceDiff > 0 ? '+' : ''}{priceDiff.toLocaleString('ru-RU')} ₽/ночь

)}
)}
) })()} {/* Dates — read or edit mode */}

Даты проживания

{!editDates && canEditDates && ( )}
{editDates ? (
{ setNewCheckIn(e.target.value); setConflictBooking(null) }} />
{ setNewCheckOut(e.target.value); setConflictBooking(null) }} />
{rooms && rooms.length > 1 && (
)} {/* Conflict warning */} {conflictBooking && (
Номер занят: {conflictBooking.guestName},  {fmtDate(conflictBooking.checkIn)} – {fmtDate(conflictBooking.checkOut)}
{conflictBooking.status !== 'checked_in' && onBulkUpdate && rooms && (

Переместить {conflictBooking.guestName} в другой номер:

)}
)}
{!conflictBooking && ( )}
) : (

Заезд

{fmtDate(booking.checkIn)}

Выезд

{fmtDate(booking.checkOut)}

)} {!editDates && (

{nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'}

)}

Гости

{booking.adults} взр.{booking.children > 0 ? ` · ${booking.children} дет.` : ''}

{booking.guestEmail && (

Контакты

{booking.guestEmail}
)} {booking.notes && (

Примечания

{booking.notes}

)} {booking.channelBookingId && (

ID канала

{booking.channelBookingId}
)}
)} {/* ── Гости ────────────────────────────────────────────────────────── */} {tab === 'guest' && slug && (
{/* Require-docs warning */} {requireDocs && adultsWithoutDocs.length > 0 && (
У {adultsWithoutDocs.length} взрослого(-ых) не заполнены паспортные данные
)} {/* Loading */} {bgLoading && (
)} {/* Guest list */} {!bgLoading && bgGuests.map(bg => (
{bg.lastName} {bg.firstName}{bg.middleName ? ` ${bg.middleName}` : ''} {bg.isMain && ( Главный )} {bg.isChild && ( Ребёнок )}
{bg.passportSeries && bg.passportNumber ? (

{bg.passportSeries} {bg.passportNumber}

) : !bg.isChild && requireDocs ? (

Паспорт не заполнен

) : null} {bg.birthDate && (

{new Date(bg.birthDate).toLocaleDateString('ru-RU')}

)}
))} {/* Empty state */} {!bgLoading && bgGuests.length === 0 && !showGuestForm && (

Гости не добавлены

)} {/* Add guest form */} {showGuestForm && (

{editingBg ? 'Редактировать гостя' : 'Добавить гостя'}

{/* Autocomplete last name */}
handleAcInput(e.target.value)} placeholder="Иванов" autoComplete="off" /> {acLoading && ( )}
{acResults.length > 0 && (
{acResults.map(g => ( ))}
)}
{/* First name + middle name */}
setBgForm(f => ({ ...f, first_name: e.target.value }))} placeholder="Иван" />
setBgForm(f => ({ ...f, middle_name: e.target.value }))} placeholder="Иванович" />
{/* Birth date + flags */}
setBgForm(f => ({ ...f, birth_date: e.target.value }))} />
{/* Passport (adults only) */} {!bgForm.is_child && (

Паспорт

handlePassportInput('passport_series', e.target.value.toUpperCase())} placeholder="4510" maxLength={10} />
handlePassportInput('passport_number', e.target.value)} placeholder="123456" maxLength={10} />
setBgForm(f => ({ ...f, passport_issued_by: e.target.value }))} placeholder="УФМС России по г. Москве" />
setBgForm(f => ({ ...f, passport_issue_date: e.target.value }))} />
)} {/* Scan + save/cancel */}
)} {/* Add button */} {!showGuestForm && ( )}
)} {/* Fallback Гость tab (no slug — old static form) */} {tab === 'guest' && !slug && (
setP('lastName', e.target.value)} placeholder="Иванов" />
setP('firstName', e.target.value)} placeholder="Иван" />
setP('middleName', e.target.value)} placeholder="Иванович" />
setP('dob', e.target.value)} />

Паспорт

setP('series', e.target.value)} placeholder="4510" maxLength={4} />
setP('number', e.target.value)} placeholder="123456" maxLength={6} />
setP('issuedBy', e.target.value)} placeholder="УФМС России по г. Москве" />
setP('issueDate', e.target.value)} />
setP('regAddress', e.target.value)} placeholder="г. Москва, ул. Примерная, д. 1" />
)} {/* ── Оплата ───────────────────────────────────────────────────────── */} {tab === 'payment' && (
{/* Discount selector */}
{/* Breakdown */}
Стоимость {formatCurrency(baseTotal)}
{discountAmt > 0 && (
Скидка «{selDiscount?.name}» −{formatCurrency(discountAmt)}
)}
Итого {formatCurrency(finalTotal)}
Оплачено {formatCurrency(totalPaid)}
0 ? 'text-red-600 dark:text-red-400' : 'text-emerald-600 dark:text-emerald-400', )}> {balance > 0 ? 'Остаток' : 'Переплата'} {balance > 0 ? formatCurrency(balance) : `+${formatCurrency(-balance)}`}
{/* Accept payment */} {!showPayForm ? ( ) : (

Новый платёж

setPayAmount(e.target.value)} placeholder="0" min="0" />
{METHODS.map(m => ( ))}
setPayNote(e.target.value)} placeholder="Необязательно" />
)} {/* Payment history */} {payments.length > 0 && (

История платежей

{payments.map(p => { const M = METHODS.find(m => m.id === p.method)! return (
{p.date} {p.note || M.label} {formatCurrency(p.amount)}
) })}
)}
)} {/* ── Документы ────────────────────────────────────────────────────── */} {tab === 'docs' && (

Распечатайте документы для гостя

{DOCUMENTS .filter(d => d.always || booking.status === 'checked_in' || booking.status === 'checked_out') .map(doc => (

{doc.label}

{doc.desc}

)) }

Состав комплекта настраивается в разделе «Документы»

)}
{/* Footer */}
{balance > 0 && booking.status === 'confirmed' && (

Долг {formatCurrency(balance)} — получите оплату перед заселением

)} {booking.status === 'confirmed' && booking.checkIn <= today && (() => { const nowTime = format(new Date(), 'HH:mm') const isEarlyArrival = earlyCheckinEnabled && nowTime < hotelCheckInTime && (room?.earlyCheckinFee ?? 0) > 0 return ( <> {isEarlyArrival && (

Ранний заезд (до {hotelCheckInTime}) — доп. оплата {formatCurrency(room!.earlyCheckinFee!)}

)} ) })()} {booking.status === 'checked_in' && !earlyOutOpen && (() => { const nowTime = format(new Date(), 'HH:mm') const isLateCheckout = lateCheckoutEnabled && nowTime > hotelCheckOutTime && (room?.lateCheckoutFee ?? 0) > 0 return ( <> {isLateCheckout && (

Поздний выезд (после {hotelCheckOutTime}) — доп. оплата {formatCurrency(room!.lateCheckoutFee!)}

)} ) })()} {booking.status === 'checked_in' && earlyOutOpen && (() => { const origNights = differenceInDays(new Date(booking.checkOut), new Date(booking.checkIn)) const actualNights = Math.max(1, differenceInDays(new Date(earlyOutDate), new Date(booking.checkIn))) const refundNights = origNights - actualNights const refundAmt = refundNights > 0 ? Math.round(refundNights * booking.totalAmount / origNights) : 0 return (

Выселение гостя

setEarlyOutDate(e.target.value)} />
{refundNights > 0 && (

Возврат за {refundNights} {refundNights === 1 ? 'ночь' : refundNights < 5 ? 'ночи' : 'ночей'}: {formatCurrency(refundAmt)}

{METHODS.map(m => ( ))}

Выдайте гостю {formatCurrency(refundAmt)} {earlyOutMethod === 'cash' ? 'наличными' : earlyOutMethod === 'card' ? 'на карту' : 'переводом'}

)} {refundNights <= 0 && (

Возврат не требуется

)}
) })()} {(booking.status === 'confirmed' || booking.status === 'inquiry') && ( )}

ID: {booking.id} · Создано: {booking.createdAt}

) }