923 lines
28 KiB
TypeScript
923 lines
28 KiB
TypeScript
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;
|