From 913ddeedc93840147b3769f2780722d32f6419ea Mon Sep 17 00:00:00 2001 From: HotelSync Date: Mon, 16 Mar 2026 15:13:49 +0300 Subject: [PATCH] Add date editing with conflict detection and rental booking editing - BookingDetailPanel: edit dates/room with inline conflict detection; options to force-save or swap conflicting booking's room - BookingsPage + CalendarPage: pass rooms/allBookings/onBulkUpdate to panel - RentalBookingModal: accept editBooking prop to pre-populate fields for editing - RentalPage: add edit (pencil) button per booking row, unify create/update handler Co-Authored-By: Claude Sonnet 4.6 --- .../bookings/BookingDetailPanel.tsx | 182 ++++++++++++++++-- src/components/calendar/BookingCalendar.tsx | 9 +- src/components/rental/RentalBookingModal.tsx | 27 +-- src/pages/BookingsPage.tsx | 10 + src/pages/CalendarPage.tsx | 9 + src/pages/RentalPage.tsx | 25 ++- 6 files changed, 224 insertions(+), 38 deletions(-) diff --git a/src/components/bookings/BookingDetailPanel.tsx b/src/components/bookings/BookingDetailPanel.tsx index 6d87c65..9a6268c 100644 --- a/src/components/bookings/BookingDetailPanel.tsx +++ b/src/components/bookings/BookingDetailPanel.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { X, Mail, Calendar, Users, CreditCard, Tag, CheckCircle, XCircle, - Printer, ScanLine, Banknote, Building2, Plus, + Printer, ScanLine, Banknote, Building2, Plus, Pencil, FileText, FileCheck, Receipt, IdCard, AlertTriangle, } from 'lucide-react' import { @@ -9,6 +9,9 @@ import { SOURCE_COLORS, SOURCE_LABELS, formatCurrency, nightsCount, } from '../../lib/utils' import type { Booking, Room } from '../../types' + +const fmtDate = (iso: string) => + new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(new Date(iso)) import { Badge } from '../ui/Badge' import { MOCK_DISCOUNTS } from '../../pages/DiscountsPage' @@ -52,13 +55,57 @@ const DOCUMENTS = [ interface BookingDetailPanelProps { booking: Booking room?: Room + rooms?: Room[] + allBookings?: Booking[] onClose: () => void onUpdate: (id: string, data: Partial) => void + onBulkUpdate?: (updates: Array<{ id: string; data: Partial }>) => void } -export function BookingDetailPanel({ booking, room, onClose, onUpdate }: BookingDetailPanelProps) { +export function BookingDetailPanel({ booking, room, rooms, allBookings, onClose, onUpdate, onBulkUpdate }: BookingDetailPanelProps) { const [tab, setTab] = useState('booking') + // Date editing + const [editDates, setEditDates] = useState(false) + const [newCheckIn, setNewCheckIn] = useState(booking.checkIn) + const [newCheckOut, setNewCheckOut] = useState(booking.checkOut) + const [newRoomId, setNewRoomId] = useState(booking.roomId) + const [conflictBooking, setConflictBooking] = useState(null) + const [conflictResolveRoomId, setConflictResolveRoomId] = useState('') + + const canEditDates = booking.status === 'confirmed' || booking.status === 'inquiry' + + const findConflict = (ci: string, co: string, rid: string): Booking | null => + (allBookings ?? []).find(b => + b.id !== booking.id && + b.roomId === rid && + b.status !== 'cancelled' && + b.status !== 'no_show' && + b.checkIn < co && b.checkOut > ci, + ) ?? null + + const handleSaveDates = () => { + if (newCheckIn >= newCheckOut) { showToast('Дата выезда должна быть позже заезда'); return } + const conflict = findConflict(newCheckIn, newCheckOut, newRoomId) + if (conflict) { setConflictBooking(conflict); setConflictResolveRoomId(''); return } + onUpdate(booking.id, { checkIn: newCheckIn, checkOut: newCheckOut, roomId: newRoomId }) + } + + const handleForceUpdate = () => + onUpdate(booking.id, { checkIn: newCheckIn, checkOut: newCheckOut, roomId: newRoomId }) + + const handleResolveWithRoomSwap = () => { + if (!conflictBooking || !onBulkUpdate || !conflictResolveRoomId) return + if (findConflict(conflictBooking.checkIn, conflictBooking.checkOut, conflictResolveRoomId)) { + showToast('Выбранный номер тоже занят на эти даты') + return + } + onBulkUpdate([ + { id: booking.id, data: { checkIn: newCheckIn, checkOut: newCheckOut, roomId: newRoomId } }, + { id: conflictBooking.id, data: { roomId: conflictResolveRoomId } }, + ]) + } + // Passport data const nameParts = booking.guestName.split(' ') const [passport, setPassport] = useState({ @@ -191,27 +238,124 @@ export function BookingDetailPanel({ booking, room, onClose, onUpdate }: Booking )} -
-
-

- Заезд -

-

- {new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(new Date(booking.checkIn))} + {/* Dates — read or edit mode */} +

+
+

+ Даты проживания

