feat: use availability rate overrides in calendar tooltip and booking modal
- Load rate_overrides in CalendarPage, pass as priceOverrides to BookingCalendar - Tooltip shows override price for the hovered date (falls back to baseRate) - BookingModal sums per-night prices from overrides for the stay period - Room baseRate now only used as initial default for availability grid Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,8 @@ const HOURS = Array.from({ length: 24 }, (_, i) => i)
|
|||||||
interface BookingModalProps {
|
interface BookingModalProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
draft: DraftBooking
|
draft: DraftBooking
|
||||||
|
/** categoryId → date (YYYY-MM-DD) → price; used to price bookings by availability rates */
|
||||||
|
priceOverrides?: Record<string, Record<string, number>>
|
||||||
rooms: Room[]
|
rooms: Room[]
|
||||||
bookings?: Booking[]
|
bookings?: Booking[]
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@@ -94,7 +96,7 @@ function getRoomCategories(rooms: Room[], bookings: Booking[], checkIn: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSave, existing, rentalObjects, rentalBookings = [], onRentalSave, slug }: BookingModalProps) {
|
export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSave, existing, rentalObjects, rentalBookings = [], onRentalSave, slug, priceOverrides }: BookingModalProps) {
|
||||||
const { statuses } = useModules()
|
const { statuses } = useModules()
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const tvEnabled = statuses['tv-welcome'] === 'active'
|
const tvEnabled = statuses['tv-welcome'] === 'active'
|
||||||
@@ -329,9 +331,22 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
|||||||
: 0
|
: 0
|
||||||
|
|
||||||
const hourlyHours = Math.max(0, endHour - startHour)
|
const hourlyHours = Math.max(0, endHour - startHour)
|
||||||
|
// Calculate room cost: sum per-night prices from availability overrides, fall back to baseRate
|
||||||
|
const roomNightlyTotal = (() => {
|
||||||
|
if (!room || !form.checkIn || !form.checkOut || nights <= 0) return 0
|
||||||
|
const catId = room.categoryId
|
||||||
|
const catOverrides = catId ? priceOverrides?.[catId] : undefined
|
||||||
|
if (!catOverrides) return (room.baseRate ?? 0) * nights
|
||||||
|
let sum = 0
|
||||||
|
for (let i = 0; i < nights; i++) {
|
||||||
|
const d = format(addDays(new Date(form.checkIn), i), 'yyyy-MM-dd')
|
||||||
|
sum += catOverrides[d] ?? room.baseRate ?? 0
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
})()
|
||||||
const roomBaseTotal = isHourly && room?.allowHourly
|
const roomBaseTotal = isHourly && room?.allowHourly
|
||||||
? (room.hourlyRate ?? 0) * hourlyHours
|
? (room.hourlyRate ?? 0) * hourlyHours
|
||||||
: (room?.baseRate ?? 0) * nights
|
: roomNightlyTotal
|
||||||
const discountAmount = selectedDiscount
|
const discountAmount = selectedDiscount
|
||||||
? selectedDiscount.valueType === 'percent'
|
? selectedDiscount.valueType === 'percent'
|
||||||
? Math.round(roomBaseTotal * Math.min(100, selectedDiscount.value) / 100)
|
? Math.round(roomBaseTotal * Math.min(100, selectedDiscount.value) / 100)
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ interface BookingCalendarProps {
|
|||||||
onDraftStart?: (roomId: string, checkIn: string, checkOut: string) => void
|
onDraftStart?: (roomId: string, checkIn: string, checkOut: string) => void
|
||||||
onDraftCancel?: (roomId: string) => void
|
onDraftCancel?: (roomId: string) => void
|
||||||
wsConnected?: boolean
|
wsConnected?: boolean
|
||||||
|
/** categoryId → date (YYYY-MM-DD) → price */
|
||||||
|
priceOverrides?: Record<string, Record<string, number>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const CATEGORY_ORDER: Record<string, number> = {
|
const CATEGORY_ORDER: Record<string, number> = {
|
||||||
@@ -59,7 +61,7 @@ function getRoomTypeColor(type: string): string {
|
|||||||
return map[type] ?? 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300'
|
return map[type] ?? 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBookingUpdate, onBookingBulkUpdate, onRoomUpdate, fadingBookingIds, rentalObjects, rentalBookings, onRentalBookingCreate, locks = new Map(), onDraftStart, onDraftCancel, wsConnected }: BookingCalendarProps) {
|
export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBookingUpdate, onBookingBulkUpdate, onRoomUpdate, fadingBookingIds, rentalObjects, rentalBookings, onRentalBookingCreate, locks = new Map(), onDraftStart, onDraftCancel, wsConnected, priceOverrides }: BookingCalendarProps) {
|
||||||
const [startDate, setStartDate] = useState(() => startOfDay(new Date()))
|
const [startDate, setStartDate] = useState(() => startOfDay(new Date()))
|
||||||
const [visibleDays, setVisibleDays] = useState(DAYS_VISIBLE)
|
const [visibleDays, setVisibleDays] = useState(DAYS_VISIBLE)
|
||||||
|
|
||||||
@@ -878,6 +880,7 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
|||||||
open={true}
|
open={true}
|
||||||
draft={bookingModalDraft}
|
draft={bookingModalDraft}
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
|
priceOverrides={priceOverrides}
|
||||||
bookings={bookings}
|
bookings={bookings}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
rentalObjects={rentalObjects}
|
rentalObjects={rentalObjects}
|
||||||
@@ -959,6 +962,12 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
|||||||
|
|
||||||
{/* Cell hover price tooltip */}
|
{/* Cell hover price tooltip */}
|
||||||
{hoverTooltip && createPortal(
|
{hoverTooltip && createPortal(
|
||||||
|
(() => {
|
||||||
|
const dateKey = format(hoverTooltip.date, 'yyyy-MM-dd')
|
||||||
|
const catId = hoverTooltip.room.categoryId
|
||||||
|
const overridePrice = catId ? priceOverrides?.[catId]?.[dateKey] : undefined
|
||||||
|
const nightPrice = overridePrice ?? hoverTooltip.room.baseRate
|
||||||
|
return (
|
||||||
<div
|
<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"
|
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 }}
|
style={{ left: hoverTooltip.x + 16, top: hoverTooltip.y + 16 }}
|
||||||
@@ -968,14 +977,16 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-slate-400 dark:text-slate-500 mb-1">Номер {hoverTooltip.room.number}{hoverTooltip.room.name ? ` · ${hoverTooltip.room.name}` : ''}</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">
|
<p className="font-bold text-slate-900 dark:text-slate-100">
|
||||||
{formatCurrency(hoverTooltip.room.baseRate)}<span className="font-normal text-slate-400"> / ночь</span>
|
{formatCurrency(nightPrice)}<span className="font-normal text-slate-400"> / ночь</span>
|
||||||
</p>
|
</p>
|
||||||
{hoverTooltip.room.allowHourly && hoverTooltip.room.hourlyRate ? (
|
{hoverTooltip.room.allowHourly && hoverTooltip.room.hourlyRate ? (
|
||||||
<p className="text-slate-500 dark:text-slate-400 mt-0.5">
|
<p className="text-slate-500 dark:text-slate-400 mt-0.5">
|
||||||
{formatCurrency(hoverTooltip.room.hourlyRate)} / час
|
{formatCurrency(hoverTooltip.room.hourlyRate)} / час
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>,
|
</div>
|
||||||
|
)
|
||||||
|
})(),
|
||||||
document.body,
|
document.body,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,20 +23,29 @@ export function CalendarPage() {
|
|||||||
const [rentalObjects, setRentalObjects] = useState<RentalObjectApi[]>([])
|
const [rentalObjects, setRentalObjects] = useState<RentalObjectApi[]>([])
|
||||||
const [rentalBookings, setRentalBookings] = useState<RentalBookingApi[]>([])
|
const [rentalBookings, setRentalBookings] = useState<RentalBookingApi[]>([])
|
||||||
const [locks, setLocks] = useState<Map<string, BookingLock>>(new Map())
|
const [locks, setLocks] = useState<Map<string, BookingLock>>(new Map())
|
||||||
|
const [priceOverrides, setPriceOverrides] = useState<Record<string, Record<string, number>>>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!slug) return
|
if (!slug) return
|
||||||
const fetches: Promise<unknown>[] = [
|
const fetches: Promise<unknown>[] = [
|
||||||
api.rooms.list(slug),
|
api.rooms.list(slug),
|
||||||
api.bookings.list(slug),
|
api.bookings.list(slug),
|
||||||
|
api.rateOverrides.list(slug).catch(() => []),
|
||||||
]
|
]
|
||||||
if (isRentalActive) {
|
if (isRentalActive) {
|
||||||
fetches.push(api.rental.listObjects(slug))
|
fetches.push(api.rental.listObjects(slug))
|
||||||
fetches.push(api.rental.listBookings(slug))
|
fetches.push(api.rental.listBookings(slug))
|
||||||
}
|
}
|
||||||
Promise.all(fetches).then(([r, b, ro, rb]) => {
|
Promise.all(fetches).then(([r, b, overrides, ro, rb]) => {
|
||||||
setRooms(r as Room[])
|
setRooms(r as Room[])
|
||||||
setBookings(b as Booking[])
|
setBookings(b as Booking[])
|
||||||
|
// Build categoryId → date → price lookup
|
||||||
|
const map: Record<string, Record<string, number>> = {}
|
||||||
|
for (const o of overrides as import('../lib/api').RateOverrideApi[]) {
|
||||||
|
if (!map[o.categoryId]) map[o.categoryId] = {}
|
||||||
|
map[o.categoryId][o.date] = o.price
|
||||||
|
}
|
||||||
|
setPriceOverrides(map)
|
||||||
if (isRentalActive) {
|
if (isRentalActive) {
|
||||||
setRentalObjects(ro as RentalObjectApi[])
|
setRentalObjects(ro as RentalObjectApi[])
|
||||||
setRentalBookings(rb as RentalBookingApi[])
|
setRentalBookings(rb as RentalBookingApi[])
|
||||||
@@ -192,6 +201,7 @@ export function CalendarPage() {
|
|||||||
onDraftStart={handleDraftStart}
|
onDraftStart={handleDraftStart}
|
||||||
onDraftCancel={handleDraftCancel}
|
onDraftCancel={handleDraftCancel}
|
||||||
wsConnected={connected}
|
wsConnected={connected}
|
||||||
|
priceOverrides={priceOverrides}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user