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

1082 lines
49 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,
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<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);
const [recaptchaToken, setRecaptchaToken] = useState<string | null>(null);
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 [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);
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;
}
// 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 (
<>
<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">
{/* 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>
</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;