From b1b68546631f50acc0a80de9e58d016547827578 Mon Sep 17 00:00:00 2001
From: HotelSync
Date: Sun, 22 Mar 2026 01:22:46 +0300
Subject: [PATCH] =?UTF-8?q?feat:=20single=20=D0=A4=D0=98=D0=9E=20field=20w?=
=?UTF-8?q?ith=20autocomplete=20replaces=203=20separate=20name=20inputs?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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
---
.../bookings/BookingDetailPanel.tsx | 39 +++--
src/components/bookings/BookingModal.tsx | 133 ++++++++++++++----
src/components/rental/RentalBookingModal.tsx | 14 +-
3 files changed, 131 insertions(+), 55 deletions(-)
diff --git a/src/components/bookings/BookingDetailPanel.tsx b/src/components/bookings/BookingDetailPanel.tsx
index 807b818..12131d4 100644
--- a/src/components/bookings/BookingDetailPanel.tsx
+++ b/src/components/bookings/BookingDetailPanel.tsx
@@ -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 ? 'Редактировать гостя' : 'Добавить гостя'}
- {/* Autocomplete last name */}
+ {/* ФИО with autocomplete */}
-
+
handleAcInput(e.target.value)}
- placeholder="Иванов"
+ placeholder="Иванов Иван Иванович"
autoComplete="off"
/>
{acLoading && (
@@ -966,7 +973,7 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
- {g.lastName} {g.firstName}
+ {[g.lastName, g.firstName, g.middleName].filter(Boolean).join(' ')}
{(g.passportSeries || g.passportNumber) && (
@@ -981,18 +988,6 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
)}
- {/* First name + middle name */}
-
-
{/* Birth date + flags */}
diff --git a/src/components/bookings/BookingModal.tsx b/src/components/bookings/BookingModal.tsx
index 17ec01a..1e3874b 100644
--- a/src/components/bookings/BookingModal.tsx
+++ b/src/components/bookings/BookingModal.tsx
@@ -60,6 +60,15 @@ function fmtTime(min: number) {
return m === 0 ? `${h}:00` : `${h}:${String(m).padStart(2, '0')}`
}
+function parseFullName(full: string) {
+ const parts = full.trim().split(/\s+/)
+ return {
+ lastName: parts[0] ?? '',
+ firstName: parts[1] ?? '',
+ middleName: parts.slice(2).join(' '),
+ }
+}
+
function isConflict(bookings: Booking[], roomId: string, checkIn: string, checkOut: string, excludeId?: string) {
if (!checkIn || !checkOut || checkIn >= checkOut) return false
return bookings.some(b =>
@@ -105,10 +114,17 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
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 [rentalDbResults, setRentalDbResults] = useState<{ id: string; firstName: string; lastName: string; middleName: string | null; phone: string | null }[]>([])
const rentalSearchTimer = useRef
| null>(null)
const rentalNameRef = useRef(null)
+ // Guest autocomplete for room tab
+ const [guestFullName, setGuestFullName] = useState(existing?.guestName ?? '')
+ const [showGuestSugg, setShowGuestSugg] = useState(false)
+ const [guestDbResults, setGuestDbResults] = useState<{ id: string; firstName: string; lastName: string; middleName: string | null; phone: string | null; email: string | null }[]>([])
+ const guestSearchTimer = useRef | null>(null)
+ const guestNameRef = useRef(null)
+
const rentalObj = rentalObjects?.find(o => o.id === rentalObjId) ?? rentalObjects?.[0]
const rentalDayBookings = rentalBookings.filter(b =>
b.objectId === rentalObjId &&
@@ -136,17 +152,29 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
}
}, [rentalEffStart])
- // Close rental guest suggestions on outside click
+ // Close suggestions on outside click
useEffect(() => {
const handler = (e: MouseEvent) => {
if (rentalNameRef.current && !rentalNameRef.current.contains(e.target as Node)) {
setRentalShowSugg(false)
}
+ if (guestNameRef.current && !guestNameRef.current.contains(e.target as Node)) {
+ setShowGuestSugg(false)
+ }
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [])
+ // Guest suggestions (room tab)
+ const guestSugg = [
+ ...(bookings ?? []).filter(b => b.status === 'checked_in'),
+ ...(bookings ?? []).filter(b => b.status === 'confirmed'),
+ ]
+ const guestFiltered = guestFullName.trim().length > 0
+ ? guestSugg.filter(b => b.guestName.toLowerCase().includes(guestFullName.toLowerCase()))
+ : guestSugg
+
// Rental guest suggestions from current bookings
const rentalGuestSugg = [
...(bookings ?? []).filter(b => b.status === 'checked_in'),
@@ -172,9 +200,10 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
setRentalSaving(true)
try {
if (slug) {
- const parts = rentalGuest.trim().split(' ')
+ const { lastName: rLast, firstName: rFirst, middleName: rMid } = parseFullName(rentalGuest)
await api.guests.create(slug, {
- last_name: parts[0] ?? '', first_name: parts[1] ?? '',
+ last_name: rLast, first_name: rFirst,
+ middle_name: rMid || undefined,
phone: rentalPhone.trim() || undefined,
}).catch(() => { /* ignore duplicate */ })
}
@@ -590,7 +619,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav
.filter(g => !rentalFiltered.some(b => b.guestName === `${g.lastName} ${g.firstName}`))
.map(g => (