From 3602ea7318d624f96df55eaa50790b5313e28e99 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Thu, 19 Mar 2026 13:09:13 +0300 Subject: [PATCH] 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 --- .../bookings/BookingDetailPanel.tsx | 1 + src/components/calendar/BookingCalendar.tsx | 69 +++++++++++++++++-- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/components/bookings/BookingDetailPanel.tsx b/src/components/bookings/BookingDetailPanel.tsx index cf7d72e..c0c37c9 100644 --- a/src/components/bookings/BookingDetailPanel.tsx +++ b/src/components/bookings/BookingDetailPanel.tsx @@ -81,6 +81,7 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, onClose, b.roomId === rid && b.status !== 'cancelled' && b.status !== 'no_show' && + b.status !== 'checked_out' && b.checkIn < co && b.checkOut > ci, ) ?? null diff --git a/src/components/calendar/BookingCalendar.tsx b/src/components/calendar/BookingCalendar.tsx index baf053c..3e867ce 100644 --- a/src/components/calendar/BookingCalendar.tsx +++ b/src/components/calendar/BookingCalendar.tsx @@ -80,6 +80,16 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd const [dragStart, setDragStart] = useState<{ roomId: string; dayIdx: number } | null>(null) const [dragEnd, setDragEnd] = useState(null) + // Drag-to-move booking state + const [movingBooking, setMovingBooking] = useState(null) + const [moveTargetRoomId, setMoveTargetRoomId] = useState(null) + const pendingMoveBooking = useRef(null) + const moveStartPos = useRef<{ x: number; y: number } | null>(null) + const movingBookingRef = useRef(null) + const moveTargetRoomIdRef = useRef(null) + movingBookingRef.current = movingBooking + moveTargetRoomIdRef.current = moveTargetRoomId + // Compact mode (default from Settings → Appearance) const [compact, setCompact] = useState( () => localStorage.getItem('calendarCompact') === 'true', @@ -130,6 +140,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd const handleCellMouseDown = useCallback((roomId: string, dayIdx: number, e: React.MouseEvent) => { if (e.button !== 0) return + if (pendingMoveBooking.current || movingBookingRef.current) return e.preventDefault() setDragStart({ roomId, dayIdx }) setDragEnd(dayIdx) @@ -140,7 +151,40 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd setDragEnd(dayIdx) }, [dragStart]) + const handleMouseMove = useCallback((e: React.MouseEvent) => { + if (!pendingMoveBooking.current || movingBookingRef.current) return + const dx = Math.abs(e.clientX - (moveStartPos.current?.x ?? 0)) + const dy = Math.abs(e.clientY - (moveStartPos.current?.y ?? 0)) + if (dx > 5 || dy > 5) { + setMovingBooking(pendingMoveBooking.current) + setMoveTargetRoomId(pendingMoveBooking.current.roomId) + } + }, []) + const handleMouseUp = useCallback(() => { + const mb = movingBookingRef.current + const targetRoom = moveTargetRoomIdRef.current + + if (mb) { + // Was dragging a booking to another room + if (targetRoom && targetRoom !== mb.roomId) { + onBookingUpdate(mb.id, { roomId: targetRoom }) + } + setMovingBooking(null) + setMoveTargetRoomId(null) + pendingMoveBooking.current = null + moveStartPos.current = null + return + } + + if (pendingMoveBooking.current) { + // Mouse went up without moving — treat as click + setSelectedBooking(pendingMoveBooking.current) + pendingMoveBooking.current = null + moveStartPos.current = null + return + } + if (!dragStart || dragEnd === null) return const minDay = Math.min(dragStart.dayIdx, dragEnd) const maxDay = Math.max(dragStart.dayIdx, dragEnd) @@ -151,7 +195,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd setDragStart(null) setDragEnd(null) setDraft(null) - }, [dragStart, dragEnd, startDate, onDraftStart]) + }, [dragStart, dragEnd, startDate, onDraftStart, onBookingUpdate]) const getLockStyle = (roomId: string) => { const lock = locks.get(roomId) @@ -170,7 +214,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd } return ( -
+
{/* Toolbar */}
@@ -259,7 +303,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd {/* Legend */}
- {(['confirmed', 'checked_in', 'checked_out', 'inquiry', 'cancelled'] as const).map(s => ( + {(['confirmed', 'checked_in', 'checked_out', 'inquiry'] as const).map(s => (
{BOOKING_STATUS_LABELS[s]} @@ -380,13 +424,17 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd ) } // Room row - const roomBookings = bookings.filter(b => b.roomId === room.id) + const roomBookings = bookings.filter(b => b.roomId === room.id && b.status !== 'cancelled') const draftStyle = getDraftStyle(room.id) rows.push(
{ if (movingBookingRef.current) setMoveTargetRoomId(room.id) }} > {/* Room label */}
{ e.stopPropagation(); setSelectedBooking(booking) }} + onMouseDown={(e) => { + if (e.button !== 0) return + e.stopPropagation() + e.preventDefault() + pendingMoveBooking.current = booking + moveStartPos.current = { x: e.clientX, y: e.clientY } + }} title={`${booking.guestName} • ${booking.checkIn}${hourlyMatch ? ` ${hourlyMatch[1]}:00–${hourlyMatch[2]}:00` : ` – ${booking.checkOut}`}${isUnpaid ? ' • Не оплачено' : ''}`} > {isUnpaid && (