Fix: rental booking save (date comparison), add DB guest search in rental modal
- BookingCalendar: normalize date comparison with .slice(0,10) to fix PostgreSQL DATE ISO format mismatch - CalendarPage: show alert on rental booking save failure - RentalBookingModal: add debounced DB guest search (api.guests.list) with 300ms delay - RentalBookingModal: combine booking suggestions + DB results in unified dropdown - Pass slug prop to RentalBookingModal for API access Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -669,7 +669,7 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
||||
const isWe = date.getDay() === 0 || date.getDay() === 6
|
||||
const isTod = isToday(date)
|
||||
const dayBookings = (rentalBookings ?? []).filter(
|
||||
b => b.objectId === obj.id && b.date === dateStr && b.status !== 'cancelled',
|
||||
b => b.objectId === obj.id && (b.date as string).slice(0, 10) === dateStr && b.status !== 'cancelled',
|
||||
)
|
||||
const hasFullDay = dayBookings.some(b => b.isFullDay)
|
||||
|
||||
@@ -791,10 +791,11 @@ export function BookingCalendar({ rooms, bookings, slug, onBookingCreate, onBook
|
||||
obj={rentalModal.obj}
|
||||
date={rentalModal.date}
|
||||
existingBookings={(rentalBookings ?? []).filter(
|
||||
b => b.objectId === rentalModal.obj.id && b.date === rentalModal.date,
|
||||
b => b.objectId === rentalModal.obj.id && (b.date as string).slice(0, 10) === rentalModal.date,
|
||||
)}
|
||||
bookings={bookings}
|
||||
rooms={rooms}
|
||||
slug={slug}
|
||||
onClose={() => setRentalModal(null)}
|
||||
onSave={(b) => {
|
||||
onRentalBookingCreate?.(b)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { X, Clock, CalendarDays, User, Phone, AlertCircle, Banknote, CreditCard, Building2, Link2 } from 'lucide-react'
|
||||
import { cn } from '../../lib/utils'
|
||||
import { api } from '../../lib/api'
|
||||
import type { RentalObject, RentalBooking } from '../../data/rentalData'
|
||||
import type { Booking, Room } from '../../types'
|
||||
|
||||
@@ -11,6 +12,7 @@ interface RentalBookingModalProps {
|
||||
editBooking?: RentalBooking
|
||||
bookings?: Booking[] // all room bookings (for guest search + link)
|
||||
rooms?: Room[] // room list (for displaying room numbers)
|
||||
slug?: string
|
||||
onClose: () => void
|
||||
onSave: (b: RentalBooking) => void
|
||||
}
|
||||
@@ -24,7 +26,7 @@ function formatHour(h: number): string {
|
||||
}
|
||||
|
||||
export function RentalBookingModal({
|
||||
obj, date, existingBookings, editBooking, bookings = [], rooms = [], onClose, onSave,
|
||||
obj, date, existingBookings, editBooking, bookings = [], rooms = [], slug, onClose, onSave,
|
||||
}: RentalBookingModalProps) {
|
||||
const isEdit = !!editBooking
|
||||
const [isFullDay, setIsFullDay] = useState(editBooking?.isFullDay ?? false)
|
||||
@@ -41,6 +43,10 @@ export function RentalBookingModal({
|
||||
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||
const nameRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// DB guest search
|
||||
const [dbResults, setDbResults] = useState<{ id: string; firstName: string; lastName: string; phone: string | null }[]>([])
|
||||
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
// Bookings with guest info — checked_in first, then confirmed
|
||||
const checkedIn = bookings.filter(b => b.status === 'checked_in')
|
||||
const confirmed = bookings.filter(b => b.status === 'confirmed')
|
||||
@@ -234,14 +240,30 @@ export function RentalBookingModal({
|
||||
className="input pl-8 text-sm"
|
||||
placeholder="Имя гостя"
|
||||
value={guestName}
|
||||
onChange={e => { setGuestName(e.target.value); setShowSuggestions(true) }}
|
||||
onChange={e => {
|
||||
const val = e.target.value
|
||||
setGuestName(val)
|
||||
setShowSuggestions(true)
|
||||
if (searchTimerRef.current) clearTimeout(searchTimerRef.current)
|
||||
if (val.trim().length >= 2 && slug) {
|
||||
searchTimerRef.current = setTimeout(async () => {
|
||||
try {
|
||||
const results = await api.guests.list(slug, val.trim())
|
||||
setDbResults(results.slice(0, 8))
|
||||
} catch { setDbResults([]) }
|
||||
}, 300)
|
||||
} else {
|
||||
setDbResults([])
|
||||
}
|
||||
}}
|
||||
onFocus={() => setShowSuggestions(true)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{/* Suggestions dropdown */}
|
||||
{showSuggestions && filtered.length > 0 && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 z-50 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-600 shadow-xl overflow-hidden max-h-52 overflow-y-auto">
|
||||
{filtered.slice(0, 10).map(b => {
|
||||
{showSuggestions && (filtered.length > 0 || dbResults.length > 0) && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 z-50 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-600 shadow-xl overflow-hidden max-h-60 overflow-y-auto">
|
||||
{/* Current/upcoming guests from bookings */}
|
||||
{filtered.slice(0, 5).map(b => {
|
||||
const rLabel = roomLabel(b)
|
||||
const isCheckedIn = b.status === 'checked_in'
|
||||
return (
|
||||
@@ -251,20 +273,41 @@ export function RentalBookingModal({
|
||||
onMouseDown={() => selectGuest(b)}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2.5 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors text-left"
|
||||
>
|
||||
<div className={cn(
|
||||
'w-2 h-2 rounded-full shrink-0',
|
||||
isCheckedIn ? 'bg-emerald-500' : 'bg-sky-400',
|
||||
)} />
|
||||
<div className={cn('w-2 h-2 rounded-full shrink-0', isCheckedIn ? 'bg-emerald-500' : 'bg-sky-400')} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-slate-800 dark:text-slate-200 truncate">{b.guestName}</p>
|
||||
<p className="text-[11px] text-slate-400 truncate">
|
||||
{isCheckedIn ? '🏠 Проживает' : '📅 Бронь'}{rLabel ? ` · ${rLabel}` : ''}
|
||||
{b.guestPhone ? ` · ${b.guestPhone}` : ''}
|
||||
{isCheckedIn ? '🏠 Проживает' : '📅 Бронь'}{rLabel ? ` · ${rLabel}` : ''}{b.guestPhone ? ` · ${b.guestPhone}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{/* DB guest search results */}
|
||||
{dbResults
|
||||
.filter(g => !filtered.some(b => b.guestName === `${g.lastName} ${g.firstName}`))
|
||||
.map(g => (
|
||||
<button
|
||||
key={g.id}
|
||||
type="button"
|
||||
onMouseDown={() => {
|
||||
setGuestName(`${g.lastName} ${g.firstName}`)
|
||||
setGuestPhone(g.phone ?? '')
|
||||
setShowSuggestions(false)
|
||||
setDbResults([])
|
||||
}}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2.5 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors text-left"
|
||||
>
|
||||
<div className="w-2 h-2 rounded-full shrink-0 bg-slate-400" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-slate-800 dark:text-slate-200 truncate">{g.lastName} {g.firstName}</p>
|
||||
<p className="text-[11px] text-slate-400 truncate">
|
||||
📋 База гостей{g.phone ? ` · ${g.phone}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -159,6 +159,8 @@ export function CalendarPage() {
|
||||
setRentalBookings(prev => [...prev, created])
|
||||
} catch (err) {
|
||||
console.error('Failed to create rental booking:', err)
|
||||
const msg = err instanceof Error ? err.message : 'Ошибка сохранения аренды'
|
||||
alert(msg)
|
||||
}
|
||||
}, [slug])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user