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'
|
||||
|
||||
@@ -44,6 +44,10 @@ interface BookingCalendarProps {
|
||||
wsConnected?: boolean
|
||||
/** categoryId → date (YYYY-MM-DD) → price */
|
||||
priceOverrides?: Record<string, Record<string, number>>
|
||||
/** categoryId → date (YYYY-MM-DD) → hourly price */
|
||||
hourlyPriceOverrides?: Record<string, Record<string, number>>
|
||||
/** categoryId → { nightlyBase, hourlyBase } */
|
||||
categoryBasePrices?: Record<string, { nightlyBase: number; hourlyBase: number }>
|
||||
}
|
||||
|
||||
const CATEGORY_ORDER: Record<string, number> = {
|
||||
@@ -67,7 +71,7 @@ function getRoomTypeColor(type: string): string {
|
||||
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, onMaintenanceTaskCreate, cleaningAssignees = {}, activeMaintenance = new Set(), fadingBookingIds, rentalObjects, rentalBookings, onRentalBookingCreate, locks = new Map(), onDraftStart, onDraftCancel, wsConnected, priceOverrides }: BookingCalendarProps) {
|
||||
export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBookingUpdate, onBookingBulkUpdate, onRoomUpdate, onMaintenanceTaskCreate, cleaningAssignees = {}, activeMaintenance = new Set(), fadingBookingIds, rentalObjects, rentalBookings, onRentalBookingCreate, locks = new Map(), onDraftStart, onDraftCancel, wsConnected, priceOverrides, hourlyPriceOverrides, categoryBasePrices }: BookingCalendarProps) {
|
||||
const [startDate, setStartDate] = useState(() => startOfDay(new Date()))
|
||||
const [visibleDays, setVisibleDays] = useState(DAYS_VISIBLE)
|
||||
|
||||
@@ -913,6 +917,8 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
||||
draft={bookingModalDraft}
|
||||
rooms={rooms}
|
||||
priceOverrides={priceOverrides}
|
||||
hourlyPriceOverrides={hourlyPriceOverrides}
|
||||
categoryBasePrices={categoryBasePrices}
|
||||
bookings={bookings}
|
||||
slug={slug}
|
||||
rentalObjects={rentalObjects}
|
||||
|
||||
Reference in New Issue
Block a user