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, FileText, Sparkles, CheckCircle, ArrowRight, ArrowLeft, Loader2, Plus, Minus, Building2, Receipt, } from 'lucide-react'; import { toast } from 'react-toastify'; import { getRoomById, getRoomBookedDates, type Room } from '../../services/api/roomService'; import { createBooking, checkRoomAvailability, type BookingData, } from '../../services/api/bookingService'; import { serviceService, Service, promotionService, Promotion } from '../../services/api'; import useAuthStore from '../../store/useAuthStore'; import { bookingValidationSchema, type BookingFormData, } from '../../validators/bookingValidator'; import { useFormatCurrency } from '../../hooks/useFormatCurrency'; import { formatDateLocal } from '../../utils/format'; import Recaptcha from '../common/Recaptcha'; import { recaptchaService } from '../../services/api/systemSettingsService'; import StripePaymentModal from '../payments/StripePaymentModal'; import PayPalPaymentModal from '../payments/PayPalPaymentModal'; import CashPaymentModal from '../payments/CashPaymentModal'; import InvoiceInfoModal from '../booking/InvoiceInfoModal'; interface LuxuryBookingModalProps { roomId: number; isOpen: boolean; onClose: () => void; onSuccess?: (bookingId: number) => void; } type BookingStep = 'dates' | 'guests' | 'services' | 'payment' | 'confirmation'; const LuxuryBookingModal: React.FC = ({ roomId, isOpen, onClose, onSuccess, }) => { const { userInfo } = useAuthStore(); const { formatCurrency, currency } = useFormatCurrency(); const [currentStep, setCurrentStep] = useState('dates'); const [room, setRoom] = useState(null); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [recaptchaToken, setRecaptchaToken] = useState(null); const [services, setServices] = useState([]); const [selectedServices, setSelectedServices] = useState>([]); const [bookedDates, setBookedDates] = useState([]); const [promotionCode, setPromotionCode] = useState(''); const [selectedPromotion, setSelectedPromotion] = useState(null); const [promotionDiscount, setPromotionDiscount] = useState(0); const [validatingPromotion, setValidatingPromotion] = useState(false); const [promotionError, setPromotionError] = useState(null); const [showInvoiceModal, setShowInvoiceModal] = useState(false); const [showPaymentModal, setShowPaymentModal] = useState(false); const [paymentMethod, setPaymentMethod] = useState<'stripe' | 'paypal' | 'cash' | null>(null); const [createdBookingId, setCreatedBookingId] = useState(null); const [totalPrice, setTotalPrice] = useState(0); const { control, register, handleSubmit, watch, formState: { errors }, setValue, } = useForm({ 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); 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: }, { key: 'guests', label: 'Guests', icon: }, { key: 'services', label: 'Services', icon: }, { key: 'payment', label: 'Payment', icon: }, ]; 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; } // 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, 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/api/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 ( <>
{/* Header */}

Complete Your Booking

{room?.room_type?.name || 'Room'} - {formatCurrency(roomPrice)}/night

{/* Progress Steps */}
{steps.map((step, index) => { const isActive = step.key === currentStep; const isCompleted = getCurrentStepIndex() > index; return (
{isCompleted ? ( ) : (
{step.icon}
)}
{index < steps.length - 1 && (
)}
); })}
{/* Content */}
{loading ? (
) : (
{/* Step 1: Dates */} {currentStep === 'dates' && (

Select Dates

( !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 && (

{errors.checkInDate.message}

)}
( { 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 && (

{errors.checkOutDate.message}

)}
{checkInDate && checkOutDate && (

{numberOfNights} night{numberOfNights !== 1 ? 's' : ''} • {formatCurrency(roomTotal)}

)}
)} {/* Step 2: Guests */} {currentStep === 'guests' && (

Guest Information

{errors.fullName && (

{errors.fullName.message}

)}
{errors.email && (

{errors.email.message}

)}
{errors.phone && (

{errors.phone.message}

)}
{errors.guestCount && (

{errors.guestCount.message}

)}