1006 lines
51 KiB
TypeScript
1006 lines
51 KiB
TypeScript
import React, { useEffect, useState, useRef } from 'react';
|
||
import { Search, Eye, XCircle, CheckCircle, Loader2, FileText, Plus, Mail } from 'lucide-react';
|
||
import bookingService, { Booking } from '../../features/bookings/services/bookingService';
|
||
import invoiceService from '../../features/payments/services/invoiceService';
|
||
import { toast } from 'react-toastify';
|
||
import Loading from '../../shared/components/Loading';
|
||
import Pagination from '../../shared/components/Pagination';
|
||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||
import { parseDateLocal } from '../../shared/utils/format';
|
||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||
import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal';
|
||
import { logger } from '../../shared/utils/logger';
|
||
|
||
const BookingManagementPage: React.FC = () => {
|
||
const { formatCurrency } = useFormatCurrency();
|
||
const navigate = useNavigate();
|
||
const [searchParams, setSearchParams] = useSearchParams();
|
||
const [bookings, setBookings] = useState<Booking[]>([]);
|
||
const [bookingsWithInvoices, setBookingsWithInvoices] = useState<Set<number>>(new Set());
|
||
const [loading, setLoading] = useState(true);
|
||
const [checkingInvoices, setCheckingInvoices] = useState(false);
|
||
const [selectedBooking, setSelectedBooking] = useState<Booking | null>(null);
|
||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||
const [updatingBookingId, setUpdatingBookingId] = useState<number | null>(null);
|
||
const [cancellingBookingId, setCancellingBookingId] = useState<number | null>(null);
|
||
const [creatingInvoice, setCreatingInvoice] = useState(false);
|
||
const [sendingEmail, setSendingEmail] = useState(false);
|
||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||
const [filters, setFilters] = useState({
|
||
search: '',
|
||
status: '',
|
||
});
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
const [totalPages, setTotalPages] = useState(1);
|
||
const [totalItems, setTotalItems] = useState(0);
|
||
const itemsPerPage = 5;
|
||
const showOnlyWithoutInvoices = searchParams.get('createInvoice') === 'true';
|
||
const abortControllerRef = useRef<AbortController | null>(null);
|
||
|
||
useEffect(() => {
|
||
setCurrentPage(1);
|
||
}, [filters]);
|
||
|
||
useEffect(() => {
|
||
// Cancel previous request if exists
|
||
if (abortControllerRef.current) {
|
||
abortControllerRef.current.abort();
|
||
}
|
||
|
||
// Create new abort controller
|
||
abortControllerRef.current = new AbortController();
|
||
|
||
fetchBookings();
|
||
|
||
// Cleanup: abort request on unmount
|
||
return () => {
|
||
if (abortControllerRef.current) {
|
||
abortControllerRef.current.abort();
|
||
}
|
||
};
|
||
}, [filters, currentPage]);
|
||
|
||
useEffect(() => {
|
||
// Fetch invoices for all bookings to check which have invoices
|
||
if (bookings.length > 0) {
|
||
fetchBookingsInvoices();
|
||
} else {
|
||
// Reset invoices set when bookings are cleared
|
||
setBookingsWithInvoices(new Set());
|
||
}
|
||
}, [bookings]);
|
||
|
||
const fetchBookings = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const response = await bookingService.getAllBookings({
|
||
...filters,
|
||
page: currentPage,
|
||
limit: itemsPerPage,
|
||
});
|
||
setBookings(response.data.bookings);
|
||
if (response.data.pagination) {
|
||
setTotalPages(response.data.pagination.totalPages);
|
||
setTotalItems(response.data.pagination.total);
|
||
}
|
||
} catch (error: any) {
|
||
// Handle AbortError silently
|
||
if (error.name === 'AbortError') {
|
||
return;
|
||
}
|
||
logger.error('Error fetching bookings', error);
|
||
toast.error(error.response?.data?.message || 'Unable to load bookings list');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const fetchBookingsInvoices = async () => {
|
||
try {
|
||
if (!bookings || bookings.length === 0) {
|
||
setBookingsWithInvoices(new Set());
|
||
setCheckingInvoices(false);
|
||
return;
|
||
}
|
||
|
||
setCheckingInvoices(true);
|
||
const invoiceChecks = await Promise.all(
|
||
bookings.map(async (booking) => {
|
||
try {
|
||
// Validate booking ID
|
||
if (!booking || !booking.id || isNaN(booking.id) || booking.id <= 0) {
|
||
return { bookingId: booking?.id || 0, hasInvoice: false };
|
||
}
|
||
|
||
const response = await invoiceService.getInvoicesByBooking(booking.id);
|
||
|
||
// Check response structure - handle both possible formats
|
||
const invoices = response.data?.invoices || response.data?.data?.invoices || [];
|
||
const hasInvoice = Array.isArray(invoices) && invoices.length > 0;
|
||
|
||
return {
|
||
bookingId: booking.id,
|
||
hasInvoice: hasInvoice
|
||
};
|
||
} catch (error: any) {
|
||
// Log error but don't fail the entire operation
|
||
logger.error(`Error checking invoice for booking ${booking?.id}`, error);
|
||
return { bookingId: booking?.id || 0, hasInvoice: false };
|
||
}
|
||
})
|
||
);
|
||
|
||
const withInvoices = new Set(
|
||
invoiceChecks
|
||
.filter(check => check.bookingId > 0 && check.hasInvoice)
|
||
.map(check => check.bookingId)
|
||
);
|
||
|
||
setBookingsWithInvoices(withInvoices);
|
||
} catch (error) {
|
||
logger.error('Error checking invoices', error);
|
||
// On error, assume no bookings have invoices to avoid blocking the UI
|
||
setBookingsWithInvoices(new Set());
|
||
} finally {
|
||
setCheckingInvoices(false);
|
||
}
|
||
};
|
||
|
||
const handleUpdateStatus = async (id: number, status: string) => {
|
||
try {
|
||
setUpdatingBookingId(id);
|
||
await bookingService.updateBooking(id, { status } as any);
|
||
toast.success('Status updated successfully');
|
||
await fetchBookings();
|
||
} catch (error: any) {
|
||
toast.error(error.response?.data?.message || 'Unable to update status');
|
||
} finally {
|
||
setUpdatingBookingId(null);
|
||
}
|
||
};
|
||
|
||
const handleCancelBooking = async (id: number) => {
|
||
if (!window.confirm('Are you sure you want to cancel this booking?')) return;
|
||
|
||
try {
|
||
setCancellingBookingId(id);
|
||
await bookingService.cancelBooking(id);
|
||
toast.success('Booking cancelled successfully');
|
||
await fetchBookings();
|
||
} catch (error: any) {
|
||
toast.error(error.response?.data?.message || 'Unable to cancel booking');
|
||
} finally {
|
||
setCancellingBookingId(null);
|
||
}
|
||
};
|
||
|
||
const handleCreateInvoice = async (bookingId: number, sendEmail: boolean = false) => {
|
||
try {
|
||
// Validate bookingId before proceeding
|
||
if (!bookingId || isNaN(bookingId) || bookingId <= 0) {
|
||
toast.error('Invalid booking ID');
|
||
return;
|
||
}
|
||
|
||
setCreatingInvoice(true);
|
||
|
||
const invoiceData = {
|
||
booking_id: bookingId,
|
||
};
|
||
|
||
const response = await invoiceService.createInvoice(invoiceData);
|
||
|
||
// Log the full response for debugging
|
||
logger.info('Invoice creation response', { response });
|
||
|
||
// Check response structure - handle different possible formats
|
||
let invoice = null;
|
||
if (response.status === 'success' && response.data) {
|
||
// Try different possible response structures
|
||
invoice = response.data.invoice || response.data.data?.invoice || response.data;
|
||
logger.debug('Extracted invoice', { invoice });
|
||
}
|
||
|
||
if (!invoice) {
|
||
logger.error('Failed to create invoice - no invoice in response', { response });
|
||
toast.error(response.message || 'Failed to create invoice - no invoice data received');
|
||
return;
|
||
}
|
||
|
||
// Extract and validate invoice ID - handle both number and string types
|
||
let invoiceId = invoice.id;
|
||
|
||
// Log the invoice ID for debugging
|
||
logger.info('Extracted invoice ID', { invoiceId, type: typeof invoiceId, invoice });
|
||
|
||
// Convert to number if it's a string
|
||
if (typeof invoiceId === 'string') {
|
||
invoiceId = parseInt(invoiceId, 10);
|
||
}
|
||
|
||
// Validate invoice ID before navigation
|
||
if (!invoiceId || isNaN(invoiceId) || invoiceId <= 0 || !isFinite(invoiceId)) {
|
||
logger.error('Invalid invoice ID received from server', {
|
||
originalInvoiceId: invoice.id,
|
||
convertedInvoiceId: invoiceId,
|
||
type: typeof invoiceId,
|
||
invoice,
|
||
response: response.data
|
||
});
|
||
logger.error('Invalid invoice ID received from server', {
|
||
originalInvoiceId: invoice.id,
|
||
convertedInvoiceId: invoiceId,
|
||
type: typeof invoiceId,
|
||
invoice,
|
||
response: response.data
|
||
});
|
||
toast.error(`Failed to create invoice: Invalid invoice ID received (${invoice.id})`);
|
||
return;
|
||
}
|
||
|
||
// Ensure it's a number
|
||
invoiceId = Number(invoiceId);
|
||
|
||
toast.success('Invoice created successfully!');
|
||
|
||
// Send email if requested
|
||
if (sendEmail) {
|
||
setSendingEmail(true);
|
||
try {
|
||
await invoiceService.sendInvoiceEmail(invoiceId);
|
||
toast.success('Invoice sent via email successfully!');
|
||
} catch (emailError: any) {
|
||
toast.warning('Invoice created but email sending failed: ' + (emailError.response?.data?.message || emailError.message));
|
||
} finally {
|
||
setSendingEmail(false);
|
||
}
|
||
}
|
||
|
||
setShowDetailModal(false);
|
||
// Update the bookings with invoices set immediately
|
||
setBookingsWithInvoices(prev => new Set(prev).add(bookingId));
|
||
// Remove the createInvoice query param if present
|
||
if (showOnlyWithoutInvoices) {
|
||
searchParams.delete('createInvoice');
|
||
setSearchParams(searchParams);
|
||
}
|
||
// Delay to ensure invoice is fully committed to database before navigation
|
||
// Increased delay to prevent race conditions
|
||
setTimeout(() => {
|
||
navigate(`/admin/invoices/${invoiceId}`);
|
||
}, 500);
|
||
} catch (error: any) {
|
||
// Don't show "Invalid invoice ID" error if it's from validation - it's already handled above
|
||
const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || 'Unable to create invoice';
|
||
|
||
// Only show error toast if it's not a validation error we've already handled
|
||
if (!errorMessage.includes('Invalid invoice ID') && !errorMessage.includes('Invalid Invoice ID')) {
|
||
toast.error(errorMessage);
|
||
} else {
|
||
// Log validation errors but don't show toast (they're handled above)
|
||
logger.error('Invoice creation validation error', error);
|
||
}
|
||
logger.error('Invoice creation error', error);
|
||
} finally {
|
||
setCreatingInvoice(false);
|
||
}
|
||
};
|
||
|
||
const getStatusBadge = (status: string) => {
|
||
const badges: Record<string, { bg: string; text: string; label: string; border: string }> = {
|
||
pending: {
|
||
bg: 'bg-gradient-to-r from-amber-50 to-yellow-50',
|
||
text: 'text-amber-800',
|
||
label: 'Pending confirmation',
|
||
border: 'border-amber-200'
|
||
},
|
||
confirmed: {
|
||
bg: 'bg-gradient-to-r from-blue-50 to-indigo-50',
|
||
text: 'text-blue-800',
|
||
label: 'Confirmed',
|
||
border: 'border-blue-200'
|
||
},
|
||
checked_in: {
|
||
bg: 'bg-gradient-to-r from-emerald-50 to-green-50',
|
||
text: 'text-emerald-800',
|
||
label: 'Checked in',
|
||
border: 'border-emerald-200'
|
||
},
|
||
checked_out: {
|
||
bg: 'bg-gradient-to-r from-slate-50 to-gray-50',
|
||
text: 'text-slate-700',
|
||
label: 'Checked out',
|
||
border: 'border-slate-200'
|
||
},
|
||
cancelled: {
|
||
bg: 'bg-gradient-to-r from-rose-50 to-red-50',
|
||
text: 'text-rose-800',
|
||
label: '❌ Canceled',
|
||
border: 'border-rose-200'
|
||
},
|
||
};
|
||
const badge = badges[status] || badges.pending;
|
||
return (
|
||
<span className={`px-4 py-1.5 rounded-full text-xs font-semibold border ${badge.bg} ${badge.text} ${badge.border} shadow-sm`}>
|
||
{badge.label}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
if (loading) {
|
||
return <Loading />;
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-4 sm:space-y-6 md:space-y-8 bg-gradient-to-br from-slate-50 via-white to-slate-50 min-h-screen -m-3 sm:-m-4 md:-m-6 p-4 sm:p-6 md:p-8">
|
||
{/* Header with Create Button */}
|
||
<div className="animate-fade-in">
|
||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 sm:gap-4 mb-4 sm:mb-6">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2 sm:gap-3 mb-2">
|
||
<div className="h-1 w-12 sm:w-16 bg-gradient-to-r from-amber-400 to-amber-600 rounded-full"></div>
|
||
<h1 className="text-xl sm:text-2xl md:text-2xl font-bold bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 bg-clip-text text-transparent tracking-tight">
|
||
Booking Management
|
||
</h1>
|
||
</div>
|
||
<p className="text-slate-600 mt-2 sm:mt-3 text-xs sm:text-sm md:text-sm font-light">Manage and track all hotel bookings with precision</p>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowCreateModal(true)}
|
||
className="flex items-center justify-center gap-2 px-4 sm:px-6 py-2 sm:py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg sm:rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all duration-200 shadow-lg hover:shadow-xl whitespace-nowrap w-full sm:w-auto text-xs sm:text-sm"
|
||
>
|
||
<Plus className="w-4 h-4 sm:w-5 sm:h-5" />
|
||
Create Booking
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{}
|
||
{showOnlyWithoutInvoices && (
|
||
<div className="bg-gradient-to-r from-amber-50 to-yellow-50 border-2 border-amber-200 rounded-xl p-4 mb-6 animate-fade-in">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<FileText className="w-5 h-5 text-amber-600" />
|
||
<div>
|
||
<h3 className="font-semibold text-amber-900">Select Booking to Create Invoice</h3>
|
||
<p className="text-sm text-amber-700 mt-1">Showing only bookings without invoices. Select a booking to create and send an invoice.</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
searchParams.delete('createInvoice');
|
||
setSearchParams(searchParams);
|
||
}}
|
||
className="px-4 py-2 text-sm font-medium text-amber-700 hover:text-amber-900 hover:bg-amber-100 rounded-lg transition-colors"
|
||
>
|
||
Show All Bookings
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="bg-white/80 backdrop-blur-sm rounded-xl sm:rounded-2xl shadow-xl border border-slate-200/60 p-4 sm:p-5 md:p-6 animate-fade-in" style={{ animationDelay: '0.1s' }}>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||
<div className="relative group">
|
||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-slate-400 w-5 h-5 group-focus-within:text-amber-500 transition-colors" />
|
||
<input
|
||
type="text"
|
||
placeholder="Search by booking number, guest name..."
|
||
value={filters.search}
|
||
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
|
||
className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-slate-700 placeholder-slate-400 font-medium shadow-sm hover:shadow-md"
|
||
/>
|
||
</div>
|
||
<select
|
||
value={filters.status}
|
||
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
|
||
className="w-full px-4 py-3.5 bg-white border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-slate-700 font-medium shadow-sm hover:shadow-md cursor-pointer"
|
||
>
|
||
<option value="">All statuses</option>
|
||
<option value="pending">Pending confirmation</option>
|
||
<option value="confirmed">Confirmed</option>
|
||
<option value="checked_in">Checked in</option>
|
||
<option value="checked_out">Checked out</option>
|
||
<option value="cancelled">Canceled</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{}
|
||
<div className="bg-white/80 backdrop-blur-sm rounded-xl sm:rounded-2xl shadow-xl border border-slate-200/60 overflow-hidden animate-fade-in" style={{ animationDelay: '0.2s' }}>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900">
|
||
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Booking Number</th>
|
||
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Customer</th>
|
||
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Room</th>
|
||
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Check-in/out</th>
|
||
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Total Price</th>
|
||
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Status</th>
|
||
<th className="px-8 py-5 text-right text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="bg-white divide-y divide-slate-100">
|
||
{loading || checkingInvoices ? (
|
||
<tr>
|
||
<td colSpan={7} className="px-8 py-12 text-center">
|
||
<div className="flex flex-col items-center justify-center">
|
||
<Loader2 className="w-8 h-8 text-amber-600 animate-spin mb-4" />
|
||
<p className="text-slate-600 font-medium">
|
||
{checkingInvoices ? 'Checking invoices...' : 'Loading bookings...'}
|
||
</p>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
) : bookings.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={7} className="px-8 py-12 text-center">
|
||
<div className="flex flex-col items-center justify-center">
|
||
<FileText className="w-12 h-12 text-slate-400 mb-4" />
|
||
<p className="text-slate-600 font-medium">
|
||
{showOnlyWithoutInvoices
|
||
? 'No bookings without invoices found'
|
||
: 'No bookings found'}
|
||
</p>
|
||
{showOnlyWithoutInvoices && (
|
||
<button
|
||
onClick={() => {
|
||
searchParams.delete('createInvoice');
|
||
setSearchParams(searchParams);
|
||
}}
|
||
className="mt-4 px-4 py-2 text-sm font-medium text-amber-700 hover:text-amber-900 hover:bg-amber-100 rounded-lg transition-colors"
|
||
>
|
||
Show All Bookings
|
||
</button>
|
||
)}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
) : (() => {
|
||
const filteredBookings = bookings.filter(booking => {
|
||
// If showOnlyWithoutInvoices is true, only show bookings without invoices
|
||
if (showOnlyWithoutInvoices) {
|
||
return !bookingsWithInvoices.has(booking.id);
|
||
}
|
||
// Otherwise, show all bookings
|
||
return true;
|
||
});
|
||
|
||
if (filteredBookings.length === 0 && showOnlyWithoutInvoices) {
|
||
return (
|
||
<tr>
|
||
<td colSpan={7} className="px-8 py-12 text-center">
|
||
<div className="flex flex-col items-center justify-center">
|
||
<FileText className="w-12 h-12 text-slate-400 mb-4" />
|
||
<p className="text-slate-600 font-medium">
|
||
All bookings already have invoices
|
||
</p>
|
||
<button
|
||
onClick={() => {
|
||
searchParams.delete('createInvoice');
|
||
setSearchParams(searchParams);
|
||
}}
|
||
className="mt-4 px-4 py-2 text-sm font-medium text-amber-700 hover:text-amber-900 hover:bg-amber-100 rounded-lg transition-colors"
|
||
>
|
||
Show All Bookings
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
);
|
||
}
|
||
|
||
return filteredBookings.map((booking, index) => (
|
||
<tr
|
||
key={booking.id}
|
||
className="hover:bg-gradient-to-r hover:from-amber-50/30 hover:to-yellow-50/30 transition-all duration-200 group border-b border-slate-100"
|
||
style={{ animationDelay: `${index * 0.05}s` }}
|
||
>
|
||
<td className="px-8 py-5 whitespace-nowrap">
|
||
<div className="text-sm font-bold text-slate-900 group-hover:text-amber-700 transition-colors font-mono">
|
||
{booking.booking_number}
|
||
</div>
|
||
</td>
|
||
<td className="px-8 py-5 whitespace-nowrap">
|
||
<div className="text-sm font-semibold text-slate-900">{booking.guest_info?.full_name || booking.user?.name}</div>
|
||
<div className="text-xs text-slate-500 mt-0.5">{booking.guest_info?.email || booking.user?.email}</div>
|
||
</td>
|
||
<td className="px-8 py-5 whitespace-nowrap">
|
||
<div className="text-sm font-medium text-slate-800">
|
||
<span className="text-amber-600 font-semibold">Room {booking.room?.room_number}</span>
|
||
<span className="text-slate-400 mx-2">•</span>
|
||
<span className="text-slate-600">{booking.room?.room_type?.name}</span>
|
||
</div>
|
||
</td>
|
||
<td className="px-8 py-5 whitespace-nowrap">
|
||
<div className="text-sm font-medium text-slate-900">
|
||
{parseDateLocal(booking.check_in_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
|
||
</div>
|
||
<div className="text-xs text-slate-500 mt-0.5">
|
||
→ {parseDateLocal(booking.check_out_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||
</div>
|
||
</td>
|
||
<td className="px-8 py-5 whitespace-nowrap">
|
||
{(() => {
|
||
const completedPayments = booking.payments?.filter(
|
||
(p) => p.payment_status === 'completed'
|
||
) || [];
|
||
const amountPaid = completedPayments.reduce(
|
||
(sum, p) => sum + (p.amount || 0),
|
||
0
|
||
);
|
||
const remainingDue = booking.total_price - amountPaid;
|
||
const hasPayments = completedPayments.length > 0;
|
||
|
||
return (
|
||
<div>
|
||
<div className="text-sm font-bold text-slate-900 bg-gradient-to-r from-amber-600 to-amber-700 bg-clip-text text-transparent">
|
||
{formatCurrency(booking.total_price)}
|
||
</div>
|
||
{hasPayments && (
|
||
<div className="text-xs mt-1">
|
||
<div className="text-green-600 font-medium">
|
||
Paid: {formatCurrency(amountPaid)}
|
||
</div>
|
||
{remainingDue > 0 && (
|
||
<div className="text-amber-600 font-medium">
|
||
Due: {formatCurrency(remainingDue)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})()}
|
||
</td>
|
||
<td className="px-8 py-5 whitespace-nowrap">
|
||
<div className="flex flex-col gap-1">
|
||
{getStatusBadge(booking.status)}
|
||
{!bookingsWithInvoices.has(booking.id) && (
|
||
<span className="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 rounded-full font-medium">
|
||
No Invoice
|
||
</span>
|
||
)}
|
||
</div>
|
||
</td>
|
||
<td className="px-8 py-5 whitespace-nowrap text-right">
|
||
<div className="flex items-center justify-end gap-2">
|
||
<button
|
||
onClick={() => {
|
||
setSelectedBooking(booking);
|
||
setShowDetailModal(true);
|
||
}}
|
||
className="p-2 rounded-lg text-slate-600 hover:text-amber-600 hover:bg-amber-50 transition-all duration-200 shadow-sm hover:shadow-md border border-slate-200 hover:border-amber-300"
|
||
title="View details"
|
||
>
|
||
<Eye className="w-5 h-5" />
|
||
</button>
|
||
{booking.status === 'pending' && (
|
||
<>
|
||
<button
|
||
onClick={() => handleUpdateStatus(booking.id, 'confirmed')}
|
||
disabled={updatingBookingId === booking.id || cancellingBookingId === booking.id}
|
||
className="p-2 rounded-lg text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-sm hover:shadow-md border border-emerald-200 hover:border-emerald-300"
|
||
title="Confirm"
|
||
>
|
||
{updatingBookingId === booking.id ? (
|
||
<Loader2 className="w-5 h-5 animate-spin" />
|
||
) : (
|
||
<CheckCircle className="w-5 h-5" />
|
||
)}
|
||
</button>
|
||
<button
|
||
onClick={() => handleCancelBooking(booking.id)}
|
||
disabled={updatingBookingId === booking.id || cancellingBookingId === booking.id}
|
||
className="p-2 rounded-lg text-rose-600 hover:text-rose-700 hover:bg-rose-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-sm hover:shadow-md border border-rose-200 hover:border-rose-300"
|
||
title="Cancel"
|
||
>
|
||
{cancellingBookingId === booking.id ? (
|
||
<Loader2 className="w-5 h-5 animate-spin" />
|
||
) : (
|
||
<XCircle className="w-5 h-5" />
|
||
)}
|
||
</button>
|
||
</>
|
||
)}
|
||
{booking.status === 'confirmed' && (
|
||
<button
|
||
onClick={() => handleUpdateStatus(booking.id, 'checked_in')}
|
||
disabled={updatingBookingId === booking.id || cancellingBookingId === booking.id}
|
||
className="p-2 rounded-lg text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-sm hover:shadow-md border border-emerald-200 hover:border-emerald-300"
|
||
title="Check-in"
|
||
>
|
||
{updatingBookingId === booking.id ? (
|
||
<Loader2 className="w-5 h-5 animate-spin" />
|
||
) : (
|
||
<CheckCircle className="w-5 h-5" />
|
||
)}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
));
|
||
})()}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<Pagination
|
||
currentPage={currentPage}
|
||
totalPages={totalPages}
|
||
onPageChange={setCurrentPage}
|
||
totalItems={totalItems}
|
||
itemsPerPage={itemsPerPage}
|
||
/>
|
||
</div>
|
||
|
||
{}
|
||
{showDetailModal && selectedBooking && (
|
||
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
|
||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl w-full max-w-3xl max-h-[95vh] sm:max-h-[90vh] overflow-hidden animate-scale-in border border-slate-200">
|
||
{}
|
||
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-8 py-6 border-b border-slate-700">
|
||
<div className="flex justify-between items-center">
|
||
<div>
|
||
<h2 className="text-xl sm:text-2xl md:text-2xl font-bold text-amber-100 mb-1">Booking Details</h2>
|
||
<p className="text-amber-200/80 text-sm font-light">Comprehensive booking information</p>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowDetailModal(false)}
|
||
className="w-10 h-10 flex items-center justify-center rounded-xl text-amber-100 hover:text-white hover:bg-slate-700/50 transition-all duration-200 border border-slate-600 hover:border-amber-400"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
||
<div className="p-4 sm:p-6 md:p-8 space-y-5 sm:space-y-6">
|
||
{}
|
||
<div className="grid grid-cols-2 gap-6">
|
||
<div className="bg-gradient-to-br from-slate-50 to-white p-5 rounded-xl border border-slate-200">
|
||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Booking Number</label>
|
||
<p className="text-base sm:text-lg md:text-lg font-bold text-slate-900 font-mono">{selectedBooking.booking_number}</p>
|
||
</div>
|
||
<div className="bg-gradient-to-br from-slate-50 to-white p-5 rounded-xl border border-slate-200">
|
||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Status</label>
|
||
<div className="mt-1">{getStatusBadge(selectedBooking.status)}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{}
|
||
<div className="bg-gradient-to-br from-amber-50/50 to-yellow-50/50 p-6 rounded-xl border border-amber-100">
|
||
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wider mb-4 block flex items-center gap-2">
|
||
<div className="w-1 h-4 bg-gradient-to-b from-amber-400 to-amber-600 rounded-full"></div>
|
||
Customer Information
|
||
</label>
|
||
<div className="space-y-2">
|
||
<p className="text-lg font-bold text-slate-900">{selectedBooking.guest_info?.full_name || selectedBooking.user?.name}</p>
|
||
<p className="text-slate-600">{selectedBooking.guest_info?.email || selectedBooking.user?.email}</p>
|
||
<p className="text-slate-600">{selectedBooking.guest_info?.phone || selectedBooking.user?.phone}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{}
|
||
<div className="bg-gradient-to-br from-blue-50/50 to-indigo-50/50 p-6 rounded-xl border border-blue-100">
|
||
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wider mb-4 block flex items-center gap-2">
|
||
<div className="w-1 h-4 bg-gradient-to-b from-blue-400 to-blue-600 rounded-full"></div>
|
||
Room Information
|
||
</label>
|
||
<p className="text-lg font-semibold text-slate-900">
|
||
<span className="text-amber-600">Room {selectedBooking.room?.room_number}</span>
|
||
<span className="text-slate-400 mx-2">•</span>
|
||
<span className="text-slate-700">{selectedBooking.room?.room_type?.name}</span>
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||
<div className="bg-gradient-to-br from-slate-50 to-white p-4 sm:p-5 rounded-xl border border-slate-200">
|
||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Check-in Date</label>
|
||
<p className="text-sm sm:text-base font-semibold text-slate-900">{parseDateLocal(selectedBooking.check_in_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}</p>
|
||
</div>
|
||
<div className="bg-gradient-to-br from-slate-50 to-white p-4 sm:p-5 rounded-xl border border-slate-200">
|
||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Check-out Date</label>
|
||
<p className="text-sm sm:text-base font-semibold text-slate-900">{parseDateLocal(selectedBooking.check_out_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-gradient-to-br from-slate-50 to-white p-5 rounded-xl border border-slate-200">
|
||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Number of Guests</label>
|
||
<p className="text-lg font-semibold text-slate-900">{selectedBooking.guest_count} guest{selectedBooking.guest_count !== 1 ? 's' : ''}</p>
|
||
</div>
|
||
|
||
{}
|
||
<div className="bg-gradient-to-br from-indigo-50/50 to-purple-50/50 p-6 rounded-xl border border-indigo-100">
|
||
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wider mb-4 block flex items-center gap-2">
|
||
<div className="w-1 h-4 bg-gradient-to-b from-indigo-400 to-indigo-600 rounded-full"></div>
|
||
Payment Information
|
||
</label>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Payment Method</p>
|
||
<p className="text-base font-semibold text-slate-900">
|
||
{selectedBooking.payment_method === 'cash'
|
||
? '💵 Pay at Hotel'
|
||
: selectedBooking.payment_method === 'stripe'
|
||
? '💳 Stripe (Card)'
|
||
: selectedBooking.payment_method === 'paypal'
|
||
? '💳 PayPal'
|
||
: selectedBooking.payment_method || 'N/A'}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Payment Status</p>
|
||
<p className={`text-base font-semibold ${
|
||
selectedBooking.payment_status === 'paid'
|
||
? 'text-green-600'
|
||
: selectedBooking.payment_status === 'refunded'
|
||
? 'text-orange-600'
|
||
: 'text-red-600'
|
||
}`}>
|
||
{selectedBooking.payment_status === 'paid'
|
||
? '✅ Paid'
|
||
: selectedBooking.payment_status === 'refunded'
|
||
? '💰 Refunded'
|
||
: '❌ Unpaid'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{}
|
||
{(selectedBooking as any).service_usages && (selectedBooking as any).service_usages.length > 0 && (
|
||
<div className="bg-gradient-to-br from-purple-50/50 to-pink-50/50 p-6 rounded-xl border border-purple-100">
|
||
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wider mb-4 block flex items-center gap-2">
|
||
<div className="w-1 h-4 bg-gradient-to-b from-purple-400 to-purple-600 rounded-full"></div>
|
||
Additional Services
|
||
</label>
|
||
<div className="space-y-2">
|
||
{(selectedBooking as any).service_usages.map((service: any, idx: number) => (
|
||
<div key={service.id || idx} className="flex justify-between items-center py-2 border-b border-purple-100 last:border-0">
|
||
<div>
|
||
<p className="text-sm font-medium text-slate-900">{service.service_name || service.name || 'Service'}</p>
|
||
<p className="text-xs text-slate-500">
|
||
{formatCurrency(service.unit_price || service.price || 0)} × {service.quantity || 1}
|
||
</p>
|
||
</div>
|
||
<p className="text-sm font-semibold text-slate-900">
|
||
{formatCurrency(service.total_price || (service.unit_price || service.price || 0) * (service.quantity || 1))}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{}
|
||
{(() => {
|
||
const completedPayments = selectedBooking.payments?.filter(
|
||
(p) => p.payment_status === 'completed'
|
||
) || [];
|
||
const allPayments = selectedBooking.payments || [];
|
||
const amountPaid = completedPayments.reduce(
|
||
(sum, p) => sum + (p.amount || 0),
|
||
0
|
||
);
|
||
const remainingDue = selectedBooking.total_price - amountPaid;
|
||
const hasPayments = allPayments.length > 0;
|
||
|
||
return (
|
||
<>
|
||
{hasPayments && (
|
||
<div className="bg-gradient-to-br from-teal-50/50 to-cyan-50/50 p-6 rounded-xl border border-teal-100">
|
||
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wider mb-4 block flex items-center gap-2">
|
||
<div className="w-1 h-4 bg-gradient-to-b from-teal-400 to-teal-600 rounded-full"></div>
|
||
Payment History
|
||
</label>
|
||
<div className="space-y-3">
|
||
{allPayments.map((payment: any, idx: number) => (
|
||
<div key={payment.id || idx} className="p-3 bg-white rounded-lg border border-teal-100">
|
||
<div className="flex justify-between items-start mb-2">
|
||
<div>
|
||
<p className="text-sm font-semibold text-slate-900">
|
||
{formatCurrency(payment.amount || 0)}
|
||
</p>
|
||
<p className="text-xs text-slate-500 mt-1">
|
||
{payment.payment_type === 'deposit' ? 'Deposit (20%)' : payment.payment_type === 'remaining' ? 'Remaining Payment' : 'Full Payment'}
|
||
{' • '}
|
||
{payment.payment_method === 'stripe' ? 'Stripe' : payment.payment_method === 'paypal' ? 'PayPal' : payment.payment_method || 'Cash'}
|
||
</p>
|
||
</div>
|
||
<span className={`text-xs px-2 py-1 rounded-full font-medium ${
|
||
payment.payment_status === 'completed' || payment.payment_status === 'paid'
|
||
? 'bg-green-100 text-green-700'
|
||
: payment.payment_status === 'pending'
|
||
? 'bg-yellow-100 text-yellow-700'
|
||
: 'bg-red-100 text-red-700'
|
||
}`}>
|
||
{payment.payment_status === 'completed' || payment.payment_status === 'paid' ? 'Paid' : payment.payment_status || 'Pending'}
|
||
</span>
|
||
</div>
|
||
{payment.transaction_id && (
|
||
<p className="text-xs text-slate-400 font-mono">ID: {payment.transaction_id}</p>
|
||
)}
|
||
{payment.payment_date && (
|
||
<p className="text-xs text-slate-400 mt-1">
|
||
{new Date(payment.payment_date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}
|
||
</p>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
{}
|
||
<div className="bg-gradient-to-br from-green-50 via-emerald-50 to-green-50 p-6 rounded-xl border-2 border-green-200 shadow-lg mb-4">
|
||
<label className="text-xs font-semibold text-green-700 uppercase tracking-wider mb-2 block">Amount Paid</label>
|
||
<p className="text-xl sm:text-2xl md:text-2xl font-bold bg-gradient-to-r from-green-600 via-emerald-700 to-green-600 bg-clip-text text-transparent">
|
||
{formatCurrency(amountPaid)}
|
||
</p>
|
||
{hasPayments && completedPayments.length > 0 && (
|
||
<p className="text-xs text-green-600 mt-2">
|
||
{completedPayments.length} payment{completedPayments.length !== 1 ? 's' : ''} completed
|
||
{amountPaid > 0 && selectedBooking.total_price > 0 && (
|
||
<span className="ml-2">
|
||
({((amountPaid / selectedBooking.total_price) * 100).toFixed(0)}% of total)
|
||
</span>
|
||
)}
|
||
</p>
|
||
)}
|
||
{amountPaid === 0 && !hasPayments && (
|
||
<p className="text-sm text-gray-500 mt-2">No payments made yet</p>
|
||
)}
|
||
</div>
|
||
|
||
{}
|
||
{remainingDue > 0 && (
|
||
<div className="bg-gradient-to-br from-amber-50 via-yellow-50 to-amber-50 p-6 rounded-xl border-2 border-amber-200 shadow-lg mb-4">
|
||
<label className="text-xs font-semibold text-amber-700 uppercase tracking-wider mb-2 block">Remaining Due (To be paid)</label>
|
||
<p className="text-xl sm:text-2xl md:text-2xl font-bold text-amber-600">
|
||
{formatCurrency(remainingDue)}
|
||
</p>
|
||
{selectedBooking.total_price > 0 && (
|
||
<p className="text-xs text-amber-600 mt-2">
|
||
({((remainingDue / selectedBooking.total_price) * 100).toFixed(0)}% of total)
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{}
|
||
<div className="bg-gradient-to-br from-slate-50 to-gray-50 p-6 rounded-xl border-2 border-slate-200 shadow-lg">
|
||
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wider mb-2 block">Total Booking Price</label>
|
||
<p className="text-lg sm:text-xl md:text-xl font-bold text-slate-700">
|
||
{formatCurrency(selectedBooking.total_price)}
|
||
</p>
|
||
<p className="text-xs text-slate-500 mt-2">
|
||
This is the total amount for the booking
|
||
</p>
|
||
</div>
|
||
</>
|
||
);
|
||
})()}
|
||
|
||
{}
|
||
<div className="bg-gradient-to-br from-slate-50 to-white p-6 rounded-xl border border-slate-200">
|
||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-4 block flex items-center gap-2">
|
||
<div className="w-1 h-4 bg-gradient-to-b from-slate-400 to-slate-600 rounded-full"></div>
|
||
Booking Metadata
|
||
</label>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{selectedBooking.createdAt && (
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Created At</p>
|
||
<p className="text-sm font-medium text-slate-900">
|
||
{new Date(selectedBooking.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
||
</p>
|
||
</div>
|
||
)}
|
||
{selectedBooking.updatedAt && (
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Last Updated</p>
|
||
<p className="text-sm font-medium text-slate-900">
|
||
{new Date(selectedBooking.updatedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
||
</p>
|
||
</div>
|
||
)}
|
||
{selectedBooking.requires_deposit !== undefined && (
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Deposit Required</p>
|
||
<p className="text-sm font-medium text-slate-900">
|
||
{selectedBooking.requires_deposit ? 'Yes (20%)' : 'No'}
|
||
</p>
|
||
</div>
|
||
)}
|
||
{selectedBooking.deposit_paid !== undefined && (
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Deposit Paid</p>
|
||
<p className={`text-sm font-medium ${selectedBooking.deposit_paid ? 'text-green-600' : 'text-amber-600'}`}>
|
||
{selectedBooking.deposit_paid ? '✅ Yes' : '❌ No'}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{}
|
||
{selectedBooking.notes && (
|
||
<div className="bg-gradient-to-br from-slate-50 to-white p-6 rounded-xl border border-slate-200">
|
||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3 block">Special Notes</label>
|
||
<p className="text-slate-700 leading-relaxed">{selectedBooking.notes}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="mt-6 sm:mt-8 pt-4 sm:pt-6 border-t border-slate-200 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
|
||
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3 w-full sm:w-auto">
|
||
<button
|
||
onClick={() => handleCreateInvoice(selectedBooking.id, false)}
|
||
disabled={creatingInvoice || sendingEmail || bookingsWithInvoices.has(selectedBooking.id)}
|
||
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl"
|
||
>
|
||
{creatingInvoice ? (
|
||
<>
|
||
<Loader2 className="w-5 h-5 animate-spin" />
|
||
Creating Invoice...
|
||
</>
|
||
) : bookingsWithInvoices.has(selectedBooking.id) ? (
|
||
<>
|
||
<FileText className="w-5 h-5" />
|
||
Invoice Already Exists
|
||
</>
|
||
) : (
|
||
<>
|
||
<FileText className="w-5 h-5" />
|
||
Create Invoice
|
||
</>
|
||
)}
|
||
</button>
|
||
{!bookingsWithInvoices.has(selectedBooking.id) && (
|
||
<button
|
||
onClick={() => handleCreateInvoice(selectedBooking.id, true)}
|
||
disabled={creatingInvoice || sendingEmail}
|
||
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl font-semibold hover:from-blue-600 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl"
|
||
>
|
||
{creatingInvoice || sendingEmail ? (
|
||
<>
|
||
<Loader2 className="w-5 h-5 animate-spin" />
|
||
{creatingInvoice ? 'Creating...' : 'Sending Email...'}
|
||
</>
|
||
) : (
|
||
<>
|
||
<Mail className="w-5 h-5" />
|
||
Create & Send Invoice
|
||
</>
|
||
)}
|
||
</button>
|
||
)}
|
||
</div>
|
||
<button
|
||
onClick={() => setShowDetailModal(false)}
|
||
className="px-8 py-3 bg-gradient-to-r from-slate-700 to-slate-800 text-white rounded-xl font-semibold hover:from-slate-800 hover:to-slate-900 transition-all duration-200 shadow-lg hover:shadow-xl border border-slate-600"
|
||
>
|
||
Close
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Create Booking Modal */}
|
||
<CreateBookingModal
|
||
isOpen={showCreateModal}
|
||
onClose={() => setShowCreateModal(false)}
|
||
onSuccess={() => {
|
||
setShowCreateModal(false);
|
||
fetchBookings();
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default BookingManagementPage;
|