From 2475c76d459ffeb19f8cf436aef6c69e294768ff Mon Sep 17 00:00:00 2001 From: HotelSync Date: Sun, 22 Mar 2026 01:38:25 +0300 Subject: [PATCH] feat: RentalBookingModal 2-column layout Left col: existing bookings list, full-day toggle, time selectors Right col: guest autocomplete, phone, link-to-room, notes, price+payment block Modal widened to max-w-2xl; payment section unified with BookingModal style Co-Authored-By: Claude Sonnet 4.6 --- src/components/rental/RentalBookingModal.tsx | 540 +++++++++---------- 1 file changed, 240 insertions(+), 300 deletions(-) diff --git a/src/components/rental/RentalBookingModal.tsx b/src/components/rental/RentalBookingModal.tsx index b8395f6..b05fd86 100644 --- a/src/components/rental/RentalBookingModal.tsx +++ b/src/components/rental/RentalBookingModal.tsx @@ -213,7 +213,7 @@ export function RentalBookingModal({ return (
-
+
{/* Header */}
{obj.icon} @@ -224,325 +224,265 @@ export function RentalBookingModal({
-
- {/* Existing bookings for the day */} - {existingBookings.filter(b => b.status !== 'cancelled').length > 0 && ( -
-

Уже забронировано

- {existingBookings.filter(b => b.status !== 'cancelled').map(b => ( -
-
- - {b.isFullDay ? 'Весь день' : `${fmtTime(b.startHour)} – ${fmtTime(b.endHour)}`} - - {b.guestName} +
+
+ + {/* ── LEFT: статус дня + переключатель + время ── */} +
+ + {/* Existing bookings */} + {existingBookings.filter(b => b.status !== 'cancelled').length > 0 && ( +
+

Уже забронировано

+ {existingBookings.filter(b => b.status !== 'cancelled').map(b => ( +
+
+ + {b.isFullDay ? 'Весь день' : `${fmtTime(b.startHour)} – ${fmtTime(b.endHour)}`} + + {b.guestName} +
+ ))}
- ))} -
- )} - - {hasFullDayConflict && ( -
- - Объект уже забронирован на весь день -
- )} - - {/* Full day toggle */} -
- - Весь день - {hasTimedConflict && ( - Есть частичные брони - )} - -
- {/* Time selection */} - {!isFullDay && !hasFullDayConflict && ( -
- - {noSlots ? ( + {hasFullDayConflict && (
- Все слоты на этот день заняты + Объект уже забронирован на весь день
- ) : ( - <> -
-
- - + )} + + {/* Full day toggle */} +
+ + Весь день + {hasTimedConflict && Есть частичные брони} + +
+ + {/* Time selection */} + {!isFullDay && !hasFullDayConflict && ( +
+ + {noSlots ? ( +
+ + Все слоты на этот день заняты
- -
- - -
-
- {obj.maxHoursPerSlot && !maxHoursOk && ( -

Максимум: {obj.maxHoursPerSlot} ч

+ ) : ( + <> +
+
+ + +
+ +
+ + +
+
+ {obj.maxHoursPerSlot && !maxHoursOk && ( +

Максимум: {obj.maxHoursPerSlot} ч

+ )} + {isTimeConflict && ( +

Время пересекается с существующей бронью

+ )} + )} - {isTimeConflict && ( -

Время пересекается с существующей бронью

- )} - +
)}
- )} - {/* Guest search */} -
-
- -
- - { - 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) { - searchTimerRef.current = setTimeout(async () => { - try { - const results = await api.guests.list(slug, val.trim()) - setDbResults(results.slice(0, 8)) - } catch { setDbResults([]) } - }, 300) - } else { - setDbResults([]) - } - }} - onFocus={() => setShowSuggestions(true)} - autoComplete="off" - /> - {/* Suggestions dropdown */} - {showSuggestions && (filtered.length > 0 || dbResults.length > 0) && ( -
- {/* Current/upcoming guests from bookings */} - {filtered.slice(0, 5).map(b => { - const rLabel = roomLabel(b) - const isCheckedIn = b.status === 'checked_in' - return ( - - ) - })} - {/* DB guest search results */} - {dbResults - .filter(g => !filtered.some(b => b.guestName.startsWith(g.lastName))) - .map(g => { + {/* ── RIGHT: гость + оплата ── */} +
+ + {/* Guest */} +
+ +
+ + setShowSuggestions(true)} autoComplete="off" + onChange={e => { + const val = e.target.value + setGuestName(val) + setGuestIsKnown(false) + setShowSuggestions(true) + if (searchTimerRef.current) clearTimeout(searchTimerRef.current) + if (val.trim().length >= 2 && slug) { + searchTimerRef.current = setTimeout(async () => { + try { setDbResults((await api.guests.list(slug, val.trim())).slice(0, 8)) } + catch { setDbResults([]) } + }, 300) + } else { setDbResults([]) } + }} + /> + {showSuggestions && (filtered.length > 0 || dbResults.length > 0) && ( +
+ {filtered.slice(0, 5).map(b => { + const rLabel = roomLabel(b) + const isCI = b.status === 'checked_in' + return ( + + ) + })} + {dbResults.filter(g => !filtered.some(b => b.guestName.startsWith(g.lastName))).map(g => { const fullName = [g.lastName, g.firstName, g.middleName].filter(Boolean).join(' ') return ( - - )}) - } -
+ + ) + })} +
+ )} +
+
+ + {/* Phone */} +
+ +
+ + setGuestPhone(maskPhone(e.target.value))} /> +
+ {phoneRequired && !guestPhone.trim() && ( +

Для нового гостя необходим телефон — он будет добавлен в базу

)}
-
-
- -
- - setGuestPhone(maskPhone(e.target.value))} - /> + {/* Link to room */} +
+ +
- {phoneRequired && !guestPhone.trim() && ( -

Для нового гостя необходим телефон — он будет добавлен в базу

+ + {/* Notes */} +
+ +