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:
2026-03-26 00:05:14 +03:00
parent b3d143f1dd
commit 588ecb0f2a
11 changed files with 232 additions and 38 deletions

View File

@@ -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'