import { useState, useRef, useCallback, useEffect } from 'react' import { addDays, format, startOfDay, differenceInDays, parseISO, isToday } from 'date-fns' import { ru } from 'date-fns/locale' import { ChevronLeft, ChevronRight, Plus, CalendarDays, ChevronDown } from 'lucide-react' import { cn, BOOKING_STATUS_COLORS, BOOKING_STATUS_LABELS, SOURCE_LABELS } from '../../lib/utils' import type { Room, Booking, DraftBooking } from '../../types' import type { RentalObject, RentalBooking } from '../../data/rentalData' import { BookingModal } from '../bookings/BookingModal' import { BookingDetailPanel } from '../bookings/BookingDetailPanel' import { RentalBookingModal } from '../rental/RentalBookingModal' const CELL_WIDTH = 52 const ROW_HEIGHT = 56 const LABEL_WIDTH = 160 const DAYS_VISIBLE = 30 interface BookingCalendarProps { rooms: Room[] bookings: Booking[] onBookingCreate: (b: Partial) => void onBookingUpdate: (id: string, b: Partial) => void fadingBookingIds?: Set rentalObjects?: RentalObject[] rentalBookings?: RentalBooking[] onRentalBookingCreate?: (b: RentalBooking) => void } function getRoomTypeColor(type: string): string { const map: Record = { 'Стандарт': 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300', 'Делюкс': 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300', 'Полулюкс': 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300', 'Люкс': 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300', 'Пентхаус': 'bg-rose-100 dark:bg-rose-900/30 text-rose-700 dark:text-rose-300', 'Апартаменты': 'bg-teal-100 dark:bg-teal-900/30 text-teal-700 dark:text-teal-300', } return map[type] ?? 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300' } export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpdate, fadingBookingIds, rentalObjects, rentalBookings, onRentalBookingCreate }: BookingCalendarProps) { const [startDate, setStartDate] = useState(() => startOfDay(new Date())) const [visibleDays, setVisibleDays] = useState(DAYS_VISIBLE) // Date picker state const [showNavPicker, setShowNavPicker] = useState(false) const [pickerDateInput, setPickerDateInput] = useState(format(new Date(), 'yyyy-MM-dd')) const navPickerRef = useRef(null) useEffect(() => { if (!showNavPicker) return const handler = (e: MouseEvent) => { if (navPickerRef.current && !navPickerRef.current.contains(e.target as Node)) { setShowNavPicker(false) } } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, [showNavPicker]) // Drag-to-book state const [draft, setDraft] = useState(null) const [dragStart, setDragStart] = useState<{ roomId: string; dayIdx: number } | null>(null) const [dragEnd, setDragEnd] = useState(null) // Modals const [bookingModalDraft, setBookingModalDraft] = useState(null) const [selectedBooking, setSelectedBooking] = useState(null) const [rentalModal, setRentalModal] = useState<{ obj: RentalObject; date: string } | null>(null) const gridRef = useRef(null) const dates = Array.from({ length: visibleDays }, (_, i) => addDays(startDate, i)) const shiftDays = (n: number) => setStartDate(d => addDays(d, n)) const getBlockStyle = (booking: Booking) => { const start = parseISO(booking.checkIn) const end = parseISO(booking.checkOut) const colStart = Math.max(0, differenceInDays(start, startDate)) const colEnd = Math.min(visibleDays, differenceInDays(end, startDate)) if (colStart >= visibleDays || colEnd <= 0) return null return { left: colStart * CELL_WIDTH, width: (colEnd - colStart) * CELL_WIDTH - 4, } } const handleCellMouseDown = useCallback((roomId: string, dayIdx: number, e: React.MouseEvent) => { if (e.button !== 0) return e.preventDefault() setDragStart({ roomId, dayIdx }) setDragEnd(dayIdx) }, []) const handleCellMouseEnter = useCallback((dayIdx: number) => { if (!dragStart) return setDragEnd(dayIdx) }, [dragStart]) const handleMouseUp = useCallback(() => { if (!dragStart || dragEnd === null) return const minDay = Math.min(dragStart.dayIdx, dragEnd) const maxDay = Math.max(dragStart.dayIdx, dragEnd) const checkIn = format(addDays(startDate, minDay), 'yyyy-MM-dd') const checkOut = format(addDays(startDate, maxDay + 1), 'yyyy-MM-dd') setBookingModalDraft({ roomId: dragStart.roomId, checkIn, checkOut }) setDragStart(null) setDragEnd(null) setDraft(null) }, [dragStart, dragEnd, startDate]) const getDraftStyle = (roomId: string) => { if (!dragStart || dragEnd === null || dragStart.roomId !== roomId) return null const minDay = Math.min(dragStart.dayIdx, dragEnd) const maxDay = Math.max(dragStart.dayIdx, dragEnd) return { left: minDay * CELL_WIDTH, width: (maxDay - minDay + 1) * CELL_WIDTH - 2, } } return (
{/* Toolbar */}
{/* Date / period picker */}
{showNavPicker && (

Начало периода

setPickerDateInput(e.target.value)} className="input text-sm py-1.5 flex-1" />

Дней в окне

{[14, 21, 30, 45, 60].map(d => ( ))}
)}
{format(startDate, 'LLLL yyyy', { locale: ru })}
{/* Legend */}
{(['confirmed', 'checked_in', 'checked_out', 'inquiry', 'cancelled'] as const).map(s => (
{BOOKING_STATUS_LABELS[s]}
))}
{/* Grid */}
{/* Date header row */}
Номер
{dates.map((date, i) => { const isWe = date.getDay() === 0 || date.getDay() === 6 const isTod = isToday(date) return (
{format(date, 'EEE', { locale: ru }).slice(0, 2)} {format(date, 'd')}
) })}
{/* Room rows */} {rooms.map(room => { const roomBookings = bookings.filter(b => b.roomId === room.id) const draftStyle = getDraftStyle(room.id) return (
{/* Room label */}
{room.number} {room.name && {room.name}}
{room.type}
{room.baseRate.toLocaleString('ru-RU')} ₽
{/* Day cells + booking blocks */}
{dates.map((date, i) => { const isWe = date.getDay() === 0 || date.getDay() === 6 const isTod = isToday(date) return (
handleCellMouseDown(room.id, i, e)} onMouseEnter={() => handleCellMouseEnter(i)} /> ) })}
{/* Booking blocks */} {roomBookings.map(booking => { const style = getBlockStyle(booking) if (!style) return null const nights = differenceInDays(parseISO(booking.checkOut), parseISO(booking.checkIn)) const isFading = fadingBookingIds?.has(booking.id) return (
{ e.stopPropagation(); setSelectedBooking(booking) }} title={`${booking.guestName} • ${booking.checkIn} – ${booking.checkOut}`} > {booking.guestName} {style.width > 80 && ( {nights}н • {SOURCE_LABELS[booking.source]} )}
) })} {/* Draft overlay */} {draftStyle && (
)}
) })} {/* ── Rental section ── */} {rentalObjects && rentalObjects.length > 0 && ( <> {/* Separator */}
Аренда объектов
{dates.map((_, i) => (
))}
{/* Rental object rows */} {rentalObjects.map(obj => { const totalSpan = obj.closeHour - obj.openHour return (
{/* Label */}
{obj.icon}

{obj.name}

{obj.pricePerHour.toLocaleString('ru-RU')} ₽/ч · {obj.pricePerDay.toLocaleString('ru-RU')} ₽/день

{/* Day cells */} {dates.map((date, i) => { const dateStr = format(date, 'yyyy-MM-dd') const isWe = date.getDay() === 0 || date.getDay() === 6 const isTod = isToday(date) const dayBookings = (rentalBookings ?? []).filter( b => b.objectId === obj.id && b.date === dateStr && b.status !== 'cancelled', ) const hasFullDay = dayBookings.some(b => b.isFullDay) return (
setRentalModal({ obj, date: dateStr })} className={cn( 'shrink-0 border-r border-slate-200 dark:border-slate-700 cursor-pointer relative', 'hover:bg-slate-50 dark:hover:bg-slate-700/40 transition-colors', isWe && 'bg-amber-50/30 dark:bg-amber-900/10', isTod && 'bg-brand-50/40 dark:bg-brand-900/10', )} style={{ width: CELL_WIDTH, height: ROW_HEIGHT }} > {hasFullDay ? ( /* Full day booking */
Весь день
) : dayBookings.length > 0 ? ( /* Hourly booking bars */
{/* Time scale bar (background) */}
{dayBookings.map(b => { const leftPct = ((b.startHour - obj.openHour) / totalSpan) * 100 const widthPct = ((b.endHour - b.startHour) / totalSpan) * 100 return (
) })}
{/* Hour labels */}
{obj.openHour}:00 {obj.closeHour}:00
{/* Booking count */}
{dayBookings.map(b => ( {b.startHour}–{b.endHour} ))}
) : ( /* Empty — show "+" hint on hover */
+
)}
) })}
) })} )}
{/* Booking create modal */} {bookingModalDraft && ( setBookingModalDraft(null)} onSave={(data) => { onBookingCreate(data) setBookingModalDraft(null) }} /> )} {/* Booking detail panel */} {selectedBooking && ( r.id === selectedBooking.roomId)} onClose={() => setSelectedBooking(null)} onUpdate={(id, data) => { onBookingUpdate(id, data) setSelectedBooking(null) }} /> )} {/* Rental booking modal */} {rentalModal && ( b.objectId === rentalModal.obj.id && b.date === rentalModal.date, )} onClose={() => setRentalModal(null)} onSave={(b) => { onRentalBookingCreate?.(b) setRentalModal(null) }} /> )}
) }