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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
X, Mail, Calendar, Users, CreditCard, Tag, CheckCircle, XCircle,
|
X, Mail, Calendar, Users, CreditCard, Tag, CheckCircle, XCircle,
|
||||||
Printer, ScanLine, Banknote, Building2, Plus,
|
Printer, ScanLine, Banknote, Building2, Plus, Pencil,
|
||||||
FileText, FileCheck, Receipt, IdCard, AlertTriangle,
|
FileText, FileCheck, Receipt, IdCard, AlertTriangle,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
SOURCE_COLORS, SOURCE_LABELS, formatCurrency, nightsCount,
|
SOURCE_COLORS, SOURCE_LABELS, formatCurrency, nightsCount,
|
||||||
} from '../../lib/utils'
|
} from '../../lib/utils'
|
||||||
import type { Booking, Room } from '../../types'
|
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 { Badge } from '../ui/Badge'
|
||||||
import { MOCK_DISCOUNTS } from '../../pages/DiscountsPage'
|
import { MOCK_DISCOUNTS } from '../../pages/DiscountsPage'
|
||||||
|
|
||||||
@@ -52,13 +55,57 @@ const DOCUMENTS = [
|
|||||||
interface BookingDetailPanelProps {
|
interface BookingDetailPanelProps {
|
||||||
booking: Booking
|
booking: Booking
|
||||||
room?: Room
|
room?: Room
|
||||||
|
rooms?: Room[]
|
||||||
|
allBookings?: Booking[]
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onUpdate: (id: string, data: Partial<Booking>) => void
|
onUpdate: (id: string, data: Partial<Booking>) => void
|
||||||
|
onBulkUpdate?: (updates: Array<{ id: string; data: Partial<Booking> }>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BookingDetailPanel({ booking, room, onClose, onUpdate }: BookingDetailPanelProps) {
|
export function BookingDetailPanel({ booking, room, rooms, allBookings, onClose, onUpdate, onBulkUpdate }: BookingDetailPanelProps) {
|
||||||
const [tab, setTab] = useState<Tab>('booking')
|
const [tab, setTab] = useState<Tab>('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<Booking | null>(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
|
// Passport data
|
||||||
const nameParts = booking.guestName.split(' ')
|
const nameParts = booking.guestName.split(' ')
|
||||||
const [passport, setPassport] = useState<PassportData>({
|
const [passport, setPassport] = useState<PassportData>({
|
||||||
@@ -191,27 +238,124 @@ export function BookingDetailPanel({ booking, room, onClose, onUpdate }: Booking
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
{/* Dates — read or edit mode */}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1 mb-1">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<Calendar size={12} /> Заезд
|
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1">
|
||||||
</p>
|
<Calendar size={12} /> Даты проживания
|
||||||
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
|
|
||||||
{new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(new Date(booking.checkIn))}
|
|
||||||
</p>
|
</p>
|
||||||
|
{!editDates && canEditDates && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setEditDates(true); setConflictBooking(null) }}
|
||||||
|
className="flex items-center gap-1 text-xs text-brand-600 dark:text-brand-400 hover:text-brand-700 dark:hover:text-brand-300 font-medium"
|
||||||
|
>
|
||||||
|
<Pencil size={11} /> Изменить
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1 mb-1">
|
{editDates ? (
|
||||||
<Calendar size={12} /> Выезд
|
<div className="space-y-3 p-3 rounded-xl border border-brand-200 dark:border-brand-700 bg-brand-50/30 dark:bg-brand-900/10">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className={lbl}>Заезд</label>
|
||||||
|
<input type="date" className="input text-sm" value={newCheckIn}
|
||||||
|
onChange={e => { setNewCheckIn(e.target.value); setConflictBooking(null) }} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={lbl}>Выезд</label>
|
||||||
|
<input type="date" className="input text-sm" value={newCheckOut} min={newCheckIn}
|
||||||
|
onChange={e => { setNewCheckOut(e.target.value); setConflictBooking(null) }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rooms && rooms.length > 1 && (
|
||||||
|
<div>
|
||||||
|
<label className={lbl}>Номер</label>
|
||||||
|
<select className="input text-sm" value={newRoomId}
|
||||||
|
onChange={e => { setNewRoomId(e.target.value); setConflictBooking(null) }}>
|
||||||
|
{rooms.map(r => (
|
||||||
|
<option key={r.id} value={r.id}>№{r.number} — {r.type}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Conflict warning */}
|
||||||
|
{conflictBooking && (
|
||||||
|
<div className="rounded-xl border border-red-200 dark:border-red-700 bg-red-50 dark:bg-red-900/20 p-3 space-y-2">
|
||||||
|
<div className="flex items-start gap-2 text-red-700 dark:text-red-400 text-xs font-medium">
|
||||||
|
<AlertTriangle size={13} className="shrink-0 mt-0.5" />
|
||||||
|
<span>
|
||||||
|
Номер занят: {conflictBooking.guestName},
|
||||||
|
{fmtDate(conflictBooking.checkIn)} – {fmtDate(conflictBooking.checkOut)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{conflictBooking.status !== 'checked_in' && onBulkUpdate && rooms && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400">
|
||||||
|
Переместить {conflictBooking.guestName} в другой номер:
|
||||||
|
</p>
|
||||||
|
<select className="input text-sm" value={conflictResolveRoomId}
|
||||||
|
onChange={e => setConflictResolveRoomId(e.target.value)}>
|
||||||
|
<option value="">— выберите номер —</option>
|
||||||
|
{rooms.filter(r => r.id !== newRoomId).map(r => (
|
||||||
|
<option key={r.id} value={r.id}>№{r.number} — {r.type}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
disabled={!conflictResolveRoomId}
|
||||||
|
onClick={handleResolveWithRoomSwap}
|
||||||
|
className="w-full btn-primary text-xs py-1.5 justify-center disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Переместить и сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button onClick={handleForceUpdate}
|
||||||
|
className="w-full btn-secondary text-xs py-1.5 justify-center text-red-600 dark:text-red-400 border-red-200 dark:border-red-700">
|
||||||
|
Сохранить принудительно
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => { setEditDates(false); setConflictBooking(null) }}
|
||||||
|
className="btn-secondary flex-1 justify-center text-sm py-1.5"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
{!conflictBooking && (
|
||||||
|
<button onClick={handleSaveDates}
|
||||||
|
className="btn-primary flex-1 justify-center text-sm py-1.5">
|
||||||
|
Сохранить
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium mb-1">Заезд</p>
|
||||||
|
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
|
||||||
|
{fmtDate(booking.checkIn)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium mb-1">Выезд</p>
|
||||||
|
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
|
||||||
|
{fmtDate(booking.checkOut)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!editDates && (
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||||
|
{nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
|
)}
|
||||||
{new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(new Date(booking.checkOut))}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
|
||||||
{nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1 mb-1">
|
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium flex items-center gap-1 mb-1">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface BookingCalendarProps {
|
|||||||
bookings: Booking[]
|
bookings: Booking[]
|
||||||
onBookingCreate: (b: Partial<Booking>) => void
|
onBookingCreate: (b: Partial<Booking>) => void
|
||||||
onBookingUpdate: (id: string, b: Partial<Booking>) => void
|
onBookingUpdate: (id: string, b: Partial<Booking>) => void
|
||||||
|
onBookingBulkUpdate?: (updates: Array<{ id: string; data: Partial<Booking> }>) => void
|
||||||
fadingBookingIds?: Set<string>
|
fadingBookingIds?: Set<string>
|
||||||
rentalObjects?: RentalObject[]
|
rentalObjects?: RentalObject[]
|
||||||
rentalBookings?: RentalBooking[]
|
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'
|
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 [startDate, setStartDate] = useState(() => startOfDay(new Date()))
|
||||||
const [visibleDays, setVisibleDays] = useState(DAYS_VISIBLE)
|
const [visibleDays, setVisibleDays] = useState(DAYS_VISIBLE)
|
||||||
|
|
||||||
@@ -602,11 +603,17 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
|
|||||||
<BookingDetailPanel
|
<BookingDetailPanel
|
||||||
booking={selectedBooking}
|
booking={selectedBooking}
|
||||||
room={rooms.find(r => r.id === selectedBooking.roomId)}
|
room={rooms.find(r => r.id === selectedBooking.roomId)}
|
||||||
|
rooms={rooms}
|
||||||
|
allBookings={bookings}
|
||||||
onClose={() => setSelectedBooking(null)}
|
onClose={() => setSelectedBooking(null)}
|
||||||
onUpdate={(id, data) => {
|
onUpdate={(id, data) => {
|
||||||
onBookingUpdate(id, data)
|
onBookingUpdate(id, data)
|
||||||
setSelectedBooking(null)
|
setSelectedBooking(null)
|
||||||
}}
|
}}
|
||||||
|
onBulkUpdate={onBookingBulkUpdate ? (updates) => {
|
||||||
|
onBookingBulkUpdate(updates)
|
||||||
|
setSelectedBooking(null)
|
||||||
|
} : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface RentalBookingModalProps {
|
|||||||
obj: RentalObject
|
obj: RentalObject
|
||||||
date: string // 'yyyy-MM-dd'
|
date: string // 'yyyy-MM-dd'
|
||||||
existingBookings: RentalBooking[]
|
existingBookings: RentalBooking[]
|
||||||
|
editBooking?: RentalBooking // pass to edit an existing booking
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSave: (b: RentalBooking) => void
|
onSave: (b: RentalBooking) => void
|
||||||
}
|
}
|
||||||
@@ -19,14 +20,15 @@ function formatHour(h: number): string {
|
|||||||
return `${h}:00`
|
return `${h}:00`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RentalBookingModal({ obj, date, existingBookings, onClose, onSave }: RentalBookingModalProps) {
|
export function RentalBookingModal({ obj, date, existingBookings, editBooking, onClose, onSave }: RentalBookingModalProps) {
|
||||||
const [isFullDay, setIsFullDay] = useState(false)
|
const isEdit = !!editBooking
|
||||||
const [startHour, setStartHour] = useState(obj.openHour)
|
const [isFullDay, setIsFullDay] = useState(editBooking?.isFullDay ?? false)
|
||||||
const [endHour, setEndHour] = useState(Math.min(obj.openHour + 2, obj.closeHour))
|
const [startHour, setStartHour] = useState(editBooking?.startHour ?? obj.openHour)
|
||||||
const [guestName, setGuestName] = useState('')
|
const [endHour, setEndHour] = useState(editBooking?.endHour ?? Math.min(obj.openHour + 2, obj.closeHour))
|
||||||
const [guestPhone, setGuestPhone] = useState('')
|
const [guestName, setGuestName] = useState(editBooking?.guestName ?? '')
|
||||||
const [notes, setNotes] = useState('')
|
const [guestPhone, setGuestPhone] = useState(editBooking?.guestPhone ?? '')
|
||||||
const [paidAmount, setPaidAmount] = useState<string>('')
|
const [notes, setNotes] = useState(editBooking?.notes ?? '')
|
||||||
|
const [paidAmount, setPaidAmount] = useState<string>(editBooking?.paidAmount ? String(editBooking.paidAmount) : '')
|
||||||
const [payMethod, setPayMethod] = useState<'cash' | 'card' | 'transfer'>('cash')
|
const [payMethod, setPayMethod] = useState<'cash' | 'card' | 'transfer'>('cash')
|
||||||
|
|
||||||
const hours = isFullDay
|
const hours = isFullDay
|
||||||
@@ -39,18 +41,19 @@ export function RentalBookingModal({ obj, date, existingBookings, onClose, onSav
|
|||||||
|
|
||||||
const maxHoursOk = !obj.maxHoursPerSlot || hours <= obj.maxHoursPerSlot
|
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.isFullDay && b.status !== 'cancelled' &&
|
||||||
b.startHour < endHour && b.endHour > startHour,
|
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 canSave = guestName.trim() !== '' && hours > 0 && maxHoursOk && !isTimeConflict && !hasFullDayConflict
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!canSave) return
|
if (!canSave) return
|
||||||
onSave({
|
onSave({
|
||||||
id: `rb-${Date.now()}`,
|
id: editBooking?.id ?? `rb-${Date.now()}`,
|
||||||
objectId: obj.id,
|
objectId: obj.id,
|
||||||
date,
|
date,
|
||||||
isFullDay,
|
isFullDay,
|
||||||
@@ -284,7 +287,7 @@ export function RentalBookingModal({ obj, date, existingBookings, onClose, onSav
|
|||||||
disabled={!canSave}
|
disabled={!canSave}
|
||||||
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Забронировать
|
{isEdit ? 'Сохранить' : 'Забронировать'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -363,11 +363,21 @@ export function BookingsPage() {
|
|||||||
<BookingDetailPanel
|
<BookingDetailPanel
|
||||||
booking={selected}
|
booking={selected}
|
||||||
room={room(selected.roomId)}
|
room={room(selected.roomId)}
|
||||||
|
rooms={MOCK_ROOMS}
|
||||||
|
allBookings={bookings}
|
||||||
onClose={() => setSelected(null)}
|
onClose={() => setSelected(null)}
|
||||||
onUpdate={(id, data) => {
|
onUpdate={(id, data) => {
|
||||||
setBookings(prev => prev.map(b => b.id === id ? { ...b, ...data } : b))
|
setBookings(prev => prev.map(b => b.id === id ? { ...b, ...data } : b))
|
||||||
setSelected(null)
|
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)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ export function CalendarPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBulkUpdate = (updates: Array<{ id: string; data: Partial<Booking> }>) => {
|
||||||
|
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) => {
|
const handleRentalCreate = (b: RentalBooking) => {
|
||||||
setRentalBookings(prev => [...prev, b])
|
setRentalBookings(prev => [...prev, b])
|
||||||
}
|
}
|
||||||
@@ -45,6 +53,7 @@ export function CalendarPage() {
|
|||||||
bookings={bookings}
|
bookings={bookings}
|
||||||
onBookingCreate={handleCreate}
|
onBookingCreate={handleCreate}
|
||||||
onBookingUpdate={handleUpdate}
|
onBookingUpdate={handleUpdate}
|
||||||
|
onBookingBulkUpdate={handleBulkUpdate}
|
||||||
fadingBookingIds={fadingBookings}
|
fadingBookingIds={fadingBookings}
|
||||||
rentalObjects={isRentalActive ? RENTAL_OBJECTS : undefined}
|
rentalObjects={isRentalActive ? RENTAL_OBJECTS : undefined}
|
||||||
rentalBookings={isRentalActive ? rentalBookings : undefined}
|
rentalBookings={isRentalActive ? rentalBookings : undefined}
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ export function RentalPage() {
|
|||||||
const [editObj, setEditObj] = useState<RentalObject | null>(null)
|
const [editObj, setEditObj] = useState<RentalObject | null>(null)
|
||||||
const [createModal, setCreateModal] = useState(false)
|
const [createModal, setCreateModal] = useState(false)
|
||||||
const [selectedObj, setSelectedObj] = useState<RentalObject | null>(null)
|
const [selectedObj, setSelectedObj] = useState<RentalObject | null>(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')
|
const today = format(new Date(), 'yyyy-MM-dd')
|
||||||
|
|
||||||
@@ -252,8 +252,11 @@ export function RentalPage() {
|
|||||||
if (selectedObj?.id === id) setSelectedObj(null)
|
if (selectedObj?.id === id) setSelectedObj(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBookingCreate = (b: RentalBooking) => {
|
const handleBookingSave = (b: RentalBooking) => {
|
||||||
setBookings(prev => [...prev, b])
|
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)
|
setRentalModal(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,8 +381,8 @@ export function RentalPage() {
|
|||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-700/40">
|
<tr className="border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-700/40">
|
||||||
{['Дата', 'Время', 'Гость', 'Телефон', 'Сумма', 'Статус'].map(h => (
|
{['Дата', 'Время', 'Гость', 'Телефон', 'Сумма', 'Статус', ''].map((h, i) => (
|
||||||
<th key={h} className="text-left px-4 py-2.5 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">{h}</th>
|
<th key={i} className="text-left px-4 py-2.5 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">{h}</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -408,6 +411,15 @@ export function RentalPage() {
|
|||||||
{b.status === 'confirmed' ? 'Подтверждено' : 'Отменено'}
|
{b.status === 'confirmed' ? 'Подтверждено' : 'Отменено'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
<button
|
||||||
|
onClick={e => { e.stopPropagation(); setRentalModal({ obj: selectedObj, date: b.date, editBooking: b }) }}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-400 hover:text-slate-700 dark:hover:text-slate-300 transition-colors"
|
||||||
|
title="Редактировать"
|
||||||
|
>
|
||||||
|
<Pencil size={13} />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -443,8 +455,9 @@ export function RentalPage() {
|
|||||||
obj={rentalModal.obj}
|
obj={rentalModal.obj}
|
||||||
date={rentalModal.date}
|
date={rentalModal.date}
|
||||||
existingBookings={bookings.filter(b => b.objectId === rentalModal.obj.id && b.date === rentalModal.date)}
|
existingBookings={bookings.filter(b => b.objectId === rentalModal.obj.id && b.date === rentalModal.date)}
|
||||||
|
editBooking={rentalModal.editBooking}
|
||||||
onClose={() => setRentalModal(null)}
|
onClose={() => setRentalModal(null)}
|
||||||
onSave={handleBookingCreate}
|
onSave={handleBookingSave}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user