Calendar: add drag ghost element + grabbing cursor; BookingDetail: add Relocate room for checked_in guests
This commit is contained in:
@@ -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
|
||||
<div className="space-y-4">
|
||||
{room && (
|
||||
<div className="rounded-xl bg-slate-50 dark:bg-slate-700/40 p-3">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium mb-1">Номер</p>
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium">Номер</p>
|
||||
{canRelocate && !relocating && (
|
||||
<button
|
||||
onClick={() => { setRelocating(true); setRelocateRoomId('') }}
|
||||
className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300 font-medium"
|
||||
>
|
||||
<Pencil size={11} /> Переселить
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="font-semibold text-slate-900 dark:text-slate-100">
|
||||
№{room.number} — {room.type}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Этаж {room.floor} · {room.bedType} bed
|
||||
</p>
|
||||
|
||||
{/* Форма переселения */}
|
||||
{relocating && (
|
||||
<div className="mt-3 space-y-2 pt-3 border-t border-slate-200 dark:border-slate-600">
|
||||
<p className="text-xs font-medium text-amber-700 dark:text-amber-400">Переселить гостя в другой номер</p>
|
||||
{freeRoomsForRelocate.length > 0 ? (
|
||||
<>
|
||||
<select
|
||||
className="input text-sm"
|
||||
value={relocateRoomId}
|
||||
onChange={e => setRelocateRoomId(e.target.value)}
|
||||
>
|
||||
<option value="">— выберите номер —</option>
|
||||
{freeRoomsForRelocate.map(r => (
|
||||
<option key={r.id} value={r.id}>
|
||||
№{r.number} — {r.type} ({r.baseRate.toLocaleString('ru-RU')} ₽/ночь)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleRelocate}
|
||||
disabled={!relocateRoomId}
|
||||
className="flex-1 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-600 disabled:opacity-50 text-white text-xs font-semibold transition-colors"
|
||||
>
|
||||
Переселить
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setRelocating(false)}
|
||||
className="px-3 py-1.5 rounded-lg border border-slate-200 dark:border-slate-600 text-xs text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Нет свободных номеров на эти даты
|
||||
<button onClick={() => setRelocating(false)} className="ml-2 text-brand-600 dark:text-brand-400 underline">Закрыть</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
||||
// Drag-to-move booking state
|
||||
const [movingBooking, setMovingBooking] = useState<Booking | null>(null)
|
||||
const [moveTargetRoomId, setMoveTargetRoomId] = useState<string | null>(null)
|
||||
const [ghostPos, setGhostPos] = useState<{ x: number; y: number } | null>(null)
|
||||
const movingBookingRef = useRef<Booking | null>(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 && (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed z-[9999] pointer-events-none px-3 py-1.5 rounded-lg text-white text-xs font-semibold shadow-xl border-2 border-white/40',
|
||||
BOOKING_STATUS_COLORS[movingBooking.status],
|
||||
)}
|
||||
style={{ left: ghostPos.x + 14, top: ghostPos.y - 16 }}
|
||||
>
|
||||
{movingBooking.guestName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rental booking modal */}
|
||||
{rentalModal && (
|
||||
<RentalBookingModal
|
||||
|
||||
Reference in New Issue
Block a user