import { useState, useRef, useEffect } from 'react' import { X, Clock, CalendarDays, User, Phone, AlertCircle, Banknote, CreditCard, Building2, Link2 } from 'lucide-react' import { cn } from '../../lib/utils' import { api } from '../../lib/api' import type { RentalObject, RentalBooking } from '../../data/rentalData' import type { Booking, Room } from '../../types' interface RentalBookingModalProps { obj: RentalObject date: string existingBookings: RentalBooking[] editBooking?: RentalBooking bookings?: Booking[] // all room bookings (for guest search + link) rooms?: Room[] // room list (for displaying room numbers) slug?: string onClose: () => void onSave: (b: RentalBooking) => void } function hourOptions(from: number, to: number): number[] { return Array.from({ length: to - from + 1 }, (_, i) => from + i) } function formatHour(h: number): string { return `${h}:00` } export function RentalBookingModal({ obj, date, existingBookings, editBooking, bookings = [], rooms = [], slug, onClose, onSave, }: RentalBookingModalProps) { const isEdit = !!editBooking const [isFullDay, setIsFullDay] = useState(editBooking?.isFullDay ?? false) const [startHour, setStartHour] = useState(editBooking?.startHour ?? obj.openHour) const [endHour, setEndHour] = useState(editBooking?.endHour ?? Math.min(obj.openHour + 2, obj.closeHour)) const [guestName, setGuestName] = useState(editBooking?.guestName ?? '') const [guestPhone, setGuestPhone] = useState(editBooking?.guestPhone ?? '') const [notes, setNotes] = useState(editBooking?.notes ?? '') const [paidAmount, setPaidAmount] = useState(editBooking?.paidAmount ? String(editBooking.paidAmount) : '') const [payMethod, setPayMethod] = useState<'cash' | 'card' | 'transfer'>('cash') const [linkedBookingId, setLinkedBookingId] = useState(editBooking?.linkedRoomId ?? '') // true = selected from dropdown (existing guest), false = typed manually (new guest) const [guestIsKnown, setGuestIsKnown] = useState(isEdit) // Guest search const [showSuggestions, setShowSuggestions] = useState(false) const nameRef = useRef(null) // DB guest search const [dbResults, setDbResults] = useState<{ id: string; firstName: string; lastName: string; phone: string | null }[]>([]) const searchTimerRef = useRef | null>(null) const [saving, setSaving] = useState(false) // Bookings with guest info — checked_in first, then confirmed const checkedIn = bookings.filter(b => b.status === 'checked_in') const confirmed = bookings.filter(b => b.status === 'confirmed') const guestSuggestions = [...checkedIn, ...confirmed] const filtered = guestName.trim().length > 0 ? guestSuggestions.filter(b => b.guestName.toLowerCase().includes(guestName.toLowerCase()), ) : guestSuggestions // Room label helper const roomLabel = (b: Booking) => { const r = rooms.find(r => r.id === b.roomId) return r ? `№${r.number}` : '' } // Close suggestions on outside click useEffect(() => { const handler = (e: MouseEvent) => { if (nameRef.current && !nameRef.current.contains(e.target as Node)) { setShowSuggestions(false) } } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, []) const selectGuest = (b: Booking) => { setGuestName(b.guestName) setGuestPhone(b.guestPhone ?? '') setLinkedBookingId(b.id) setShowSuggestions(false) setGuestIsKnown(true) } const otherBookings = existingBookings.filter(b => b.id !== editBooking?.id && b.status !== 'cancelled') const timedBookings = otherBookings.filter(b => !b.isFullDay) const hasFullDayConflict = otherBookings.some(b => b.isFullDay) // If any timed slot is booked, "весь день" is unavailable const hasTimedConflict = timedBookings.length > 0 // Break between sessions (ceil to whole hours) const breakHours = Math.ceil((obj.bufferMinutes ?? 0) / 60) // Available start hours: exclude hours inside any booked slot + its buffer const availableStartHours = hourOptions(obj.openHour, obj.closeHour - 1).filter(h => !timedBookings.some(b => h >= b.startHour && h < b.endHour + breakHours), ) // Max end hour for a given start: limited by the next booking that starts after `start` const getMaxEndHour = (start: number) => { const next = timedBookings .filter(b => b.startHour >= start + 1) .sort((a, b) => a.startHour - b.startHour)[0] return next ? next.startHour : obj.closeHour } const availableEndHours = hourOptions(startHour + 1, getMaxEndHour(startHour)) // If current startHour is blocked, snap to first available const effectiveStartHour = availableStartHours.includes(startHour) ? startHour : (availableStartHours[0] ?? obj.openHour) const hours = isFullDay ? obj.closeHour - obj.openHour : Math.max(0, endHour - effectiveStartHour) const totalAmount = isFullDay ? obj.pricePerDay : hours * obj.pricePerHour const maxHoursOk = !obj.maxHoursPerSlot || hours <= obj.maxHoursPerSlot const isTimeConflict = !isFullDay && timedBookings.some(b => b.startHour < endHour && b.endHour > effectiveStartHour, ) const noSlots = !hasFullDayConflict && availableStartHours.length === 0 // For new (unknown) guests, phone is required const phoneRequired = !guestIsKnown const canSave = guestName.trim() !== '' && (!phoneRequired || guestPhone.trim() !== '') && hours > 0 && maxHoursOk && !isTimeConflict && !hasFullDayConflict && !noSlots && (!isFullDay || (!hasFullDayConflict && !hasTimedConflict)) const handleSave = async () => { if (!canSave || saving) return setSaving(true) try { // If guest is new (typed manually), create them in the guests DB if (!guestIsKnown && slug) { const nameParts = guestName.trim().split(' ') await api.guests.create(slug, { last_name: nameParts[0] ?? '', first_name: nameParts[1] ?? '', middle_name: nameParts.slice(2).join(' ') || undefined, phone: guestPhone.trim() || undefined, }) } const linked = bookings.find(b => b.id === linkedBookingId) onSave({ id: editBooking?.id ?? `rb-${Date.now()}`, objectId: obj.id, date, isFullDay, startHour: isFullDay ? obj.openHour : startHour, endHour: isFullDay ? obj.closeHour : endHour, guestName: guestName.trim(), guestPhone: guestPhone.trim(), linkedRoomId: (linked?.roomId ?? linkedBookingId) || undefined, notes: notes.trim() || undefined, totalAmount, paidAmount: parseFloat(paidAmount) || 0, status: 'confirmed', }) } finally { setSaving(false) } } const displayDate = new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long', weekday: 'short', }).format(new Date(date)) // For link dropdown — show checked-in first, label includes room number const linkableBookings = [...checkedIn, ...confirmed.filter(b => !checkedIn.includes(b))] return (
{/* Header */}
{obj.icon}

{obj.name}

{displayDate}

{/* Existing bookings for the day */} {existingBookings.filter(b => b.status !== 'cancelled').length > 0 && (

Уже забронировано

{existingBookings.filter(b => b.status !== 'cancelled').map(b => (
{b.isFullDay ? 'Весь день' : `${b.startHour}:00 – ${b.endHour}:00`} {b.guestName}
))}
)} {hasFullDayConflict && (
Объект уже забронирован на весь день
)} {/* Full day toggle */}
Весь день {hasTimedConflict && ( Есть частичные брони )}
{/* Time selection */} {!isFullDay && !hasFullDayConflict && (
{noSlots ? (
Все слоты на этот день заняты
) : ( <>
{obj.maxHoursPerSlot && !maxHoursOk && (

Максимум: {obj.maxHoursPerSlot} ч

)} {isTimeConflict && (

Время пересекается с существующей бронью

)} )}
)} {/* Guest search */}
{ const val = e.target.value setGuestName(val) setGuestIsKnown(false) // manual typing = new guest setShowSuggestions(true) if (searchTimerRef.current) clearTimeout(searchTimerRef.current) if (val.trim().length >= 2 && slug) { searchTimerRef.current = setTimeout(async () => { try { const results = await api.guests.list(slug, val.trim()) setDbResults(results.slice(0, 8)) } catch { setDbResults([]) } }, 300) } else { setDbResults([]) } }} onFocus={() => setShowSuggestions(true)} autoComplete="off" /> {/* Suggestions dropdown */} {showSuggestions && (filtered.length > 0 || dbResults.length > 0) && (
{/* Current/upcoming guests from bookings */} {filtered.slice(0, 5).map(b => { const rLabel = roomLabel(b) const isCheckedIn = b.status === 'checked_in' return ( ) })} {/* DB guest search results */} {dbResults .filter(g => !filtered.some(b => b.guestName === `${g.lastName} ${g.firstName}`)) .map(g => ( )) }
)}
setGuestPhone(e.target.value)} />
{phoneRequired && !guestPhone.trim() && (

Для нового гостя необходим телефон — он будет добавлен в базу

)}
{/* Link to room */}