import { useState } from 'react' import { X, Mail, Calendar, Users, CreditCard, Tag, CheckCircle, XCircle, Printer, ScanLine, Banknote, Building2, Plus, Pencil, FileText, FileCheck, Receipt, IdCard, AlertTriangle, } 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' 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[] onClose: () => void onUpdate: (id: string, data: Partial) => void onBulkUpdate?: (updates: Array<{ id: string; data: Partial }>) => void } export function BookingDetailPanel({ booking, room, rooms, allBookings, onClose, onUpdate, onBulkUpdate }: BookingDetailPanelProps) { const [tab, setTab] = useState('booking') // 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 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 && (

Номер

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

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

)} {/* 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' && (
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.status === 'checked_in' && ( )} {(booking.status === 'confirmed' || booking.status === 'inquiry') && ( )}

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

) }