feat: connect RentalPage to API (objects + bookings persisted in DB)
- Add createObject/updateObject/deleteObject to api.rental - Add RentalObjectPayload interface - RentalPage now loads objects and bookings from API on mount - Save/delete object operations call the API instead of only local state - Save booking operations call the API (create or update) - bufferMinutes from DB is now correctly passed to booking modal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -317,6 +317,15 @@ export const api = {
|
|||||||
listObjects: (slug: string) =>
|
listObjects: (slug: string) =>
|
||||||
req<RentalObjectApi[]>('GET', `/api/hotels/${slug}/rental-objects`),
|
req<RentalObjectApi[]>('GET', `/api/hotels/${slug}/rental-objects`),
|
||||||
|
|
||||||
|
createObject: (slug: string, data: RentalObjectPayload) =>
|
||||||
|
req<RentalObjectApi>('POST', `/api/hotels/${slug}/rental-objects`, data),
|
||||||
|
|
||||||
|
updateObject: (slug: string, id: string, data: Partial<RentalObjectPayload>) =>
|
||||||
|
req<RentalObjectApi>('PATCH', `/api/hotels/${slug}/rental-objects/${id}`, data),
|
||||||
|
|
||||||
|
deleteObject: (slug: string, id: string) =>
|
||||||
|
req<void>('DELETE', `/api/hotels/${slug}/rental-objects/${id}`),
|
||||||
|
|
||||||
listBookings: (slug: string, from?: string, to?: string) => {
|
listBookings: (slug: string, from?: string, to?: string) => {
|
||||||
const qs = new URLSearchParams()
|
const qs = new URLSearchParams()
|
||||||
if (from) qs.set('from', from)
|
if (from) qs.set('from', from)
|
||||||
@@ -591,6 +600,20 @@ export interface RentalBookingPayload {
|
|||||||
notes?: string
|
notes?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RentalObjectPayload {
|
||||||
|
name: string
|
||||||
|
icon?: string
|
||||||
|
color?: string
|
||||||
|
text_color?: string
|
||||||
|
price_per_hour?: number
|
||||||
|
price_per_day?: number
|
||||||
|
open_hour?: number
|
||||||
|
close_hour?: number
|
||||||
|
max_hours_per_slot?: number | null
|
||||||
|
buffer_minutes?: number
|
||||||
|
sort_order?: number
|
||||||
|
}
|
||||||
|
|
||||||
function toHotelPayload(h: HotelPayload): Record<string, unknown> {
|
function toHotelPayload(h: HotelPayload): Record<string, unknown> {
|
||||||
const out: Record<string, unknown> = {}
|
const out: Record<string, unknown> = {}
|
||||||
if (h.name !== undefined) out.name = h.name
|
if (h.name !== undefined) out.name = h.name
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Plus, Pencil, Trash2, Clock, CalendarDays, X, Save } from 'lucide-react'
|
import { Plus, Pencil, Trash2, CalendarDays, X, Save } from 'lucide-react'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { ru } from 'date-fns/locale'
|
import { ru } from 'date-fns/locale'
|
||||||
import { cn, formatCurrency } from '../lib/utils'
|
import { cn, formatCurrency } from '../lib/utils'
|
||||||
import { Badge } from '../components/ui/Badge'
|
import { Badge } from '../components/ui/Badge'
|
||||||
import { RENTAL_OBJECTS, MOCK_RENTAL_BOOKINGS } from '../data/rentalData'
|
|
||||||
import type { RentalObject, RentalBooking } from '../data/rentalData'
|
import type { RentalObject, RentalBooking } from '../data/rentalData'
|
||||||
import { RentalBookingModal } from '../components/rental/RentalBookingModal'
|
import { RentalBookingModal } from '../components/rental/RentalBookingModal'
|
||||||
|
import { api } from '../lib/api'
|
||||||
|
import type { RentalObjectApi, RentalBookingApi } from '../lib/api'
|
||||||
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
|
||||||
// ── Object form ───────────────────────────────────────────────────────────────
|
// ── Object form ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -257,9 +259,34 @@ function ObjectFormModal({
|
|||||||
|
|
||||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Convert API response to local type
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function RentalPage() {
|
export function RentalPage() {
|
||||||
const [objects, setObjects] = useState<RentalObject[]>(RENTAL_OBJECTS)
|
const { user } = useAuth()
|
||||||
const [bookings, setBookings] = useState<RentalBooking[]>(MOCK_RENTAL_BOOKINGS)
|
const slug = user?.hotelSlug ?? ''
|
||||||
|
|
||||||
|
const [objects, setObjects] = useState<RentalObject[]>([])
|
||||||
|
const [bookings, setBookings] = useState<RentalBooking[]>([])
|
||||||
const [editObj, setEditObj] = useState<RentalObject | null>(null)
|
const [editObj, setEditObj] = useState<RentalObject | null>(null)
|
||||||
const [createModal, setCreateModal] = useState(false)
|
const [createModal, setCreateModal] = useState(false)
|
||||||
const [selectedObj, setSelectedObj] = useState<RentalObject | null>(null)
|
const [selectedObj, setSelectedObj] = useState<RentalObject | null>(null)
|
||||||
@@ -267,25 +294,62 @@ export function RentalPage() {
|
|||||||
|
|
||||||
const today = format(new Date(), 'yyyy-MM-dd')
|
const today = format(new Date(), 'yyyy-MM-dd')
|
||||||
|
|
||||||
const handleSaveObject = (obj: RentalObject) => {
|
// Load from API on mount
|
||||||
setObjects(prev => {
|
useEffect(() => {
|
||||||
const exists = prev.find(o => o.id === obj.id)
|
if (!slug) return
|
||||||
return exists ? prev.map(o => o.id === obj.id ? obj : o) : [...prev, obj]
|
Promise.all([
|
||||||
})
|
api.rental.listObjects(slug),
|
||||||
|
api.rental.listBookings(slug),
|
||||||
|
]).then(([objs, bks]) => {
|
||||||
|
setObjects(objs.map(fromObjApi))
|
||||||
|
setBookings(bks.map(fromBookingApi))
|
||||||
|
}).catch(console.error)
|
||||||
|
}, [slug])
|
||||||
|
|
||||||
|
const handleSaveObject = async (obj: RentalObject) => {
|
||||||
|
if (!slug) return
|
||||||
|
const payload = {
|
||||||
|
name: obj.name, icon: obj.icon, color: obj.color, text_color: obj.textColor,
|
||||||
|
price_per_hour: obj.pricePerHour, price_per_day: obj.pricePerDay,
|
||||||
|
open_hour: obj.openHour, close_hour: obj.closeHour,
|
||||||
|
max_hours_per_slot: obj.maxHoursPerSlot ?? null,
|
||||||
|
buffer_minutes: obj.bufferMinutes ?? 0,
|
||||||
|
}
|
||||||
|
const isNew = !objects.find(o => o.id === obj.id)
|
||||||
|
const saved = isNew
|
||||||
|
? await api.rental.createObject(slug, payload)
|
||||||
|
: await api.rental.updateObject(slug, obj.id, payload)
|
||||||
|
const converted = fromObjApi(saved)
|
||||||
|
setObjects(prev => isNew ? [...prev, converted] : prev.map(o => o.id === obj.id ? converted : o))
|
||||||
|
// Update selectedObj if it was the edited one
|
||||||
|
setSelectedObj(prev => prev?.id === obj.id ? converted : prev)
|
||||||
setEditObj(null)
|
setEditObj(null)
|
||||||
setCreateModal(false)
|
setCreateModal(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteObject = (id: string) => {
|
const handleDeleteObject = async (id: string) => {
|
||||||
|
if (!slug) return
|
||||||
|
await api.rental.deleteObject(slug, id)
|
||||||
setObjects(prev => prev.filter(o => o.id !== id))
|
setObjects(prev => prev.filter(o => o.id !== id))
|
||||||
if (selectedObj?.id === id) setSelectedObj(null)
|
if (selectedObj?.id === id) setSelectedObj(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBookingSave = (b: RentalBooking) => {
|
const handleBookingSave = async (b: RentalBooking) => {
|
||||||
setBookings(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 = !bookings.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)
|
||||||
|
setBookings(prev => isNew ? [...prev, converted] : prev.map(x => x.id === b.id ? converted : x))
|
||||||
setRentalModal(null)
|
setRentalModal(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user