feat: single ФИО field with autocomplete replaces 3 separate name inputs

- BookingModal (room tab): replace Фамилия/Имя/Отчество grid with single 'ФИО *' input; parseFullName() splits by spaces into last/first/middle; DB search via api.guests.list (debounced 300ms); dropdown shows current bookings + DB guests with phone/email autofill
- BookingDetailPanel: merge Фамилия (with autocomplete) + Имя + Отчество into single ФИО * input; existing acQuery/acResults infrastructure reused; parseFio() updates all 3 bgForm fields on every keystroke; openEditForm initializes ФИО from full name
- RentalBookingModal + BookingModal rental tab: include middleName when displaying DB guest results and when setting guestName on selection; update type to include middleName field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 01:22:46 +03:00
parent e403689522
commit b1b6854663
3 changed files with 131 additions and 55 deletions

View File

@@ -146,10 +146,16 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
setEarlyOpen(false)
}, [booking.id])
// Parse ФИО string into form fields
const parseFio = (val: string) => {
const parts = val.trim().split(/\s+/)
return { last_name: parts[0] ?? '', first_name: parts[1] ?? '', middle_name: parts.slice(2).join(' ') || undefined }
}
// Debounced guest autocomplete
const handleAcInput = (val: string) => {
setBgForm(f => ({ ...f, last_name: val }))
setAcQuery(val)
setBgForm(f => ({ ...f, ...parseFio(val) }))
if (acTimerRef.current) clearTimeout(acTimerRef.current)
if (val.length < 2 || !slug) { setAcResults([]); return }
acTimerRef.current = setTimeout(() => {
@@ -162,10 +168,11 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
}
const selectAcGuest = (g: GuestApiType) => {
const fullName = [g.lastName, g.firstName, g.middleName].filter(Boolean).join(' ')
setBgForm(f => ({
...f,
first_name: g.firstName,
last_name: g.lastName,
first_name: g.firstName,
last_name: g.lastName,
middle_name: g.middleName ?? undefined,
birth_date: g.birthDate ?? undefined,
passport_series: g.passportSeries ?? undefined,
@@ -174,7 +181,7 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
passport_issue_date: g.passportIssueDate ?? undefined,
nationality: g.nationality ?? undefined,
}))
setAcQuery(g.lastName)
setAcQuery(fullName)
setAcResults([])
}
@@ -219,7 +226,7 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
passport_issue_date: bg.passportIssueDate?.slice(0, 10) ?? undefined,
nationality: bg.nationality ?? undefined,
})
setAcQuery('')
setAcQuery([bg.lastName, bg.firstName, bg.middleName].filter(Boolean).join(' '))
setAcResults([])
setShowGuestForm(true)
}
@@ -938,15 +945,15 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
{editingBg ? 'Редактировать гостя' : 'Добавить гостя'}
</p>
{/* Autocomplete last name */}
{/* ФИО with autocomplete */}
<div ref={acWrapRef} className="relative">
<label className={lbl}>Фамилия *</label>
<label className={lbl}>ФИО *</label>
<div className="relative">
<input
className={fld}
value={bgForm.last_name ?? ''}
value={acQuery}
onChange={e => handleAcInput(e.target.value)}
placeholder="Иванов"
placeholder="Иванов Иван Иванович"
autoComplete="off"
/>
{acLoading && (
@@ -966,7 +973,7 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
<Search size={11} className="text-slate-400 shrink-0" />
<div>
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
{g.lastName} {g.firstName}
{[g.lastName, g.firstName, g.middleName].filter(Boolean).join(' ')}
</p>
{(g.passportSeries || g.passportNumber) && (
<p className="text-xs text-slate-400 font-mono">
@@ -981,18 +988,6 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
)}
</div>
{/* First name + middle name */}
<div className="grid grid-cols-2 gap-2">
<div>
<label className={lbl}>Имя *</label>
<input className={fld} value={bgForm.first_name ?? ''} onChange={e => setBgForm(f => ({ ...f, first_name: e.target.value }))} placeholder="Иван" />
</div>
<div>
<label className={lbl}>Отчество</label>
<input className={fld} value={bgForm.middle_name ?? ''} onChange={e => setBgForm(f => ({ ...f, middle_name: e.target.value }))} placeholder="Иванович" />
</div>
</div>
{/* Birth date + flags */}
<div className="grid grid-cols-2 gap-2">
<div>