+ {!editDates && canEditDates && ( + + )}
-
-

- Выезд + + {editDates ? ( +

+
+
+ + { setNewCheckIn(e.target.value); setConflictBooking(null) }} /> +
+
+ + { setNewCheckOut(e.target.value); setConflictBooking(null) }} /> +
+
+ + {rooms && rooms.length > 1 && ( +
+ + +
+ )} + + {/* Conflict warning */} + {conflictBooking && ( +
+
+ + + Номер занят: {conflictBooking.guestName},  + {fmtDate(conflictBooking.checkIn)} – {fmtDate(conflictBooking.checkOut)} + +
+ {conflictBooking.status !== 'checked_in' && onBulkUpdate && rooms && ( +
+

+ Переместить {conflictBooking.guestName} в другой номер: +

+ + +
+ )} + +
+ )} + +
+ + {!conflictBooking && ( + + )} +
+
+ ) : ( +
+
+

Заезд

+

+ {fmtDate(booking.checkIn)} +

+
+
+

Выезд

+

+ {fmtDate(booking.checkOut)} +

+
+
+ )} + {!editDates && ( +

+ {nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'}

-

- {new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(new Date(booking.checkOut))} -

-
+ )}
-

- {nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'} -

diff --git a/src/components/calendar/BookingCalendar.tsx b/src/components/calendar/BookingCalendar.tsx index 8d542b8..bc8fec5 100644 --- a/src/components/calendar/BookingCalendar.tsx +++ b/src/components/calendar/BookingCalendar.tsx @@ -21,6 +21,7 @@ interface BookingCalendarProps { bookings: Booking[] onBookingCreate: (b: Partial) => void onBookingUpdate: (id: string, b: Partial) => void + onBookingBulkUpdate?: (updates: Array<{ id: string; data: Partial }>) => void fadingBookingIds?: Set rentalObjects?: RentalObject[] rentalBookings?: RentalBooking[] @@ -48,7 +49,7 @@ function getRoomTypeColor(type: string): string { return map[type] ?? 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300' } -export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpdate, fadingBookingIds, rentalObjects, rentalBookings, onRentalBookingCreate }: BookingCalendarProps) { +export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpdate, onBookingBulkUpdate, fadingBookingIds, rentalObjects, rentalBookings, onRentalBookingCreate }: BookingCalendarProps) { const [startDate, setStartDate] = useState(() => startOfDay(new Date())) const [visibleDays, setVisibleDays] = useState(DAYS_VISIBLE) @@ -602,11 +603,17 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd r.id === selectedBooking.roomId)} + rooms={rooms} + allBookings={bookings} onClose={() => setSelectedBooking(null)} onUpdate={(id, data) => { onBookingUpdate(id, data) setSelectedBooking(null) }} + onBulkUpdate={onBookingBulkUpdate ? (updates) => { + onBookingBulkUpdate(updates) + setSelectedBooking(null) + } : undefined} /> )} diff --git a/src/components/rental/RentalBookingModal.tsx b/src/components/rental/RentalBookingModal.tsx index ee3635f..611809a 100644 --- a/src/components/rental/RentalBookingModal.tsx +++ b/src/components/rental/RentalBookingModal.tsx @@ -7,6 +7,7 @@ interface RentalBookingModalProps { obj: RentalObject date: string // 'yyyy-MM-dd' existingBookings: RentalBooking[] + editBooking?: RentalBooking // pass to edit an existing booking onClose: () => void onSave: (b: RentalBooking) => void } @@ -19,14 +20,15 @@ function formatHour(h: number): string { return `${h}:00` } -export function RentalBookingModal({ obj, date, existingBookings, onClose, onSave }: RentalBookingModalProps) { - const [isFullDay, setIsFullDay] = useState(false) - const [startHour, setStartHour] = useState(obj.openHour) - const [endHour, setEndHour] = useState(Math.min(obj.openHour + 2, obj.closeHour)) - const [guestName, setGuestName] = useState('') - const [guestPhone, setGuestPhone] = useState('') - const [notes, setNotes] = useState('') - const [paidAmount, setPaidAmount] = useState('') +export function RentalBookingModal({ obj, date, existingBookings, editBooking, onClose, onSave }: RentalBookingModalProps) { + const isEdit = !!editBooking + const [isFullDay, setIsFullDay] = useState(editBooking?.isFullDay ?? false) + const [startHour, setStartHour] = useState(editBooking?.startHour ?? obj.openHour) + const [endHour, setEndHour] = useState(editBooking?.endHour ?? Math.min(obj.openHour + 2, obj.closeHour)) + const [guestName, setGuestName] = useState(editBooking?.guestName ?? '') + const [guestPhone, setGuestPhone] = useState(editBooking?.guestPhone ?? '') + const [notes, setNotes] = useState(editBooking?.notes ?? '') + const [paidAmount, setPaidAmount] = useState(editBooking?.paidAmount ? String(editBooking.paidAmount) : '') const [payMethod, setPayMethod] = useState<'cash' | 'card' | 'transfer'>('cash') const hours = isFullDay @@ -39,18 +41,19 @@ export function RentalBookingModal({ obj, date, existingBookings, onClose, onSav const maxHoursOk = !obj.maxHoursPerSlot || hours <= obj.maxHoursPerSlot - const isTimeConflict = !isFullDay && existingBookings.some(b => + const otherBookings = existingBookings.filter(b => b.id !== editBooking?.id) + const isTimeConflict = !isFullDay && otherBookings.some(b => !b.isFullDay && b.status !== 'cancelled' && b.startHour < endHour && b.endHour > startHour, ) - const hasFullDayConflict = existingBookings.some(b => b.isFullDay && b.status !== 'cancelled') + const hasFullDayConflict = otherBookings.some(b => b.isFullDay && b.status !== 'cancelled') const canSave = guestName.trim() !== '' && hours > 0 && maxHoursOk && !isTimeConflict && !hasFullDayConflict const handleSave = () => { if (!canSave) return onSave({ - id: `rb-${Date.now()}`, + id: editBooking?.id ?? `rb-${Date.now()}`, objectId: obj.id, date, isFullDay, @@ -284,7 +287,7 @@ export function RentalBookingModal({ obj, date, existingBookings, onClose, onSav disabled={!canSave} className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed" > - Забронировать + {isEdit ? 'Сохранить' : 'Забронировать'}

diff --git a/src/pages/BookingsPage.tsx b/src/pages/BookingsPage.tsx index 55e2e96..40e1e44 100644 --- a/src/pages/BookingsPage.tsx +++ b/src/pages/BookingsPage.tsx @@ -363,11 +363,21 @@ export function BookingsPage() { setSelected(null)} onUpdate={(id, data) => { setBookings(prev => prev.map(b => b.id === id ? { ...b, ...data } : b)) setSelected(null) }} + onBulkUpdate={(updates) => { + setBookings(prev => { + let next = [...prev] + updates.forEach(u => { next = next.map(b => b.id === u.id ? { ...b, ...u.data } : b) }) + return next + }) + setSelected(null) + }} /> )} diff --git a/src/pages/CalendarPage.tsx b/src/pages/CalendarPage.tsx index 66e406f..1528aaf 100644 --- a/src/pages/CalendarPage.tsx +++ b/src/pages/CalendarPage.tsx @@ -33,6 +33,14 @@ export function CalendarPage() { } } + const handleBulkUpdate = (updates: Array<{ id: string; data: Partial }>) => { + setBookings(prev => { + let next = [...prev] + updates.forEach(u => { next = next.map(b => b.id === u.id ? { ...b, ...u.data } : b) }) + return next + }) + } + const handleRentalCreate = (b: RentalBooking) => { setRentalBookings(prev => [...prev, b]) } @@ -45,6 +53,7 @@ export function CalendarPage() { bookings={bookings} onBookingCreate={handleCreate} onBookingUpdate={handleUpdate} + onBookingBulkUpdate={handleBulkUpdate} fadingBookingIds={fadingBookings} rentalObjects={isRentalActive ? RENTAL_OBJECTS : undefined} rentalBookings={isRentalActive ? rentalBookings : undefined} diff --git a/src/pages/RentalPage.tsx b/src/pages/RentalPage.tsx index cb951ca..7c80bf0 100644 --- a/src/pages/RentalPage.tsx +++ b/src/pages/RentalPage.tsx @@ -234,7 +234,7 @@ export function RentalPage() { const [editObj, setEditObj] = useState(null) const [createModal, setCreateModal] = useState(false) const [selectedObj, setSelectedObj] = useState(null) - const [rentalModal, setRentalModal] = useState<{ obj: RentalObject; date: string } | null>(null) + const [rentalModal, setRentalModal] = useState<{ obj: RentalObject; date: string; editBooking?: RentalBooking } | null>(null) const today = format(new Date(), 'yyyy-MM-dd') @@ -252,8 +252,11 @@ export function RentalPage() { if (selectedObj?.id === id) setSelectedObj(null) } - const handleBookingCreate = (b: RentalBooking) => { - setBookings(prev => [...prev, b]) + const handleBookingSave = (b: RentalBooking) => { + setBookings(prev => { + const exists = prev.find(x => x.id === b.id) + return exists ? prev.map(x => x.id === b.id ? b : x) : [...prev, b] + }) setRentalModal(null) } @@ -378,8 +381,8 @@ export function RentalPage() { - {['Дата', 'Время', 'Гость', 'Телефон', 'Сумма', 'Статус'].map(h => ( - + {['Дата', 'Время', 'Гость', 'Телефон', 'Сумма', 'Статус', ''].map((h, i) => ( + ))} @@ -408,6 +411,15 @@ export function RentalPage() { {b.status === 'confirmed' ? 'Подтверждено' : 'Отменено'} + ))} @@ -443,8 +455,9 @@ export function RentalPage() { obj={rentalModal.obj} date={rentalModal.date} existingBookings={bookings.filter(b => b.objectId === rentalModal.obj.id && b.date === rentalModal.date)} + editBooking={rentalModal.editBooking} onClose={() => setRentalModal(null)} - onSave={handleBookingCreate} + onSave={handleBookingSave} /> )}
{h}{h}
+ +