feat: BookingsPage rental tab — connect to API (replace mock data)
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import { useState, useMemo, useEffect } from 'react'
|
import { useState, useMemo, useEffect } from 'react'
|
||||||
import { Search, Plus, ArrowUp, ArrowDown, ArrowUpDown, Clock, Pencil, Loader2 } from 'lucide-react'
|
import { Search, Plus, ArrowUp, ArrowDown, ArrowUpDown, Clock, Pencil, Loader2 } from 'lucide-react'
|
||||||
import { RENTAL_OBJECTS, MOCK_RENTAL_BOOKINGS } from '../data/rentalData'
|
|
||||||
import { useModules } from '../contexts/ModulesContext'
|
import { useModules } from '../contexts/ModulesContext'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { api } from '../lib/api'
|
import { api } from '../lib/api'
|
||||||
|
import type { RentalObjectApi, RentalBookingApi } from '../lib/api'
|
||||||
import type { Booking, BookingStatus, Room } from '../types'
|
import type { Booking, BookingStatus, Room } from '../types'
|
||||||
import type { RentalBooking } from '../data/rentalData'
|
import type { RentalObject, RentalBooking } from '../data/rentalData'
|
||||||
import { RentalBookingModal } from '../components/rental/RentalBookingModal'
|
import { RentalBookingModal } from '../components/rental/RentalBookingModal'
|
||||||
import { cn, BOOKING_STATUS_BADGE, BOOKING_STATUS_LABELS, SOURCE_COLORS, SOURCE_LABELS, formatCurrency, nightsCount } from '../lib/utils'
|
import { cn, BOOKING_STATUS_BADGE, BOOKING_STATUS_LABELS, SOURCE_COLORS, SOURCE_LABELS, formatCurrency, nightsCount } from '../lib/utils'
|
||||||
import { Badge } from '../components/ui/Badge'
|
import { Badge } from '../components/ui/Badge'
|
||||||
@@ -37,6 +37,31 @@ const COLUMNS: { key: SortKey | null; label: string }[] = [
|
|||||||
|
|
||||||
type Tab = 'rooms' | 'rental'
|
type Tab = 'rooms' | 'rental'
|
||||||
|
|
||||||
|
function fromObjApi(o: RentalObjectApi): RentalObject {
|
||||||
|
return {
|
||||||
|
id: o.id, name: o.name, icon: o.icon, color: o.color,
|
||||||
|
textColor: o.textColor, pricePerHour: o.pricePerHour, pricePerDay: o.pricePerDay,
|
||||||
|
openHour: o.openHour, closeHour: o.closeHour,
|
||||||
|
maxHoursPerSlot: o.maxHoursPerSlot ?? undefined,
|
||||||
|
bufferMinutes: o.bufferMinutes ?? undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromBookingApi(b: RentalBookingApi): RentalBooking {
|
||||||
|
return {
|
||||||
|
id: b.id, objectId: b.objectId, date: (b.date as string).slice(0, 10),
|
||||||
|
isFullDay: b.isFullDay, startHour: b.startHour, endHour: b.endHour,
|
||||||
|
guestName: b.guestName, guestPhone: b.guestPhone ?? '',
|
||||||
|
linkedRoomId: b.linkedRoomId ?? undefined,
|
||||||
|
totalAmount: b.totalAmount, paidAmount: b.paidAmount,
|
||||||
|
status: b.status, notes: b.notes ?? undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtMin(m: number) {
|
||||||
|
return `${Math.floor(m / 60).toString().padStart(2, '0')}:${(m % 60).toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
export function BookingsPage() {
|
export function BookingsPage() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const slug = user?.hotelSlug ?? ''
|
const slug = user?.hotelSlug ?? ''
|
||||||
@@ -47,11 +72,12 @@ export function BookingsPage() {
|
|||||||
const [rooms, setRooms] = useState<Room[]>([])
|
const [rooms, setRooms] = useState<Room[]>([])
|
||||||
const [bookings, setBookings] = useState<Booking[]>([])
|
const [bookings, setBookings] = useState<Booking[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [rentalBookings, setRentalBookings] = useState<RentalBooking[]>(MOCK_RENTAL_BOOKINGS)
|
const [rentalObjects, setRentalObjects] = useState<RentalObject[]>([])
|
||||||
|
const [rentalBookings, setRentalBookings] = useState<RentalBooking[]>([])
|
||||||
const [newRentalStep, setNewRentalStep] = useState<'idle' | 'pick'>('idle')
|
const [newRentalStep, setNewRentalStep] = useState<'idle' | 'pick'>('idle')
|
||||||
const [rentalPickObj, setRentalPickObj] = useState(RENTAL_OBJECTS[0]?.id ?? '')
|
const [rentalPickObj, setRentalPickObj] = useState('')
|
||||||
const [rentalPickDate, setRentalPickDate] = useState(format(new Date(), 'yyyy-MM-dd'))
|
const [rentalPickDate, setRentalPickDate] = useState(format(new Date(), 'yyyy-MM-dd'))
|
||||||
const [showRentalModal, setShowRentalModal] = useState<{ obj: typeof RENTAL_OBJECTS[0]; date: string; editBooking?: RentalBooking } | null>(null)
|
const [showRentalModal, setShowRentalModal] = useState<{ obj: RentalObject; date: string; editBooking?: RentalBooking } | null>(null)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [statusFilter, setStatusFilter] = useState<BookingStatus | 'all'>('all')
|
const [statusFilter, setStatusFilter] = useState<BookingStatus | 'all'>('all')
|
||||||
const [selected, setSelected] = useState<Booking | null>(null)
|
const [selected, setSelected] = useState<Booking | null>(null)
|
||||||
@@ -61,8 +87,20 @@ export function BookingsPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!slug) return
|
if (!slug) return
|
||||||
Promise.all([api.rooms.list(slug), api.bookings.list(slug)])
|
Promise.all([
|
||||||
.then(([r, b]) => { setRooms(r); setBookings(b) })
|
api.rooms.list(slug),
|
||||||
|
api.bookings.list(slug),
|
||||||
|
api.rental.listObjects(slug),
|
||||||
|
api.rental.listBookings(slug),
|
||||||
|
])
|
||||||
|
.then(([r, b, objs, rbks]) => {
|
||||||
|
setRooms(r)
|
||||||
|
setBookings(b)
|
||||||
|
const convertedObjs = objs.map(fromObjApi)
|
||||||
|
setRentalObjects(convertedObjs)
|
||||||
|
setRentalBookings(rbks.map(fromBookingApi))
|
||||||
|
setRentalPickObj(convertedObjs[0]?.id ?? '')
|
||||||
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}, [slug])
|
}, [slug])
|
||||||
@@ -103,7 +141,7 @@ export function BookingsPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const room = (id: string) => rooms.find(r => r.id === id)
|
const room = (id: string) => rooms.find(r => r.id === id)
|
||||||
const rentalObj = (id: string) => RENTAL_OBJECTS.find(o => o.id === id)
|
const rentalObj = (id: string) => rentalObjects.find(o => o.id === id)
|
||||||
|
|
||||||
const handleCreateBooking = async (data: Partial<Booking>) => {
|
const handleCreateBooking = async (data: Partial<Booking>) => {
|
||||||
try {
|
try {
|
||||||
@@ -384,7 +422,7 @@ export function BookingsPage() {
|
|||||||
<td className="px-4 py-3 text-slate-700 dark:text-slate-300">
|
<td className="px-4 py-3 text-slate-700 dark:text-slate-300">
|
||||||
{b.isFullDay
|
{b.isFullDay
|
||||||
? <span className="text-xs font-medium px-2 py-0.5 bg-slate-100 dark:bg-slate-700 rounded-full">Весь день</span>
|
? <span className="text-xs font-medium px-2 py-0.5 bg-slate-100 dark:bg-slate-700 rounded-full">Весь день</span>
|
||||||
: `${b.startHour}:00 – ${b.endHour}:00`
|
: `${fmtMin(b.startHour)} – ${fmtMin(b.endHour)}`
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 font-semibold text-slate-900 dark:text-slate-100">
|
<td className="px-4 py-3 font-semibold text-slate-900 dark:text-slate-100">
|
||||||
@@ -459,7 +497,7 @@ export function BookingsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Объект</label>
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Объект</label>
|
||||||
<select className="input" value={rentalPickObj} onChange={e => setRentalPickObj(e.target.value)}>
|
<select className="input" value={rentalPickObj} onChange={e => setRentalPickObj(e.target.value)}>
|
||||||
{RENTAL_OBJECTS.map(o => (
|
{rentalObjects.map(o => (
|
||||||
<option key={o.id} value={o.id}>{o.icon} {o.name}</option>
|
<option key={o.id} value={o.id}>{o.icon} {o.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -473,7 +511,7 @@ export function BookingsPage() {
|
|||||||
<button
|
<button
|
||||||
className="btn-primary"
|
className="btn-primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const obj = RENTAL_OBJECTS.find(o => o.id === rentalPickObj)
|
const obj = rentalObjects.find(o => o.id === rentalPickObj)
|
||||||
if (obj) { setShowRentalModal({ obj, date: rentalPickDate }); setNewRentalStep('idle') }
|
if (obj) { setShowRentalModal({ obj, date: rentalPickDate }); setNewRentalStep('idle') }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -492,11 +530,22 @@ export function BookingsPage() {
|
|||||||
existingBookings={rentalBookings.filter(b => b.objectId === showRentalModal.obj.id && b.date === showRentalModal.date)}
|
existingBookings={rentalBookings.filter(b => b.objectId === showRentalModal.obj.id && b.date === showRentalModal.date)}
|
||||||
editBooking={showRentalModal.editBooking}
|
editBooking={showRentalModal.editBooking}
|
||||||
onClose={() => setShowRentalModal(null)}
|
onClose={() => setShowRentalModal(null)}
|
||||||
onSave={(b) => {
|
onSave={async (b) => {
|
||||||
setRentalBookings(prev => {
|
if (!slug) return
|
||||||
const exists = prev.find(x => x.id === b.id)
|
const payload = {
|
||||||
return exists ? prev.map(x => x.id === b.id ? b : x) : [...prev, b]
|
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 || undefined,
|
||||||
|
linked_room_id: b.linkedRoomId || undefined,
|
||||||
|
total_amount: b.totalAmount, paid_amount: b.paidAmount ?? 0,
|
||||||
|
status: b.status, notes: b.notes || undefined,
|
||||||
|
}
|
||||||
|
const isNew = !rentalBookings.find(x => x.id === b.id)
|
||||||
|
const saved = isNew
|
||||||
|
? await api.rental.createBooking(slug, payload)
|
||||||
|
: await api.rental.updateBooking(slug, b.id, payload)
|
||||||
|
const converted = fromBookingApi(saved)
|
||||||
|
setRentalBookings(prev => isNew ? [...prev, converted] : prev.map(x => x.id === b.id ? converted : x))
|
||||||
setShowRentalModal(null)
|
setShowRentalModal(null)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user