Files
Hotel-Booking/Frontend/src/pages/admin/BookingManagementPage.tsx
Iliyan Angelov 62c1fe5951 updates
2025-12-01 06:50:10 +02:00

1006 lines
51 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 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;