Files
hotelsync/src/components/bookings/BookingDetailPanel.tsx
HotelSync edd493e24e feat: early check-in / late checkout fees with room-level configuration
- DB migration 016: add early_checkin_fee, late_checkout_fee to rooms table
- Backend rooms: support new fee columns in POST/PATCH
- Backend hotel-settings GET: also returns check_in_time, check_out_time
- Room type + RoomPayload: add earlyCheckinFee, lateCheckoutFee fields
- RoomModal: add fee amount inputs in pricing section
- SettingsPage: add toggles for early_checkin_enabled / late_checkout_enabled
- BookingDetailPanel: on check-in shows fee warning + adds payment record if
  current time is before checkInTime and fee is set on the room; same logic
  for check-out after checkOutTime

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 19:24:36 +03:00

1449 lines
74 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, 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<Booking>) => void
onBulkUpdate?: (updates: Array<{ id: string; data: Partial<Booking> }>) => void
}
export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, onClose, onUpdate, onBulkUpdate }: BookingDetailPanelProps) {
const [tab, setTab] = useState<Tab>('booking')
// ── Multi-guest state ────────────────────────────────────────────────────
const [bgGuests, setBgGuests] = useState<BookingGuest[]>([])
const [bgLoading, setBgLoading] = useState(false)
const [bgLoaded, setBgLoaded] = useState(false)
const [requireDocs, setRequireDocs] = useState(false)
const [showGuestForm, setShowGuestForm] = useState(false)
const [editingBg, setEditingBg] = useState<BookingGuest | null>(null)
const [bgForm, setBgForm] = useState<BookingGuestPayload>({})
const [bgSaving, setBgSaving] = useState(false)
// Autocomplete
const [acQuery, setAcQuery] = useState('')
const [acResults, setAcResults] = useState<GuestApiType[]>([])
const [acLoading, setAcLoading] = useState(false)
const acTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const acWrapRef = useRef<HTMLDivElement | null>(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<string, unknown>
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<ReturnType<typeof setTimeout> | 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<Booking | null>(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<PassportData>({
lastName: nameParts[0] ?? '', firstName: nameParts[1] ?? '', middleName: nameParts[2] ?? '',
dob: '', series: '', number: '', issuedBy: '', issueDate: '', regAddress: '',
})
const setP = <K extends keyof PassportData>(k: K, v: PassportData[K]) =>
setPassport(prev => ({ ...prev, [k]: v }))
// Payments
const [payments, setPayments] = useState<Payment[]>(() =>
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 (
<>
<div className="fixed inset-0 z-40" onClick={onClose} />
<div className="fixed right-0 top-0 bottom-0 z-50 w-full sm:w-[400px] bg-white dark:bg-slate-800 border-l border-slate-200 dark:border-slate-700 shadow-2xl flex flex-col">
{/* Toast notification */}
{toast && (
<div className="absolute top-3 left-4 right-4 z-20 bg-slate-900 dark:bg-slate-700 text-white text-sm rounded-xl px-4 py-2.5 text-center shadow-lg pointer-events-none">
{toast}
</div>
)}
{/* Header */}
<div className="px-5 pt-4 pb-3 border-b border-slate-200 dark:border-slate-700 shrink-0">
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="text-base font-semibold text-slate-900 dark:text-slate-100">
{booking.guestName}
</h3>
<div className="flex items-center gap-2 mt-1">
<Badge className={BOOKING_STATUS_BADGE[booking.status]}>
{BOOKING_STATUS_LABELS[booking.status]}
</Badge>
<Badge className={SOURCE_COLORS[booking.source]}>
{SOURCE_LABELS[booking.source]}
</Badge>
</div>
</div>
<button onClick={onClose} className="btn-ghost p-1.5 mt-0.5">
<X size={18} />
</button>
</div>
{/* Tab bar */}
<div className="flex gap-0.5 mt-3">
{TABS.map(t => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={cn(
'flex-1 py-1.5 text-xs font-medium rounded-lg transition-colors',
tab === t.id
? 'bg-brand-600 text-white'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700',
)}
>
{t.label}
</button>
))}
</div>
</div>
{/* Tab content */}
<div className="flex-1 overflow-y-auto px-5 py-4">
{/* ── Бронь ────────────────────────────────────────────────────────── */}
{tab === 'booking' && (
<div className="space-y-4">
{room && (
<div className="rounded-xl bg-slate-50 dark:bg-slate-700/40 p-3">
<div className="flex items-start justify-between mb-1">
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium">Номер</p>
{canRelocate && !relocating && (
<button
onClick={() => { setRelocating(true); setRelocateRoomId('') }}
className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300 font-medium"
>
<Pencil size={11} /> Переселить
</button>
)}
</div>
<p className="font-semibold text-slate-900 dark:text-slate-100">
{room.number} {room.type}
</p>
<p className="text-sm text-slate-500 dark:text-slate-400">
Этаж {room.floor} · {room.bedType} bed
</p>
{/* Форма переселения */}
{relocating && (
<div className="mt-3 space-y-2 pt-3 border-t border-slate-200 dark:border-slate-600">
<p className="text-xs font-medium text-amber-700 dark:text-amber-400">Переселить гостя в другой номер</p>
{freeRoomsForRelocate.length > 0 ? (
<>
<select
className="input text-sm"
value={relocateRoomId}
onChange={e => setRelocateRoomId(e.target.value)}
>
<option value=""> выберите номер </option>
{freeRoomsForRelocate.map(r => (
<option key={r.id} value={r.id}>
{r.number} {r.type} ({r.baseRate.toLocaleString('ru-RU')} /ночь)
</option>
))}
</select>
<div className="flex gap-2">
<button
onClick={handleRelocate}
disabled={!relocateRoomId}
className="flex-1 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-600 disabled:opacity-50 text-white text-xs font-semibold transition-colors"
>
Переселить
</button>
<button
onClick={() => setRelocating(false)}
className="px-3 py-1.5 rounded-lg border border-slate-200 dark:border-slate-600 text-xs text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
>
Отмена
</button>
</div>
</>
) : (
<div className="text-xs text-slate-500 dark:text-slate-400">
Нет свободных номеров на эти даты
<button onClick={() => setRelocating(false)} className="ml-2 text-brand-600 dark:text-brand-400 underline">Закрыть</button>
</div>
)}
</div>
)}
</div>
)}
{/* Early check-in banner */}
{canEarlyCheckIn && !earlyOpen && (
<div className="rounded-xl border border-amber-200 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-3">
<div className="flex items-start justify-between gap-2">
<div className="flex items-start gap-2">
<LogIn size={15} className="text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
<div>
<p className="text-xs font-semibold text-amber-800 dark:text-amber-300">Гость приехал раньше</p>
<p className="text-xs text-amber-700 dark:text-amber-400 mt-0.5">
Бронь начинается {fmtDate(booking.checkIn)}, но можно заселить сейчас
</p>
</div>
</div>
<button
onClick={() => { setEarlyOpen(true); setEarlyRoomId(booking.roomId); setEarlyNewCheckOut(booking.checkOut) }}
className="shrink-0 text-xs font-semibold text-amber-700 dark:text-amber-300 bg-amber-100 dark:bg-amber-800/40 hover:bg-amber-200 dark:hover:bg-amber-700/50 px-2.5 py-1 rounded-lg transition-colors"
>
Заселить
</button>
</div>
</div>
)}
{/* 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 (
<div className="rounded-xl border border-amber-300 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 p-3 space-y-3">
<div className="flex items-center gap-2">
<LogIn size={14} className="text-amber-600 dark:text-amber-400" />
<p className="text-xs font-semibold text-amber-800 dark:text-amber-300">Ранний заезд</p>
</div>
{/* Room selector */}
{currentRoomFree ? (
<div className="text-xs text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-700 rounded-lg px-2.5 py-2">
Текущий номер свободен можно заселить без переноса
</div>
) : (
<div className="space-y-1.5">
<p className="text-xs text-amber-700 dark:text-amber-400">
Текущий номер занят. Выберите свободный:
</p>
{availableRooms.length > 0 ? (
<select
className="input text-sm"
value={earlyRoomId}
onChange={e => setEarlyRoomId(e.target.value)}
>
<option value=""> выберите номер </option>
{availableRooms.map(r => {
const diff = r.baseRate - (origRoom?.baseRate ?? 0)
const diffStr = diff > 0 ? ` (+${diff.toLocaleString('ru-RU')} ₽/н)` : diff < 0 ? ` (${diff.toLocaleString('ru-RU')} ₽/н)` : ''
return (
<option key={r.id} value={r.id}>
{r.number} {r.type}{diffStr}
</option>
)
})}
</select>
) : (
<p className="text-xs text-red-600 dark:text-red-400">Нет свободных номеров</p>
)}
</div>
)}
{/* Checkout date */}
<div>
<label className={lbl}>Дата выезда</label>
<input
type="date"
className="input text-sm"
value={earlyNewCheckOut}
min={today}
max={booking.checkOut}
onChange={e => setEarlyNewCheckOut(e.target.value)}
/>
</div>
{/* Extra nights info */}
{earlyNewCheckOut > today && (
<div className="text-xs text-slate-500 dark:text-slate-400 space-y-0.5">
<p>Заезд сегодня доп. {extraNights} {extraNights === 1 ? 'ночь' : extraNights < 5 ? 'ночи' : 'ночей'}</p>
{priceDiff !== 0 && selectedRoom && (
<p className={priceDiff > 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}>
Разница в стоимости: {priceDiff > 0 ? '+' : ''}{priceDiff.toLocaleString('ru-RU')} /ночь
</p>
)}
</div>
)}
<div className="flex gap-2">
<button
onClick={() => setEarlyOpen(false)}
className="btn-secondary flex-1 justify-center text-xs py-1.5"
>
Отмена
</button>
<button
onClick={handleEarlyCheckIn}
disabled={earlySaving || (!currentRoomFree && !earlyRoomId) || availableRooms.length === 0}
className="flex-1 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-600 disabled:opacity-50 disabled:cursor-not-allowed text-white text-xs font-semibold transition-colors flex items-center justify-center gap-1"
>
{earlySaving ? <Loader2 size={12} className="animate-spin" /> : <LogIn size={12} />}
Заселить сейчас
</button>
</div>
</div>
)
})()}
{/* Dates — read or edit mode */}
<div>
<div className="flex items-center justify-between mb-2">
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1">
<Calendar size={12} /> Даты проживания
</p>
{!editDates && canEditDates && (
<button
onClick={() => { setEditDates(true); setConflictBooking(null) }}
className="flex items-center gap-1 text-xs text-brand-600 dark:text-brand-400 hover:text-brand-700 dark:hover:text-brand-300 font-medium"
>
<Pencil size={11} /> Изменить
</button>
)}
</div>
{editDates ? (
<div className="space-y-3 p-3 rounded-xl border border-brand-200 dark:border-brand-700 bg-brand-50/30 dark:bg-brand-900/10">
<div className="grid grid-cols-2 gap-3">
<div>
<label className={lbl}>Заезд</label>
<input type="date" className="input text-sm" value={newCheckIn}
onChange={e => { setNewCheckIn(e.target.value); setConflictBooking(null) }} />
</div>
<div>
<label className={lbl}>Выезд</label>
<input type="date" className="input text-sm" value={newCheckOut} min={newCheckIn}
onChange={e => { setNewCheckOut(e.target.value); setConflictBooking(null) }} />
</div>
</div>
{rooms && rooms.length > 1 && (
<div>
<label className={lbl}>Номер</label>
<select className="input text-sm" value={newRoomId}
onChange={e => { setNewRoomId(e.target.value); setConflictBooking(null) }}>
{rooms.map(r => (
<option key={r.id} value={r.id}>{r.number} {r.type}</option>
))}
</select>
</div>
)}
{/* Conflict warning */}
{conflictBooking && (
<div className="rounded-xl border border-red-200 dark:border-red-700 bg-red-50 dark:bg-red-900/20 p-3 space-y-2">
<div className="flex items-start gap-2 text-red-700 dark:text-red-400 text-xs font-medium">
<AlertTriangle size={13} className="shrink-0 mt-0.5" />
<span>
Номер занят: {conflictBooking.guestName},&nbsp;
{fmtDate(conflictBooking.checkIn)} {fmtDate(conflictBooking.checkOut)}
</span>
</div>
{conflictBooking.status !== 'checked_in' && onBulkUpdate && rooms && (
<div className="space-y-1.5">
<p className="text-xs text-red-600 dark:text-red-400">
Переместить {conflictBooking.guestName} в другой номер:
</p>
<select className="input text-sm" value={conflictResolveRoomId}
onChange={e => setConflictResolveRoomId(e.target.value)}>
<option value=""> выберите номер </option>
{rooms.filter(r => r.id !== newRoomId).map(r => (
<option key={r.id} value={r.id}>{r.number} {r.type}</option>
))}
</select>
<button
disabled={!conflictResolveRoomId}
onClick={handleResolveWithRoomSwap}
className="w-full btn-primary text-xs py-1.5 justify-center disabled:opacity-50 disabled:cursor-not-allowed"
>
Переместить и сохранить
</button>
</div>
)}
<button onClick={handleForceUpdate}
className="w-full btn-secondary text-xs py-1.5 justify-center text-red-600 dark:text-red-400 border-red-200 dark:border-red-700">
Сохранить принудительно
</button>
</div>
)}
<div className="flex gap-2">
<button
onClick={() => { setEditDates(false); setConflictBooking(null) }}
className="btn-secondary flex-1 justify-center text-sm py-1.5"
>
Отмена
</button>
{!conflictBooking && (
<button onClick={handleSaveDates}
className="btn-primary flex-1 justify-center text-sm py-1.5">
Сохранить
</button>
)}
</div>
</div>
) : (
<div className="grid grid-cols-2 gap-3">
<div>
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium mb-1">Заезд</p>
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
{fmtDate(booking.checkIn)}
</p>
</div>
<div>
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium mb-1">Выезд</p>
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
{fmtDate(booking.checkOut)}
</p>
</div>
</div>
)}
{!editDates && (
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
{nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'}
</p>
)}
</div>
<div>
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1 mb-1">
<Users size={12} /> Гости
</p>
<p className="text-sm text-slate-900 dark:text-slate-100">
{booking.adults} взр.{booking.children > 0 ? ` · ${booking.children} дет.` : ''}
</p>
</div>
{booking.guestEmail && (
<div className="space-y-1.5">
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium">Контакты</p>
<div className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
<Mail size={13} className="text-slate-400" />
{booking.guestEmail}
</div>
</div>
)}
{booking.notes && (
<div>
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1 mb-1">
<Tag size={12} /> Примечания
</p>
<p className="text-sm text-slate-700 dark:text-slate-300 bg-slate-50 dark:bg-slate-700/40 rounded-lg p-2.5">
{booking.notes}
</p>
</div>
)}
{booking.channelBookingId && (
<div>
<p className="text-xs text-slate-500 mb-0.5">ID канала</p>
<code className="text-xs text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-900/20 px-2 py-1 rounded">
{booking.channelBookingId}
</code>
</div>
)}
</div>
)}
{/* ── Гости ────────────────────────────────────────────────────────── */}
{tab === 'guest' && slug && (
<div className="space-y-3">
{/* Require-docs warning */}
{requireDocs && adultsWithoutDocs.length > 0 && (
<div className="flex items-start gap-2 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 p-2.5 text-xs text-amber-700 dark:text-amber-400">
<AlertTriangle size={13} className="shrink-0 mt-0.5" />
<span>У {adultsWithoutDocs.length} взрослого(-ых) не заполнены паспортные данные</span>
</div>
)}
{/* Loading */}
{bgLoading && (
<div className="flex items-center justify-center py-6">
<Loader2 size={20} className="animate-spin text-slate-400" />
</div>
)}
{/* Guest list */}
{!bgLoading && bgGuests.map(bg => (
<div key={bg.id} className="rounded-xl border border-slate-200 dark:border-slate-600 p-3">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-sm font-medium text-slate-900 dark:text-slate-100">
{bg.lastName} {bg.firstName}{bg.middleName ? ` ${bg.middleName}` : ''}
</span>
{bg.isMain && (
<span className="flex items-center gap-0.5 px-1.5 py-0.5 rounded-md text-xs bg-brand-100 text-brand-700 dark:bg-brand-900/30 dark:text-brand-400">
<UserCheck size={10} /> Главный
</span>
)}
{bg.isChild && (
<span className="flex items-center gap-0.5 px-1.5 py-0.5 rounded-md text-xs bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400">
<Baby size={10} /> Ребёнок
</span>
)}
</div>
{bg.passportSeries && bg.passportNumber ? (
<p className="text-xs text-slate-400 font-mono mt-0.5">
{bg.passportSeries} {bg.passportNumber}
</p>
) : !bg.isChild && requireDocs ? (
<p className="text-xs text-amber-500 mt-0.5">Паспорт не заполнен</p>
) : null}
{bg.birthDate && (
<p className="text-xs text-slate-400 mt-0.5">
{new Date(bg.birthDate).toLocaleDateString('ru-RU')}
</p>
)}
</div>
<div className="flex gap-1 shrink-0">
<button
onClick={() => openEditForm(bg)}
className="p-1.5 rounded-lg text-slate-400 hover:text-brand-600 hover:bg-brand-50 dark:hover:bg-brand-900/20 transition-colors"
>
<Pencil size={13} />
</button>
<button
onClick={() => removeGuest(bg.id)}
className="p-1.5 rounded-lg text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
>
<Trash2 size={13} />
</button>
</div>
</div>
</div>
))}
{/* Empty state */}
{!bgLoading && bgGuests.length === 0 && !showGuestForm && (
<p className="text-xs text-center text-slate-400 py-4">Гости не добавлены</p>
)}
{/* Add guest form */}
{showGuestForm && (
<div className="rounded-xl border border-brand-200 dark:border-brand-700 bg-brand-50/30 dark:bg-brand-900/10 p-3 space-y-3">
<p className="text-xs font-semibold text-brand-700 dark:text-brand-400">
{editingBg ? 'Редактировать гостя' : 'Добавить гостя'}
</p>
{/* Autocomplete last name */}
<div ref={acWrapRef} className="relative">
<label className={lbl}>Фамилия *</label>
<div className="relative">
<input
className={fld}
value={bgForm.last_name ?? ''}
onChange={e => handleAcInput(e.target.value)}
placeholder="Иванов"
autoComplete="off"
/>
{acLoading && (
<Loader2 size={12} className="absolute right-2.5 top-1/2 -translate-y-1/2 animate-spin text-slate-400" />
)}
</div>
{acResults.length > 0 && (
<div className="absolute z-20 left-0 right-0 mt-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-xl shadow-lg overflow-hidden max-h-48 overflow-y-auto">
{acResults.map(g => (
<button
key={g.id}
type="button"
onClick={() => selectAcGuest(g)}
className="w-full text-left px-3 py-2 hover:bg-brand-50 dark:hover:bg-brand-900/20 transition-colors"
>
<div className="flex items-center gap-2">
<Search size={11} className="text-slate-400 shrink-0" />
<div>
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
{g.lastName} {g.firstName}
</p>
{(g.passportSeries || g.passportNumber) && (
<p className="text-xs text-slate-400 font-mono">
{[g.passportSeries, g.passportNumber].filter(Boolean).join(' ')}
</p>
)}
</div>
</div>
</button>
))}
</div>
)}
</div>
{/* First name + middle name */}
<div className="grid grid-cols-2 gap-2">
<div>
<label className={lbl}>Имя *</label>
<input className={fld} value={bgForm.first_name ?? ''} onChange={e => setBgForm(f => ({ ...f, first_name: e.target.value }))} placeholder="Иван" />
</div>
<div>
<label className={lbl}>Отчество</label>
<input className={fld} value={bgForm.middle_name ?? ''} onChange={e => setBgForm(f => ({ ...f, middle_name: e.target.value }))} placeholder="Иванович" />
</div>
</div>
{/* Birth date + flags */}
<div className="grid grid-cols-2 gap-2">
<div>
<label className={lbl}>Дата рождения</label>
<input type="date" className={fld} value={bgForm.birth_date ?? ''} onChange={e => setBgForm(f => ({ ...f, birth_date: e.target.value }))} />
</div>
<div className="flex flex-col gap-2 pt-4">
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={bgForm.is_child ?? false} onChange={e => setBgForm(f => ({ ...f, is_child: e.target.checked }))} className="rounded" />
<span className="text-xs text-slate-600 dark:text-slate-400">Ребёнок (до 16 лет)</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={bgForm.is_main ?? false} onChange={e => setBgForm(f => ({ ...f, is_main: e.target.checked }))} className="rounded" />
<span className="text-xs text-slate-600 dark:text-slate-400">Главный гость</span>
</label>
</div>
</div>
{/* Passport (adults only) */}
{!bgForm.is_child && (
<div className="space-y-2 pt-2 border-t border-brand-100 dark:border-brand-800">
<p className="text-xs font-semibold text-slate-500 dark:text-slate-400">Паспорт</p>
<div className="grid grid-cols-2 gap-2">
<div>
<label className={lbl}>Серия</label>
<input className={`${fld} font-mono uppercase`} value={bgForm.passport_series ?? ''} onChange={e => handlePassportInput('passport_series', e.target.value.toUpperCase())} placeholder="4510" maxLength={10} />
</div>
<div>
<label className={lbl}>Номер</label>
<input className={`${fld} font-mono`} value={bgForm.passport_number ?? ''} onChange={e => handlePassportInput('passport_number', e.target.value)} placeholder="123456" maxLength={10} />
</div>
</div>
<div>
<label className={lbl}>Кем выдан</label>
<input className={fld} value={bgForm.passport_issued_by ?? ''} onChange={e => setBgForm(f => ({ ...f, passport_issued_by: e.target.value }))} placeholder="УФМС России по г. Москве" />
</div>
<div>
<label className={lbl}>Дата выдачи</label>
<input type="date" className={fld} value={bgForm.passport_issue_date ?? ''} onChange={e => setBgForm(f => ({ ...f, passport_issue_date: e.target.value }))} />
</div>
</div>
)}
{/* Scan + save/cancel */}
<button
onClick={() => showToast('Для сканирования подключите RFID/OCR сканер')}
className="w-full flex items-center justify-center gap-2 py-2 rounded-xl border border-dashed border-brand-300 dark:border-brand-700 text-brand-600 dark:text-brand-400 text-xs font-medium hover:bg-brand-50 dark:hover:bg-brand-900/20 transition-colors"
>
<ScanLine size={13} /> Сканировать паспорт
</button>
<div className="flex gap-2">
<button onClick={cancelGuestForm} className="btn-secondary flex-1 justify-center text-sm py-1.5">Отмена</button>
<button onClick={saveGuest} disabled={bgSaving} className="btn-primary flex-1 justify-center text-sm py-1.5">
{bgSaving ? <Loader2 size={13} className="animate-spin" /> : null}
{editingBg ? 'Сохранить' : 'Добавить'}
</button>
</div>
</div>
)}
{/* Add button */}
{!showGuestForm && (
<button
onClick={openAddForm}
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-600 text-slate-500 dark:text-slate-400 text-sm font-medium hover:border-brand-300 dark:hover:border-brand-700 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-brand-50 dark:hover:bg-brand-900/20 transition-colors"
>
<Plus size={15} /> Добавить гостя
</button>
)}
</div>
)}
{/* Fallback Гость tab (no slug — old static form) */}
{tab === 'guest' && !slug && (
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3">
<div>
<label className={lbl}>Фамилия</label>
<input className={fld} value={passport.lastName} onChange={e => setP('lastName', e.target.value)} placeholder="Иванов" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className={lbl}>Имя</label>
<input className={fld} value={passport.firstName} onChange={e => setP('firstName', e.target.value)} placeholder="Иван" />
</div>
<div>
<label className={lbl}>Отчество</label>
<input className={fld} value={passport.middleName} onChange={e => setP('middleName', e.target.value)} placeholder="Иванович" />
</div>
</div>
<div>
<label className={lbl}>Дата рождения</label>
<input type="date" className={fld} value={passport.dob} onChange={e => setP('dob', e.target.value)} />
</div>
</div>
<div className="pt-2 border-t border-slate-100 dark:border-slate-700">
<p className="text-xs font-semibold text-slate-600 dark:text-slate-400 mb-3">Паспорт</p>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className={lbl}>Серия</label>
<input className={fld} value={passport.series} onChange={e => setP('series', e.target.value)} placeholder="4510" maxLength={4} />
</div>
<div>
<label className={lbl}>Номер</label>
<input className={fld} value={passport.number} onChange={e => setP('number', e.target.value)} placeholder="123456" maxLength={6} />
</div>
</div>
<div>
<label className={lbl}>Кем выдан</label>
<input className={fld} value={passport.issuedBy} onChange={e => setP('issuedBy', e.target.value)} placeholder="УФМС России по г. Москве" />
</div>
<div>
<label className={lbl}>Дата выдачи</label>
<input type="date" className={fld} value={passport.issueDate} onChange={e => setP('issueDate', e.target.value)} />
</div>
<div>
<label className={lbl}>Адрес регистрации</label>
<input className={fld} value={passport.regAddress} onChange={e => setP('regAddress', e.target.value)} placeholder="г. Москва, ул. Примерная, д. 1" />
</div>
</div>
</div>
<button
onClick={() => showToast('Для сканирования подключите RFID/OCR сканер')}
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl border-2 border-dashed border-brand-300 dark:border-brand-700 text-brand-600 dark:text-brand-400 text-sm font-medium hover:bg-brand-50 dark:hover:bg-brand-900/20 transition-colors"
>
<ScanLine size={16} /> Сканировать паспорт
</button>
<button
onClick={() => showToast('✓ Данные гостя сохранены')}
className="w-full btn-primary justify-center"
>
Сохранить данные
</button>
</div>
)}
{/* ── Оплата ───────────────────────────────────────────────────────── */}
{tab === 'payment' && (
<div className="space-y-4">
{/* Discount selector */}
<div>
<label className={lbl}>Скидка</label>
<select className="input text-sm" value={discountId} onChange={e => setDiscountId(e.target.value)}>
<option value=""> Без скидки </option>
{activeDiscs.map(d => (
<option key={d.id} value={d.id}>
{d.name} ({d.valueType === 'percent' ? `${d.value}%` : formatCurrency(d.value)})
</option>
))}
</select>
</div>
{/* Breakdown */}
<div className="rounded-xl border border-slate-200 dark:border-slate-600 divide-y divide-slate-200 dark:divide-slate-600 text-sm">
<div className="flex justify-between px-3 py-2">
<span className="text-slate-500 dark:text-slate-400">Стоимость</span>
<span className="font-medium text-slate-900 dark:text-slate-100">{formatCurrency(baseTotal)}</span>
</div>
{discountAmt > 0 && (
<div className="flex justify-between px-3 py-2">
<span className="text-slate-500 dark:text-slate-400">Скидка «{selDiscount?.name}»</span>
<span className="font-medium text-emerald-600 dark:text-emerald-400">{formatCurrency(discountAmt)}</span>
</div>
)}
<div className="flex justify-between px-3 py-2 font-semibold">
<span className="text-slate-700 dark:text-slate-300">Итого</span>
<span className="text-slate-900 dark:text-slate-100">{formatCurrency(finalTotal)}</span>
</div>
<div className="flex justify-between px-3 py-2">
<span className="text-slate-500 dark:text-slate-400">Оплачено</span>
<span className="font-medium text-emerald-600 dark:text-emerald-400">{formatCurrency(totalPaid)}</span>
</div>
<div className={cn(
'flex justify-between px-3 py-2 font-bold',
balance > 0 ? 'text-red-600 dark:text-red-400' : 'text-emerald-600 dark:text-emerald-400',
)}>
<span>{balance > 0 ? 'Остаток' : 'Переплата'}</span>
<span>{balance > 0 ? formatCurrency(balance) : `+${formatCurrency(-balance)}`}</span>
</div>
</div>
{/* Accept payment */}
{!showPayForm ? (
<button
onClick={() => { setPayAmount(balance > 0 ? String(balance) : ''); setShowPayForm(true) }}
className="w-full btn-primary justify-center"
disabled={balance <= 0}
>
<Plus size={15} /> Принять оплату
</button>
) : (
<div className="rounded-xl border border-brand-200 dark:border-brand-700 p-3 space-y-3 bg-brand-50/40 dark:bg-brand-900/10">
<p className="text-xs font-semibold text-brand-700 dark:text-brand-400">Новый платёж</p>
<div>
<label className={lbl}>Сумма, </label>
<input type="number" className="input text-sm" value={payAmount}
onChange={e => setPayAmount(e.target.value)} placeholder="0" min="0" />
</div>
<div>
<label className={lbl}>Способ оплаты</label>
<div className="grid grid-cols-3 gap-1.5">
{METHODS.map(m => (
<button
key={m.id} type="button" onClick={() => setPayMethod(m.id)}
className={cn(
'flex flex-col items-center gap-1 py-2 rounded-lg border text-xs font-medium transition-colors',
payMethod === m.id
? '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',
)}
>
<m.icon size={14} /> {m.label}
</button>
))}
</div>
</div>
<div>
<label className={lbl}>Комментарий</label>
<input className="input text-sm" value={payNote}
onChange={e => setPayNote(e.target.value)} placeholder="Необязательно" />
</div>
<div className="flex gap-2">
<button onClick={() => setShowPayForm(false)} className="btn-secondary flex-1 justify-center text-sm py-2">Отмена</button>
<button onClick={addPayment} disabled={!payAmount || parseFloat(payAmount) <= 0}
className="btn-primary flex-1 justify-center text-sm py-2">Принять</button>
</div>
</div>
)}
{/* Payment history */}
{payments.length > 0 && (
<div>
<p className={lbl + ' mt-1'}>История платежей</p>
<div className="space-y-1.5">
{payments.map(p => {
const M = METHODS.find(m => m.id === p.method)!
return (
<div key={p.id} className="flex items-center gap-2 text-sm px-2.5 py-2 rounded-lg bg-slate-50 dark:bg-slate-700/40">
<M.icon size={13} className="text-slate-400 shrink-0" />
<span className="text-slate-400 text-xs shrink-0">{p.date}</span>
<span className="flex-1 text-slate-600 dark:text-slate-300 text-xs truncate">{p.note || M.label}</span>
<span className="font-semibold text-emerald-600 dark:text-emerald-400 shrink-0">{formatCurrency(p.amount)}</span>
</div>
)
})}
</div>
</div>
)}
</div>
)}
{/* ── Документы ────────────────────────────────────────────────────── */}
{tab === 'docs' && (
<div className="space-y-2">
<p className="text-xs text-slate-500 dark:text-slate-400 mb-3">
Распечатайте документы для гостя
</p>
{DOCUMENTS
.filter(d => d.always || booking.status === 'checked_in' || booking.status === 'checked_out')
.map(doc => (
<div key={doc.id} className="flex items-center gap-3 p-3 rounded-xl border border-slate-200 dark:border-slate-600 hover:border-brand-300 dark:hover:border-brand-600 transition-colors">
<div className="w-9 h-9 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center shrink-0">
<doc.icon size={17} className="text-slate-500 dark:text-slate-400" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">{doc.label}</p>
<p className="text-xs text-slate-500 dark:text-slate-400">{doc.desc}</p>
</div>
<button
onClick={() => printDoc(doc.label)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-brand-600 text-white text-xs font-medium hover:bg-brand-700 transition-colors shrink-0"
>
<Printer size={12} /> Печать
</button>
</div>
))
}
<div className="pt-3 mt-1 border-t border-slate-100 dark:border-slate-700">
<button
onClick={() => showToast('🖨 Комплект документов отправлен на печать')}
className="w-full btn-primary justify-center"
>
<Printer size={15} /> Напечатать комплект
</button>
<p className="text-xs text-center text-slate-400 dark:text-slate-500 mt-1.5">
Состав комплекта настраивается в разделе «Документы»
</p>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="shrink-0 px-5 py-4 border-t border-slate-200 dark:border-slate-700 space-y-2">
{balance > 0 && booking.status === 'confirmed' && (
<div className="flex items-center gap-2 p-2.5 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700">
<AlertTriangle size={14} className="text-amber-500 shrink-0" />
<p className="text-xs text-amber-700 dark:text-amber-400">
Долг {formatCurrency(balance)} получите оплату перед заселением
</p>
</div>
)}
{booking.status === 'confirmed' && booking.checkIn <= today && (() => {
const nowTime = format(new Date(), 'HH:mm')
const isEarlyArrival = earlyCheckinEnabled && nowTime < hotelCheckInTime && (room?.earlyCheckinFee ?? 0) > 0
return (
<>
{isEarlyArrival && (
<div className="flex items-center gap-2 p-2.5 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700">
<AlertTriangle size={14} className="text-blue-500 shrink-0" />
<p className="text-xs text-blue-700 dark:text-blue-400">
Ранний заезд (до {hotelCheckInTime}) доп. оплата {formatCurrency(room!.earlyCheckinFee!)}
</p>
</div>
)}
<button
onClick={() => {
if (isEarlyArrival) {
setPayments(prev => [...prev, {
id: `early-${Date.now()}`,
date: new Date().toLocaleDateString('ru-RU'),
amount: room!.earlyCheckinFee!,
method: 'cash' as const,
note: `Ранний заезд (заезд до ${hotelCheckInTime})`,
}])
}
setStatus('checked_in')
}}
className="w-full btn-primary justify-center"
>
<CheckCircle size={15} /> {isEarlyArrival ? `Заселить (+ ${formatCurrency(room!.earlyCheckinFee!)})` : 'Заселить'}
</button>
</>
)
})()}
{booking.status === 'checked_in' && !earlyOutOpen && (() => {
const nowTime = format(new Date(), 'HH:mm')
const isLateCheckout = lateCheckoutEnabled && nowTime > hotelCheckOutTime && (room?.lateCheckoutFee ?? 0) > 0
return (
<>
{isLateCheckout && (
<div className="flex items-center gap-2 p-2.5 rounded-xl bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-700">
<AlertTriangle size={14} className="text-orange-500 shrink-0" />
<p className="text-xs text-orange-700 dark:text-orange-400">
Поздний выезд (после {hotelCheckOutTime}) доп. оплата {formatCurrency(room!.lateCheckoutFee!)}
</p>
</div>
)}
<button
onClick={() => {
if (isLateCheckout) {
setPayments(prev => [...prev, {
id: `late-${Date.now()}`,
date: new Date().toLocaleDateString('ru-RU'),
amount: room!.lateCheckoutFee!,
method: 'cash' as const,
note: `Поздний выезд (выезд после ${hotelCheckOutTime})`,
}])
}
setEarlyOutOpen(true)
setEarlyOutDate(today)
}}
className="w-full btn-primary justify-center bg-emerald-600 hover:bg-emerald-700"
>
<CheckCircle size={15} /> {isLateCheckout ? `Выселить (+ ${formatCurrency(room!.lateCheckoutFee!)})` : 'Выселить'}
</button>
</>
)
})()}
{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 (
<div className="rounded-xl border border-emerald-200 dark:border-emerald-700 bg-emerald-50 dark:bg-emerald-900/20 p-3 space-y-3">
<p className="text-xs font-semibold text-emerald-800 dark:text-emerald-300 flex items-center gap-1.5">
<CheckCircle size={13} /> Выселение гостя
</p>
<div>
<label className={lbl}>Дата выезда</label>
<input
type="date"
className="input text-sm"
value={earlyOutDate}
min={booking.checkIn}
max={booking.checkOut}
onChange={e => setEarlyOutDate(e.target.value)}
/>
</div>
{refundNights > 0 && (
<div className="rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 p-2.5 space-y-2">
<p className="text-xs font-medium text-amber-800 dark:text-amber-300">
Возврат за {refundNights} {refundNights === 1 ? 'ночь' : refundNights < 5 ? 'ночи' : 'ночей'}: {formatCurrency(refundAmt)}
</p>
<div className="flex gap-1.5">
{METHODS.map(m => (
<button
key={m.id}
onClick={() => setEarlyOutMethod(m.id)}
className={cn(
'flex-1 py-1 rounded-lg text-xs font-medium border transition-colors',
earlyOutMethod === m.id
? 'bg-amber-500 border-amber-500 text-white'
: 'border-amber-200 dark:border-amber-700 text-amber-700 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-800/30',
)}
>
{m.label}
</button>
))}
</div>
<p className="text-[11px] text-amber-600 dark:text-amber-400">
Выдайте гостю {formatCurrency(refundAmt)} {earlyOutMethod === 'cash' ? 'наличными' : earlyOutMethod === 'card' ? 'на карту' : 'переводом'}
</p>
</div>
)}
{refundNights <= 0 && (
<p className="text-xs text-slate-500 dark:text-slate-400">Возврат не требуется</p>
)}
<div className="flex gap-2">
<button
onClick={() => setEarlyOutOpen(false)}
className="btn-secondary flex-1 justify-center text-xs py-1.5"
>
Отмена
</button>
<button
onClick={handleEarlyCheckOut}
disabled={earlyOutSaving}
className="flex-1 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-700 disabled:opacity-50 text-white text-xs font-semibold transition-colors flex items-center justify-center gap-1"
>
{earlyOutSaving ? <Loader2 size={12} className="animate-spin" /> : <CheckCircle size={12} />}
Подтвердить выезд
</button>
</div>
</div>
)
})()}
{(booking.status === 'confirmed' || booking.status === 'inquiry') && (
<button
onClick={() => setStatus('cancelled')}
className="w-full btn-secondary justify-center text-red-600 dark:text-red-400 border-red-200 dark:border-red-800 hover:bg-red-50 dark:hover:bg-red-900/20"
>
<XCircle size={15} /> Отменить
</button>
)}
<p className="text-xs text-center text-slate-400 dark:text-slate-500">
ID: {booking.id} · Создано: {booking.createdAt}
</p>
</div>
</div>
</>
)
}