Rental: connect to API, improve cell UI; remove price from room labels

Backend:
- Migration 015_rental.sql: rental_objects + rental_bookings tables, seed demo data
- New routes: GET/POST/PATCH/DELETE rental-objects and rental-bookings per hotel
- Register rentalRoutes in app.ts

Frontend:
- api.ts: add rental.listObjects, listBookings, createBooking, updateBooking, deleteBooking
- CalendarPage: fetch rental objects + bookings from API instead of mock data
- BookingCalendar: rental cell UI redesigned — colored squares + working hours + time slots (matches design spec)
- BookingCalendar: remove base rate price from desktop room label column (irrelevant with dynamic pricing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 15:46:01 +03:00
parent 3f4d0dac4c
commit b48e50e62b
6 changed files with 396 additions and 43 deletions

View File

@@ -1,9 +1,9 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { BookingCalendar } from '../components/calendar/BookingCalendar'
import { RENTAL_OBJECTS, MOCK_RENTAL_BOOKINGS } from '../data/rentalData'
import { useModules } from '../contexts/ModulesContext'
import { useAuth } from '../contexts/AuthContext'
import { api } from '../lib/api'
import type { RentalObjectApi, RentalBookingApi } from '../lib/api'
import { useHotelSocket } from '../hooks/useHotelSocket'
import type { WsMessage } from '../hooks/useHotelSocket'
import type { Room, Booking } from '../types'
@@ -20,19 +20,29 @@ export function CalendarPage() {
const [rooms, setRooms] = useState<Room[]>([])
const [bookings, setBookings] = useState<Booking[]>([])
const [fadingBookings, setFadingBookings] = useState<Set<string>>(new Set())
const [rentalBookings, setRentalBookings] = useState<RentalBooking[]>(MOCK_RENTAL_BOOKINGS)
const [rentalObjects, setRentalObjects] = useState<RentalObjectApi[]>([])
const [rentalBookings, setRentalBookings] = useState<RentalBookingApi[]>([])
const [locks, setLocks] = useState<Map<string, BookingLock>>(new Map())
useEffect(() => {
if (!slug) return
Promise.all([
const fetches: Promise<unknown>[] = [
api.rooms.list(slug),
api.bookings.list(slug),
]).then(([r, b]) => {
setRooms(r)
setBookings(b)
]
if (isRentalActive) {
fetches.push(api.rental.listObjects(slug))
fetches.push(api.rental.listBookings(slug))
}
Promise.all(fetches).then(([r, b, ro, rb]) => {
setRooms(r as Room[])
setBookings(b as Booking[])
if (isRentalActive) {
setRentalObjects(ro as RentalObjectApi[])
setRentalBookings(rb as RentalBookingApi[])
}
}).catch(console.error)
}, [slug])
}, [slug, isRentalActive])
const handleWsMessage = useCallback((msg: WsMessage) => {
if (msg.type === 'lock') {
@@ -130,6 +140,28 @@ export function CalendarPage() {
send({ type: 'unlock', roomId })
}, [send])
const handleRentalBookingCreate = useCallback(async (b: RentalBooking) => {
try {
const created = await api.rental.createBooking(slug, {
object_id: b.objectId,
date: b.date,
is_full_day: b.isFullDay,
start_hour: b.startHour,
end_hour: b.endHour,
guest_name: b.guestName,
guest_phone: b.guestPhone ?? '',
linked_room_id: b.linkedRoomId,
total_amount: b.totalAmount,
paid_amount: b.paidAmount ?? 0,
status: b.status,
notes: b.notes,
})
setRentalBookings(prev => [...prev, created])
} catch (err) {
console.error('Failed to create rental booking:', err)
}
}, [slug])
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-hidden">
@@ -141,9 +173,9 @@ export function CalendarPage() {
onBookingUpdate={handleUpdate}
onBookingBulkUpdate={handleBulkUpdate}
fadingBookingIds={fadingBookings}
rentalObjects={isRentalActive ? RENTAL_OBJECTS : undefined}
rentalBookings={isRentalActive ? rentalBookings : undefined}
onRentalBookingCreate={isRentalActive ? (b: RentalBooking) => setRentalBookings(prev => [...prev, b]) : undefined}
rentalObjects={isRentalActive ? rentalObjects as unknown as import('../data/rentalData').RentalObject[] : undefined}
rentalBookings={isRentalActive ? rentalBookings as unknown as RentalBooking[] : undefined}
onRentalBookingCreate={isRentalActive ? handleRentalBookingCreate : undefined}
locks={locks}
onDraftStart={handleDraftStart}
onDraftCancel={handleDraftCancel}