Files
hotelsync/src/components/bookings/BookingDetailPanel.tsx
HotelSync 420d55d57e Initial commit: HotelSync PMS v0.1.0
- React 18 + TypeScript + Vite + Tailwind CSS
- Шахматка бронирований (drag-to-book)
- Страницы: Calendar, Bookings, Rooms, Housekeeping, Channels, API Docs, Settings
- Роли: super_admin, hotel_manager, housekeeper
- Светлая/тёмная тема
- Docker + Nginx конфигурация
- Лендинг hotelsync.ru
2026-03-10 20:38:32 +03:00

191 lines
8.1 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 { X, Mail, Phone, Calendar, Users, CreditCard, Tag, Edit2, CheckCircle, XCircle } 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 { Badge } from '../ui/Badge'
interface BookingDetailPanelProps {
booking: Booking
room?: Room
onClose: () => void
onUpdate: (id: string, data: Partial<Booking>) => void
}
export function BookingDetailPanel({ booking, room, onClose, onUpdate }: BookingDetailPanelProps) {
const nights = nightsCount(booking.checkIn, booking.checkOut)
const balance = booking.totalAmount - booking.paidAmount
const setStatus = (status: typeof booking.status) => onUpdate(booking.id, { status })
return (
<>
{/* Backdrop */}
<div className="fixed inset-0 z-40" onClick={onClose} />
{/* Panel */}
<div className="fixed right-0 top-0 bottom-0 z-50 w-96 bg-white dark:bg-slate-800 border-l border-slate-200 dark:border-slate-700 shadow-2xl flex flex-col animate-slide-in">
{/* Header */}
<div className={cn(
'px-5 py-4 border-b border-slate-200 dark:border-slate-700',
'flex items-center justify-between shrink-0',
)}>
<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">
<X size={18} />
</button>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-5">
{/* Room */}
{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 */}
<div className="grid grid-cols-2 gap-3">
<div>
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1 mb-1">
<Calendar size={12} /> Заезд
</p>
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
{new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(new Date(booking.checkIn))}
</p>
</div>
<div>
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1 mb-1">
<Calendar size={12} /> Выезд
</p>
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
{new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(new Date(booking.checkOut))}
</p>
</div>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-3">
{nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'}
</p>
{/* Guests */}
<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>
{/* Contact */}
<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>
{/* Payment */}
<div className="rounded-xl border border-slate-200 dark:border-slate-600 p-3 space-y-2">
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1">
<CreditCard size={12} /> Оплата
</p>
<div className="flex justify-between text-sm">
<span className="text-slate-600 dark:text-slate-400">Стоимость</span>
<span className="font-semibold text-slate-900 dark:text-slate-100">
{formatCurrency(booking.totalAmount)}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-600 dark:text-slate-400">Оплачено</span>
<span className="font-semibold text-emerald-600 dark:text-emerald-400">
{formatCurrency(booking.paidAmount)}
</span>
</div>
{balance > 0 && (
<div className="flex justify-between text-sm border-t border-slate-200 dark:border-slate-600 pt-2">
<span className="text-slate-600 dark:text-slate-400">Остаток</span>
<span className="font-bold text-red-600 dark:text-red-400">
{formatCurrency(balance)}
</span>
</div>
)}
</div>
{/* Notes */}
{booking.notes && (
<div>
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium mb-1 flex items-center gap-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>
)}
{/* Channel ID */}
{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>
{/* Actions */}
<div className="shrink-0 px-5 py-4 border-t border-slate-200 dark:border-slate-700 space-y-2">
{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>
</>
)
}