From db05898c0829794d70f939601d98f6030d16e38e Mon Sep 17 00:00:00 2001 From: HotelSync Date: Sat, 21 Mar 2026 23:02:24 +0300 Subject: [PATCH] feat: 15-minute rental slots + compact calendar cell display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rental bookings now use minutes since midnight (start_hour/end_hour store e.g. 495 for 8:15, 570 for 9:30) — DB migration 017 - Buffer applied in exact minutes (no hour ceiling): 15min buffer after 9:00 booking → next slot available at 9:15 - Calendar cell: removed working-hours line (8:00–22:00), now shows colored dot + compact time per booking (8–10, 8:15–9:15) - Time selects show HH:MM format for 15-min intervals Co-Authored-By: Claude Sonnet 4.6 --- backend/migrations/017_rental_minutes.sql | 6 ++ src/components/bookings/BookingModal.tsx | 65 +++++++------ src/components/calendar/BookingCalendar.tsx | 35 +++---- src/components/rental/RentalBookingModal.tsx | 98 ++++++++++++-------- src/pages/RentalPage.tsx | 5 +- 5 files changed, 121 insertions(+), 88 deletions(-) create mode 100644 backend/migrations/017_rental_minutes.sql diff --git a/backend/migrations/017_rental_minutes.sql b/backend/migrations/017_rental_minutes.sql new file mode 100644 index 0000000..58af453 --- /dev/null +++ b/backend/migrations/017_rental_minutes.sql @@ -0,0 +1,6 @@ +-- Convert rental_bookings start_hour/end_hour from whole hours to minutes since midnight +-- Safe: only converts rows where start_hour < 25 (i.e., stored as hours, not minutes yet) +UPDATE rental_bookings +SET start_hour = start_hour * 60, + end_hour = end_hour * 60 +WHERE start_hour < 25; diff --git a/src/components/bookings/BookingModal.tsx b/src/components/bookings/BookingModal.tsx index 1f306d6..d416930 100644 --- a/src/components/bookings/BookingModal.tsx +++ b/src/components/bookings/BookingModal.tsx @@ -44,10 +44,21 @@ interface BookingModalProps { } // ── Rental time helpers ──────────────────────────────────────────────────────── -function rentalHourOptions(from: number, to: number) { - return Array.from({ length: Math.max(0, to - from + 1) }, (_, i) => from + i) +function rentalTimeOptions(openHour: number, closeHour: number): number[] { + const result: number[] = [] + for (let m = openHour * 60; m < closeHour * 60; m += 15) result.push(m) + return result +} +function rentalEndOptions(fromMin: number, toMin: number): number[] { + const result: number[] = [] + for (let m = fromMin + 15; m <= toMin; m += 15) result.push(m) + return result +} +function fmtTime(min: number) { + const h = Math.floor(min / 60) + const m = min % 60 + return m === 0 ? `${h}:00` : `${h}:${String(m).padStart(2, '0')}` } -function fmtH(h: number) { return `${h}:00` } function isConflict(bookings: Booking[], roomId: string, checkIn: string, checkOut: string, excludeId?: string) { if (!checkIn || !checkOut || checkIn >= checkOut) return false @@ -86,8 +97,8 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav const [rentalObjId, setRentalObjId] = useState(rentalObjects?.[0]?.id ?? '') const [rentalDate, setRentalDate] = useState(draft.checkIn) const [rentalIsFullDay, setRentalIsFullDay] = useState(false) - const [rentalStartH, setRentalStartH] = useState(0) - const [rentalEndH, setRentalEndH] = useState(2) + const [rentalStartM, setRentalStartM] = useState(0) // minutes since midnight + const [rentalEndM, setRentalEndM] = useState(60) const [rentalGuest, setRentalGuest] = useState('') const [rentalPhone, setRentalPhone] = useState('') const [rentalNotes, setRentalNotes] = useState('') @@ -99,24 +110,24 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav (b.date as string).slice(0, 10) === rentalDate && b.status !== 'cancelled', ) - const rentalBreakH = Math.ceil((rentalObj?.bufferMinutes ?? 0) / 60) + const rentalBufMin = rentalObj?.bufferMinutes ?? 0 const rentalAvailStarts = rentalObj - ? rentalHourOptions(rentalObj.openHour, rentalObj.closeHour - 1).filter(h => - !rentalDayBookings.some(b => !b.isFullDay && h >= b.startHour && h < b.endHour + rentalBreakH), + ? rentalTimeOptions(rentalObj.openHour, rentalObj.closeHour).filter(m => + !rentalDayBookings.some(b => !b.isFullDay && m >= b.startHour && m < b.endHour + rentalBufMin), ) : [] const rentalGetMaxEnd = (start: number) => { - const next = rentalDayBookings.filter(b => !b.isFullDay && b.startHour >= start + 1) + const next = rentalDayBookings.filter(b => !b.isFullDay && b.startHour >= start + 15) .sort((a, b) => a.startHour - b.startHour)[0] - return next ? next.startHour - rentalBreakH : (rentalObj?.closeHour ?? 22) + return next ? next.startHour - rentalBufMin : (rentalObj?.closeHour ?? 22) * 60 } - const rentalEffStart = rentalAvailStarts.includes(rentalStartH) ? rentalStartH : (rentalAvailStarts[0] ?? rentalObj?.openHour ?? 8) - const rentalAvailEnds = rentalHourOptions(rentalEffStart + 1, rentalGetMaxEnd(rentalEffStart)) + const rentalEffStart = rentalAvailStarts.includes(rentalStartM) ? rentalStartM : (rentalAvailStarts[0] ?? (rentalObj?.openHour ?? 8) * 60) + const rentalAvailEnds = rentalEndOptions(rentalEffStart, rentalGetMaxEnd(rentalEffStart)) - // Sync rentalEndH when rentalEffStart shifts past it + // Sync rentalEndM when rentalEffStart shifts past it useLayoutEffect(() => { - if (!rentalIsFullDay && rentalEndH <= rentalEffStart) { - setRentalEndH(rentalEffStart + 1) + if (!rentalIsFullDay && rentalEndM <= rentalEffStart) { + setRentalEndM(rentalEffStart + 60) } }, [rentalEffStart]) const rentalHasFullDay = rentalDayBookings.some(b => b.isFullDay) @@ -125,7 +136,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav const rentalHours = rentalIsFullDay ? (rentalObj ? rentalObj.closeHour - rentalObj.openHour : 0) - : Math.max(0, rentalEndH - rentalEffStart) + : Math.max(0, (rentalEndM - rentalEffStart) / 60) const rentalTotal = rentalObj ? (rentalIsFullDay ? rentalObj.pricePerDay : rentalHours * rentalObj.pricePerHour) : 0 @@ -146,8 +157,8 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav objectId: rentalObj.id, date: rentalDate, isFullDay: rentalIsFullDay, - startHour: rentalIsFullDay ? rentalObj.openHour : rentalEffStart, - endHour: rentalIsFullDay ? rentalObj.closeHour : rentalEndH, + startHour: rentalIsFullDay ? rentalObj.openHour * 60 : rentalEffStart, + endHour: rentalIsFullDay ? rentalObj.closeHour * 60 : rentalEndM, guestName: rentalGuest.trim(), guestPhone: rentalPhone.trim(), notes: rentalNotes.trim() || undefined, @@ -407,7 +418,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav {rentalObjects.map(o => (