diff --git a/src/components/bookings/BookingDetailPanel.tsx b/src/components/bookings/BookingDetailPanel.tsx
index dd5b132..95a1964 100644
--- a/src/components/bookings/BookingDetailPanel.tsx
+++ b/src/components/bookings/BookingDetailPanel.tsx
@@ -250,6 +250,26 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
const [conflictResolveRoomId, setConflictResolveRoomId] = useState('')
const canEditDates = booking.status === 'confirmed' || booking.status === 'inquiry'
+ const canRelocate = booking.status === 'checked_in'
+
+ const [relocating, setRelocating] = useState(false)
+ const [relocateRoomId, setRelocateRoomId] = useState(booking.roomId)
+
+ const freeRoomsForRelocate = (rooms ?? []).filter(r =>
+ r.id !== booking.roomId &&
+ !(allBookings ?? []).some(b =>
+ b.id !== booking.id &&
+ b.roomId === r.id &&
+ b.status !== 'cancelled' && b.status !== 'no_show' && b.status !== 'checked_out' &&
+ b.checkIn < booking.checkOut && b.checkOut > booking.checkIn,
+ ),
+ )
+
+ const handleRelocate = () => {
+ if (!relocateRoomId || relocateRoomId === booking.roomId) return
+ onUpdate(booking.id, { roomId: relocateRoomId })
+ setRelocating(false)
+ }
const findConflict = (ci: string, co: string, rid: string): Booking | null =>
(allBookings ?? []).find(b =>
@@ -405,13 +425,66 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
{room && (
-
Номер
+
+
Номер
+ {canRelocate && !relocating && (
+
+ )}
+
№{room.number} — {room.type}
Этаж {room.floor} · {room.bedType} bed
+
+ {/* Форма переселения */}
+ {relocating && (
+
+
Переселить гостя в другой номер
+ {freeRoomsForRelocate.length > 0 ? (
+ <>
+
+
+
+
+
+ >
+ ) : (
+
+ Нет свободных номеров на эти даты
+
+
+ )}
+
+ )}
)}
diff --git a/src/components/calendar/BookingCalendar.tsx b/src/components/calendar/BookingCalendar.tsx
index 2b3e51c..135f4da 100644
--- a/src/components/calendar/BookingCalendar.tsx
+++ b/src/components/calendar/BookingCalendar.tsx
@@ -84,6 +84,7 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
// Drag-to-move booking state
const [movingBooking, setMovingBooking] = useState
(null)
const [moveTargetRoomId, setMoveTargetRoomId] = useState(null)
+ const [ghostPos, setGhostPos] = useState<{ x: number; y: number } | null>(null)
const movingBookingRef = useRef(null)
const didDragRef = useRef(false)
movingBookingRef.current = movingBooking
@@ -486,9 +487,11 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
dragging = true
setMovingBooking(booking)
setMoveTargetRoomId(booking.roomId)
+ document.body.style.cursor = 'grabbing'
}
return
}
+ setGhostPos({ x: ev.clientX, y: ev.clientY })
const el = document.elementFromPoint(ev.clientX, ev.clientY)
const row = el?.closest('[data-room-id]') as HTMLElement | null
if (row?.dataset.roomId) setMoveTargetRoomId(row.dataset.roomId)
@@ -496,6 +499,7 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
const onUp = (ev: MouseEvent) => {
window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onUp)
+ document.body.style.cursor = ''
if (dragging) {
const el = document.elementFromPoint(ev.clientX, ev.clientY)
const row = el?.closest('[data-room-id]') as HTMLElement | null
@@ -506,6 +510,7 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
}
setMovingBooking(null)
setMoveTargetRoomId(null)
+ setGhostPos(null)
}
}
window.addEventListener('mousemove', onMove)
@@ -741,6 +746,19 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
/>
)}
+ {/* Drag ghost element */}
+ {movingBooking && ghostPos && (
+
+ {movingBooking.guestName}
+
+ )}
+
{/* Rental booking modal */}
{rentalModal && (