diff --git a/src/components/bookings/BookingDetailPanel.tsx b/src/components/bookings/BookingDetailPanel.tsx index 95a1964..da2a249 100644 --- a/src/components/bookings/BookingDetailPanel.tsx +++ b/src/components/bookings/BookingDetailPanel.tsx @@ -1,9 +1,10 @@ import { useState, useEffect, useRef } from 'react' +import { format, differenceInDays } from 'date-fns' import { X, Mail, Calendar, Users, CreditCard, Tag, CheckCircle, XCircle, Printer, ScanLine, Banknote, Building2, Plus, Pencil, FileText, FileCheck, Receipt, IdCard, AlertTriangle, - Trash2, Loader2, UserCheck, Baby, Search, + Trash2, Loader2, UserCheck, Baby, Search, LogIn, } from 'lucide-react' import { cn, BOOKING_STATUS_BADGE, BOOKING_STATUS_LABELS, @@ -252,9 +253,17 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on const canEditDates = booking.status === 'confirmed' || booking.status === 'inquiry' const canRelocate = booking.status === 'checked_in' + const today = format(new Date(), 'yyyy-MM-dd') + const canEarlyCheckIn = booking.status === 'confirmed' && booking.checkIn > today + const [relocating, setRelocating] = useState(false) const [relocateRoomId, setRelocateRoomId] = useState(booking.roomId) + const [earlyOpen, setEarlyOpen] = useState(false) + const [earlyNewCheckOut, setEarlyNewCheckOut] = useState(booking.checkOut) + const [earlyRoomId, setEarlyRoomId] = useState(booking.roomId) + const [earlySaving, setEarlySaving] = useState(false) + const freeRoomsForRelocate = (rooms ?? []).filter(r => r.id !== booking.roomId && !(allBookings ?? []).some(b => @@ -271,6 +280,23 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on setRelocating(false) } + const handleEarlyCheckIn = async () => { + if (earlyNewCheckOut <= today) { showToast('Дата выезда должна быть позже сегодня'); return } + setEarlySaving(true) + try { + await onUpdate(booking.id, { + checkIn: today, + checkOut: earlyNewCheckOut, + roomId: earlyRoomId, + status: 'checked_in', + }) + setEarlyOpen(false) + showToast('Гость заселён досрочно') + } finally { + setEarlySaving(false) + } + } + const findConflict = (ci: string, co: string, rid: string): Booking | null => (allBookings ?? []).find(b => b.id !== booking.id && @@ -488,6 +514,132 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on )} + {/* Early check-in banner */} + {canEarlyCheckIn && !earlyOpen && ( +
+
+
+ +
+

Гость приехал раньше

+

+ Бронь начинается {fmtDate(booking.checkIn)}, но можно заселить сейчас +

+
+
+ +
+
+ )} + + {/* Early check-in form */} + {canEarlyCheckIn && earlyOpen && (() => { + const currentRoomFree = !((allBookings ?? []).some(b => + b.id !== booking.id && b.roomId === booking.roomId && + b.status !== 'cancelled' && b.status !== 'no_show' && b.status !== 'checked_out' && + b.checkIn < earlyNewCheckOut && b.checkOut > today, + )) + const availableRooms = (rooms ?? []).filter(r => + !((allBookings ?? []).some(b => + b.id !== booking.id && b.roomId === r.id && + b.status !== 'cancelled' && b.status !== 'no_show' && b.status !== 'checked_out' && + b.checkIn < earlyNewCheckOut && b.checkOut > today, + )), + ) + const extraNights = differenceInDays(new Date(booking.checkIn), new Date(today)) + const selectedRoom = (rooms ?? []).find(r => r.id === earlyRoomId) + const origRoom = (rooms ?? []).find(r => r.id === booking.roomId) + const priceDiff = selectedRoom && origRoom ? selectedRoom.baseRate - origRoom.baseRate : 0 + + return ( +
+
+ +

Ранний заезд

+
+ + {/* Room selector */} + {currentRoomFree ? ( +
+ Текущий номер свободен — можно заселить без переноса +
+ ) : ( +
+

+ Текущий номер занят. Выберите свободный: +

+ {availableRooms.length > 0 ? ( + + ) : ( +

Нет свободных номеров

+ )} +
+ )} + + {/* Checkout date */} +
+ + setEarlyNewCheckOut(e.target.value)} + /> +
+ + {/* Extra nights info */} + {earlyNewCheckOut > today && ( +
+

Заезд сегодня → доп. {extraNights} {extraNights === 1 ? 'ночь' : extraNights < 5 ? 'ночи' : 'ночей'}

+ {priceDiff !== 0 && selectedRoom && ( +

0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}> + Разница в стоимости: {priceDiff > 0 ? '+' : ''}{priceDiff.toLocaleString('ru-RU')} ₽/ночь +

+ )} +
+ )} + +
+ + +
+
+ ) + })()} + {/* Dates — read or edit mode */}