Files
Hotel-Booking/client/src/pages/customer/BookingSuccessPage.tsx
Iliyan Angelov 824eec6190 Hotel Booking
2025-11-16 14:19:13 +02:00

827 lines
25 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';
const BookingSuccessPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
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);
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);
// Redirect to deposit payment page if required and not yet paid
if (
bookingData.requires_deposit &&
!bookingData.deposit_paid
) {
navigate(`/deposit-payment/${bookingId}`, { replace: true });
return;
}
} else {
throw new Error(
'Không thể tải thông tin đặt phòng'
);
}
} catch (err: any) {
console.error('Error fetching booking:', err);
const message =
err.response?.data?.message ||
'Không thể tải thông tin đặt phòng';
setError(message);
toast.error(message);
} finally {
setLoading(false);
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
const formatPrice = (price: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'VND',
}).format(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 'Đã xác nhận';
case 'pending':
return 'Chờ xác nhận';
case 'cancelled':
return 'Đã hủy';
case 'checked_in':
return 'Đã nhận phòng';
case 'checked_out':
return 'Đã trả phòng';
default:
return status;
}
};
const copyBookingNumber = async () => {
if (!booking?.booking_number) return;
try {
await navigator.clipboard.writeText(
booking.booking_number
);
setCopiedBookingNumber(true);
toast.success('Đã sao chép mã đặt phòng');
setTimeout(() => setCopiedBookingNumber(false), 2000);
} catch (err) {
toast.error('Không thể sao chép');
}
};
const handleFileSelect = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const file = e.target.files?.[0];
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
toast.error('Vui lòng chọn file ảnh');
return;
}
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
toast.error('Kích thước ảnh không được vượt quá 5MB');
return;
}
setSelectedFile(file);
// Create preview
const reader = new FileReader();
reader.onloadend = () => {
setPreviewUrl(reader.result as string);
};
reader.readAsDataURL(file);
};
const handleUploadReceipt = async () => {
if (!selectedFile || !booking) return;
try {
setUploadingReceipt(true);
// Generate transaction ID based on booking number
const transactionId =
`TXN-${booking.booking_number}-${Date.now()}`;
const response = await confirmBankTransfer(
booking.id,
transactionId,
selectedFile
);
if (response.success) {
toast.success(
'✅ Đã gửi xác nhận thanh toán thành công! ' +
'Chúng tôi sẽ xác nhận trong thời gian sớm nhất.'
);
setReceiptUploaded(true);
// Update booking payment status locally
setBooking((prev) =>
prev
? {
...prev,
payment_status: 'paid',
status: prev.status === 'pending'
? 'confirmed'
: prev.status
}
: null
);
} else {
throw new Error(
response.message ||
'Không thể xác nhận thanh toán'
);
}
} catch (err: any) {
console.error('Error uploading receipt:', err);
const message =
err.response?.data?.message ||
'Không thể gửi xác nhận thanh toán. ' +
'Vui lòng thử lại.';
toast.error(message);
} finally {
setUploadingReceipt(false);
}
};
const qrCodeUrl = booking
? generateQRCode(
booking.booking_number,
booking.total_price
)
: null;
if (loading) {
return <Loading fullScreen text="Đang tải..." />;
}
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 || 'Không tìm thấy đặt phòng'}
</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"
>
Quay lại danh sách phòng
</button>
</div>
</div>
</div>
);
}
const room = booking.room;
const roomType = room?.room_type;
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-4xl mx-auto px-4">
{/* Success Header */}
<div
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"
>
Đt phòng thành công!
</h1>
<p className="text-gray-600 mb-4">
Cảm ơn bạn đã đt phòng tại khách sạn của chúng
tôi
</p>
{/* Booking Number */}
<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"
>
đt phòng:
</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="Sao chép mã"
>
{copiedBookingNumber ? (
<Check className="w-4 h-4 text-green-600" />
) : (
<Copy className="w-4 h-4 text-indigo-600" />
)}
</button>
</div>
{/* Status Badge */}
<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>
{/* Booking Details */}
<div className="bg-white rounded-lg shadow-md
p-6 mb-6"
>
<h2 className="text-xl font-bold text-gray-900
mb-4"
>
Chi tiết đt phòng
</h2>
<div className="space-y-4">
{/* Room Information */}
{roomType && (
<div className="border-b pb-4">
<div className="flex items-start gap-4">
{roomType.images?.[0] && (
<img
src={roomType.images[0]}
alt={roomType.name}
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"
/>
Phòng {room.room_number} -
Tầng {room.floor}
</p>
)}
<p className="text-indigo-600
font-semibold mt-1"
>
{formatPrice(roomType.base_price)}/đêm
</p>
</div>
</div>
</div>
)}
{/* Dates */}
<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" />
Ngày nhận phòng
</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" />
Ngày trả phòng
</p>
<p className="font-medium text-gray-900">
{formatDate(booking.check_out_date)}
</p>
</div>
</div>
{/* Guest Count */}
<div>
<p className="text-sm text-gray-600 mb-1">
<Users className="w-4 h-4 inline mr-1" />
Số người
</p>
<p className="font-medium text-gray-900">
{booking.guest_count} người
</p>
</div>
{/* Notes */}
{booking.notes && (
<div>
<p className="text-sm text-gray-600 mb-1">
<FileText className="w-4 h-4 inline mr-1" />
Ghi chú
</p>
<p className="font-medium text-gray-900">
{booking.notes}
</p>
</div>
)}
{/* Payment Method */}
<div className="border-t pt-4">
<p className="text-sm text-gray-600 mb-1">
<CreditCard className="w-4 h-4 inline mr-1" />
Phương thức thanh toán
</p>
<p className="font-medium text-gray-900">
{booking.payment_method === 'cash'
? '💵 Thanh toán tại chỗ'
: '🏦 Chuyển khoản ngân hàng'}
</p>
</div>
{/* Total Price */}
<div className="border-t pt-4">
<div className="flex justify-between
items-center"
>
<span className="text-lg font-semibold
text-gray-900"
>
Tổng thanh toán
</span>
<span className="text-2xl font-bold
text-indigo-600"
>
{formatPrice(booking.total_price)}
</span>
</div>
</div>
</div>
</div>
{/* Guest Information */}
{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"
>
Thông tin khách hàng
</h2>
<div className="space-y-3">
<div>
<p className="text-sm text-gray-600">
<User className="w-4 h-4 inline mr-1" />
Họ tên
</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" />
Số điện thoại
</p>
<p className="font-medium text-gray-900">
{booking.guest_info.phone}
</p>
</div>
</div>
</div>
)}
{/* Bank Transfer Instructions */}
{booking.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">
Hướng dẫn chuyển khoản
</h3>
<div className="space-y-2 text-sm
text-blue-800"
>
<p>
Vui lòng chuyển khoản theo thông tin sau:
</p>
<div className="grid grid-cols-1
md:grid-cols-2 gap-4"
>
{/* Bank Info */}
<div className="bg-white rounded-lg
p-4 space-y-2"
>
<p>
<strong>Ngân hàng:</strong>
Vietcombank (VCB)
</p>
<p>
<strong>Số tài khoản:</strong>
0123456789
</p>
<p>
<strong>Chủ tài khoản:</strong>
KHACH SAN ABC
</p>
<p>
<strong>Số tiền:</strong>{' '}
<span className="text-indigo-600
font-bold"
>
{formatPrice(booking.total_price)}
</span>
</p>
<p>
<strong>Nội dung:</strong>{' '}
<span className="font-mono
text-indigo-600"
>
{booking.booking_number}
</span>
</p>
</div>
{/* QR Code */}
{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"
>
Quét QR đ chuyển khoản
</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 đã bao gồm đy đ thông tin
</p>
</div>
)}
</div>
<p className="text-xs italic mt-2">
💡 Lưu ý: Vui lòng ghi đúng đt phòng
vào nội dung chuyển khoản đ chúng tôi
thể xác nhận thanh toán của bạn.
</p>
</div>
</div>
</div>
{/* Upload Receipt Section */}
{!receiptUploaded ? (
<div className="border-t border-blue-200
pt-4"
>
<h4 className="font-semibold text-blue-900
mb-3"
>
📎 Xác nhận thanh toán
</h4>
<p className="text-sm text-blue-700 mb-3">
Sau khi chuyển khoản, vui lòng tải lên
nh biên lai đ chúng tôi xác nhận nhanh
hơn.
</p>
<div className="space-y-3">
{/* File Input */}
<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={handleFileSelect}
className="hidden"
/>
<div className="flex flex-col
items-center gap-2"
>
{previewUrl ? (
<>
<img
src={previewUrl}
alt="Preview"
className="w-32 h-32
object-cover rounded"
/>
<p className="text-sm
text-blue-600 font-medium"
>
{selectedFile?.name}
</p>
<p className="text-xs
text-gray-500"
>
Click đ chọn nh khác
</p>
</>
) : (
<>
<FileText
className="w-8 h-8
text-blue-400"
/>
<p className="text-sm
text-blue-600 font-medium"
>
Chọn nh biên lai
</p>
<p className="text-xs
text-gray-500"
>
PNG, JPG, JPEG (Tối đa 5MB)
</p>
</>
)}
</div>
</label>
</div>
{/* Upload Button */}
{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"
/>
Đang gửi...
</>
) : (
<>
<CheckCircle
className="w-5 h-5"
/>
Xác nhận đã thanh toán
</>
)}
</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>
)}
{/* Important Notice */}
<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 === 'bank_transfer' && (
<li>
Please transfer within 24 hours
to secure your room
</li>
)}
</ul>
</div>
{/* Action Buttons */}
<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" />
Xem đơn của tôi
</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" />
Về trang chủ
</Link>
</div>
</div>
</div>
);
};
export default BookingSuccessPage;