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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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<number | null>(null)
|
||||
|
||||
// Drag-to-move booking state
|
||||
const [movingBooking, setMovingBooking] = useState<Booking | null>(null)
|
||||
const [moveTargetRoomId, setMoveTargetRoomId] = useState<string | null>(null)
|
||||
const pendingMoveBooking = useRef<Booking | null>(null)
|
||||
const moveStartPos = useRef<{ x: number; y: number } | null>(null)
|
||||
const movingBookingRef = useRef<Booking | null>(null)
|
||||
const moveTargetRoomIdRef = useRef<string | null>(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 (
|
||||
<div className="flex flex-col h-full" onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp}>
|
||||
<div className="flex flex-col h-full" onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} onMouseMove={handleMouseMove}>
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center gap-3 px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 shrink-0 flex-wrap gap-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -259,7 +303,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
|
||||
|
||||
{/* Legend */}
|
||||
<div className="hidden lg:flex items-center gap-3 text-xs text-slate-500">
|
||||
{(['confirmed', 'checked_in', 'checked_out', 'inquiry', 'cancelled'] as const).map(s => (
|
||||
{(['confirmed', 'checked_in', 'checked_out', 'inquiry'] as const).map(s => (
|
||||
<div key={s} className="flex items-center gap-1.5">
|
||||
<div className={cn('w-3 h-3 rounded-sm', BOOKING_STATUS_COLORS[s].split(' ')[0])} />
|
||||
<span>{BOOKING_STATUS_LABELS[s]}</span>
|
||||
@@ -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(
|
||||
<div
|
||||
key={room.id}
|
||||
className="flex border-b border-slate-200 dark:border-slate-700 group hover:bg-slate-50/50 dark:hover:bg-slate-800/30"
|
||||
className={cn(
|
||||
'flex border-b border-slate-200 dark:border-slate-700 group hover:bg-slate-50/50 dark:hover:bg-slate-800/30',
|
||||
movingBooking && moveTargetRoomId === room.id && 'bg-brand-50/60 dark:bg-brand-900/20',
|
||||
)}
|
||||
style={{ height: rowHeight }}
|
||||
onMouseEnter={() => { if (movingBookingRef.current) setMoveTargetRoomId(room.id) }}
|
||||
>
|
||||
{/* Room label */}
|
||||
<div
|
||||
@@ -449,6 +497,9 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
|
||||
'booking-block border-l-4 transition-all duration-700',
|
||||
BOOKING_STATUS_COLORS[booking.status],
|
||||
isFading && 'opacity-0 scale-y-0',
|
||||
booking.status === 'checked_out' && 'opacity-50 pointer-events-none',
|
||||
movingBooking?.id === booking.id && 'opacity-40 pointer-events-none',
|
||||
booking.status !== 'checked_out' && !movingBooking && 'cursor-grab',
|
||||
)}
|
||||
style={{
|
||||
left: style.left + 2,
|
||||
@@ -457,7 +508,13 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
|
||||
bottom: compact ? 2 : 4,
|
||||
position: 'absolute',
|
||||
}}
|
||||
onClick={(e) => { 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 && (
|
||||
|
||||
Reference in New Issue
Block a user