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([]); const [bookingsWithInvoices, setBookingsWithInvoices] = useState>(new Set()); const [loading, setLoading] = useState(true); const [checkingInvoices, setCheckingInvoices] = useState(false); const [selectedBooking, setSelectedBooking] = useState(null); const [showDetailModal, setShowDetailModal] = useState(false); const [updatingBookingId, setUpdatingBookingId] = useState(null); const [cancellingBookingId, setCancellingBookingId] = useState(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(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 = { 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 ( {badge.label} ); }; if (loading) { return ; } return (
{/* Header with Create Button */}

Booking Management

Manage and track all hotel bookings with precision

{} {showOnlyWithoutInvoices && (

Select Booking to Create Invoice

Showing only bookings without invoices. Select a booking to create and send an invoice.

)}
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" />
{}
{loading || checkingInvoices ? ( ) : bookings.length === 0 ? ( ) : (() => { 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 ( ); } return filteredBookings.map((booking, index) => ( )); })()}
Booking Number Customer Room Check-in/out Total Price Status Actions

{checkingInvoices ? 'Checking invoices...' : 'Loading bookings...'}

{showOnlyWithoutInvoices ? 'No bookings without invoices found' : 'No bookings found'}

{showOnlyWithoutInvoices && ( )}

All bookings already have invoices

{booking.booking_number}
{booking.guest_info?.full_name || booking.user?.name}
{booking.guest_info?.email || booking.user?.email}
Room {booking.room?.room_number} {booking.room?.room_type?.name}
{parseDateLocal(booking.check_in_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
→ {parseDateLocal(booking.check_out_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
{(() => { 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 (
{formatCurrency(booking.total_price)}
{hasPayments && (
Paid: {formatCurrency(amountPaid)}
{remainingDue > 0 && (
Due: {formatCurrency(remainingDue)}
)}
)}
); })()}
{getStatusBadge(booking.status)} {!bookingsWithInvoices.has(booking.id) && ( No Invoice )}
{booking.status === 'pending' && ( <> )} {booking.status === 'confirmed' && ( )}
{} {showDetailModal && selectedBooking && (
{}

Booking Details

Comprehensive booking information

{}

{selectedBooking.booking_number}

{getStatusBadge(selectedBooking.status)}
{}

{selectedBooking.guest_info?.full_name || selectedBooking.user?.name}

{selectedBooking.guest_info?.email || selectedBooking.user?.email}

{selectedBooking.guest_info?.phone || selectedBooking.user?.phone}

{}

Room {selectedBooking.room?.room_number} {selectedBooking.room?.room_type?.name}

{parseDateLocal(selectedBooking.check_in_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}

{parseDateLocal(selectedBooking.check_out_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}

{selectedBooking.guest_count} guest{selectedBooking.guest_count !== 1 ? 's' : ''}

{}

Payment Method

{selectedBooking.payment_method === 'cash' ? '💵 Pay at Hotel' : selectedBooking.payment_method === 'stripe' ? '💳 Stripe (Card)' : selectedBooking.payment_method === 'paypal' ? '💳 PayPal' : selectedBooking.payment_method || 'N/A'}

Payment Status

{selectedBooking.payment_status === 'paid' ? '✅ Paid' : selectedBooking.payment_status === 'refunded' ? '💰 Refunded' : '❌ Unpaid'}

{} {(selectedBooking as any).service_usages && (selectedBooking as any).service_usages.length > 0 && (
{(selectedBooking as any).service_usages.map((service: any, idx: number) => (

{service.service_name || service.name || 'Service'}

{formatCurrency(service.unit_price || service.price || 0)} × {service.quantity || 1}

{formatCurrency(service.total_price || (service.unit_price || service.price || 0) * (service.quantity || 1))}

))}
)} {} {(() => { 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 && (
{allPayments.map((payment: any, idx: number) => (

{formatCurrency(payment.amount || 0)}

{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'}

{payment.payment_status === 'completed' || payment.payment_status === 'paid' ? 'Paid' : payment.payment_status || 'Pending'}
{payment.transaction_id && (

ID: {payment.transaction_id}

)} {payment.payment_date && (

{new Date(payment.payment_date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}

)}
))}
)} {}

{formatCurrency(amountPaid)}

{hasPayments && completedPayments.length > 0 && (

{completedPayments.length} payment{completedPayments.length !== 1 ? 's' : ''} completed {amountPaid > 0 && selectedBooking.total_price > 0 && ( ({((amountPaid / selectedBooking.total_price) * 100).toFixed(0)}% of total) )}

)} {amountPaid === 0 && !hasPayments && (

No payments made yet

)}
{} {remainingDue > 0 && (

{formatCurrency(remainingDue)}

{selectedBooking.total_price > 0 && (

({((remainingDue / selectedBooking.total_price) * 100).toFixed(0)}% of total)

)}
)} {}

{formatCurrency(selectedBooking.total_price)}

This is the total amount for the booking

); })()} {}
{selectedBooking.createdAt && (

Created At

{new Date(selectedBooking.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}

)} {selectedBooking.updatedAt && (

Last Updated

{new Date(selectedBooking.updatedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}

)} {selectedBooking.requires_deposit !== undefined && (

Deposit Required

{selectedBooking.requires_deposit ? 'Yes (20%)' : 'No'}

)} {selectedBooking.deposit_paid !== undefined && (

Deposit Paid

{selectedBooking.deposit_paid ? '✅ Yes' : '❌ No'}

)}
{} {selectedBooking.notes && (

{selectedBooking.notes}

)}
{!bookingsWithInvoices.has(selectedBooking.id) && ( )}
)} {/* Create Booking Modal */} setShowCreateModal(false)} onSuccess={() => { setShowCreateModal(false); fetchBookings(); }} />
); }; export default BookingManagementPage;