feat: calendar hover price tooltip + AvailabilityPage API integration
- Calendar cells: hold mouse 600ms to show price tooltip for that date/room (baseRate + hourly rate if enabled)
- AvailabilityPage: load real categories from API, derive basePrice from room baseRates, fall back to room types if no categories
- Rate periods: persisted to DB via new /api/hotels/:slug/rate-periods endpoint
- New backend migration 021_rate_periods.sql + rate-periods route
- Added api.ratePeriods.{list,create,update,delete} to frontend API client
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { useState, useRef, useCallback, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import type { ReactNode } from 'react'
|
||||
import { addDays, format, startOfDay, differenceInDays, parseISO, isToday } from 'date-fns'
|
||||
import { ru } from 'date-fns/locale'
|
||||
import { ChevronLeft, ChevronRight, Plus, CalendarDays, ChevronDown, AlignJustify, Clock } from 'lucide-react'
|
||||
import { cn, BOOKING_STATUS_COLORS, BOOKING_STATUS_LABELS, SOURCE_LABELS } from '../../lib/utils'
|
||||
import { cn, BOOKING_STATUS_COLORS, BOOKING_STATUS_LABELS, SOURCE_LABELS, formatCurrency } from '../../lib/utils'
|
||||
import type { Room, Booking, DraftBooking, RoomStatus, HousekeepingStatus } from '../../types'
|
||||
import type { RentalObject, RentalBooking } from '../../data/rentalData'
|
||||
import { BookingModal } from '../bookings/BookingModal'
|
||||
@@ -83,6 +84,22 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
||||
onRoomUpdate?.(roomId, { housekeepingStatus: status })
|
||||
}
|
||||
|
||||
// Cell hover price tooltip
|
||||
const [hoverTooltip, setHoverTooltip] = useState<{ room: Room; date: Date; x: number; y: number } | null>(null)
|
||||
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const handleCellHoverEnter = useCallback((room: Room, date: Date, e: React.MouseEvent) => {
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current)
|
||||
const x = e.clientX
|
||||
const y = e.clientY
|
||||
hoverTimerRef.current = setTimeout(() => setHoverTooltip({ room, date, x, y }), 600)
|
||||
}, [])
|
||||
|
||||
const handleCellHoverLeave = useCallback(() => {
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current)
|
||||
setHoverTooltip(null)
|
||||
}, [])
|
||||
|
||||
// Date picker state
|
||||
const [showNavPicker, setShowNavPicker] = useState(false)
|
||||
const [pickerDateInput, setPickerDateInput] = useState(format(new Date(), 'yyyy-MM-dd'))
|
||||
@@ -544,8 +561,9 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
||||
isTod && !room.status.match(/maintenance|blocked/) && 'bg-brand-50/50 dark:bg-brand-900/10',
|
||||
)}
|
||||
style={{ width: CELL_WIDTH }}
|
||||
onMouseDown={(e) => handleCellMouseDown(room.id, i, e)}
|
||||
onMouseEnter={() => handleCellMouseEnter(i)}
|
||||
onMouseDown={(e) => { handleCellHoverLeave(); handleCellMouseDown(room.id, i, e) }}
|
||||
onMouseEnter={(e) => { handleCellMouseEnter(i); handleCellHoverEnter(room, date, e) }}
|
||||
onMouseLeave={handleCellHoverLeave}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -936,6 +954,28 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
||||
onHkStatusChange={handleCtxHkChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Cell hover price tooltip */}
|
||||
{hoverTooltip && createPortal(
|
||||
<div
|
||||
className="fixed z-[9990] pointer-events-none px-3 py-2 rounded-xl bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shadow-xl text-xs"
|
||||
style={{ left: hoverTooltip.x + 16, top: hoverTooltip.y + 16 }}
|
||||
>
|
||||
<p className="font-semibold text-slate-700 dark:text-slate-200 mb-0.5">
|
||||
{format(hoverTooltip.date, 'd MMMM yyyy', { locale: ru })}
|
||||
</p>
|
||||
<p className="text-slate-400 dark:text-slate-500 mb-1">Номер {hoverTooltip.room.number}{hoverTooltip.room.name ? ` · ${hoverTooltip.room.name}` : ''}</p>
|
||||
<p className="font-bold text-slate-900 dark:text-slate-100">
|
||||
{formatCurrency(hoverTooltip.room.baseRate)}<span className="font-normal text-slate-400"> / ночь</span>
|
||||
</p>
|
||||
{hoverTooltip.room.allowHourly && hoverTooltip.room.hourlyRate ? (
|
||||
<p className="text-slate-500 dark:text-slate-400 mt-0.5">
|
||||
{formatCurrency(hoverTooltip.room.hourlyRate)} / час
|
||||
</p>
|
||||
) : null}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user