Files
hotelsync/src/components/bookings/BookingDetailPanel.tsx
HotelSync 3602ea7318 Calendar: hide cancelled bookings, drag-to-move rooms, allow booking through checked-out
- Filter cancelled bookings from calendar grid (they no longer appear as red blocks)
- Remove cancelled from legend
- Drag-to-move: left-click drag a booking block to a different room row; click without dragging still opens the detail panel
- checked_out bookings are now semi-transparent and pointer-events-none so clicking through them creates a new booking
- Conflict detection in BookingDetailPanel now excludes checked_out bookings

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

664 lines
33 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 {
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<Booking>) => void
onBulkUpdate?: (updates: Array<{ id: string; data: Partial<Booking> }>) => void
}
export function BookingDetailPanel({ booking, room, rooms, allBookings, onClose, onUpdate, onBulkUpdate }: BookingDetailPanelProps) {
const [tab, setTab] = useState<Tab>('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<Booking | null>(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<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">
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium mb-1">Номер</p>
<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>
</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' && (
<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' && (
<button onClick={() => setStatus('checked_in')} className="w-full btn-primary justify-center">
<CheckCircle size={15} /> Заселить
</button>
)}
{booking.status === 'checked_in' && (
<button onClick={() => setStatus('checked_out')} className="w-full btn-primary justify-center bg-emerald-600 hover:bg-emerald-700">
<CheckCircle size={15} /> Выселить
</button>
)}
{(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>
</>
)
}