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'

View File

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