From 15936753b9c76243f7a2993640fefd8eeb70e7a1 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Mon, 23 Mar 2026 16:35:13 +0300 Subject: [PATCH] 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 --- src/components/bookings/BookingModal.tsx | 19 ++++++++- src/components/calendar/BookingCalendar.tsx | 47 +++++++++++++-------- src/pages/CalendarPage.tsx | 12 +++++- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/components/bookings/BookingModal.tsx b/src/components/bookings/BookingModal.tsx index aee0496..0936d30 100644 --- a/src/components/bookings/BookingModal.tsx +++ b/src/components/bookings/BookingModal.tsx @@ -31,6 +31,8 @@ const HOURS = Array.from({ length: 24 }, (_, i) => i) interface BookingModalProps { open: boolean draft: DraftBooking + /** categoryId → date (YYYY-MM-DD) → price; used to price bookings by availability rates */ + priceOverrides?: Record> rooms: Room[] bookings?: Booking[] 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 { user } = useAuth() const tvEnabled = statuses['tv-welcome'] === 'active' @@ -329,9 +331,22 @@ 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 + 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 ? (room.hourlyRate ?? 0) * hourlyHours - : (room?.baseRate ?? 0) * nights + : roomNightlyTotal const discountAmount = selectedDiscount ? selectedDiscount.valueType === 'percent' ? Math.round(roomBaseTotal * Math.min(100, selectedDiscount.value) / 100) diff --git a/src/components/calendar/BookingCalendar.tsx b/src/components/calendar/BookingCalendar.tsx index 6ee6a67..eeef34e 100644 --- a/src/components/calendar/BookingCalendar.tsx +++ b/src/components/calendar/BookingCalendar.tsx @@ -36,6 +36,8 @@ interface BookingCalendarProps { onDraftStart?: (roomId: string, checkIn: string, checkOut: string) => void onDraftCancel?: (roomId: string) => void wsConnected?: boolean + /** categoryId → date (YYYY-MM-DD) → price */ + priceOverrides?: Record> } const CATEGORY_ORDER: Record = { @@ -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' } -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 [visibleDays, setVisibleDays] = useState(DAYS_VISIBLE) @@ -878,6 +880,7 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook open={true} draft={bookingModalDraft} rooms={rooms} + priceOverrides={priceOverrides} bookings={bookings} slug={slug} rentalObjects={rentalObjects} @@ -959,23 +962,31 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook {/* Cell hover price tooltip */} {hoverTooltip && createPortal( -
-

- {format(hoverTooltip.date, 'd MMMM yyyy', { locale: ru })} -

-

Номер {hoverTooltip.room.number}{hoverTooltip.room.name ? ` · ${hoverTooltip.room.name}` : ''}

-

- {formatCurrency(hoverTooltip.room.baseRate)} / ночь -

- {hoverTooltip.room.allowHourly && hoverTooltip.room.hourlyRate ? ( -

- {formatCurrency(hoverTooltip.room.hourlyRate)} / час -

- ) : null} -
, + (() => { + 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 ( +
+

+ {format(hoverTooltip.date, 'd MMMM yyyy', { locale: ru })} +

+

Номер {hoverTooltip.room.number}{hoverTooltip.room.name ? ` · ${hoverTooltip.room.name}` : ''}

+

+ {formatCurrency(nightPrice)} / ночь +

+ {hoverTooltip.room.allowHourly && hoverTooltip.room.hourlyRate ? ( +

+ {formatCurrency(hoverTooltip.room.hourlyRate)} / час +

+ ) : null} +
+ ) + })(), document.body, )} diff --git a/src/pages/CalendarPage.tsx b/src/pages/CalendarPage.tsx index 9da8713..485c65f 100644 --- a/src/pages/CalendarPage.tsx +++ b/src/pages/CalendarPage.tsx @@ -23,20 +23,29 @@ export function CalendarPage() { const [rentalObjects, setRentalObjects] = useState([]) const [rentalBookings, setRentalBookings] = useState([]) const [locks, setLocks] = useState>(new Map()) + const [priceOverrides, setPriceOverrides] = useState>>({}) useEffect(() => { if (!slug) return const fetches: Promise[] = [ api.rooms.list(slug), api.bookings.list(slug), + api.rateOverrides.list(slug).catch(() => []), ] if (isRentalActive) { fetches.push(api.rental.listObjects(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[]) setBookings(b as Booking[]) + // Build categoryId → date → price lookup + const map: Record> = {} + 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) { setRentalObjects(ro as RentalObjectApi[]) setRentalBookings(rb as RentalBookingApi[]) @@ -192,6 +201,7 @@ export function CalendarPage() { onDraftStart={handleDraftStart} onDraftCancel={handleDraftCancel} wsConnected={connected} + priceOverrides={priceOverrides} />