Rental modal: require phone for new guests, auto-create in guests DB

- Track guestIsKnown: true when selected from dropdown, false when typed manually
- If guest is new (typed manually): phone field is required (red border + hint)
- On save: if new guest + slug available, call api.guests.create before saving rental
- Button shows 'Сохранение...' while saving, disabled during async flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 17:02:59 +03:00
parent 033882790d
commit b831106ae2

View File

@@ -38,6 +38,8 @@ export function RentalBookingModal({
const [paidAmount, setPaidAmount] = useState<string>(editBooking?.paidAmount ? String(editBooking.paidAmount) : '')
const [payMethod, setPayMethod] = useState<'cash' | 'card' | 'transfer'>('cash')
const [linkedBookingId, setLinkedBookingId] = useState<string>(editBooking?.linkedRoomId ?? '')
// true = selected from dropdown (existing guest), false = typed manually (new guest)
const [guestIsKnown, setGuestIsKnown] = useState(isEdit)
// Guest search
const [showSuggestions, setShowSuggestions] = useState(false)
@@ -46,6 +48,7 @@ export function RentalBookingModal({
// DB guest search
const [dbResults, setDbResults] = useState<{ id: string; firstName: string; lastName: string; phone: string | null }[]>([])
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [saving, setSaving] = useState(false)
// Bookings with guest info — checked_in first, then confirmed
const checkedIn = bookings.filter(b => b.status === 'checked_in')
@@ -80,6 +83,7 @@ export function RentalBookingModal({
setGuestPhone(b.guestPhone ?? '')
setLinkedBookingId(b.id)
setShowSuggestions(false)
setGuestIsKnown(true)
}
const hours = isFullDay
@@ -96,10 +100,26 @@ export function RentalBookingModal({
)
const hasFullDayConflict = otherBookings.some(b => b.isFullDay && b.status !== 'cancelled')
const canSave = guestName.trim() !== '' && hours > 0 && maxHoursOk && !isTimeConflict && !hasFullDayConflict
// For new (unknown) guests, phone is required
const phoneRequired = !guestIsKnown
const canSave = guestName.trim() !== '' &&
(!phoneRequired || guestPhone.trim() !== '') &&
hours > 0 && maxHoursOk && !isTimeConflict && !hasFullDayConflict
const handleSave = () => {
if (!canSave) return
const handleSave = async () => {
if (!canSave || saving) return
setSaving(true)
try {
// If guest is new (typed manually), create them in the guests DB
if (!guestIsKnown && slug) {
const nameParts = guestName.trim().split(' ')
await api.guests.create(slug, {
last_name: nameParts[0] ?? '',
first_name: nameParts[1] ?? '',
middle_name: nameParts.slice(2).join(' ') || undefined,
phone: guestPhone.trim() || undefined,
})
}
const linked = bookings.find(b => b.id === linkedBookingId)
onSave({
id: editBooking?.id ?? `rb-${Date.now()}`,
@@ -116,6 +136,9 @@ export function RentalBookingModal({
paidAmount: parseFloat(paidAmount) || 0,
status: 'confirmed',
})
} finally {
setSaving(false)
}
}
const displayDate = new Intl.DateTimeFormat('ru-RU', {
@@ -243,6 +266,7 @@ export function RentalBookingModal({
onChange={e => {
const val = e.target.value
setGuestName(val)
setGuestIsKnown(false) // manual typing = new guest
setShowSuggestions(true)
if (searchTimerRef.current) clearTimeout(searchTimerRef.current)
if (val.trim().length >= 2 && slug) {
@@ -298,6 +322,7 @@ export function RentalBookingModal({
setGuestPhone(g.phone ?? '')
setShowSuggestions(false)
setDbResults([])
setGuestIsKnown(true)
}}
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"
>
@@ -318,18 +343,21 @@ export function RentalBookingModal({
<div>
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">
Телефон
Телефон {phoneRequired && <span className="text-red-500">*</span>}
</label>
<div className="relative">
<Phone size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
type="tel"
className="input pl-8 text-sm"
placeholder="+7 999 000 11 22"
className={cn('input pl-8 text-sm', phoneRequired && !guestPhone.trim() && 'border-red-400 focus:ring-red-400')}
placeholder={phoneRequired ? '+7 999 000 11 22 (обязательно)' : '+7 999 000 11 22'}
value={guestPhone}
onChange={e => setGuestPhone(e.target.value)}
/>
</div>
{phoneRequired && !guestPhone.trim() && (
<p className="mt-1 text-[11px] text-red-500">Для нового гостя необходим телефон он будет добавлен в базу</p>
)}
</div>
{/* Link to room */}
@@ -437,11 +465,11 @@ export function RentalBookingModal({
<div className="flex items-center justify-end gap-2 px-5 py-4 border-t border-slate-200 dark:border-slate-700 shrink-0">
<button onClick={onClose} className="btn-secondary">Отмена</button>
<button
onClick={handleSave}
disabled={!canSave}
onClick={() => { void handleSave() }}
disabled={!canSave || saving}
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
>
{isEdit ? 'Сохранить' : 'Забронировать'}
{saving ? 'Сохранение...' : isEdit ? 'Сохранить' : 'Забронировать'}
</button>
</div>
</div>