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 */} -
-
- - setBgForm(f => ({ ...f, first_name: e.target.value }))} placeholder="Иван" /> -
-
- - setBgForm(f => ({ ...f, middle_name: e.target.value }))} placeholder="Иванович" /> -
-
- {/* 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 => ( + ))} + {guestDbResults + .filter(g => !guestFiltered.some(b => b.guestName.startsWith(g.lastName))) + .map(g => { + const fullName = [g.lastName, g.firstName, g.middleName].filter(Boolean).join(' ') + return ( + + ) + })} +
+ )}
diff --git a/src/components/rental/RentalBookingModal.tsx b/src/components/rental/RentalBookingModal.tsx index 7f5f1ce..b8395f6 100644 --- a/src/components/rental/RentalBookingModal.tsx +++ b/src/components/rental/RentalBookingModal.tsx @@ -73,7 +73,7 @@ export function RentalBookingModal({ const nameRef = useRef(null) // DB guest search - const [dbResults, setDbResults] = useState<{ id: string; firstName: string; lastName: string; phone: string | null }[]>([]) + const [dbResults, setDbResults] = useState<{ id: string; firstName: string; lastName: string; middleName: string | null; phone: string | null }[]>([]) const searchTimerRef = useRef | null>(null) const [saving, setSaving] = useState(false) @@ -395,13 +395,15 @@ export function RentalBookingModal({ })} {/* DB guest search results */} {dbResults - .filter(g => !filtered.some(b => b.guestName === `${g.lastName} ${g.firstName}`)) - .map(g => ( + .filter(g => !filtered.some(b => b.guestName.startsWith(g.lastName))) + .map(g => { + const fullName = [g.lastName, g.firstName, g.middleName].filter(Boolean).join(' ') + return ( - )) + )}) }
)}