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:
2026-03-23 15:05:12 +03:00
parent 63da3d21cc
commit c3a30be74e
6 changed files with 411 additions and 45 deletions

View File

@@ -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>
)
}