1146 lines
52 KiB
TypeScript
1146 lines
52 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useForm, Controller } from 'react-hook-form';
|
|
import { yupResolver } from '@hookform/resolvers/yup';
|
|
import DatePicker from 'react-datepicker';
|
|
import 'react-datepicker/dist/react-datepicker.css';
|
|
import {
|
|
X,
|
|
Calendar,
|
|
Users,
|
|
CreditCard,
|
|
Sparkles,
|
|
CheckCircle,
|
|
ArrowRight,
|
|
ArrowLeft,
|
|
Loader2,
|
|
Plus,
|
|
Minus,
|
|
Receipt,
|
|
} from 'lucide-react';
|
|
import { toast } from 'react-toastify';
|
|
import { getRoomById, getRoomBookedDates, type Room } from '../../rooms/services/roomService';
|
|
import {
|
|
createBooking,
|
|
checkRoomAvailability,
|
|
type BookingData,
|
|
} from '../services/bookingService';
|
|
import serviceService, { Service } from '../../hotel_services/services/serviceService';
|
|
import promotionService, { Promotion } from '../../loyalty/services/promotionService';
|
|
import useAuthStore from '../../../store/useAuthStore';
|
|
import {
|
|
bookingValidationSchema,
|
|
type BookingFormData,
|
|
} from '../../../shared/utils/bookingValidator';
|
|
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
|
import { formatDateLocal } from '../../../shared/utils/format';
|
|
import Recaptcha from '../../../shared/components/Recaptcha';
|
|
import { recaptchaService } from '../../../features/system/services/systemSettingsService';
|
|
import StripePaymentModal from '../../payments/components/StripePaymentModal';
|
|
import PayPalPaymentModal from '../../payments/components/PayPalPaymentModal';
|
|
import CashPaymentModal from '../../payments/components/CashPaymentModal';
|
|
import InvoiceInfoModal from './InvoiceInfoModal';
|
|
import { useAntibotForm } from '../../auth/hooks/useAntibotForm';
|
|
import HoneypotField from '../../../shared/components/HoneypotField';
|
|
|
|
interface LuxuryBookingModalProps {
|
|
roomId: number;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSuccess?: (bookingId: number) => void;
|
|
}
|
|
|
|
type BookingStep = 'dates' | 'guests' | 'services' | 'payment' | 'confirmation';
|
|
|
|
const LuxuryBookingModal: React.FC<LuxuryBookingModalProps> = ({
|
|
roomId,
|
|
isOpen,
|
|
onClose,
|
|
onSuccess,
|
|
}) => {
|
|
const { userInfo } = useAuthStore();
|
|
const { formatCurrency, currency } = useFormatCurrency();
|
|
const [currentStep, setCurrentStep] = useState<BookingStep>('dates');
|
|
const [room, setRoom] = useState<Room | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
// Enhanced antibot protection
|
|
const {
|
|
honeypotValue,
|
|
setHoneypotValue,
|
|
recaptchaToken,
|
|
setRecaptchaToken,
|
|
validate: validateAntibot,
|
|
rateLimitInfo,
|
|
} = useAntibotForm({
|
|
formId: 'booking',
|
|
minTimeOnPage: 10000,
|
|
minTimeToFill: 5000,
|
|
requireRecaptcha: false,
|
|
maxAttempts: 5,
|
|
onValidationError: (errors) => {
|
|
errors.forEach((err) => toast.error(err));
|
|
},
|
|
});
|
|
const [services, setServices] = useState<Service[]>([]);
|
|
const [selectedServices, setSelectedServices] = useState<Array<{ service: Service; quantity: number }>>([]);
|
|
const [bookedDates, setBookedDates] = useState<Date[]>([]);
|
|
const [promotionCode, setPromotionCode] = useState('');
|
|
const [selectedPromotion, setSelectedPromotion] = useState<Promotion | null>(null);
|
|
const [promotionDiscount, setPromotionDiscount] = useState(0);
|
|
const [validatingPromotion, setValidatingPromotion] = useState(false);
|
|
const [promotionError, setPromotionError] = useState<string | null>(null);
|
|
const [referralCode, setReferralCode] = useState('');
|
|
const [referralError, setReferralError] = useState<string | null>(null);
|
|
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
|
|
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
|
const [paymentMethod, setPaymentMethod] = useState<'stripe' | 'paypal' | 'cash' | null>(null);
|
|
const [createdBookingId, setCreatedBookingId] = useState<number | null>(null);
|
|
const [totalPrice, setTotalPrice] = useState(0);
|
|
|
|
const {
|
|
control,
|
|
register,
|
|
handleSubmit,
|
|
watch,
|
|
formState: { errors },
|
|
setValue,
|
|
} = useForm<BookingFormData & { invoiceInfo?: any }>({
|
|
resolver: yupResolver(bookingValidationSchema),
|
|
defaultValues: {
|
|
checkInDate: undefined,
|
|
checkOutDate: undefined,
|
|
guestCount: 1,
|
|
notes: '',
|
|
paymentMethod: 'cash',
|
|
fullName: userInfo?.name || '',
|
|
email: userInfo?.email || '',
|
|
phone: userInfo?.phone || '',
|
|
invoiceInfo: {
|
|
company_name: '',
|
|
company_address: '',
|
|
company_tax_id: '',
|
|
customer_tax_id: '',
|
|
},
|
|
},
|
|
});
|
|
|
|
const checkInDate = watch('checkInDate');
|
|
const checkOutDate = watch('checkOutDate');
|
|
const paymentMethodForm = watch('paymentMethod');
|
|
|
|
useEffect(() => {
|
|
if (isOpen && roomId) {
|
|
fetchRoomDetails(roomId);
|
|
fetchServices();
|
|
fetchBookedDates(roomId);
|
|
}
|
|
}, [isOpen, roomId]);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
setCurrentStep('dates');
|
|
setSelectedServices([]);
|
|
setPromotionCode('');
|
|
setSelectedPromotion(null);
|
|
setPromotionDiscount(0);
|
|
setReferralCode('');
|
|
setReferralError(null);
|
|
setShowPaymentModal(false);
|
|
setPaymentMethod(null);
|
|
setCreatedBookingId(null);
|
|
}
|
|
}, [isOpen]);
|
|
|
|
const fetchBookedDates = async (roomId: number) => {
|
|
try {
|
|
const response = await getRoomBookedDates(roomId);
|
|
const isSuccess = response.success === true || (response as any).status === 'success';
|
|
if (isSuccess && response.data?.booked_dates) {
|
|
const dates = response.data.booked_dates.map((dateStr: string) => {
|
|
const [year, month, day] = dateStr.split('-').map(Number);
|
|
const date = new Date(year, month - 1, day);
|
|
date.setHours(0, 0, 0, 0);
|
|
return date;
|
|
});
|
|
setBookedDates(dates);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching booked dates:', err);
|
|
}
|
|
};
|
|
|
|
const isDateBooked = (date: Date): boolean => {
|
|
if (!date || bookedDates.length === 0) return false;
|
|
const normalizedDate = new Date(date);
|
|
normalizedDate.setHours(0, 0, 0, 0);
|
|
return bookedDates.some(bookedDate => {
|
|
const normalizedBooked = new Date(bookedDate);
|
|
normalizedBooked.setHours(0, 0, 0, 0);
|
|
return normalizedDate.getTime() === normalizedBooked.getTime();
|
|
});
|
|
};
|
|
|
|
const doesRangeIncludeBookedDates = (startDate: Date, endDate: Date): boolean => {
|
|
if (!startDate || !endDate || bookedDates.length === 0) return false;
|
|
const start = new Date(startDate);
|
|
start.setHours(0, 0, 0, 0);
|
|
const end = new Date(endDate);
|
|
end.setHours(0, 0, 0, 0);
|
|
let currentDate = new Date(start);
|
|
while (currentDate < end) {
|
|
if (isDateBooked(currentDate)) {
|
|
return true;
|
|
}
|
|
currentDate.setDate(currentDate.getDate() + 1);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const fetchServices = async () => {
|
|
try {
|
|
const response = await serviceService.getServices({
|
|
status: 'active',
|
|
limit: 100,
|
|
});
|
|
setServices(response.data.services || []);
|
|
} catch (err: any) {
|
|
console.error('Error fetching services:', err);
|
|
}
|
|
};
|
|
|
|
const fetchRoomDetails = async (roomId: number) => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await getRoomById(roomId);
|
|
if (
|
|
(response.success || (response as any).status === 'success') &&
|
|
response.data?.room
|
|
) {
|
|
setRoom(response.data.room);
|
|
} else {
|
|
throw new Error('Unable to load room information');
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Error fetching room:', err);
|
|
toast.error(err.response?.data?.message || 'Unable to load room information');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const numberOfNights =
|
|
checkInDate && checkOutDate
|
|
? Math.ceil((checkOutDate.getTime() - checkInDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
: 0;
|
|
|
|
const roomPrice = (room?.price && room.price > 0) ? room.price : (room?.room_type?.base_price || 0);
|
|
const roomTotal = numberOfNights * roomPrice;
|
|
const servicesTotal = selectedServices.reduce((sum, item) => {
|
|
return sum + (item.service.price * item.quantity);
|
|
}, 0);
|
|
const subtotal = roomTotal + servicesTotal;
|
|
const finalTotal = Math.max(0, subtotal - promotionDiscount);
|
|
|
|
useEffect(() => {
|
|
setTotalPrice(finalTotal);
|
|
}, [finalTotal]);
|
|
|
|
const handleValidatePromotion = async () => {
|
|
if (!promotionCode.trim()) {
|
|
setPromotionError('Please enter a promotion code');
|
|
return;
|
|
}
|
|
if (subtotal === 0) {
|
|
setPromotionError('Please select dates first');
|
|
return;
|
|
}
|
|
try {
|
|
setValidatingPromotion(true);
|
|
setPromotionError(null);
|
|
const response = await promotionService.validatePromotion(
|
|
promotionCode.toUpperCase().trim(),
|
|
subtotal
|
|
);
|
|
if (response.success && response.data) {
|
|
setSelectedPromotion(response.data.promotion);
|
|
setPromotionDiscount(response.data.discount);
|
|
toast.success(`Promotion "${response.data.promotion.name}" applied!`);
|
|
} else {
|
|
throw new Error(response.message || 'Invalid promotion code');
|
|
}
|
|
} catch (error: any) {
|
|
setPromotionError(error.response?.data?.message || error.message || 'Invalid promotion code');
|
|
setSelectedPromotion(null);
|
|
setPromotionDiscount(0);
|
|
toast.error(error.response?.data?.message || error.message || 'Invalid promotion code');
|
|
} finally {
|
|
setValidatingPromotion(false);
|
|
}
|
|
};
|
|
|
|
const handleRemovePromotion = () => {
|
|
setPromotionCode('');
|
|
setSelectedPromotion(null);
|
|
setPromotionDiscount(0);
|
|
setPromotionError(null);
|
|
toast.info('Promotion removed');
|
|
};
|
|
|
|
const steps: { key: BookingStep; label: string; icon: React.ReactNode }[] = [
|
|
{ key: 'dates', label: 'Dates', icon: <Calendar className="w-4 h-4" /> },
|
|
{ key: 'guests', label: 'Guests', icon: <Users className="w-4 h-4" /> },
|
|
{ key: 'services', label: 'Services', icon: <Sparkles className="w-4 h-4" /> },
|
|
{ key: 'payment', label: 'Payment', icon: <CreditCard className="w-4 h-4" /> },
|
|
];
|
|
|
|
const getCurrentStepIndex = () => {
|
|
return steps.findIndex(s => s.key === currentStep);
|
|
};
|
|
|
|
const canGoToNextStep = () => {
|
|
switch (currentStep) {
|
|
case 'dates':
|
|
return checkInDate && checkOutDate;
|
|
case 'guests':
|
|
return true;
|
|
case 'services':
|
|
return true;
|
|
case 'payment':
|
|
return paymentMethodForm;
|
|
default:
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (!canGoToNextStep()) {
|
|
toast.error('Please complete the current step');
|
|
return;
|
|
}
|
|
const currentIndex = getCurrentStepIndex();
|
|
if (currentIndex < steps.length - 1) {
|
|
setCurrentStep(steps[currentIndex + 1].key as BookingStep);
|
|
}
|
|
};
|
|
|
|
const handleBack = () => {
|
|
const currentIndex = getCurrentStepIndex();
|
|
if (currentIndex > 0) {
|
|
setCurrentStep(steps[currentIndex - 1].key as BookingStep);
|
|
}
|
|
};
|
|
|
|
const onSubmit = async (data: BookingFormData) => {
|
|
console.log('Form submitted with data:', data);
|
|
console.log('Current form errors:', errors);
|
|
|
|
if (!room) {
|
|
toast.error('Room information is missing. Please try again.');
|
|
return;
|
|
}
|
|
|
|
// Validate antibot protection
|
|
const isValid = await validateAntibot();
|
|
if (!isValid) {
|
|
return;
|
|
}
|
|
|
|
// Verify reCAPTCHA if token is provided (reCAPTCHA is optional)
|
|
if (recaptchaToken) {
|
|
try {
|
|
const verifyResponse = await recaptchaService.verifyRecaptcha(recaptchaToken);
|
|
if (verifyResponse.status === 'error' || !verifyResponse.data.verified) {
|
|
toast.error('reCAPTCHA verification failed. Please try again.');
|
|
setRecaptchaToken(null);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
toast.error('reCAPTCHA verification failed. Please try again.');
|
|
setRecaptchaToken(null);
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
setSubmitting(true);
|
|
const checkInDateStr = formatDateLocal(data.checkInDate);
|
|
const checkOutDateStr = formatDateLocal(data.checkOutDate);
|
|
const checkIn = new Date(data.checkInDate);
|
|
checkIn.setHours(0, 0, 0, 0);
|
|
const checkOut = new Date(data.checkOutDate);
|
|
checkOut.setHours(0, 0, 0, 0);
|
|
|
|
if (isDateBooked(checkIn)) {
|
|
toast.error('Check-in date is already booked. Please select another date.');
|
|
return;
|
|
}
|
|
|
|
const availability = await checkRoomAvailability(
|
|
room.id,
|
|
checkInDateStr,
|
|
checkOutDateStr
|
|
);
|
|
|
|
if (!availability.available) {
|
|
toast.error(availability.message || 'Room is already booked during this time');
|
|
return;
|
|
}
|
|
|
|
const bookingData: BookingData & { invoice_info?: any } = {
|
|
room_id: room.id,
|
|
check_in_date: checkInDateStr,
|
|
check_out_date: checkOutDateStr,
|
|
guest_count: data.guestCount,
|
|
notes: data.notes || '',
|
|
payment_method: data.paymentMethod,
|
|
total_price: totalPrice,
|
|
guest_info: {
|
|
full_name: data.fullName,
|
|
email: data.email,
|
|
phone: data.phone,
|
|
},
|
|
services: selectedServices.map(item => ({
|
|
service_id: item.service.id,
|
|
quantity: item.quantity,
|
|
})),
|
|
promotion_code: selectedPromotion?.code || undefined,
|
|
referral_code: referralCode.trim() || undefined,
|
|
invoice_info: (data as any).invoiceInfo ? {
|
|
company_name: (data as any).invoiceInfo.company_name || undefined,
|
|
company_address: (data as any).invoiceInfo.company_address || undefined,
|
|
company_tax_id: (data as any).invoiceInfo.company_tax_id || undefined,
|
|
customer_tax_id: (data as any).invoiceInfo.customer_tax_id || undefined,
|
|
} : undefined,
|
|
};
|
|
|
|
const response = await createBooking(bookingData);
|
|
|
|
if (response.success && response.data?.booking) {
|
|
const bookingId = response.data.booking.id;
|
|
setCreatedBookingId(bookingId);
|
|
setPaymentMethod(data.paymentMethod as 'stripe' | 'paypal' | 'cash');
|
|
setShowPaymentModal(true);
|
|
} else {
|
|
throw new Error(response.message || 'Unable to create booking');
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Error creating booking:', err);
|
|
if (err.response?.status === 409) {
|
|
toast.error('❌ Room is already booked during this time. Please select different dates.');
|
|
} else if (err.response?.status === 400) {
|
|
toast.error(err.response?.data?.message || 'Invalid booking information');
|
|
} else {
|
|
toast.error(err.response?.data?.message || 'Unable to book room. Please try again.');
|
|
}
|
|
setRecaptchaToken(null);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handlePaymentSuccess = async () => {
|
|
setShowPaymentModal(false);
|
|
if (createdBookingId) {
|
|
// Check booking status before showing success toast
|
|
try {
|
|
const { getBookingById } = await import('../services/bookingService');
|
|
const bookingResponse = await getBookingById(createdBookingId);
|
|
if (bookingResponse.success && bookingResponse.data?.booking) {
|
|
const booking = bookingResponse.data.booking;
|
|
// Only show success toast if booking is not cancelled
|
|
if (booking.status !== 'cancelled') {
|
|
toast.success('🎉 Booking successful!');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// If we can't check status, don't show toast to avoid confusion
|
|
console.error('Error checking booking status:', err);
|
|
}
|
|
if (onSuccess) {
|
|
onSuccess(createdBookingId);
|
|
}
|
|
}
|
|
onClose();
|
|
};
|
|
|
|
const handlePaymentClose = () => {
|
|
setShowPaymentModal(false);
|
|
if (createdBookingId && onSuccess) {
|
|
onSuccess(createdBookingId);
|
|
}
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-2 sm:p-4">
|
|
<div
|
|
className="fixed inset-0 bg-black/80 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
/>
|
|
<div className="relative w-full max-w-4xl max-h-[95vh] bg-gradient-to-br from-[#0a0a0a] via-[#1a1a1a] to-[#0a0a0a] rounded-xl sm:rounded-2xl border border-[#d4af37]/30 shadow-2xl shadow-[#d4af37]/20 overflow-hidden flex flex-col">
|
|
{/* Header */}
|
|
<div className="relative px-3 sm:px-4 md:px-6 py-3 sm:py-4 border-b border-[#d4af37]/20 bg-gradient-to-r from-[#1a1a1a] to-[#0a0a0a] flex-shrink-0">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="min-w-0 flex-1">
|
|
<h2 className="text-base sm:text-lg md:text-xl lg:text-2xl font-serif font-bold text-white tracking-tight truncate">
|
|
Complete Your Booking
|
|
</h2>
|
|
<p className="text-xs sm:text-sm text-gray-400 mt-0.5 sm:mt-1 font-light truncate">
|
|
{room?.room_type?.name || 'Room'} - {formatCurrency(roomPrice)}/night
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1.5 sm:p-2 hover:bg-[#d4af37]/10 rounded-lg transition-colors text-gray-400 hover:text-white flex-shrink-0"
|
|
>
|
|
<X className="w-4 h-4 sm:w-5 sm:h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Progress Steps */}
|
|
<div className="mt-3 sm:mt-4 flex items-center justify-between gap-1 sm:gap-2">
|
|
{steps.map((step, index) => {
|
|
const isActive = step.key === currentStep;
|
|
const isCompleted = getCurrentStepIndex() > index;
|
|
return (
|
|
<div key={step.key} className="flex items-center flex-1 min-w-0">
|
|
<div className="flex items-center flex-1 min-w-0">
|
|
<div
|
|
className={`flex items-center justify-center w-6 h-6 sm:w-7 sm:h-7 md:w-8 md:h-8 rounded-full border-2 transition-all flex-shrink-0 ${
|
|
isActive
|
|
? 'bg-[#d4af37] border-[#d4af37] text-black'
|
|
: isCompleted
|
|
? 'bg-green-500/20 border-green-500 text-green-400'
|
|
: 'bg-transparent border-gray-600 text-gray-500'
|
|
}`}
|
|
>
|
|
{isCompleted ? (
|
|
<CheckCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 md:w-5 md:h-5" />
|
|
) : (
|
|
<div className="w-3.5 h-3.5 sm:w-4 sm:h-4 md:w-5 md:h-5">
|
|
{step.icon}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<span
|
|
className={`ml-1 sm:ml-2 text-[10px] sm:text-xs font-medium hidden md:block truncate ${
|
|
isActive ? 'text-[#d4af37]' : isCompleted ? 'text-green-400' : 'text-gray-500'
|
|
}`}
|
|
>
|
|
{step.label}
|
|
</span>
|
|
</div>
|
|
{index < steps.length - 1 && (
|
|
<div
|
|
className={`h-0.5 flex-1 mx-1 sm:mx-2 transition-all flex-shrink ${
|
|
isCompleted ? 'bg-green-500' : 'bg-gray-700'
|
|
}`}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 px-4 sm:px-6 py-4 sm:py-6 overflow-hidden">
|
|
<div className="h-full overflow-y-auto">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 animate-spin text-[#d4af37]" />
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 relative">
|
|
{/* Honeypot field - hidden from users */}
|
|
<HoneypotField value={honeypotValue} onChange={setHoneypotValue} />
|
|
|
|
{rateLimitInfo && !rateLimitInfo.allowed && (
|
|
<div className="bg-yellow-900/50 backdrop-blur-sm border border-yellow-500/50 text-yellow-200 px-4 py-3 rounded-lg text-sm font-light mb-4">
|
|
Too many booking attempts. Please try again later.
|
|
</div>
|
|
)}
|
|
{/* Step 1: Dates */}
|
|
{currentStep === 'dates' && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3 className="text-lg font-serif font-semibold text-white mb-4 flex items-center gap-2">
|
|
<Calendar className="w-5 h-5 text-[#d4af37]" />
|
|
Select Dates
|
|
</h3>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2">
|
|
Check-in Date <span className="text-[#d4af37]">*</span>
|
|
</label>
|
|
<Controller
|
|
control={control}
|
|
name="checkInDate"
|
|
render={({ field }) => (
|
|
<DatePicker
|
|
selected={field.value}
|
|
minDate={new Date()}
|
|
selectsStart
|
|
startDate={checkInDate}
|
|
endDate={checkOutDate}
|
|
dateFormat="dd/MM/yyyy"
|
|
placeholderText="Select check-in"
|
|
excludeDates={bookedDates}
|
|
filterDate={(date) => !isDateBooked(date)}
|
|
onChange={(date) => {
|
|
if (date && isDateBooked(date)) {
|
|
toast.error('This date is already booked');
|
|
return;
|
|
}
|
|
field.onChange(date);
|
|
}}
|
|
className="w-full px-3 py-2 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white text-sm focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37]"
|
|
wrapperClassName="w-full"
|
|
/>
|
|
)}
|
|
/>
|
|
{errors.checkInDate && (
|
|
<p className="text-xs text-red-400 mt-1">{errors.checkInDate.message}</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2">
|
|
Check-out Date <span className="text-[#d4af37]">*</span>
|
|
</label>
|
|
<Controller
|
|
control={control}
|
|
name="checkOutDate"
|
|
render={({ field }) => (
|
|
<DatePicker
|
|
selected={field.value}
|
|
minDate={checkInDate || new Date()}
|
|
selectsEnd
|
|
startDate={checkInDate}
|
|
endDate={checkOutDate}
|
|
dateFormat="dd/MM/yyyy"
|
|
placeholderText="Select check-out"
|
|
excludeDates={bookedDates}
|
|
filterDate={(date) => {
|
|
if (!checkInDate) return !isDateBooked(date);
|
|
return !isDateBooked(date) && !doesRangeIncludeBookedDates(checkInDate, date);
|
|
}}
|
|
onChange={(date) => {
|
|
if (date && checkInDate && doesRangeIncludeBookedDates(checkInDate, date)) {
|
|
toast.error('Selected range includes booked dates');
|
|
return;
|
|
}
|
|
field.onChange(date);
|
|
}}
|
|
className="w-full px-3 py-2 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white text-sm focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37]"
|
|
wrapperClassName="w-full"
|
|
/>
|
|
)}
|
|
/>
|
|
{errors.checkOutDate && (
|
|
<p className="text-xs text-red-400 mt-1">{errors.checkOutDate.message}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{checkInDate && checkOutDate && (
|
|
<div className="mt-4 p-3 bg-[#d4af37]/10 border border-[#d4af37]/20 rounded-lg">
|
|
<p className="text-sm text-gray-300">
|
|
<span className="text-[#d4af37] font-semibold">{numberOfNights}</span> night{numberOfNights !== 1 ? 's' : ''} •
|
|
<span className="text-[#d4af37] font-semibold ml-1">{formatCurrency(roomTotal)}</span>
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 2: Guests */}
|
|
{currentStep === 'guests' && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3 className="text-lg font-serif font-semibold text-white mb-4 flex items-center gap-2">
|
|
<Users className="w-5 h-5 text-[#d4af37]" />
|
|
Guest Information
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2">
|
|
Full Name <span className="text-[#d4af37]">*</span>
|
|
</label>
|
|
<input
|
|
{...register('fullName')}
|
|
type="text"
|
|
className="w-full px-3 py-2 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white text-sm focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37]"
|
|
placeholder="John Doe"
|
|
/>
|
|
{errors.fullName && (
|
|
<p className="text-xs text-red-400 mt-1">{errors.fullName.message}</p>
|
|
)}
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2">
|
|
Email <span className="text-[#d4af37]">*</span>
|
|
</label>
|
|
<input
|
|
{...register('email')}
|
|
type="email"
|
|
className="w-full px-3 py-2 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white text-sm focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37]"
|
|
placeholder="email@example.com"
|
|
/>
|
|
{errors.email && (
|
|
<p className="text-xs text-red-400 mt-1">{errors.email.message}</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2">
|
|
Phone <span className="text-[#d4af37]">*</span>
|
|
</label>
|
|
<input
|
|
{...register('phone')}
|
|
type="tel"
|
|
className="w-full px-3 py-2 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white text-sm focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37]"
|
|
placeholder="0123456789"
|
|
/>
|
|
{errors.phone && (
|
|
<p className="text-xs text-red-400 mt-1">{errors.phone.message}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2">
|
|
Number of Guests <span className="text-[#d4af37]">*</span>
|
|
</label>
|
|
<input
|
|
{...register('guestCount')}
|
|
type="number"
|
|
min="1"
|
|
max={room?.capacity || room?.room_type?.capacity || 10}
|
|
className="w-full px-3 py-2 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white text-sm focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37]"
|
|
/>
|
|
{errors.guestCount && (
|
|
<p className="text-xs text-red-400 mt-1">{errors.guestCount.message}</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2">
|
|
Special Requests (optional)
|
|
</label>
|
|
<textarea
|
|
{...register('notes')}
|
|
rows={2}
|
|
className="w-full px-3 py-2 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white text-sm focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] resize-none"
|
|
placeholder="Any special requests..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 3: Services */}
|
|
{currentStep === 'services' && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3 className="text-lg font-serif font-semibold text-white mb-4 flex items-center gap-2">
|
|
<Sparkles className="w-5 h-5 text-[#d4af37]" />
|
|
Additional Services
|
|
</h3>
|
|
{services.length > 0 ? (
|
|
<div className="space-y-3 max-h-48 overflow-y-auto">
|
|
{services.map((service) => {
|
|
const selectedItem = selectedServices.find(item => item.service.id === service.id);
|
|
const quantity = selectedItem?.quantity || 0;
|
|
return (
|
|
<div
|
|
key={service.id}
|
|
className="p-3 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1">
|
|
<h4 className="text-sm font-semibold text-white">{service.name}</h4>
|
|
<p className="text-xs text-gray-400 mt-1">{formatCurrency(service.price)} / {service.unit || 'unit'}</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (quantity > 0) {
|
|
if (quantity === 1) {
|
|
setSelectedServices(prev => prev.filter(item => item.service.id !== service.id));
|
|
} else {
|
|
setSelectedServices(prev => prev.map(item =>
|
|
item.service.id === service.id
|
|
? { ...item, quantity: item.quantity - 1 }
|
|
: item
|
|
));
|
|
}
|
|
}
|
|
}}
|
|
disabled={quantity === 0}
|
|
className="w-7 h-7 flex items-center justify-center bg-[#1a1a1a] border border-[#d4af37]/20 rounded text-[#d4af37] hover:bg-[#d4af37]/10 disabled:opacity-50"
|
|
>
|
|
<Minus className="w-3.5 h-3.5" />
|
|
</button>
|
|
<span className="text-white font-semibold w-6 text-center text-sm">{quantity}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (quantity === 0) {
|
|
setSelectedServices(prev => [...prev, { service, quantity: 1 }]);
|
|
} else {
|
|
setSelectedServices(prev => prev.map(item =>
|
|
item.service.id === service.id
|
|
? { ...item, quantity: item.quantity + 1 }
|
|
: item
|
|
));
|
|
}
|
|
}}
|
|
className="w-7 h-7 flex items-center justify-center bg-[#1a1a1a] border border-[#d4af37]/20 rounded text-[#d4af37] hover:bg-[#d4af37]/10"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
{quantity > 0 && (
|
|
<span className="text-sm font-semibold text-[#d4af37] w-20 text-right">
|
|
{formatCurrency(service.price * quantity)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-gray-400">No additional services available</p>
|
|
)}
|
|
|
|
{/* Promotion Code */}
|
|
<div className="mt-6 pt-6 border-t border-[#d4af37]/20">
|
|
<h4 className="text-sm font-semibold text-white mb-3">Promotion Code</h4>
|
|
{!selectedPromotion ? (
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={promotionCode}
|
|
onChange={(e) => {
|
|
setPromotionCode(e.target.value.toUpperCase());
|
|
setPromotionError(null);
|
|
}}
|
|
placeholder="Enter code"
|
|
className="flex-1 px-3 py-2 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white text-sm focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37]"
|
|
disabled={validatingPromotion || subtotal === 0}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={handleValidatePromotion}
|
|
disabled={validatingPromotion || !promotionCode.trim() || subtotal === 0}
|
|
className="px-4 py-2 bg-[#d4af37] text-black font-semibold rounded-lg hover:bg-[#c9a227] disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
|
>
|
|
{validatingPromotion ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Apply'}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="p-3 bg-green-500/10 border border-green-500/30 rounded-lg flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-semibold text-green-400">{selectedPromotion.name}</p>
|
|
<p className="text-xs text-gray-400">Discount: -{formatCurrency(promotionDiscount)}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={handleRemovePromotion}
|
|
className="text-red-400 hover:text-red-300"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
{promotionError && <p className="text-xs text-red-400 mt-2">{promotionError}</p>}
|
|
</div>
|
|
|
|
{/* Referral Code */}
|
|
<div className="mt-6 pt-6 border-t border-[#d4af37]/20">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Users className="w-4 h-4 text-[#d4af37]" />
|
|
<h4 className="text-sm font-semibold text-white">Referral Code (Optional)</h4>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<input
|
|
type="text"
|
|
value={referralCode}
|
|
onChange={(e) => {
|
|
setReferralCode(e.target.value.toUpperCase().trim());
|
|
setReferralError(null);
|
|
}}
|
|
placeholder="Enter referral code from a friend"
|
|
className="w-full px-3 py-2 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg text-white text-sm focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37]"
|
|
/>
|
|
{referralCode && (
|
|
<p className="text-xs text-gray-400">
|
|
You'll both earn bonus points when your booking is confirmed!
|
|
</p>
|
|
)}
|
|
</div>
|
|
{referralError && <p className="text-xs text-red-400 mt-2">{referralError}</p>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 4: Payment */}
|
|
{currentStep === 'payment' && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3 className="text-lg font-serif font-semibold text-white mb-4 flex items-center gap-2">
|
|
<CreditCard className="w-5 h-5 text-[#d4af37]" />
|
|
Payment Method
|
|
</h3>
|
|
<div className="space-y-3">
|
|
<label className="flex items-start p-4 bg-[#0a0a0a] border-2 border-[#d4af37]/20 rounded-lg cursor-pointer hover:border-[#d4af37]/40 transition-all">
|
|
<input
|
|
{...register('paymentMethod')}
|
|
type="radio"
|
|
value="cash"
|
|
className="mt-1 mr-3 w-4 h-4 text-[#d4af37] border-[#d4af37]/30 focus:ring-[#d4af37]/50"
|
|
/>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<CreditCard className="w-4 h-4 text-[#d4af37]" />
|
|
<span className="font-semibold text-white text-sm">Pay on Arrival</span>
|
|
<span className="text-xs bg-orange-500/20 text-orange-300 px-2 py-0.5 rounded">20% deposit</span>
|
|
</div>
|
|
<p className="text-xs text-gray-400">Pay 20% deposit now, remaining on arrival</p>
|
|
</div>
|
|
</label>
|
|
<label className="flex items-start p-4 bg-[#0a0a0a] border-2 border-[#d4af37]/20 rounded-lg cursor-pointer hover:border-[#d4af37]/40 transition-all">
|
|
<input
|
|
{...register('paymentMethod')}
|
|
type="radio"
|
|
value="stripe"
|
|
className="mt-1 mr-3 w-4 h-4 text-[#d4af37] border-[#d4af37]/30 focus:ring-[#d4af37]/50"
|
|
/>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<CreditCard className="w-4 h-4 text-[#d4af37]" />
|
|
<span className="font-semibold text-white text-sm">Credit/Debit Card</span>
|
|
</div>
|
|
<p className="text-xs text-gray-400">Secure payment via Stripe</p>
|
|
</div>
|
|
</label>
|
|
<label className="flex items-start p-4 bg-[#0a0a0a] border-2 border-[#d4af37]/20 rounded-lg cursor-pointer hover:border-[#d4af37]/40 transition-all">
|
|
<input
|
|
{...register('paymentMethod')}
|
|
type="radio"
|
|
value="paypal"
|
|
className="mt-1 mr-3 w-4 h-4 text-[#d4af37] border-[#d4af37]/30 focus:ring-[#d4af37]/50"
|
|
/>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<CreditCard className="w-4 h-4 text-[#d4af37]" />
|
|
<span className="font-semibold text-white text-sm">PayPal</span>
|
|
</div>
|
|
<p className="text-xs text-gray-400">Pay securely with PayPal</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Invoice Info Button */}
|
|
<div className="mt-6">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowInvoiceModal(true)}
|
|
className="flex items-center gap-2 px-4 py-2 text-sm text-[#d4af37] hover:text-[#f5d76e] transition-colors"
|
|
>
|
|
<Receipt className="w-4 h-4" />
|
|
Add Invoice Information (Optional)
|
|
</button>
|
|
</div>
|
|
|
|
{/* Summary */}
|
|
<div className="mt-6 p-4 bg-[#0a0a0a] border border-[#d4af37]/20 rounded-lg">
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between text-gray-300">
|
|
<span>Room ({numberOfNights} nights)</span>
|
|
<span>{formatCurrency(roomTotal)}</span>
|
|
</div>
|
|
{servicesTotal > 0 && (
|
|
<div className="flex justify-between text-gray-300">
|
|
<span>Services</span>
|
|
<span>{formatCurrency(servicesTotal)}</span>
|
|
</div>
|
|
)}
|
|
{promotionDiscount > 0 && (
|
|
<div className="flex justify-between text-green-400">
|
|
<span>Discount</span>
|
|
<span>-{formatCurrency(promotionDiscount)}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex justify-between text-lg font-bold text-[#d4af37] pt-2 border-t border-[#d4af37]/20">
|
|
<span>Total</span>
|
|
<span>{formatCurrency(totalPrice)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Validation Summary */}
|
|
{(errors.checkInDate || errors.checkOutDate || errors.fullName || errors.email || errors.phone || errors.guestCount) && (
|
|
<div className="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
|
<p className="text-xs text-red-400 font-medium mb-1">Please complete all required fields:</p>
|
|
<ul className="text-xs text-red-300 space-y-1">
|
|
{errors.checkInDate && <li>• Check-in date is required</li>}
|
|
{errors.checkOutDate && <li>• Check-out date is required</li>}
|
|
{errors.fullName && <li>• Full name is required</li>}
|
|
{errors.email && <li>• Email is required</li>}
|
|
{errors.phone && <li>• Phone number is required</li>}
|
|
{errors.guestCount && <li>• Number of guests is required</li>}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* reCAPTCHA */}
|
|
<div className="mt-4">
|
|
<Recaptcha
|
|
onChange={(token) => {
|
|
console.log('reCAPTCHA token received:', token ? 'Yes' : 'No');
|
|
setRecaptchaToken(token || null);
|
|
}}
|
|
onError={(error) => {
|
|
console.error('reCAPTCHA error:', error);
|
|
setRecaptchaToken(null);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="px-3 sm:px-4 md:px-6 py-3 sm:py-4 border-t border-[#d4af37]/20 bg-gradient-to-r from-[#1a1a1a] to-[#0a0a0a] flex-shrink-0">
|
|
<div className="flex items-center justify-between gap-2 sm:gap-3">
|
|
<button
|
|
onClick={currentStep === 'dates' ? onClose : handleBack}
|
|
className="px-3 sm:px-4 py-2 text-xs sm:text-sm font-medium text-gray-300 hover:text-white transition-colors flex items-center gap-1.5 sm:gap-2 min-h-[44px]"
|
|
>
|
|
<ArrowLeft className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
|
<span className="hidden xs:inline">{currentStep === 'dates' ? 'Cancel' : 'Back'}</span>
|
|
<span className="xs:hidden">{currentStep === 'dates' ? 'Cancel' : 'Back'}</span>
|
|
</button>
|
|
{currentStep !== 'payment' ? (
|
|
<button
|
|
onClick={handleNext}
|
|
disabled={!canGoToNextStep()}
|
|
className="px-4 sm:px-6 py-2 bg-[#d4af37] text-black font-semibold rounded-lg hover:bg-[#c9a227] disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm sm:text-base min-h-[44px]"
|
|
>
|
|
<span>Next</span>
|
|
<ArrowRight className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
console.log('Complete Booking button clicked');
|
|
console.log('Payment method:', paymentMethodForm);
|
|
console.log('Form errors:', errors);
|
|
console.log('Submitting:', submitting);
|
|
handleSubmit(onSubmit, (errors) => {
|
|
console.error('Form validation errors:', errors);
|
|
// Show validation errors
|
|
if (errors.checkInDate || errors.checkOutDate) {
|
|
toast.error('Please select check-in and check-out dates');
|
|
setCurrentStep('dates');
|
|
} else if (errors.fullName || errors.email || errors.phone || errors.guestCount) {
|
|
toast.error('Please complete all guest information fields');
|
|
setCurrentStep('guests');
|
|
} else if (errors.paymentMethod) {
|
|
toast.error('Please select a payment method');
|
|
} else {
|
|
toast.error('Please complete all required fields');
|
|
}
|
|
})();
|
|
}}
|
|
disabled={submitting || !paymentMethodForm}
|
|
className="px-4 sm:px-6 py-2 bg-[#d4af37] text-black font-semibold rounded-lg hover:bg-[#c9a227] disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm sm:text-base min-h-[44px]"
|
|
>
|
|
{submitting ? (
|
|
<>
|
|
<Loader2 className="w-3.5 h-3.5 sm:w-4 sm:h-4 animate-spin" />
|
|
<span className="hidden xs:inline">Processing...</span>
|
|
<span className="xs:hidden">Processing...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<span className="hidden sm:inline">Complete Booking</span>
|
|
<span className="sm:hidden">Complete</span>
|
|
<CheckCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Invoice Info Modal */}
|
|
{showInvoiceModal && (
|
|
<InvoiceInfoModal
|
|
isOpen={showInvoiceModal}
|
|
onClose={() => setShowInvoiceModal(false)}
|
|
onSave={(invoiceInfo) => {
|
|
setValue('invoiceInfo', invoiceInfo);
|
|
setShowInvoiceModal(false);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Payment Modals */}
|
|
{showPaymentModal && createdBookingId && paymentMethod && (
|
|
<>
|
|
{paymentMethod === 'stripe' && (
|
|
<StripePaymentModal
|
|
isOpen={showPaymentModal}
|
|
bookingId={createdBookingId}
|
|
amount={totalPrice}
|
|
currency={currency || 'USD'}
|
|
onSuccess={handlePaymentSuccess}
|
|
onClose={handlePaymentClose}
|
|
/>
|
|
)}
|
|
{paymentMethod === 'paypal' && (
|
|
<PayPalPaymentModal
|
|
isOpen={showPaymentModal}
|
|
bookingId={createdBookingId}
|
|
amount={totalPrice}
|
|
currency={currency || 'USD'}
|
|
onSuccess={handlePaymentSuccess}
|
|
onClose={handlePaymentClose}
|
|
/>
|
|
)}
|
|
{paymentMethod === 'cash' && (
|
|
<CashPaymentModal
|
|
isOpen={showPaymentModal}
|
|
bookingId={createdBookingId}
|
|
amount={totalPrice}
|
|
depositAmount={totalPrice * 0.2}
|
|
onSuccess={handlePaymentSuccess}
|
|
onClose={handlePaymentClose}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default LuxuryBookingModal;
|
|
|