fix: toggle CSS + guest autocomplete in BookingModal rental tab
- Toggle: add type=button, overflow-hidden, p-0, explicit left-[2px] + translate-x-[18px] pattern for reliable knob positioning across all toggles - Guest field in BookingModal rental tab: add debounced DB search (api.guests.list) + suggestions dropdown (same as RentalBookingModal) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useLayoutEffect } from 'react'
|
||||
import { useState, useLayoutEffect, useRef, useEffect } from 'react'
|
||||
import { format, addDays } from 'date-fns'
|
||||
import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2, Tag, Minus, BedDouble, Tv2, Send, Loader2, CheckCircle2, Clock, CalendarDays, User, Phone } from 'lucide-react'
|
||||
import { MOCK_DISCOUNTS } from '../../pages/DiscountsPage'
|
||||
@@ -103,6 +103,11 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
const [rentalPhone, setRentalPhone] = useState('')
|
||||
const [rentalNotes, setRentalNotes] = useState('')
|
||||
const [rentalSaving, setRentalSaving] = useState(false)
|
||||
// Rental guest autocomplete
|
||||
const [rentalShowSugg, setRentalShowSugg] = useState(false)
|
||||
const [rentalDbResults, setRentalDbResults] = useState<{ id: string; firstName: string; lastName: string; phone: string | null }[]>([])
|
||||
const rentalSearchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const rentalNameRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const rentalObj = rentalObjects?.find(o => o.id === rentalObjId) ?? rentalObjects?.[0]
|
||||
const rentalDayBookings = rentalBookings.filter(b =>
|
||||
@@ -130,6 +135,27 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
setRentalEndM(rentalEffStart + 60)
|
||||
}
|
||||
}, [rentalEffStart])
|
||||
|
||||
// Close rental guest suggestions on outside click
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (rentalNameRef.current && !rentalNameRef.current.contains(e.target as Node)) {
|
||||
setRentalShowSugg(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handler)
|
||||
return () => document.removeEventListener('mousedown', handler)
|
||||
}, [])
|
||||
|
||||
// Rental guest suggestions from current bookings
|
||||
const rentalGuestSugg = [
|
||||
...(bookings ?? []).filter(b => b.status === 'checked_in'),
|
||||
...(bookings ?? []).filter(b => b.status === 'confirmed'),
|
||||
]
|
||||
const rentalFiltered = rentalGuest.trim().length > 0
|
||||
? rentalGuestSugg.filter(b => b.guestName.toLowerCase().includes(rentalGuest.toLowerCase()))
|
||||
: rentalGuestSugg
|
||||
|
||||
const rentalHasFullDay = rentalDayBookings.some(b => b.isFullDay)
|
||||
const rentalHasTimed = rentalDayBookings.some(b => !b.isFullDay)
|
||||
const rentalNoSlots = !rentalHasFullDay && rentalAvailStarts.length === 0
|
||||
@@ -470,11 +496,12 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
<span className="text-sm font-medium text-slate-700 dark:text-slate-300 flex-1">Весь день</span>
|
||||
{rentalHasTimed && <span className="text-xs text-slate-400">Есть частичные брони</span>}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => !rentalHasTimed && setRentalIsFullDay(v => !v)}
|
||||
className={cn('relative rounded-full transition-colors shrink-0', rentalIsFullDay ? 'bg-brand-600' : 'bg-slate-200 dark:bg-slate-600')}
|
||||
className={cn('relative rounded-full overflow-hidden transition-colors shrink-0 p-0', rentalIsFullDay ? 'bg-brand-600' : 'bg-slate-200 dark:bg-slate-600')}
|
||||
style={{ height: 22, width: 40 }}
|
||||
>
|
||||
<span className={cn('absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform', rentalIsFullDay ? 'translate-x-5' : 'translate-x-0.5')} />
|
||||
<span className={cn('absolute top-[3px] left-[2px] w-4 h-4 rounded-full bg-white shadow transition-transform duration-200', rentalIsFullDay ? 'translate-x-[18px]' : 'translate-x-0')} />
|
||||
</button>
|
||||
</div>
|
||||
{!rentalIsFullDay && (
|
||||
@@ -524,8 +551,58 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
|
||||
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
<User size={13} className="inline mr-1" />Гость *
|
||||
</label>
|
||||
<input type="text" className="input" placeholder="Имя Фамилия"
|
||||
value={rentalGuest} onChange={e => setRentalGuest(e.target.value)} />
|
||||
<div className="relative" ref={rentalNameRef}>
|
||||
<input type="text" className="input" placeholder="Имя Фамилия" autoComplete="off"
|
||||
value={rentalGuest}
|
||||
onFocus={() => setRentalShowSugg(true)}
|
||||
onChange={e => {
|
||||
const val = e.target.value
|
||||
setRentalGuest(val)
|
||||
setRentalShowSugg(true)
|
||||
if (rentalSearchTimer.current) clearTimeout(rentalSearchTimer.current)
|
||||
if (val.trim().length >= 2 && slug) {
|
||||
rentalSearchTimer.current = setTimeout(async () => {
|
||||
try {
|
||||
const results = await api.guests.list(slug, val.trim())
|
||||
setRentalDbResults(results.slice(0, 8))
|
||||
} catch { setRentalDbResults([]) }
|
||||
}, 300)
|
||||
} else {
|
||||
setRentalDbResults([])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{rentalShowSugg && (rentalFiltered.length > 0 || rentalDbResults.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">
|
||||
{rentalFiltered.slice(0, 5).map(b => (
|
||||
<button key={b.id} type="button"
|
||||
onMouseDown={() => { setRentalGuest(b.guestName); setRentalPhone(b.guestPhone ?? ''); setRentalShowSugg(false); setRentalDbResults([]) }}
|
||||
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', b.status === 'checked_in' ? '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">{b.guestPhone || (b.status === 'checked_in' ? '🏠 Проживает' : '📅 Бронь')}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{rentalDbResults
|
||||
.filter(g => !rentalFiltered.some(b => b.guestName === `${g.lastName} ${g.firstName}`))
|
||||
.map(g => (
|
||||
<button key={g.id} type="button"
|
||||
onMouseDown={() => { setRentalGuest(`${g.lastName} ${g.firstName}`); setRentalPhone(g.phone ?? ''); setRentalShowSugg(false); setRentalDbResults([]) }}
|
||||
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 ?? 'нет телефона'}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
|
||||
Reference in New Issue
Block a user