feat: hourly pricing + category base prices in booking modal + availability grid
- Add hourly_price column to rate_overrides, base_price/allow_hourly/hourly_base_price to room_categories (migration 033) - Update categories and rate-overrides backend routes to handle new fields - AvailabilityPage: hourly/nightly mode toggle, period prices auto-applied to grid with 'П' indicator, confirmation dialog when overriding period prices - BookingModal: nightly/hourly toggle at top, pricing hierarchy (override → category base → room rate) - RoomCategoriesPage: base_price/allow_hourly/hourly_base_price fields in category form; inline rooms panel - CalendarPage + BookingCalendar: pass hourlyPriceOverrides and categoryBasePrices down to BookingModal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,10 @@ interface BookingModalProps {
|
||||
draft: DraftBooking
|
||||
/** categoryId → date (YYYY-MM-DD) → price; used to price bookings by availability rates */
|
||||
priceOverrides?: Record<string, Record<string, number>>
|
||||
/** categoryId → date (YYYY-MM-DD) → hourlyPrice; per-date hourly rate overrides */
|
||||
hourlyPriceOverrides?: Record<string, Record<string, number>>
|
||||
/** categoryId → { nightlyBase, hourlyBase } from category settings */
|
||||
categoryBasePrices?: Record<string, { nightlyBase: number; hourlyBase: number }>
|
||||
rooms: Room[]
|
||||
bookings?: Booking[]
|
||||
onClose: () => void
|
||||
@@ -96,7 +100,7 @@ function getRoomCategories(rooms: Room[], bookings: Booking[], checkIn: string,
|
||||
})
|
||||
}
|
||||
|
||||
export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSave, existing, rentalObjects, rentalBookings = [], onRentalSave, slug, priceOverrides }: BookingModalProps) {
|
||||
export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSave, existing, rentalObjects, rentalBookings = [], onRentalSave, slug, priceOverrides, hourlyPriceOverrides, categoryBasePrices }: BookingModalProps) {
|
||||
const { statuses } = useModules()
|
||||
const { user } = useAuth()
|
||||
const tvEnabled = statuses['tv-welcome'] === 'active'
|
||||
@@ -331,14 +335,15 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
: 0
|
||||
|
||||
const hourlyHours = Math.max(0, endHour - startHour)
|
||||
// Calculate room cost: sum per-night prices from availability overrides, fall back to baseRate
|
||||
// Calculate room cost: priority = availability override → category base price → room base rate
|
||||
const nightCount = Math.floor(nights)
|
||||
const roomNightlyTotal = (() => {
|
||||
if (!room || !form.checkIn || nightCount <= 0) return 0
|
||||
const catId = room.categoryId
|
||||
const catOverrides = catId ? priceOverrides?.[catId] : undefined
|
||||
const catBase = catId ? categoryBasePrices?.[catId]?.nightlyBase : undefined
|
||||
// room.baseRate is NUMERIC in DB — pg returns it as a string; always coerce
|
||||
const fallback = Number(room.baseRate) || 0
|
||||
const fallback = (catBase && catBase > 0) ? catBase : (Number(room.baseRate) || 0)
|
||||
if (!catOverrides) return fallback * nightCount
|
||||
let sum = 0
|
||||
for (let i = 0; i < nightCount; i++) {
|
||||
@@ -348,8 +353,17 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
}
|
||||
return sum
|
||||
})()
|
||||
const hourlyRateForDate = (() => {
|
||||
if (!room?.allowHourly) return 0
|
||||
const catId = room.categoryId
|
||||
const hourlyOverride = catId ? hourlyPriceOverrides?.[catId]?.[hourlyDate] : undefined
|
||||
if (hourlyOverride && hourlyOverride > 0) return hourlyOverride
|
||||
const catHourlyBase = catId ? categoryBasePrices?.[catId]?.hourlyBase : undefined
|
||||
if (catHourlyBase && catHourlyBase > 0) return catHourlyBase
|
||||
return room.hourlyRate ?? 0
|
||||
})()
|
||||
const roomBaseTotal = isHourly && room?.allowHourly
|
||||
? (room.hourlyRate ?? 0) * hourlyHours
|
||||
? hourlyRateForDate * hourlyHours
|
||||
: roomNightlyTotal
|
||||
const discountAmount = selectedDiscount
|
||||
? selectedDiscount.valueType === 'percent'
|
||||
|
||||
Reference in New Issue
Block a user