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:
2026-03-22 01:08:25 +03:00
parent f21c5d5054
commit e403689522
2 changed files with 86 additions and 8 deletions

View File

@@ -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">