Files
Hotel-Booking/Frontend/src/pages/customer/BookingSuccessPage.tsx
Iliyan Angelov 9842cc3a4a updates
2025-11-21 19:44:42 +02:00

923 lines
28 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import {
useParams,
useNavigate,
Link
} from 'react-router-dom';
import {
CheckCircle,
Home,
ListOrdered,
Calendar,
Users,
CreditCard,
MapPin,
Mail,
Phone,
User,
FileText,
Building2,
AlertCircle,
Copy,
Check,
Loader2,
} from 'lucide-react';
import { toast } from 'react-toastify';
import {
getBookingById,
generateQRCode,
type Booking,
} from '../../services/api/bookingService';
import { confirmBankTransfer } from
'../../services/api/paymentService';
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 }>();
const navigate = useNavigate();
const { formatCurrency } = useFormatCurrency();
const [booking, setBooking] = useState<Booking | null>(
null
);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [copiedBookingNumber, setCopiedBookingNumber] =
useState(false);
const [uploadingReceipt, setUploadingReceipt] =
useState(false);
const [receiptUploaded, setReceiptUploaded] =
useState(false);
const [selectedFile, setSelectedFile] =
useState<File | null>(null);
const [previewUrl, setPreviewUrl] =
useState<string | null>(null);
const [showDepositModal, setShowDepositModal] = useState(false);
useEffect(() => {
if (id) {
fetchBookingDetails(Number(id));
}
}, [id]);
const fetchBookingDetails = async (bookingId: number) => {
try {
setLoading(true);
setError(null);
const response = await getBookingById(bookingId);
if (
response.success &&
response.data?.booking
) {
const bookingData = response.data.booking;
setBooking(bookingData);
if (bookingData.payment_method === 'stripe' && bookingData.payments) {
const pendingStripePayment = bookingData.payments.find(
(p: any) =>
p.payment_method === 'stripe' &&
p.payment_status === 'pending'
);
if (pendingStripePayment) {
navigate(`/payment/${bookingId}`, { replace: true });
return;
}
}
// Only auto-open deposit modal if booking is not cancelled
if (
bookingData.requires_deposit &&
!bookingData.deposit_paid &&
bookingData.status !== 'cancelled'
) {
setShowDepositModal(true);
}
} else {
throw new Error(
'Unable to load booking information'
);
}
} catch (err: any) {
console.error('Error fetching booking:', err);
const message =
err.response?.data?.message ||
'Unable to load booking information';
setError(message);
toast.error(message);
} finally {
setLoading(false);
}
};
const formatDate = (dateString: string) => {
const date = parseDateLocal(dateString);
return date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
const formatPrice = (price: number) => formatCurrency(price);
const getStatusColor = (status: string) => {
switch (status) {
case 'confirmed':
return 'bg-green-100 text-green-800';
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'cancelled':
return 'bg-red-100 text-red-800';
case 'checked_in':
return 'bg-blue-100 text-blue-800';
case 'checked_out':
return 'bg-gray-100 text-gray-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'confirmed':
return 'Confirmed';
case 'pending':
return 'Pending confirmation';
case 'cancelled':
return 'Cancelled';
case 'checked_in':
return 'Checked in';
case 'checked_out':
return 'Checked out';
default:
return status;
}
};
const copyBookingNumber = async () => {
if (!booking?.booking_number) return;
try {
await navigator.clipboard.writeText(
booking.booking_number
);
setCopiedBookingNumber(true);
toast.success('Booking number copied');
setTimeout(() => setCopiedBookingNumber(false), 2000);
} catch (err) {
toast.error('Unable to copy');
}
};
const handleFileSelect = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
toast.error('Please select an image file');
return;
}
if (file.size > 5 * 1024 * 1024) {
toast.error('Image size must not exceed 5MB');
return;
}
setSelectedFile(file);
const reader = new FileReader();
reader.onloadend = () => {
setPreviewUrl(reader.result as string);
};
reader.readAsDataURL(file);
};
const handleUploadReceipt = async () => {
if (!selectedFile || !booking) return;
try {
setUploadingReceipt(true);
const transactionId =
`TXN-${booking.booking_number}-${Date.now()}`;
const response = await confirmBankTransfer(
booking.id,
transactionId,
selectedFile
);
if (response.success) {
toast.success(
'✅ Payment confirmation sent successfully! ' +
'We will confirm as soon as possible.'
);
setReceiptUploaded(true);
setBooking((prev) =>
prev
? {
...prev,
payment_status: 'paid',
status: prev.status === 'pending'
? 'confirmed'
: prev.status
}
: null
);
} else {
throw new Error(
response.message ||
'Unable to confirm payment'
);
}
} catch (err: any) {
console.error('Error uploading receipt:', err);
const message =
err.response?.data?.message ||
'Unable to send payment confirmation. ' +
'Please try again.';
toast.error(message);
} finally {
setUploadingReceipt(false);
}
};
const qrCodeUrl = booking
? generateQRCode(
booking.booking_number,
booking.total_price
)
: null;
if (loading) {
return <Loading fullScreen text="Loading..." />;
}
if (error || !booking) {
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-4xl mx-auto px-4">
<div
className="bg-red-50 border border-red-200
rounded-lg p-8 text-center"
>
<AlertCircle
className="w-12 h-12 text-red-500
mx-auto mb-3"
/>
<p className="text-red-700 font-medium mb-4">
{error || 'Booking not found'}
</p>
<button
onClick={() => navigate('/rooms')}
className="inline-flex items-center gap-2 bg-indigo-600
text-white px-3 py-2 rounded-md hover:bg-indigo-700
disabled:bg-gray-400 mb-6 transition-colors"
>
Back to room list
</button>
</div>
</div>
</div>
);
}
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">
{}
<div
className="bg-white rounded-lg shadow-md
p-8 mb-6 text-center"
>
{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
className="inline-flex items-center gap-2
bg-indigo-50 px-6 py-3 rounded-lg"
>
<span className="text-sm text-indigo-600
font-medium"
>
Booking Number:
</span>
<span className="text-lg font-bold
text-indigo-900"
>
{booking.booking_number}
</span>
<button
onClick={copyBookingNumber}
className="ml-2 p-1 hover:bg-indigo-100
rounded transition-colors"
title="Copy booking number"
>
{copiedBookingNumber ? (
<Check className="w-4 h-4 text-green-600" />
) : (
<Copy className="w-4 h-4 text-indigo-600" />
)}
</button>
</div>
{}
<div className="mt-4">
<span
className={`inline-block px-4 py-2
rounded-full text-sm font-medium
${getStatusColor(booking.status)}`}
>
{getStatusText(booking.status)}
</span>
</div>
</div>
{}
<div className="bg-white rounded-lg shadow-md
p-6 mb-6"
>
<h2 className="text-xl font-bold text-gray-900
mb-4"
>
Booking Details
</h2>
<div className="space-y-4">
{}
{roomType && (
<div className="border-b pb-4">
<div className="flex items-start gap-4">
{((room?.room_type?.images && room.room_type.images.length > 0)
? room.room_type.images[0]
: roomType?.images?.[0]) && (
<img
src={(room?.room_type?.images && room.room_type.images.length > 0)
? room.room_type.images[0]
: (roomType?.images?.[0] || '')}
alt={roomType?.name || 'Room'}
className="w-24 h-24 object-cover
rounded-lg"
/>
)}
<div className="flex-1">
<h3 className="font-bold text-lg
text-gray-900"
>
{roomType.name}
</h3>
{room && (
<p className="text-gray-600 text-sm">
<MapPin className="w-4 h-4
inline mr-1"
/>
Room {room.room_number} -
Floor {room.floor}
</p>
)}
<p className="text-indigo-600
font-semibold mt-1"
>
{formatPrice(room?.room_type?.base_price || roomType?.base_price || 0)}/night
</p>
</div>
</div>
</div>
)}
{}
<div className="grid grid-cols-1 md:grid-cols-2
gap-4"
>
<div>
<p className="text-sm text-gray-600 mb-1">
<Calendar className="w-4 h-4 inline mr-1" />
Check-in Date
</p>
<p className="font-medium text-gray-900">
{formatDate(booking.check_in_date)}
</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">
<Calendar className="w-4 h-4 inline mr-1" />
Check-out Date
</p>
<p className="font-medium text-gray-900">
{formatDate(booking.check_out_date)}
</p>
</div>
</div>
{}
<div>
<p className="text-sm text-gray-600 mb-1">
<Users className="w-4 h-4 inline mr-1" />
Number of Guests
</p>
<p className="font-medium text-gray-900">
{booking.guest_count} guest(s)
</p>
</div>
{}
{booking.notes && (
<div>
<p className="text-sm text-gray-600 mb-1">
<FileText className="w-4 h-4 inline mr-1" />
Notes
</p>
<p className="font-medium text-gray-900">
{booking.notes}
</p>
</div>
)}
{}
<div className="border-t pt-4">
<p className="text-sm text-gray-600 mb-1">
<CreditCard className="w-4 h-4 inline mr-1" />
Payment Method
</p>
<p className="font-medium text-gray-900">
{booking.payment_method === 'cash'
? '💵 Pay at hotel'
: '🏦 Bank transfer'}
</p>
</div>
{}
<div className="border-t pt-4">
{booking.original_price && booking.discount_amount && booking.discount_amount > 0 ? (
<>
<div className="mb-2">
<div className="flex justify-between items-center mb-1">
<span className="text-sm text-gray-600">Subtotal:</span>
<span className="text-base font-semibold text-gray-900">{formatPrice(booking.original_price)}</span>
</div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm text-green-600">
Discount{booking.promotion_code ? ` (${booking.promotion_code})` : ''}:
</span>
<span className="text-base font-semibold text-green-600">-{formatPrice(booking.discount_amount)}</span>
</div>
<div className="border-t border-gray-300 pt-2 mt-2">
<div className="flex justify-between items-center">
<span className="text-lg font-semibold text-gray-900">Total Payment</span>
<span className="text-2xl font-bold text-indigo-600">{formatPrice(booking.total_price)}</span>
</div>
</div>
</div>
</>
) : (
<div className="flex justify-between items-center">
<span className="text-lg font-semibold text-gray-900">Total Payment</span>
<span className="text-2xl font-bold text-indigo-600">{formatPrice(booking.total_price)}</span>
</div>
)}
</div>
</div>
</div>
{}
{booking.guest_info && (
<div className="bg-white rounded-lg shadow-md
p-6 mb-6"
>
<h2 className="text-xl font-bold text-gray-900
mb-4"
>
Customer Information
</h2>
<div className="space-y-3">
<div>
<p className="text-sm text-gray-600">
<User className="w-4 h-4 inline mr-1" />
Full Name
</p>
<p className="font-medium text-gray-900">
{booking.guest_info.full_name}
</p>
</div>
<div>
<p className="text-sm text-gray-600">
<Mail className="w-4 h-4 inline mr-1" />
Email
</p>
<p className="font-medium text-gray-900">
{booking.guest_info.email}
</p>
</div>
<div>
<p className="text-sm text-gray-600">
<Phone className="w-4 h-4 inline mr-1" />
Phone Number
</p>
<p className="font-medium text-gray-900">
{booking.guest_info.phone}
</p>
</div>
</div>
</div>
)}
{}
{(booking.payment_method === 'cash' || (booking as any).payment_method === 'bank_transfer') && (
<div
className="bg-blue-50 border border-blue-200
rounded-lg p-6 mb-6"
>
<div className="flex items-start gap-3 mb-4">
<Building2
className="w-6 h-6 text-blue-600
mt-1 flex-shrink-0"
/>
<div className="flex-1">
<h3 className="font-bold text-blue-900 mb-2">
Bank Transfer Instructions
</h3>
<div className="space-y-2 text-sm
text-blue-800"
>
<p>
Please transfer according to the following information:
</p>
<div className="grid grid-cols-1
md:grid-cols-2 gap-4"
>
{}
<div className="bg-white rounded-lg
p-4 space-y-2"
>
<p>
<strong>Bank:</strong>
Vietcombank (VCB)
</p>
<p>
<strong>Account Number:</strong>
0123456789
</p>
<p>
<strong>Account Holder:</strong>
KHACH SAN ABC
</p>
<p>
<strong>Amount:</strong>{' '}
<span className="text-indigo-600
font-bold"
>
{formatPrice(booking.total_price)}
</span>
</p>
<p>
<strong>Content:</strong>{' '}
<span className="font-mono
text-indigo-600"
>
{booking.booking_number}
</span>
</p>
</div>
{}
{qrCodeUrl && (
<div className="bg-white rounded-lg
p-4 flex flex-col items-center
justify-center"
>
<p className="text-sm font-medium
text-gray-700 mb-2"
>
Scan QR code to transfer
</p>
<img
src={qrCodeUrl}
alt="QR Code"
className="w-48 h-48 border-2
border-gray-200 rounded-lg"
/>
<p className="text-xs text-gray-500
mt-2 text-center"
>
QR code includes all information
</p>
</div>
)}
</div>
<p className="text-xs italic mt-2">
💡 Note: Please enter the correct booking number
in the transfer content so we can confirm your payment.
</p>
</div>
</div>
</div>
{}
{!receiptUploaded ? (
<div className="border-t border-blue-200
pt-4"
>
<h4 className="font-semibold text-blue-900
mb-3"
>
📎 Payment Confirmation
</h4>
<p className="text-sm text-blue-700 mb-3">
After transferring, please upload
the receipt image so we can confirm faster.
</p>
<div className="space-y-3">
{}
<div>
<label
htmlFor="receipt-upload"
className="block w-full px-4 py-3
border-2 border-dashed
border-blue-300 rounded-lg
text-center cursor-pointer
hover:border-blue-400
hover:bg-blue-100/50
transition-colors"
>
<input
id="receipt-upload"
type="file"
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"
>
{uploadingReceipt ? (
<>
<Loader2
className="w-5 h-5
animate-spin"
/>
Sending...
</>
) : (
<>
<CheckCircle
className="w-5 h-5"
/>
Confirm payment completed
</>
)}
</button>
)}
</div>
</div>
) : (
<div className="border-t border-green-200
pt-4 bg-green-50 rounded-lg p-4"
>
<div className="flex items-center
gap-3"
>
<CheckCircle
className="w-6 h-6 text-green-600
flex-shrink-0"
/>
<div>
<p className="font-semibold
text-green-900"
>
Payment confirmation sent
</p>
<p className="text-sm text-green-700">
We will confirm your order
as soon as possible.
</p>
</div>
</div>
</div>
)}
</div>
)}
{}
<div
className="bg-yellow-50 border border-yellow-200
rounded-lg p-4 mb-6"
>
<p className="text-sm text-yellow-800">
<strong>Important Notice:</strong>
</p>
<ul className="text-sm text-yellow-700 mt-2
space-y-1 ml-4 list-disc"
>
<li>
Please bring your ID card when checking in
</li>
<li>
Check-in time: 14:00 /
Check-out time: 12:00
</li>
<li>
If you cancel the booking, 20% of
the total order value will be charged
</li>
{(booking.payment_method === 'cash' || (booking as any).payment_method === 'bank_transfer') && (
<li>
Please transfer within 24 hours
to secure your room
</li>
)}
</ul>
</div>
{}
<div className="flex flex-col sm:flex-row gap-4">
<Link
to="/bookings"
className="flex-1 flex items-center
justify-center gap-2 px-6 py-3
bg-indigo-600 text-white rounded-lg
hover:bg-indigo-700 transition-colors
font-semibold"
>
<ListOrdered className="w-5 h-5" />
View My Bookings
</Link>
<Link
to="/"
className="flex-1 flex items-center
justify-center gap-2 px-6 py-3
bg-gray-600 text-white rounded-lg
hover:bg-gray-700 transition-colors
font-semibold"
>
<Home className="w-5 h-5" />
Go to Home
</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>
);
};
export default BookingSuccessPage;