updates
This commit is contained in:
@@ -26,7 +26,6 @@ import {
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
getBookingById,
|
||||
cancelBooking,
|
||||
type Booking,
|
||||
} from '../../services/api/bookingService';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
@@ -36,6 +35,7 @@ import PaymentStatusBadge from
|
||||
'../../components/common/PaymentStatusBadge';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import CancelBookingModal from '../../components/booking/CancelBookingModal';
|
||||
|
||||
const BookingDetailPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -49,7 +49,7 @@ const BookingDetailPage: React.FC = () => {
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
const [showCancelModal, setShowCancelModal] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -102,69 +102,19 @@ const BookingDetailPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelBooking = async () => {
|
||||
console.log('=== handleCancelBooking called ===');
|
||||
const handleCancelBookingClick = () => {
|
||||
if (!booking) {
|
||||
console.error('No booking found');
|
||||
toast.error('Booking not found');
|
||||
return;
|
||||
}
|
||||
setShowCancelModal(true);
|
||||
};
|
||||
|
||||
console.log('Cancel booking clicked', { bookingId: booking.id, status: booking.status });
|
||||
|
||||
|
||||
const confirmed = window.confirm(
|
||||
`⚠️ CANCEL BOOKING CONFIRMATION ⚠️\n\n` +
|
||||
`Booking Number: ${booking.booking_number}\n\n` +
|
||||
`Are you sure you want to cancel this booking?\n\n` +
|
||||
`IMPORTANT NOTES:\n` +
|
||||
`• You will be charged 20% cancellation fee\n` +
|
||||
`• The remaining 80% will be refunded\n` +
|
||||
`• This action cannot be undone\n\n` +
|
||||
`Click OK to confirm cancellation\n` +
|
||||
`Click Cancel to keep your booking`
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
console.log('User chose to keep the booking');
|
||||
toast.info('Booking cancellation cancelled. Your booking remains active.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('User confirmed cancellation, proceeding...');
|
||||
|
||||
try {
|
||||
setCancelling(true);
|
||||
console.log('Calling cancelBooking API...', booking.id);
|
||||
|
||||
const response = await cancelBooking(booking.id);
|
||||
console.log('Cancel booking response:', response);
|
||||
|
||||
|
||||
if (response.success || (response as any).status === 'success') {
|
||||
toast.success(
|
||||
`✅ Booking ${booking.booking_number} cancelled successfully!`
|
||||
);
|
||||
|
||||
|
||||
await fetchBookingDetails(booking.id);
|
||||
} else {
|
||||
throw new Error(
|
||||
response.message ||
|
||||
'Unable to cancel booking'
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error cancelling booking:', err);
|
||||
const message =
|
||||
err.response?.data?.detail ||
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
'Unable to cancel booking. Please try again.';
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setCancelling(false);
|
||||
const handleCancelSuccess = async () => {
|
||||
if (booking) {
|
||||
await fetchBookingDetails(booking.id);
|
||||
}
|
||||
setShowCancelModal(false);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
@@ -828,38 +778,20 @@ const BookingDetailPage: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
console.log('=== BUTTON CLICKED ===', {
|
||||
bookingId: booking.id,
|
||||
status: booking.status,
|
||||
cancelling
|
||||
});
|
||||
e.stopPropagation();
|
||||
handleCancelBooking();
|
||||
handleCancelBookingClick();
|
||||
}}
|
||||
disabled={cancelling}
|
||||
className="flex-1 flex items-center
|
||||
justify-center gap-2 px-6 py-3
|
||||
bg-red-600 text-white rounded-lg
|
||||
hover:bg-red-700 active:bg-red-800
|
||||
transition-colors
|
||||
font-semibold disabled:bg-gray-400
|
||||
disabled:cursor-not-allowed cursor-pointer
|
||||
font-semibold cursor-pointer
|
||||
relative z-10"
|
||||
aria-label="Cancel booking"
|
||||
>
|
||||
{cancelling ? (
|
||||
<>
|
||||
<Loader2
|
||||
className="w-5 h-5 animate-spin"
|
||||
/>
|
||||
Cancelling...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="w-5 h-5" />
|
||||
Cancel Booking
|
||||
</>
|
||||
)}
|
||||
<XCircle className="w-5 h-5" />
|
||||
Cancel Booking
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex-1 text-center text-sm text-gray-500 py-3">
|
||||
@@ -868,13 +800,6 @@ const BookingDetailPage: React.FC = () => {
|
||||
<small className="text-xs">(Only pending bookings can be cancelled)</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{}
|
||||
{import.meta.env.DEV && (
|
||||
<div className="text-xs text-gray-400 mt-2 p-2 bg-gray-100 rounded">
|
||||
Debug: Status={booking.status}, CanCancel={canCancelBooking(booking) ? 'true' : 'false'}, Cancelling={cancelling ? 'true' : 'false'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Link
|
||||
to="/bookings"
|
||||
@@ -888,6 +813,16 @@ const BookingDetailPage: React.FC = () => {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cancel Booking Modal */}
|
||||
{showCancelModal && booking && (
|
||||
<CancelBookingModal
|
||||
isOpen={showCancelModal}
|
||||
booking={booking}
|
||||
onClose={() => setShowCancelModal(false)}
|
||||
onSuccess={handleCancelSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ import { confirmBankTransfer } from
|
||||
import Loading from '../../components/common/Loading';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import DepositPaymentModal from '../../components/payments/DepositPaymentModal';
|
||||
|
||||
const BookingSuccessPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -54,6 +55,7 @@ const BookingSuccessPage: React.FC = () => {
|
||||
useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] =
|
||||
useState<string | null>(null);
|
||||
const [showDepositModal, setShowDepositModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
@@ -91,12 +93,13 @@ const BookingSuccessPage: React.FC = () => {
|
||||
}
|
||||
|
||||
|
||||
// Only auto-open deposit modal if booking is not cancelled
|
||||
if (
|
||||
bookingData.requires_deposit &&
|
||||
!bookingData.deposit_paid
|
||||
!bookingData.deposit_paid &&
|
||||
bookingData.status !== 'cancelled'
|
||||
) {
|
||||
navigate(`/deposit-payment/${bookingId}`, { replace: true });
|
||||
return;
|
||||
setShowDepositModal(true);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -301,6 +304,51 @@ const BookingSuccessPage: React.FC = () => {
|
||||
const room = booking.room;
|
||||
const roomType = room?.room_type;
|
||||
|
||||
// Check if payment is completed
|
||||
const isPaymentCompleted = (() => {
|
||||
// Check if booking is cancelled
|
||||
if (booking.status === 'cancelled') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check payment_status
|
||||
if (booking.payment_status === 'paid') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check payment_balance
|
||||
if (booking.payment_balance?.is_fully_paid === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For deposit bookings, check if deposit is paid
|
||||
if (booking.requires_deposit && booking.deposit_paid === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check payments array
|
||||
if (booking.payments && Array.isArray(booking.payments)) {
|
||||
const totalPaid = booking.payments
|
||||
.filter((p: any) => p.payment_status === 'completed')
|
||||
.reduce((sum: number, p: any) => sum + parseFloat(p.amount?.toString() || '0'), 0);
|
||||
|
||||
// For deposit bookings, check if deposit is paid
|
||||
if (booking.requires_deposit) {
|
||||
const depositPayment = booking.payments.find((p: any) => p.payment_type === 'deposit' && p.payment_status === 'completed');
|
||||
if (depositPayment) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// For full payment bookings, check if fully paid
|
||||
return totalPaid >= booking.total_price - 0.01;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})();
|
||||
|
||||
const isCancelled = booking.status === 'cancelled';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
@@ -309,24 +357,70 @@ const BookingSuccessPage: React.FC = () => {
|
||||
className="bg-white rounded-lg shadow-md
|
||||
p-8 mb-6 text-center"
|
||||
>
|
||||
<div
|
||||
className="w-20 h-20 bg-green-100
|
||||
rounded-full flex items-center
|
||||
justify-center mx-auto mb-4"
|
||||
>
|
||||
<CheckCircle
|
||||
className="w-12 h-12 text-green-600"
|
||||
/>
|
||||
</div>
|
||||
<h1
|
||||
className="text-3xl font-bold text-gray-900
|
||||
mb-2"
|
||||
>
|
||||
Booking Successful!
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Thank you for booking with our hotel
|
||||
</p>
|
||||
{isCancelled ? (
|
||||
<>
|
||||
<div
|
||||
className="w-20 h-20 bg-red-100
|
||||
rounded-full flex items-center
|
||||
justify-center mx-auto mb-4"
|
||||
>
|
||||
<AlertCircle
|
||||
className="w-12 h-12 text-red-600"
|
||||
/>
|
||||
</div>
|
||||
<h1
|
||||
className="text-3xl font-bold text-gray-900
|
||||
mb-2"
|
||||
>
|
||||
Booking Cancelled
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-4">
|
||||
This booking has been cancelled
|
||||
</p>
|
||||
</>
|
||||
) : isPaymentCompleted ? (
|
||||
<>
|
||||
<div
|
||||
className="w-20 h-20 bg-green-100
|
||||
rounded-full flex items-center
|
||||
justify-center mx-auto mb-4"
|
||||
>
|
||||
<CheckCircle
|
||||
className="w-12 h-12 text-green-600"
|
||||
/>
|
||||
</div>
|
||||
<h1
|
||||
className="text-3xl font-bold text-gray-900
|
||||
mb-2"
|
||||
>
|
||||
Booking Successful!
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Thank you for booking with our hotel
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className="w-20 h-20 bg-yellow-100
|
||||
rounded-full flex items-center
|
||||
justify-center mx-auto mb-4"
|
||||
>
|
||||
<AlertCircle
|
||||
className="w-12 h-12 text-yellow-600"
|
||||
/>
|
||||
</div>
|
||||
<h1
|
||||
className="text-3xl font-bold text-gray-900
|
||||
mb-2"
|
||||
>
|
||||
Booking Pending Payment
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Please complete your payment to confirm your booking
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{}
|
||||
<div
|
||||
@@ -681,19 +775,25 @@ const BookingSuccessPage: React.FC = () => {
|
||||
<input
|
||||
id="receipt-upload"
|
||||
type="file"
|
||||
accept="image}
|
||||
accept="image/*"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setSelectedFile(file);
|
||||
}
|
||||
}}
|
||||
className="hidden"
|
||||
/>
|
||||
<span className="text-sm text-gray-600">
|
||||
Click to upload receipt
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{selectedFile && (
|
||||
<button
|
||||
onClick={handleUploadReceipt}
|
||||
disabled={uploadingReceipt}
|
||||
className="w-full px-4 py-3
|
||||
bg-blue-600 text-white
|
||||
rounded-lg hover:bg-blue-700
|
||||
transition-colors font-semibold
|
||||
disabled:bg-gray-400
|
||||
disabled:cursor-not-allowed
|
||||
flex items-center
|
||||
justify-center gap-2"
|
||||
className="w-full px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-semibold disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{uploadingReceipt ? (
|
||||
<>
|
||||
@@ -800,6 +900,21 @@ const BookingSuccessPage: React.FC = () => {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Deposit Payment Modal */}
|
||||
{showDepositModal && id && (
|
||||
<DepositPaymentModal
|
||||
isOpen={showDepositModal}
|
||||
bookingId={Number(id)}
|
||||
onClose={() => setShowDepositModal(false)}
|
||||
onSuccess={() => {
|
||||
setShowDepositModal(false);
|
||||
if (id) {
|
||||
fetchBookingDetails(Number(id));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,505 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import {
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
CreditCard,
|
||||
ArrowLeft,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getBookingById, cancelBooking, type Booking } from
|
||||
'../../services/api/bookingService';
|
||||
import {
|
||||
getPaymentsByBookingId,
|
||||
type Payment,
|
||||
} from '../../services/api/paymentService';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import StripePaymentWrapper from '../../components/payments/StripePaymentWrapper';
|
||||
import PayPalPaymentWrapper from '../../components/payments/PayPalPaymentWrapper';
|
||||
|
||||
const DepositPaymentPage: React.FC = () => {
|
||||
const { bookingId } = useParams<{ bookingId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { formatCurrency, currency } = useFormatCurrency();
|
||||
|
||||
const [booking, setBooking] = useState<Booking | null>(null);
|
||||
const [depositPayment, setDepositPayment] = useState<Payment | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [paymentSuccess, setPaymentSuccess] = useState(false);
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<'stripe' | 'paypal' | null>(null);
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (bookingId) {
|
||||
fetchData(Number(bookingId));
|
||||
}
|
||||
}, [bookingId]);
|
||||
|
||||
const fetchData = async (id: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
|
||||
const bookingResponse = await getBookingById(id);
|
||||
if (!bookingResponse.success || !bookingResponse.data?.booking) {
|
||||
throw new Error('Booking not found');
|
||||
}
|
||||
|
||||
const bookingData = bookingResponse.data.booking;
|
||||
setBooking(bookingData);
|
||||
|
||||
|
||||
if (bookingData.status === 'confirmed' || bookingData.status === 'checked_in') {
|
||||
toast.success('Booking is already confirmed!');
|
||||
navigate(`/bookings/${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!bookingData.requires_deposit) {
|
||||
toast.info('This booking does not require a deposit');
|
||||
navigate(`/bookings/${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const paymentsResponse = await getPaymentsByBookingId(id);
|
||||
if (paymentsResponse.success) {
|
||||
const deposit = paymentsResponse.data.payments.find(
|
||||
(p) => p.payment_type === 'deposit'
|
||||
);
|
||||
if (deposit) {
|
||||
setDepositPayment(deposit);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching data:', err);
|
||||
const message =
|
||||
err.response?.data?.message || 'Unable to load payment information';
|
||||
setError(message);
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatPrice = (price: number) => formatCurrency(price);
|
||||
|
||||
const handleCancelBooking = async () => {
|
||||
if (!booking) return;
|
||||
|
||||
const confirmed = window.confirm(
|
||||
`Are you sure you want to cancel this booking?\n\n` +
|
||||
`Booking Number: ${booking.booking_number}\n\n` +
|
||||
`⚠️ Note: This will cancel your booking and free up the room.`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
setCancelling(true);
|
||||
const response = await cancelBooking(booking.id);
|
||||
|
||||
if (response.success) {
|
||||
toast.success(
|
||||
`✅ Booking ${booking.booking_number} has been cancelled successfully!`
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
navigate('/bookings');
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(response.message || 'Unable to cancel booking');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error cancelling booking:', err);
|
||||
const message =
|
||||
err.response?.data?.message ||
|
||||
'Unable to cancel booking. Please try again.';
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setCancelling(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] py-8">
|
||||
<Loading fullScreen text="Loading payment information..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !booking || !depositPayment) {
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] py-8 sm:py-12 w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-2 sm:px-4 md:px-6 lg:px-8 py-8">
|
||||
<div
|
||||
className="bg-gradient-to-br from-red-900/20 to-red-800/10
|
||||
border border-red-500/30 rounded-xl p-6 sm:p-12 text-center
|
||||
backdrop-blur-xl shadow-2xl shadow-red-500/10"
|
||||
>
|
||||
<AlertCircle className="w-10 h-10 sm:w-12 sm:h-12 text-red-400 mx-auto mb-4" />
|
||||
<p className="text-red-300 font-light text-base sm:text-lg mb-6 tracking-wide px-2">
|
||||
{error || 'Payment information not found'}
|
||||
</p>
|
||||
<Link
|
||||
to="/bookings"
|
||||
className="inline-flex items-center gap-2 bg-gradient-to-r
|
||||
from-[#d4af37] to-[#c9a227] text-[#0f0f0f]
|
||||
px-4 py-2 sm:px-6 sm:py-3 rounded-sm hover:from-[#f5d76e]
|
||||
hover:to-[#d4af37] transition-all duration-300
|
||||
font-medium tracking-wide shadow-lg shadow-[#d4af37]/30 text-sm sm:text-base"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
Back to booking list
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const depositAmount = parseFloat(depositPayment.amount.toString());
|
||||
const remainingAmount = booking.total_price - depositAmount;
|
||||
const isDepositPaid = depositPayment.payment_status === 'completed';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] py-8 sm:py-12 w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-3 sm:py-4">
|
||||
{}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 mb-3 sm:mb-4">
|
||||
<Link
|
||||
to={`/bookings/${bookingId}`}
|
||||
className="inline-flex items-center gap-1
|
||||
text-[#d4af37]/80 hover:text-[#d4af37]
|
||||
transition-colors font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
<span>Back to booking details</span>
|
||||
</Link>
|
||||
|
||||
{}
|
||||
{!isDepositPaid && booking && (
|
||||
<button
|
||||
onClick={handleCancelBooking}
|
||||
disabled={cancelling}
|
||||
className="inline-flex items-center gap-1
|
||||
bg-gradient-to-br from-red-900/20 to-red-800/10
|
||||
border border-red-500/30 text-red-300
|
||||
px-2.5 py-1 rounded-sm
|
||||
hover:border-red-400/50 hover:bg-gradient-to-br
|
||||
hover:from-red-800/30 hover:to-red-700/20
|
||||
transition-all duration-300 font-light tracking-wide text-xs sm:text-sm
|
||||
backdrop-blur-sm shadow-sm shadow-red-500/10
|
||||
hover:shadow-md hover:shadow-red-500/20
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
w-full sm:w-auto"
|
||||
>
|
||||
<XCircle className="w-3.5 h-3.5" />
|
||||
{cancelling ? 'Cancelling...' : 'Cancel Booking'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{}
|
||||
{isDepositPaid && (
|
||||
<div
|
||||
className="bg-gradient-to-br from-green-900/20 to-green-800/10
|
||||
border border-green-500/30 rounded-lg p-3 sm:p-4 mb-3 sm:mb-4
|
||||
backdrop-blur-xl shadow-lg shadow-green-500/10"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-2.5 sm:gap-3">
|
||||
<div
|
||||
className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-green-500/20 to-green-600/20
|
||||
rounded-full flex items-center justify-center flex-shrink-0
|
||||
border border-green-500/30 shadow-sm shadow-green-500/20"
|
||||
>
|
||||
<CheckCircle className="w-6 h-6 sm:w-7 sm:h-7 text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1 text-center sm:text-left">
|
||||
<h1 className="text-base sm:text-lg font-serif font-semibold text-green-300 mb-1 tracking-wide">
|
||||
Deposit Payment Successful!
|
||||
</h1>
|
||||
<p className="text-green-200/80 font-light text-xs sm:text-sm tracking-wide">
|
||||
Your booking has been confirmed.
|
||||
Remaining amount to be paid on arrival at the hotel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{}
|
||||
{!isDepositPaid && (
|
||||
<div
|
||||
className="bg-gradient-to-br from-[#d4af37]/10 to-[#c9a227]/5
|
||||
border border-[#d4af37]/30 rounded-lg p-3 sm:p-4 mb-3 sm:mb-4
|
||||
backdrop-blur-xl shadow-lg shadow-[#d4af37]/10"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-2.5 sm:gap-3">
|
||||
<div
|
||||
className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-[#d4af37]/20 to-[#f5d76e]/20
|
||||
rounded-full flex items-center justify-center flex-shrink-0
|
||||
border border-[#d4af37]/30 shadow-sm shadow-[#d4af37]/20"
|
||||
>
|
||||
<CreditCard className="w-6 h-6 sm:w-7 sm:h-7 text-[#d4af37]" />
|
||||
</div>
|
||||
<div className="flex-1 text-center sm:text-left">
|
||||
<h1 className="text-base sm:text-lg font-elegant font-semibold text-[#d4af37] mb-1 tracking-wide">
|
||||
Deposit Payment Required
|
||||
</h1>
|
||||
<p className="text-gray-300/80 font-light text-xs sm:text-sm tracking-wide">
|
||||
Please pay <strong className="text-[#d4af37] font-medium">20% deposit</strong> to confirm your booking. Pay the remaining balance on arrival at the hotel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3 sm:gap-4">
|
||||
{}
|
||||
<div className="lg:col-span-2 space-y-3">
|
||||
{}
|
||||
<div className="bg-gradient-to-br from-gray-900/40 to-gray-800/20
|
||||
border border-gray-700/50 rounded-lg p-3 sm:p-4
|
||||
backdrop-blur-xl shadow-lg shadow-black/20">
|
||||
<h2 className="text-sm sm:text-base font-serif font-semibold text-[#d4af37] mb-2.5 sm:mb-3 tracking-wide">
|
||||
Payment Information
|
||||
</h2>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-1 sm:gap-0 py-1.5 border-b border-gray-700/30">
|
||||
<span className="text-gray-300 font-light tracking-wide text-xs sm:text-sm">Total Room Price</span>
|
||||
<span className="font-medium text-gray-100 text-xs sm:text-sm">
|
||||
{formatPrice(booking.total_price)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-1 sm:gap-0 py-2
|
||||
border-t-2 border-[#d4af37]/30 pt-2"
|
||||
>
|
||||
<span className="font-medium text-[#d4af37] text-xs sm:text-sm tracking-wide">
|
||||
Deposit Amount to Pay (20%)
|
||||
</span>
|
||||
<span className="text-base sm:text-lg font-bold text-[#d4af37]">
|
||||
{formatPrice(depositAmount)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-0.5 text-[10px] sm:text-xs text-gray-400/80 font-light pt-1">
|
||||
<span>Remaining amount to be paid at check-in</span>
|
||||
<span className="text-gray-300">{formatPrice(remainingAmount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isDepositPaid && (
|
||||
<div className="mt-4 sm:mt-6 bg-gradient-to-br from-green-900/20 to-green-800/10
|
||||
border border-green-500/30 rounded-lg p-3 sm:p-4 backdrop-blur-sm">
|
||||
<p className="text-xs sm:text-sm text-green-300 font-light break-words">
|
||||
✓ Deposit paid on:{' '}
|
||||
{depositPayment.payment_date
|
||||
? new Date(depositPayment.payment_date).toLocaleString('en-US')
|
||||
: 'N/A'}
|
||||
</p>
|
||||
{depositPayment.transaction_id && (
|
||||
<p className="text-xs text-green-400/70 mt-2 font-mono break-all">
|
||||
Transaction ID: {depositPayment.transaction_id}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{}
|
||||
{!isDepositPaid && !selectedPaymentMethod && (
|
||||
<div className="bg-gradient-to-br from-gray-900/40 to-gray-800/20
|
||||
border border-gray-700/50 rounded-lg p-3 sm:p-4
|
||||
backdrop-blur-xl shadow-lg shadow-black/20">
|
||||
<h2 className="text-sm sm:text-base font-serif font-semibold text-[#d4af37] mb-1.5 tracking-wide">
|
||||
Choose Payment Method
|
||||
</h2>
|
||||
<p className="text-gray-300/80 font-light mb-3 sm:mb-4 tracking-wide text-xs sm:text-sm">
|
||||
Please select how you would like to pay the deposit:
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 sm:gap-3">
|
||||
{}
|
||||
<button
|
||||
onClick={() => setSelectedPaymentMethod('stripe')}
|
||||
className="bg-gradient-to-br from-gray-800/40 to-gray-700/20
|
||||
border-2 border-gray-600/30 rounded-lg p-3
|
||||
hover:border-[#d4af37]/50 hover:bg-gradient-to-br
|
||||
hover:from-[#d4af37]/10 hover:to-[#c9a227]/5
|
||||
transition-all duration-300 text-left group
|
||||
backdrop-blur-sm shadow-sm shadow-black/10
|
||||
hover:shadow-md hover:shadow-[#d4af37]/20"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-indigo-500/20 to-indigo-600/20
|
||||
rounded-lg flex items-center justify-center
|
||||
border border-indigo-500/30 group-hover:border-[#d4af37]/50
|
||||
transition-colors">
|
||||
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-indigo-400 group-hover:text-[#d4af37] transition-colors" />
|
||||
</div>
|
||||
<span className="text-[10px] sm:text-xs font-semibold text-indigo-400 group-hover:text-[#d4af37]
|
||||
transition-colors tracking-wide">Card Payment</span>
|
||||
</div>
|
||||
<p className="text-[10px] sm:text-xs text-gray-300/70 font-light group-hover:text-gray-200 transition-colors">
|
||||
Pay with credit or debit card via Stripe
|
||||
</p>
|
||||
</button>
|
||||
|
||||
{}
|
||||
<button
|
||||
onClick={() => setSelectedPaymentMethod('paypal')}
|
||||
className="bg-gradient-to-br from-gray-800/40 to-gray-700/20
|
||||
border-2 border-gray-600/30 rounded-lg p-3
|
||||
hover:border-[#d4af37]/50 hover:bg-gradient-to-br
|
||||
hover:from-[#d4af37]/10 hover:to-[#c9a227]/5
|
||||
transition-all duration-300 text-left group
|
||||
backdrop-blur-sm shadow-sm shadow-black/10
|
||||
hover:shadow-md hover:shadow-[#d4af37]/20"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-blue-500/20 to-blue-600/20
|
||||
rounded-lg flex items-center justify-center
|
||||
border border-blue-500/30 group-hover:border-[#d4af37]/50
|
||||
transition-colors">
|
||||
<svg
|
||||
className="w-4 h-4 sm:w-5 sm:h-5 text-blue-400 group-hover:text-[#d4af37] transition-colors"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M7.076 21.337H2.47a.641.641 0 0 1-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797h-2.19c-.524 0-.968.382-1.05.9l-1.12 7.203zm14.146-14.42a.477.477 0 0 0-.414-.24h-3.84c-.48 0-.856.355-.932.826-.075.47-.232 1.21-.232 1.21s-.156-.74-.232-1.21a.957.957 0 0 0-.932-.826H5.342a.957.957 0 0 0-.932.826c-.076.47-.232 1.21-.232 1.21s-.156-.74-.232-1.21a.957.957 0 0 0-.932-.826H.477a.477.477 0 0 0-.414.24c-.11.19-.14.426-.08.643.06.217.2.4.388.51l.04.02c.19.11.426.14.643.08.217-.06.4-.2.51-.388l.01-.02c.11-.19.14-.426.08-.643a.955.955 0 0 0-.388-.51l-.01-.01a.955.955 0 0 0-.51-.388.955.955 0 0 0-.643.08l-.01.01a.955.955 0 0 0-.388.51c-.06.217-.03.453.08.643l.01.02c.11.188.293.328.51.388.217.06.453.03.643-.08l.01-.02c.188-.11.328-.293.388-.51.06-.217.03-.453-.08-.643l-.01-.01z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-[10px] sm:text-xs font-semibold text-blue-400 group-hover:text-[#d4af37]
|
||||
transition-colors tracking-wide">PayPal</span>
|
||||
</div>
|
||||
<p className="text-[10px] sm:text-xs text-gray-300/70 font-light group-hover:text-gray-200 transition-colors">
|
||||
Pay securely with your PayPal account
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{}
|
||||
{!isDepositPaid && selectedPaymentMethod && (
|
||||
<div className="bg-gradient-to-br from-gray-900/40 to-gray-800/20
|
||||
border border-gray-700/50 rounded-lg p-3 sm:p-4 mb-3 sm:mb-4
|
||||
backdrop-blur-xl shadow-lg shadow-black/10">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-0">
|
||||
<div>
|
||||
<h3 className="text-sm sm:text-base font-serif font-semibold text-[#d4af37] mb-0.5 tracking-wide">
|
||||
{selectedPaymentMethod === 'stripe' ? 'Card Payment' : 'PayPal Payment'}
|
||||
</h3>
|
||||
<p className="text-xs sm:text-sm text-gray-300/80 font-light tracking-wide">
|
||||
Pay deposit of <span className="text-[#d4af37] font-medium">{formatPrice(depositAmount)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedPaymentMethod(null)}
|
||||
className="text-[10px] sm:text-xs text-gray-400 hover:text-[#d4af37]
|
||||
underline transition-colors font-light tracking-wide self-start sm:self-auto"
|
||||
>
|
||||
Change method
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{}
|
||||
{!isDepositPaid && booking && depositPayment && selectedPaymentMethod === 'stripe' && (
|
||||
<div className="bg-gradient-to-br from-gray-900/40 to-gray-800/20
|
||||
border border-gray-700/50 rounded-lg p-3 sm:p-4
|
||||
backdrop-blur-xl shadow-lg shadow-black/20">
|
||||
{paymentSuccess ? (
|
||||
<div className="bg-gradient-to-br from-green-900/20 to-green-800/10
|
||||
border border-green-500/30 rounded-lg p-4 sm:p-5 text-center
|
||||
backdrop-blur-sm">
|
||||
<CheckCircle className="w-10 h-10 sm:w-12 sm:h-12 text-green-400 mx-auto mb-3" />
|
||||
<h3 className="text-base sm:text-lg font-serif font-semibold text-green-300 mb-2 tracking-wide">
|
||||
Payment Successful!
|
||||
</h3>
|
||||
<p className="text-green-200/80 mb-4 font-light tracking-wide text-xs sm:text-sm">
|
||||
Your deposit payment has been confirmed.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate(`/bookings/${booking.id}`)}
|
||||
className="bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
||||
text-[#0f0f0f] px-4 py-1.5 sm:px-5 sm:py-2 rounded-sm
|
||||
hover:from-[#f5d76e] hover:to-[#d4af37]
|
||||
transition-all duration-300 font-medium tracking-wide
|
||||
shadow-sm shadow-[#d4af37]/30 text-xs sm:text-sm"
|
||||
>
|
||||
View Booking
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<StripePaymentWrapper
|
||||
bookingId={booking.id}
|
||||
amount={depositAmount}
|
||||
onSuccess={() => {
|
||||
setPaymentSuccess(true);
|
||||
toast.success('✅ Payment successful! Your booking has been confirmed.');
|
||||
|
||||
setTimeout(() => {
|
||||
navigate(`/bookings/${booking.id}`);
|
||||
}, 2000);
|
||||
}}
|
||||
onError={(error) => {
|
||||
toast.error(error || 'Payment failed');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{}
|
||||
{!isDepositPaid && booking && depositPayment && selectedPaymentMethod === 'paypal' && (
|
||||
<div className="bg-gradient-to-br from-gray-900/40 to-gray-800/20
|
||||
border border-gray-700/50 rounded-lg p-3 sm:p-4
|
||||
backdrop-blur-xl shadow-lg shadow-black/20">
|
||||
<PayPalPaymentWrapper
|
||||
bookingId={booking.id}
|
||||
amount={depositAmount}
|
||||
currency={currency || 'USD'}
|
||||
onError={(error) => {
|
||||
toast.error(error || 'Payment failed');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepositPaymentPage;
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
getMyBookings,
|
||||
cancelBooking,
|
||||
type Booking,
|
||||
} from '../../services/api/bookingService';
|
||||
import CancelBookingModal from '../../components/booking/CancelBookingModal';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||
import Loading from '../../components/common/Loading';
|
||||
@@ -40,8 +40,8 @@ const MyBookingsPage: React.FC = () => {
|
||||
useState<Booking[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [cancellingId, setCancellingId] =
|
||||
useState<number | null>(null);
|
||||
const [showCancelModal, setShowCancelModal] = useState(false);
|
||||
const [selectedBooking, setSelectedBooking] = useState<Booking | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] =
|
||||
useState<string>('all');
|
||||
@@ -119,53 +119,23 @@ const MyBookingsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelBooking = async (
|
||||
bookingId: number,
|
||||
bookingNumber: string
|
||||
) => {
|
||||
const confirmed = window.confirm(
|
||||
`Are you sure you want to cancel booking ${bookingNumber}?\n\n` +
|
||||
`⚠️ Note:\n` +
|
||||
`- You will be charged 20% of the order value\n` +
|
||||
`- The remaining 80% will be refunded\n` +
|
||||
`- Room status will be updated to "available"`
|
||||
);
|
||||
const handleCancelBookingClick = (booking: Booking) => {
|
||||
setSelectedBooking(booking);
|
||||
setShowCancelModal(true);
|
||||
};
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
setCancellingId(bookingId);
|
||||
|
||||
const response = await cancelBooking(bookingId);
|
||||
|
||||
if (response.success) {
|
||||
toast.success(
|
||||
`✅ Successfully cancelled booking ${bookingNumber}!`
|
||||
);
|
||||
|
||||
|
||||
setBookings((prev) =>
|
||||
prev.map((b) =>
|
||||
b.id === bookingId
|
||||
? { ...b, status: 'cancelled' }
|
||||
: b
|
||||
)
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
response.message ||
|
||||
'Unable to cancel booking'
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error cancelling booking:', err);
|
||||
const message =
|
||||
err.response?.data?.message ||
|
||||
'Unable to cancel booking. Please try again.';
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setCancellingId(null);
|
||||
const handleCancelSuccess = () => {
|
||||
if (selectedBooking) {
|
||||
setBookings((prev) =>
|
||||
prev.map((b) =>
|
||||
b.id === selectedBooking.id
|
||||
? { ...b, status: 'cancelled' }
|
||||
: b
|
||||
)
|
||||
);
|
||||
}
|
||||
setShowCancelModal(false);
|
||||
setSelectedBooking(null);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
@@ -619,38 +589,19 @@ const MyBookingsPage: React.FC = () => {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCancelBooking(
|
||||
booking.id,
|
||||
booking.booking_number
|
||||
);
|
||||
handleCancelBookingClick(booking);
|
||||
}}
|
||||
disabled={
|
||||
cancellingId === booking.id
|
||||
}
|
||||
className="inline-flex
|
||||
items-center gap-2 px-4 py-2
|
||||
bg-red-600 text-white
|
||||
rounded-lg hover:bg-red-700
|
||||
transition-colors font-medium
|
||||
text-sm disabled:bg-gray-400
|
||||
disabled:cursor-not-allowed cursor-pointer"
|
||||
text-sm cursor-pointer"
|
||||
>
|
||||
{cancellingId === booking.id ? (
|
||||
<>
|
||||
<Loader2
|
||||
className="w-4 h-4
|
||||
animate-spin"
|
||||
/>
|
||||
Cancelling...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
Cancel booking
|
||||
</>
|
||||
)}
|
||||
<XCircle
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
Cancel booking
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -663,6 +614,19 @@ const MyBookingsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cancel Booking Modal */}
|
||||
{showCancelModal && selectedBooking && (
|
||||
<CancelBookingModal
|
||||
isOpen={showCancelModal}
|
||||
booking={selectedBooking}
|
||||
onClose={() => {
|
||||
setShowCancelModal(false);
|
||||
setSelectedBooking(null);
|
||||
}}
|
||||
onSuccess={handleCancelSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -70,7 +70,7 @@ const PayPalCancelPage: React.FC = () => {
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
||||
{bookingId && (
|
||||
<button
|
||||
onClick={() => navigate(`/payment/deposit/${bookingId}`)}
|
||||
onClick={() => navigate(`/bookings/${bookingId}`)}
|
||||
className="flex-1 bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
||||
text-[#0f0f0f] px-4 py-2 sm:px-6 sm:py-3 rounded-sm
|
||||
hover:from-[#f5d76e] hover:to-[#d4af37]
|
||||
@@ -80,7 +80,7 @@ const PayPalCancelPage: React.FC = () => {
|
||||
disabled={cancelling}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
Try Again
|
||||
View Booking Details
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
|
||||
@@ -125,16 +125,18 @@ const PayPalReturnPage: React.FC = () => {
|
||||
{error || 'Unable to process your payment. Please try again.'}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
||||
<button
|
||||
onClick={() => navigate(`/payment/deposit/${bookingId}`)}
|
||||
className="flex-1 bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
||||
text-[#0f0f0f] px-4 py-2 sm:px-6 sm:py-3 rounded-sm
|
||||
hover:from-[#f5d76e] hover:to-[#d4af37]
|
||||
transition-all duration-300 font-medium tracking-wide
|
||||
shadow-lg shadow-[#d4af37]/30 text-sm sm:text-base"
|
||||
>
|
||||
Retry Payment
|
||||
</button>
|
||||
{bookingId && (
|
||||
<button
|
||||
onClick={() => navigate(`/bookings/${bookingId}`)}
|
||||
className="flex-1 bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
||||
text-[#0f0f0f] px-4 py-2 sm:px-6 sm:py-3 rounded-sm
|
||||
hover:from-[#f5d76e] hover:to-[#d4af37]
|
||||
transition-all duration-300 font-medium tracking-wide
|
||||
shadow-lg shadow-[#d4af37]/30 text-sm sm:text-base"
|
||||
>
|
||||
View Booking
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => navigate('/bookings')}
|
||||
className="flex-1 bg-gradient-to-br from-gray-800/40 to-gray-700/20
|
||||
|
||||
@@ -197,12 +197,12 @@ const PaymentResultPage: React.FC = () => {
|
||||
) : status === 'failed' && bookingId ? (
|
||||
<>
|
||||
<Link
|
||||
to={`/deposit-payment/${bookingId}`}
|
||||
to={`/bookings/${bookingId}`}
|
||||
className="flex-1 px-6 py-3 bg-indigo-600
|
||||
text-white rounded-lg hover:bg-indigo-700
|
||||
transition-colors font-medium text-center"
|
||||
>
|
||||
Retry payment
|
||||
View Booking
|
||||
</Link>
|
||||
<Link
|
||||
to="/bookings"
|
||||
|
||||
@@ -42,8 +42,8 @@ const profileValidationSchema = yup.object().shape({
|
||||
.string()
|
||||
.required('Phone number is required')
|
||||
.matches(
|
||||
/^[0-9]{10,11}$/,
|
||||
'Phone number must have 10-11 digits'
|
||||
/^[\d\s\-\+\(\)]{5,}$/,
|
||||
'Please enter a valid phone number'
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Award,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { getRoomByNumber, type Room } from
|
||||
import { getRoomByNumber, getRoomBookedDates, type Room } from
|
||||
'../../services/api/roomService';
|
||||
import RoomGallery from '../../components/rooms/RoomGallery';
|
||||
import RoomAmenities from '../../components/rooms/RoomAmenities';
|
||||
@@ -18,15 +18,22 @@ import ReviewSection from '../../components/rooms/ReviewSection';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import LuxuryBookingModal from '../../components/booking/LuxuryBookingModal';
|
||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const RoomDetailPage: React.FC = () => {
|
||||
const { room_number } = useParams<{ room_number: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
const { userInfo } = useAuthStore();
|
||||
const { userInfo, isAuthenticated } = useAuthStore();
|
||||
const { openModal } = useAuthModal();
|
||||
const [room, setRoom] = useState<Room | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showBookingModal, setShowBookingModal] = useState(false);
|
||||
const [nextAvailableDate, setNextAvailableDate] = useState<Date | null>(null);
|
||||
const [bookedUntilDate, setBookedUntilDate] = useState<Date | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (room_number) {
|
||||
@@ -52,6 +59,11 @@ const RoomDetailPage: React.FC = () => {
|
||||
}
|
||||
|
||||
setRoom(fetchedRoom);
|
||||
|
||||
// Fetch booked dates to calculate availability
|
||||
if (fetchedRoom.id) {
|
||||
await fetchBookedDates(fetchedRoom.id);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to fetch room details');
|
||||
}
|
||||
@@ -70,6 +82,65 @@ const RoomDetailPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBookedDates = async (roomId: number) => {
|
||||
try {
|
||||
const response = await getRoomBookedDates(roomId);
|
||||
if (response.success && response.data?.booked_dates) {
|
||||
const bookedDates = response.data.booked_dates.map((date: string) => {
|
||||
const d = new Date(date);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
});
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
// Sort booked dates
|
||||
const sortedBookedDates = bookedDates.sort((a: Date, b: Date) => a.getTime() - b.getTime());
|
||||
|
||||
// Find if today is booked
|
||||
const todayBooked = sortedBookedDates.some((date: Date) => {
|
||||
return date.getTime() === today.getTime();
|
||||
});
|
||||
|
||||
if (todayBooked) {
|
||||
// Find consecutive booked dates starting from today
|
||||
let lastConsecutiveDate = new Date(today);
|
||||
|
||||
for (let i = 0; i < sortedBookedDates.length; i++) {
|
||||
const bookedDate = sortedBookedDates[i];
|
||||
|
||||
// Check if this date is consecutive from the last found date
|
||||
const daysDiff = Math.floor((bookedDate.getTime() - lastConsecutiveDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysDiff === 0 || daysDiff === 1) {
|
||||
// This date is part of the consecutive booking
|
||||
lastConsecutiveDate = new Date(bookedDate);
|
||||
} else if (daysDiff > 1) {
|
||||
// Gap found, stop looking
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Set booked until date (check-out date is the last booked date)
|
||||
setBookedUntilDate(lastConsecutiveDate);
|
||||
|
||||
// Next available date is the day after the last booked date
|
||||
const nextAvailable = new Date(lastConsecutiveDate);
|
||||
nextAvailable.setDate(nextAvailable.getDate() + 1);
|
||||
setNextAvailableDate(nextAvailable);
|
||||
} else {
|
||||
// Today is not booked, so it's available now
|
||||
setNextAvailableDate(today);
|
||||
setBookedUntilDate(null);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching booked dates:', err);
|
||||
// Don't show error to user, just don't show availability info
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div
|
||||
@@ -191,23 +262,35 @@ const RoomDetailPage: React.FC = () => {
|
||||
Featured
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`px-2 py-0.5 rounded-sm
|
||||
text-[10px] sm:text-xs font-medium tracking-wide
|
||||
backdrop-blur-sm shadow-sm
|
||||
${
|
||||
room.status === 'available'
|
||||
? 'bg-green-500/90 text-white border border-green-400/50'
|
||||
: room.status === 'occupied'
|
||||
? 'bg-red-500/90 text-white border border-red-400/50'
|
||||
: 'bg-gray-500/90 text-white border border-gray-400/50'
|
||||
}`}
|
||||
>
|
||||
{room.status === 'available'
|
||||
? 'Available Now'
|
||||
: room.status === 'occupied'
|
||||
? 'Booked'
|
||||
: 'Maintenance'}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div
|
||||
className={`px-2 py-0.5 rounded-sm
|
||||
text-[10px] sm:text-xs font-medium tracking-wide
|
||||
backdrop-blur-sm shadow-sm
|
||||
${
|
||||
room.status === 'available'
|
||||
? 'bg-green-500/90 text-white border border-green-400/50'
|
||||
: room.status === 'occupied'
|
||||
? 'bg-red-500/90 text-white border border-red-400/50'
|
||||
: 'bg-gray-500/90 text-white border border-gray-400/50'
|
||||
}`}
|
||||
>
|
||||
{room.status === 'available'
|
||||
? 'Available Now'
|
||||
: room.status === 'occupied'
|
||||
? 'Booked'
|
||||
: 'Maintenance'}
|
||||
</div>
|
||||
{room.status === 'occupied' && bookedUntilDate && (
|
||||
<p className="text-[9px] sm:text-[10px] text-gray-300 font-light leading-tight">
|
||||
Booked until {bookedUntilDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||||
</p>
|
||||
)}
|
||||
{room.status === 'occupied' && nextAvailableDate && (
|
||||
<p className="text-[9px] sm:text-[10px] text-[#d4af37] font-light leading-tight">
|
||||
Available from {nextAvailableDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -371,8 +454,20 @@ const RoomDetailPage: React.FC = () => {
|
||||
{userInfo?.role !== 'admin' && userInfo?.role !== 'staff' && (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<Link
|
||||
to={`/booking/${room.id}`}
|
||||
<button
|
||||
onClick={() => {
|
||||
if (room.status !== 'available') {
|
||||
toast.error('This room is not available for booking');
|
||||
return;
|
||||
}
|
||||
if (!isAuthenticated) {
|
||||
toast.error('Please login to make a booking');
|
||||
openModal('login');
|
||||
return;
|
||||
}
|
||||
setShowBookingModal(true);
|
||||
}}
|
||||
disabled={room.status !== 'available'}
|
||||
className={`block w-full py-2 text-center
|
||||
font-medium rounded-sm transition-all duration-300
|
||||
tracking-wide relative overflow-hidden group text-xs sm:text-sm
|
||||
@@ -381,9 +476,6 @@ const RoomDetailPage: React.FC = () => {
|
||||
? 'bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] hover:from-[#f5d76e] hover:to-[#d4af37] shadow-sm shadow-[#d4af37]/30 hover:shadow-[#d4af37]/50'
|
||||
: 'bg-gray-800 text-gray-500 cursor-not-allowed border border-gray-700'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
if (room.status !== 'available') e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<span className="relative z-10 flex items-center justify-center gap-1.5">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
@@ -392,7 +484,7 @@ const RoomDetailPage: React.FC = () => {
|
||||
{room.status === 'available' && (
|
||||
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
||||
)}
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{room.status === 'available' && (
|
||||
@@ -449,6 +541,19 @@ const RoomDetailPage: React.FC = () => {
|
||||
<ReviewSection roomId={room.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Booking Modal */}
|
||||
{room && (
|
||||
<LuxuryBookingModal
|
||||
roomId={room.id}
|
||||
isOpen={showBookingModal}
|
||||
onClose={() => setShowBookingModal(false)}
|
||||
onSuccess={(bookingId) => {
|
||||
setShowBookingModal(false);
|
||||
navigate(`/booking-success/${bookingId}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user