Files
hotelsync/src/pages/BookingsPage.tsx

624 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useMemo, useEffect } from 'react'
import { Search, Plus, ArrowUp, ArrowDown, ArrowUpDown, Clock, Pencil, Loader2 } from 'lucide-react'
import { useModules } from '../contexts/ModulesContext'
import { useAuth } from '../contexts/AuthContext'
import { api } from '../lib/api'
import type { RentalObjectApi, RentalBookingApi } from '../lib/api'
import type { Booking, BookingStatus, Room } from '../types'
import type { RentalObject, RentalBooking } from '../data/rentalData'
import { RentalBookingModal } from '../components/rental/RentalBookingModal'
import { cn, BOOKING_STATUS_BADGE, BOOKING_STATUS_LABELS, SOURCE_COLORS, SOURCE_LABELS, formatCurrency, nightsCount } from '../lib/utils'
import { Badge } from '../components/ui/Badge'
import { BookingModal } from '../components/bookings/BookingModal'
import { BookingDetailPanel } from '../components/bookings/BookingDetailPanel'
import { format, addDays } from 'date-fns'
const STATUS_FILTERS: { label: string; value: BookingStatus | 'all' }[] = [
{ label: 'Все', value: 'all' },
{ label: 'Подтверждённые', value: 'confirmed' },
{ label: 'Заселены', value: 'checked_in' },
{ label: 'Запросы', value: 'inquiry' },
{ label: 'Выехали', value: 'checked_out' },
{ label: 'Отменены', value: 'cancelled' },
]
type SortKey = 'guestName' | 'checkIn' | 'checkOut' | 'status' | 'source' | 'totalAmount'
const COLUMNS: { key: SortKey | null; label: string }[] = [
{ key: 'guestName', label: 'Гость' },
{ key: null, label: 'Номер' },
{ key: 'checkIn', label: 'Заезд' },
{ key: 'checkOut', label: 'Выезд' },
{ key: 'status', label: 'Статус' },
{ key: 'source', label: 'Источник' },
{ key: 'totalAmount', label: 'Сумма' },
{ key: null, label: '' },
]
type Tab = 'rooms' | 'rental'
function fromObjApi(o: RentalObjectApi): RentalObject {
return {
id: o.id, name: o.name, icon: o.icon, color: o.color,
textColor: o.textColor, pricePerHour: o.pricePerHour, pricePerDay: o.pricePerDay,
openHour: o.openHour, closeHour: o.closeHour,
maxHoursPerSlot: o.maxHoursPerSlot ?? undefined,
bufferMinutes: o.bufferMinutes ?? undefined,
}
}
function fromBookingApi(b: RentalBookingApi): RentalBooking {
return {
id: b.id, objectId: b.objectId, date: (b.date as string).slice(0, 10),
isFullDay: b.isFullDay, startHour: b.startHour, endHour: b.endHour,
guestName: b.guestName, guestPhone: b.guestPhone ?? '',
linkedRoomId: b.linkedRoomId ?? undefined,
totalAmount: b.totalAmount, paidAmount: b.paidAmount,
status: b.status, notes: b.notes ?? undefined,
}
}
function fmtMin(m: number) {
return `${Math.floor(m / 60).toString().padStart(2, '0')}:${(m % 60).toString().padStart(2, '0')}`
}
export function BookingsPage() {
const { user } = useAuth()
const slug = user?.hotelSlug ?? ''
const { statuses } = useModules()
const isRentalActive = statuses['rental'] === 'active' || statuses['rental'] === 'trial'
const [tab, setTab] = useState<Tab>('rooms')
const [rooms, setRooms] = useState<Room[]>([])
const [bookings, setBookings] = useState<Booking[]>([])
const [loading, setLoading] = useState(true)
const [rentalObjects, setRentalObjects] = useState<RentalObject[]>([])
const [rentalBookings, setRentalBookings] = useState<RentalBooking[]>([])
const [newRentalStep, setNewRentalStep] = useState<'idle' | 'pick'>('idle')
const [rentalPickObj, setRentalPickObj] = useState('')
const [rentalPickDate, setRentalPickDate] = useState(format(new Date(), 'yyyy-MM-dd'))
const [showRentalModal, setShowRentalModal] = useState<{ obj: RentalObject; date: string; editBooking?: RentalBooking } | null>(null)
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState<BookingStatus | 'all'>('all')
const [selected, setSelected] = useState<Booking | null>(null)
const [showCreateModal, setShowCreateModal] = useState(false)
const [sortKey, setSortKey] = useState<SortKey | null>(null)
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
useEffect(() => {
if (!slug) return
Promise.all([
api.rooms.list(slug),
api.bookings.list(slug),
api.rental.listObjects(slug),
api.rental.listBookings(slug),
])
.then(([r, b, objs, rbks]) => {
setRooms(r)
setBookings(b)
const convertedObjs = objs.map(fromObjApi)
setRentalObjects(convertedObjs)
setRentalBookings(rbks.map(fromBookingApi))
setRentalPickObj(convertedObjs[0]?.id ?? '')
})
.catch(console.error)
.finally(() => setLoading(false))
}, [slug])
const handleSort = (key: SortKey) => {
if (sortKey === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc')
else { setSortKey(key); setSortDir('asc') }
}
const filtered = bookings.filter(b => {
const matchSearch = search === '' ||
b.guestName.toLowerCase().includes(search.toLowerCase()) ||
b.guestEmail.toLowerCase().includes(search.toLowerCase()) ||
b.id.includes(search)
const matchStatus = statusFilter === 'all' || b.status === statusFilter
return matchSearch && matchStatus
})
const sorted = useMemo(() => {
if (!sortKey) return filtered
return [...filtered].sort((a, b) => {
const av = a[sortKey]
const bv = b[sortKey]
let cmp = 0
if (typeof av === 'string' && typeof bv === 'string') {
cmp = av.localeCompare(bv, 'ru')
} else {
cmp = (av as number) - (bv as number)
}
return sortDir === 'asc' ? cmp : -cmp
})
}, [filtered, sortKey, sortDir])
const filteredRental = rentalBookings.filter(b => {
if (search === '') return true
return b.guestName.toLowerCase().includes(search.toLowerCase()) ||
b.guestPhone.includes(search)
})
const room = (id: string) => rooms.find(r => r.id === id)
const rentalObj = (id: string) => rentalObjects.find(o => o.id === id)
const handleCreateBooking = async (data: Partial<Booking>) => {
try {
const created = await api.bookings.create(slug, {
roomId: data.roomId, guestName: data.guestName, guestEmail: data.guestEmail, guestPhone: data.guestPhone,
checkIn: data.checkIn, checkOut: data.checkOut,
adults: data.adults, children: data.children,
status: data.status, source: data.source,
totalAmount: data.totalAmount, notes: data.notes,
})
setBookings(prev => [...prev, created])
setShowCreateModal(false)
} catch (err) {
console.error('Failed to create booking:', err)
}
}
const handleUpdateBooking = async (id: string, data: Partial<Booking>) => {
try {
const updated = await api.bookings.update(slug, id, {
guestName: data.guestName, guestEmail: data.guestEmail, guestPhone: data.guestPhone,
checkIn: data.checkIn, checkOut: data.checkOut,
adults: data.adults, children: data.children,
status: data.status, source: data.source,
totalAmount: data.totalAmount, paidAmount: data.paidAmount, notes: data.notes,
})
setBookings(prev => prev.map(b => b.id === id ? updated : b))
setSelected(null)
} catch (err) {
console.error('Failed to update booking:', err)
}
}
const handleBulkUpdate = async (updates: Array<{ id: string; data: Partial<Booking> }>) => {
try {
const results = await Promise.all(
updates.map(u => api.bookings.update(slug, u.id, { status: u.data.status })),
)
setBookings(prev => {
let next = [...prev]
results.forEach(r => { next = next.map(b => b.id === r.id ? r : b) })
return next
})
setSelected(null)
} catch (err) {
console.error('Failed to bulk update bookings:', err)
}
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 size={24} className="animate-spin text-brand-600" />
</div>
)
}
// Step-1 modal: occupied slots for selected object + date
const pickObj = rentalObjects.find(o => o.id === rentalPickObj)
const pickBookings = rentalBookings.filter(
b => b.objectId === rentalPickObj && b.date === rentalPickDate && b.status === 'confirmed',
)
const pickOpenMin = (pickObj?.openHour ?? 8) * 60
const pickCloseMin = (pickObj?.closeHour ?? 22) * 60
const pickTotalMin = pickCloseMin - pickOpenMin
const pickHasFullDay = pickBookings.some(b => b.isFullDay)
const fmtPickMin = (m: number) => `${Math.floor(m / 60)}:${String(m % 60).padStart(2, '0')}`
return (
<div className="p-4 md:p-6 space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">Бронирования</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">
{tab === 'rooms' ? `${sorted.length} из ${bookings.length}` : `${filteredRental.length} аренд`}
</p>
</div>
{tab === 'rooms' && (
<button onClick={() => setShowCreateModal(true)} className="btn-primary">
<Plus size={15} />
<span className="hidden sm:inline">Новое</span>
</button>
)}
{tab === 'rental' && (
<button onClick={() => setNewRentalStep('pick')} className="btn-primary">
<Plus size={15} />
<span className="hidden sm:inline">Новая аренда</span>
</button>
)}
</div>
{/* Tabs */}
{isRentalActive && (
<div className="flex gap-1 p-1 bg-slate-100 dark:bg-slate-700/50 rounded-xl w-fit">
<button
onClick={() => setTab('rooms')}
className={cn(
'px-4 py-1.5 rounded-lg text-sm font-medium transition-colors',
tab === 'rooms'
? 'bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 shadow-sm'
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200',
)}
>
Номера
</button>
<button
onClick={() => setTab('rental')}
className={cn(
'px-4 py-1.5 rounded-lg text-sm font-medium transition-colors flex items-center gap-1.5',
tab === 'rental'
? 'bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 shadow-sm'
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200',
)}
>
<Clock size={13} />
Аренда
</button>
</div>
)}
{/* Search */}
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
type="text"
className="input pl-9"
placeholder={tab === 'rooms' ? 'Поиск по гостю, email, ID...' : 'Поиск по гостю, телефону...'}
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
{tab === 'rooms' && (
<div className="flex gap-1.5 flex-wrap">
{STATUS_FILTERS.map(f => (
<button
key={f.value}
onClick={() => setStatusFilter(f.value)}
className={cn(
'px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors',
statusFilter === f.value
? 'bg-brand-600 text-white border-brand-600'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-600 hover:border-brand-400',
)}
>
{f.label}
</button>
))}
</div>
)}
</div>
{/* Room bookings table */}
{tab === 'rooms' && (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-700/40">
{COLUMNS.map(col => (
<th
key={col.label}
className={cn(
'text-left px-4 py-3 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide',
(col.label === 'Выезд' || col.label === 'Источник') && 'hidden sm:table-cell',
col.label === 'Номер' && 'hidden md:table-cell',
)}
>
{col.key ? (
<button
onClick={() => handleSort(col.key!)}
className="flex items-center gap-1 hover:text-slate-700 dark:hover:text-slate-200 transition-colors"
>
{col.label}
{sortKey === col.key
? sortDir === 'asc'
? <ArrowUp size={12} className="text-brand-600" />
: <ArrowDown size={12} className="text-brand-600" />
: <ArrowUpDown size={12} className="opacity-30" />
}
</button>
) : col.label}
</th>
))}
</tr>
</thead>
<tbody>
{sorted.map(b => {
const r = room(b.roomId)
const nights = nightsCount(b.checkIn, b.checkOut)
return (
<tr
key={b.id}
className="border-b border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-700/30 cursor-pointer transition-colors"
onClick={() => setSelected(b)}
>
<td className="px-4 py-3">
<p className="font-medium text-slate-900 dark:text-slate-100">{b.guestName}</p>
<p className="text-xs text-slate-500 dark:text-slate-400">{b.guestEmail}</p>
</td>
<td className="px-4 py-3 hidden md:table-cell">
<p className="font-medium text-slate-900 dark:text-slate-100">
{r ? `${r.number}` : '—'}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400">{r?.type}</p>
</td>
<td className="px-4 py-3 text-slate-700 dark:text-slate-300">
{new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'short' }).format(new Date(b.checkIn))}
<span className="ml-1 text-xs text-slate-400 sm:hidden">· {nights}н</span>
</td>
<td className="px-4 py-3 text-slate-700 dark:text-slate-300 hidden sm:table-cell">
<span>{new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'short' }).format(new Date(b.checkOut))}</span>
<span className="ml-1.5 text-xs text-slate-400">{nights}н</span>
</td>
<td className="px-4 py-3">
<Badge className={BOOKING_STATUS_BADGE[b.status]}>
{BOOKING_STATUS_LABELS[b.status]}
</Badge>
</td>
<td className="px-4 py-3 hidden sm:table-cell">
<Badge className={SOURCE_COLORS[b.source]}>
{SOURCE_LABELS[b.source]}
</Badge>
</td>
<td className="px-4 py-3">
<p className="font-semibold text-slate-900 dark:text-slate-100">
{formatCurrency(b.totalAmount)}
</p>
{b.paidAmount < b.totalAmount && (
<p className="text-xs text-red-500">
-{formatCurrency(b.totalAmount - b.paidAmount)}
</p>
)}
</td>
<td className="px-4 py-3">
<span className="text-brand-600 dark:text-brand-400 text-xs font-medium">
Открыть
</span>
</td>
</tr>
)
})}
</tbody>
</table>
{sorted.length === 0 && (
<div className="text-center py-12 text-slate-500 dark:text-slate-400">
Бронирования не найдены
</div>
)}
</div>
</div>
)}
{/* Rental bookings table */}
{tab === 'rental' && (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-700/40">
{['Гость', 'Объект', 'Дата', 'Время', 'Сумма', 'Статус', ''].map((h, i) => (
<th key={i} className="text-left px-4 py-3 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">
{h}
</th>
))}
</tr>
</thead>
<tbody>
{filteredRental.map(b => {
const obj = rentalObj(b.objectId)
return (
<tr key={b.id} onClick={() => obj && setShowRentalModal({ obj, date: b.date, editBooking: b })} className="border-b border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors cursor-pointer">
<td className="px-4 py-3">
<p className="font-medium text-slate-900 dark:text-slate-100">{b.guestName}</p>
<p className="text-xs text-slate-500 dark:text-slate-400">{b.guestPhone}</p>
</td>
<td className="px-4 py-3">
{obj && (
<div className="flex items-center gap-1.5">
<span className="text-base">{obj.icon}</span>
<span className="text-slate-800 dark:text-slate-200 font-medium">{obj.name}</span>
</div>
)}
</td>
<td className="px-4 py-3 text-slate-700 dark:text-slate-300">
{new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'short', weekday: 'short' }).format(new Date(b.date))}
</td>
<td className="px-4 py-3 text-slate-700 dark:text-slate-300">
{b.isFullDay
? <span className="text-xs font-medium px-2 py-0.5 bg-slate-100 dark:bg-slate-700 rounded-full">Весь день</span>
: `${fmtMin(b.startHour)} ${fmtMin(b.endHour)}`
}
</td>
<td className="px-4 py-3 font-semibold text-slate-900 dark:text-slate-100">
{formatCurrency(b.totalAmount)}
</td>
<td className="px-4 py-3">
<Badge className={b.status === 'confirmed'
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300'
: 'bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400'
}>
{b.status === 'confirmed' ? 'Подтверждено' : 'Отменено'}
</Badge>
</td>
<td className="px-4 py-3">
{obj && (
<button
onClick={() => setShowRentalModal({ obj, 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"
>
<Pencil size={13} />
</button>
)}
</td>
</tr>
)
})}
</tbody>
</table>
{filteredRental.length === 0 && (
<div className="text-center py-12 text-slate-500 dark:text-slate-400">
Записи об аренде не найдены
</div>
)}
</div>
</div>
)}
{/* Create booking modal */}
{showCreateModal && rooms.length > 0 && (
<BookingModal
open
draft={{
roomId: rooms[0].id,
checkIn: format(new Date(), 'yyyy-MM-dd'),
checkOut: format(addDays(new Date(), 1), 'yyyy-MM-dd'),
}}
rooms={rooms}
onClose={() => setShowCreateModal(false)}
onSave={handleCreateBooking}
/>
)}
{/* Detail panel */}
{selected && (
<BookingDetailPanel
booking={selected}
room={room(selected.roomId)}
rooms={rooms}
allBookings={bookings}
slug={slug}
onClose={() => setSelected(null)}
onUpdate={handleUpdateBooking}
onBulkUpdate={handleBulkUpdate}
/>
)}
{/* New rental — step 1 */}
{newRentalStep === 'pick' && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-sm p-6 space-y-4">
<h2 className="font-semibold text-slate-900 dark:text-slate-100 text-lg">Новая аренда</h2>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Объект</label>
<select className="input" value={rentalPickObj} onChange={e => setRentalPickObj(e.target.value)}>
{rentalObjects.map(o => (
<option key={o.id} value={o.id}>{o.icon} {o.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Дата</label>
<input type="date" className="input" value={rentalPickDate} onChange={e => setRentalPickDate(e.target.value)} />
</div>
{/* Occupied slots preview */}
{pickObj && (
<div className="space-y-2">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide">
Занятость · {new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(new Date(rentalPickDate))}
</p>
{pickHasFullDay ? (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700">
<span className="w-2 h-2 rounded-full bg-red-500 shrink-0" />
<span className="text-sm text-red-700 dark:text-red-300 font-medium">Весь день занят</span>
</div>
) : pickBookings.length === 0 ? (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-700">
<span className="w-2 h-2 rounded-full bg-emerald-500 shrink-0" />
<span className="text-sm text-emerald-700 dark:text-emerald-300 font-medium">Свободно весь день</span>
</div>
) : (
<div className="space-y-2">
{/* Timeline bar */}
<div className="relative h-5 rounded-md overflow-hidden bg-emerald-100 dark:bg-emerald-900/20">
{pickBookings.filter(b => !b.isFullDay).map(b => {
const left = ((b.startHour - pickOpenMin) / pickTotalMin) * 100
const width = ((b.endHour - b.startHour) / pickTotalMin) * 100
return (
<div
key={b.id}
className="absolute top-0 h-full bg-red-400 dark:bg-red-600 opacity-80"
style={{ left: `${Math.max(0, left)}%`, width: `${Math.min(100 - Math.max(0, left), width)}%` }}
title={`${b.guestName} ${fmtPickMin(b.startHour)}${fmtPickMin(b.endHour)}`}
/>
)
})}
</div>
{/* Hour labels */}
<div className="flex justify-between text-[10px] text-slate-400 dark:text-slate-500">
<span>{fmtPickMin(pickOpenMin)}</span>
<span>{fmtPickMin(Math.round((pickOpenMin + pickCloseMin) / 2))}</span>
<span>{fmtPickMin(pickCloseMin)}</span>
</div>
{/* Booking list */}
<div className="space-y-1 max-h-24 overflow-y-auto">
{pickBookings.filter(b => !b.isFullDay).map(b => (
<div key={b.id} className="flex items-center gap-2 text-xs">
<span className="w-1.5 h-1.5 rounded-full bg-red-400 shrink-0" />
<span className="font-medium text-slate-700 dark:text-slate-300 tabular-nums">
{fmtPickMin(b.startHour)}{fmtPickMin(b.endHour)}
</span>
<span className="text-slate-400 dark:text-slate-500 truncate">{b.guestName}</span>
</div>
))}
</div>
</div>
)}
</div>
)}
<div className="flex gap-2 justify-end">
<button onClick={() => setNewRentalStep('idle')} className="btn-secondary">Отмена</button>
<button
className="btn-primary"
onClick={() => {
const obj = rentalObjects.find(o => o.id === rentalPickObj)
if (obj) { setShowRentalModal({ obj, date: rentalPickDate }); setNewRentalStep('idle') }
}}
>
Далее
</button>
</div>
</div>
</div>
)}
{/* New rental — step 2 */}
{showRentalModal && (
<RentalBookingModal
obj={showRentalModal.obj}
date={showRentalModal.date}
existingBookings={rentalBookings.filter(b => b.objectId === showRentalModal.obj.id && b.date === showRentalModal.date)}
editBooking={showRentalModal.editBooking}
onClose={() => setShowRentalModal(null)}
onSave={async (b) => {
if (!slug) return
const payload = {
object_id: b.objectId, date: b.date, is_full_day: b.isFullDay,
start_hour: b.startHour, end_hour: b.endHour,
guest_name: b.guestName, guest_phone: b.guestPhone || undefined,
linked_room_id: b.linkedRoomId || undefined,
total_amount: b.totalAmount, paid_amount: b.paidAmount ?? 0,
status: b.status, notes: b.notes || undefined,
}
const isNew = !rentalBookings.find(x => x.id === b.id)
const saved = isNew
? await api.rental.createBooking(slug, payload)
: await api.rental.updateBooking(slug, b.id, payload)
const converted = fromBookingApi(saved)
setRentalBookings(prev => isNew ? [...prev, converted] : prev.map(x => x.id === b.id ? converted : x))
setShowRentalModal(null)
}}
/>
)}
</div>
)
}