diff --git a/Components/admin/Owners/NewReservationModal.tsx b/Components/admin/Owners/NewReservationModal.tsx new file mode 100644 index 00000000..2cf3629f --- /dev/null +++ b/Components/admin/Owners/NewReservationModal.tsx @@ -0,0 +1,830 @@ +import { Calendar, User, Users, Mail, Phone, ArrowLeft, Upload, Plus, Minus, CreditCard, AlertCircle, CheckCircle, Clock, Package, Camera, Wallet, Info, ChevronRight, Building2, Receipt, LogIn, LogOut, X as XIcon } from "lucide-react"; +import { useState, useRef } from "react"; +import Image from "next/image"; +import DateRangePicker from '@/Components/HeroSection/DateRangePicker'; + +interface GuestInfo { + firstName: string; + lastName: string; + age: string; + gender: string; + validId: File | null; + validIdPreview: string; +} + +interface AddOns { + poolPass: number; + towels: number; + bathRobe: number; + extraComforter: number; + guestKit: number; + extraSlippers: number; +} + +const ADD_ON_PRICES = { + poolPass: 100, + towels: 50, + bathRobe: 150, + extraComforter: 100, + guestKit: 75, + extraSlippers: 30, +}; + +interface NewReservationModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (bookingData: any) => Promise; +} + +const NewReservationModal = ({ isOpen, onClose, onSubmit }: NewReservationModalProps) => { + const [currentStep, setCurrentStep] = useState(1); + const [completedSteps, setCompletedSteps] = useState([]); + const [errors, setErrors] = useState>({}); + const errorRefs = useRef>({}); + + const initialFormData = { + firstName: "", + lastName: "", + age: "", + gender: "", + email: "", + phone: "", + facebookLink: "", + validId: null as File | null, + validIdPreview: "", + adults: 1, + children: 0, + infants: 0, + stayType: "", + checkInDate: "", + checkOutDate: "", + checkInTime: "", + checkOutTime: "", + roomName: "", + paymentProof: null as File | null, + paymentProofPreview: "", + termsAccepted: false, + paymentMethod: "gcash", + }; + + const [formData, setFormData] = useState(initialFormData); + + const initialAddOns = { + poolPass: 0, + towels: 0, + bathRobe: 0, + extraComforter: 0, + guestKit: 0, + extraSlippers: 0, + }; + + const [additionalGuests, setAdditionalGuests] = useState([]); + const [addOns, setAddOns] = useState(initialAddOns); + + // Reset function to clear all form data + const resetForm = () => { + setFormData(initialFormData); + setAdditionalGuests([]); + setAddOns(initialAddOns); + setCurrentStep(1); + setCompletedSteps([]); + setErrors({}); + }; + + // Reset form when modal opens/closes + const handleClose = () => { + resetForm(); + onClose(); + }; + + const getRoomRateFromStayType = (): number => { + if (!formData.stayType) return 0; + if (formData.stayType === "10 Hours - ₱1,599") return 1599; + else if (formData.stayType.includes("weekday")) return 1799; + else if (formData.stayType.includes("Fri-Sat")) return 1999; + else if (formData.stayType === "Multi-Day Stay") { + const numberOfDays = calculateNumberOfDays(); + return 1799 * numberOfDays; + } + return 0; + }; + + const calculateNumberOfDays = (): number => { + if (!formData.checkInDate || !formData.checkOutDate) return 0; + const checkIn = new Date(formData.checkInDate); + const checkOut = new Date(formData.checkOutDate); + const diffTime = Math.abs(checkOut.getTime() - checkIn.getTime()); + return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + }; + + const roomRate = getRoomRateFromStayType(); + const numberOfDays = calculateNumberOfDays(); + const securityDeposit = formData.stayType ? 1000 : 0; + const downPayment = 500; + const addOnsTotal = Object.entries(addOns).reduce((total, [key, quantity]) => { + return total + quantity * ADD_ON_PRICES[key as keyof AddOns]; + }, 0); + const totalAmount = roomRate + securityDeposit + addOnsTotal; + + const updateAdditionalGuests = (adults: number, children: number) => { + const totalAdditionalGuests = adults + children - 1; + setAdditionalGuests(prev => { + const currentLength = prev.length; + if (totalAdditionalGuests > currentLength) { + const newGuests = Array(totalAdditionalGuests - currentLength).fill(null).map(() => ({ + firstName: "", + lastName: "", + age: "", + gender: "", + validId: null, + validIdPreview: "", + })); + return [...prev, ...newGuests]; + } else if (totalAdditionalGuests < currentLength) { + return prev.slice(0, totalAdditionalGuests); + } + return prev; + }); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value, type } = e.target; + + if (name === "adults" || name === "children") { + const newValue = parseInt(value) || 0; + const currentAdults = name === "adults" ? newValue : formData.adults; + const currentChildren = name === "children" ? newValue : formData.children; + const newTotal = currentAdults + currentChildren; + + if (newTotal > 4) { + alert("Maximum 4 guests allowed (adults + children)."); + return; + } + + const updatedFormData = { + ...formData, + [name]: type === "number" ? newValue : value, + }; + setFormData(updatedFormData); + updateAdditionalGuests(currentAdults, currentChildren); + } else { + setFormData((prev) => ({ + ...prev, + [name]: type === "checkbox" ? (e.target as HTMLInputElement).checked : type === "number" ? parseInt(value) || 0 : value, + })); + } + }; + + const handleFileChange = (e: React.ChangeEvent, type: 'payment' | 'id', guestIndex?: number) => { + const file = e.target.files?.[0]; + if (file) { + if (type === 'payment') { + setFormData((prev) => ({ + ...prev, + paymentProof: file, + paymentProofPreview: URL.createObjectURL(file), + })); + } else if (guestIndex === undefined) { + setFormData((prev) => ({ + ...prev, + validId: file, + validIdPreview: URL.createObjectURL(file), + })); + } else { + const updatedGuests = [...additionalGuests]; + updatedGuests[guestIndex].validId = file; + updatedGuests[guestIndex].validIdPreview = URL.createObjectURL(file); + setAdditionalGuests(updatedGuests); + } + } + }; + + const handleAdditionalGuestChange = (index: number, field: keyof GuestInfo, value: string) => { + const updatedGuests = [...additionalGuests]; + updatedGuests[index] = { ...updatedGuests[index], [field]: value }; + setAdditionalGuests(updatedGuests); + }; + + const handleStayTypeChange = (e: React.ChangeEvent) => { + const selectedStayType = e.target.value; + let defaultCheckInTime = ""; + let defaultCheckOutTime = ""; + + if (selectedStayType === "10 Hours - ₱1,599") { + defaultCheckInTime = "14:00"; + defaultCheckOutTime = "00:00"; + } else if (selectedStayType.includes("21 Hours")) { + defaultCheckInTime = "14:00"; + defaultCheckOutTime = "11:00"; + } else if (selectedStayType === "Multi-Day Stay") { + defaultCheckInTime = "14:00"; + defaultCheckOutTime = "11:00"; + } + + setFormData((prev) => ({ + ...prev, + stayType: selectedStayType, + checkInTime: defaultCheckInTime, + checkOutTime: defaultCheckOutTime, + })); + + if (formData.checkInDate && selectedStayType) { + const checkInDate = new Date(formData.checkInDate); + const checkOutDate = new Date(checkInDate); + + if (selectedStayType === "10 Hours - ₱1,599" || selectedStayType.includes("21 Hours")) { + checkOutDate.setDate(checkOutDate.getDate() + 1); + } else if (selectedStayType === "Multi-Day Stay") { + checkOutDate.setDate(checkOutDate.getDate() + 1); + } + + const checkOutStr = checkOutDate.toISOString().split('T')[0]; + setFormData(prev => ({ ...prev, checkOutDate: checkOutStr })); + } + }; + + const handleAddOnChange = (item: keyof AddOns, increment: boolean) => { + setAddOns((prev) => ({ + ...prev, + [item]: Math.max(0, prev[item] + (increment ? 1 : -1)), + })); + }; + + const validateStep1 = (): boolean => { + const newErrors: Record = {}; + if (!formData.firstName) newErrors.firstName = "First name is required"; + if (!formData.lastName) newErrors.lastName = "Last name is required"; + if (!formData.age) newErrors.age = "Age is required"; + if (!formData.gender) newErrors.gender = "Please select a gender"; + if (!formData.email) newErrors.email = "Email is required"; + if (!formData.phone) newErrors.phone = "Phone number is required"; + if (formData.age && parseInt(formData.age) >= 10 && !formData.validId) { + newErrors.validId = "Valid ID is required for guests 10+ years old"; + } + + for (let i = 0; i < additionalGuests.length; i++) { + const guest = additionalGuests[i]; + if (!guest.firstName) newErrors[`guest${i}FirstName`] = `Guest ${i + 2} first name is required`; + if (!guest.lastName) newErrors[`guest${i}LastName`] = `Guest ${i + 2} last name is required`; + if (!guest.age) newErrors[`guest${i}Age`] = `Guest ${i + 2} age is required`; + if (!guest.gender) newErrors[`guest${i}Gender`] = `Guest ${i + 2} gender is required`; + if (guest.age && parseInt(guest.age) >= 10 && !guest.validId) { + newErrors[`guest${i}ValidId`] = `Valid ID required for Guest ${i + 2}`; + } + } + + setErrors(newErrors); + + if (Object.keys(newErrors).length > 0) { + const errorMessages = Object.values(newErrors); + alert(`Please fill in all required fields:\n\n${errorMessages.join('\n')}`); + } + + return Object.keys(newErrors).length === 0; + }; + + const validateStep2 = (): boolean => { + const newErrors: Record = {}; + if (!formData.stayType) newErrors.stayType = "Please select a stay type"; + if (!formData.checkInDate) newErrors.checkInDate = "Check-in date is required"; + if (!formData.checkOutDate) newErrors.checkOutDate = "Check-out date is required"; + if (!formData.checkInTime) newErrors.checkInTime = "Check-in time is required"; + if (!formData.checkOutTime) newErrors.checkOutTime = "Check-out time is required"; + if (!formData.roomName) newErrors.roomName = "Room/Haven name is required"; + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const validateStep4 = (): boolean => { + const newErrors: Record = {}; + if (!formData.paymentProof) newErrors.paymentProof = "Proof of payment is required"; + if (!formData.termsAccepted) newErrors.termsAccepted = "You must accept the terms"; + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleNext = () => { + if (currentStep === 1 && validateStep1()) { + setCompletedSteps(prev => [...prev, 1]); + setCurrentStep(2); + } else if (currentStep === 2 && validateStep2()) { + setCompletedSteps(prev => [...prev, 2]); + setCurrentStep(3); + } else if (currentStep === 3) { + setCompletedSteps(prev => [...prev, 3]); + setCurrentStep(4); + } + }; + + const handleBack = () => { + if (currentStep > 1) setCurrentStep(currentStep - 1); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!validateStep4()) return; + + const bookingData = { + ...formData, + additionalGuests, + addOns, + roomRate, + securityDeposit, + addOnsTotal, + totalAmount, + downPayment, + remainingBalance: totalAmount - downPayment, + }; + + try { + // Wait for the async submission to complete + await onSubmit(bookingData); + // Only reset and close after successful submission + resetForm(); + onClose(); + } catch (error) { + console.error('Submission error:', error); + // Don't close modal or reset on error, let parent handle the error message + } + }; + + if (!isOpen) return null; + + const getStepTitle = () => { + const titles = ["Guest Information", "Booking Details", "Optional Add-ons", "Payment & Review"]; + return titles[currentStep - 1]; + }; + + const inputClass = "w-full px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-2 focus:ring-[#A1823D] focus:border-transparent"; + const labelClass = "block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300"; + const sectionClass = "border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded-xl p-6"; + + return ( +
+
+ {/* Header */} +
+
+

{getStepTitle()}

+

Step {currentStep} of 4

+
+ +
+ + {/* Progress Steps */} +
+
+ {[1, 2, 3, 4].map((step, idx) => ( +
+
+
+ {completedSteps.includes(step) ? : step} +
+ + {["Guest", "Booking", "Add-ons", "Payment"][idx]} + +
+ {idx < 3 && ( +
+ )} +
+ ))} +
+
+ + {/* Content - Scrollable */} +
+
+
+ {/* Step 1: Guest Information */} + {currentStep === 1 && ( +
+
+

+ + Main Guest Information +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

+ + Valid ID (Required for 10+ years old) +

+ handleFileChange(e, 'id')} className="hidden" id="valid-id" /> + + {formData.validIdPreview && ( +
+ ID preview +
+ )} +
+ +
+

+ + Number of Guests +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {additionalGuests.map((guest, index) => ( +
+

+ + {index < formData.adults - 1 ? `Adult ${index + 2}` : `Child ${index - (formData.adults - 1) + 1}`} +

+
+
+ handleAdditionalGuestChange(index, 'firstName', e.target.value)} + placeholder="First Name *" + className={`${inputClass} ${errors[`guest${index}FirstName`] ? 'border-red-500' : ''}`} + /> + {errors[`guest${index}FirstName`] &&

{errors[`guest${index}FirstName`]}

} +
+
+ handleAdditionalGuestChange(index, 'lastName', e.target.value)} + placeholder="Last Name *" + className={`${inputClass} ${errors[`guest${index}LastName`] ? 'border-red-500' : ''}`} + /> + {errors[`guest${index}LastName`] &&

{errors[`guest${index}LastName`]}

} +
+
+ handleAdditionalGuestChange(index, 'age', e.target.value)} + placeholder="Age *" + className={`${inputClass} ${errors[`guest${index}Age`] ? 'border-red-500' : ''}`} + /> + {errors[`guest${index}Age`] &&

{errors[`guest${index}Age`]}

} +
+
+ + {errors[`guest${index}Gender`] &&

{errors[`guest${index}Gender`]}

} +
+
+ + {/* Valid ID Upload for Additional Guest (if age >= 10) */} + {guest.age && parseInt(guest.age) >= 10 && ( +
+

+ + Valid ID Required (10+ years old) +

+ handleFileChange(e, 'id', index)} + className="hidden" + id={`valid-id-guest-${index}`} + /> + + {errors[`guest${index}ValidId`] &&

{errors[`guest${index}ValidId`]}

} + {guest.validIdPreview && ( +
+ {`Guest +
+ )} +
+ )} +
+ ))} +
+ )} + + {/* Step 2: Booking Details */} + {currentStep === 2 && ( +
+
+

Stay Type & Room

+
+
+ + +
+
+ + +
+
+
+ +
+

Dates & Times

+ + {/* DateRangePicker Integration */} +
+ + { + setFormData((prev) => ({ + ...prev, + checkInDate: date, + })); + + // Auto-calculate checkout date based on stay type if already selected + if (formData.stayType && date) { + const checkInDate = new Date(date); + const checkOutDate = new Date(checkInDate); + + if (formData.stayType === "10 Hours - ₱1,599" || formData.stayType.includes("21 Hours")) { + checkOutDate.setDate(checkOutDate.getDate() + 1); + } else if (formData.stayType === "Multi-Day Stay") { + checkOutDate.setDate(checkOutDate.getDate() + 1); + } + + const checkOutStr = checkOutDate.toISOString().split('T')[0]; + setFormData(prev => ({ ...prev, checkOutDate: checkOutStr })); + } + }} + onCheckOutChange={(date) => { + setFormData((prev) => ({ + ...prev, + checkOutDate: date, + })); + }} + /> +
+ + {/* Keep the time inputs */} +
+
+ + +
+
+ + +
+
+
+
+ )} + + {/* Step 3: Add-ons */} + {currentStep === 3 && ( +
+
+

Optional Add-ons

+
+ {[ + { key: "poolPass", label: "Pool Pass", price: ADD_ON_PRICES.poolPass }, + { key: "towels", label: "Towels", price: ADD_ON_PRICES.towels }, + { key: "bathRobe", label: "Bath Robe", price: ADD_ON_PRICES.bathRobe }, + { key: "extraComforter", label: "Extra Comforter", price: ADD_ON_PRICES.extraComforter }, + { key: "guestKit", label: "Guest Kit", price: ADD_ON_PRICES.guestKit }, + { key: "extraSlippers", label: "Extra Slippers", price: ADD_ON_PRICES.extraSlippers }, + ].map((item) => ( +
+
+

{item.label}

+

₱{item.price} each

+
+
+ + {addOns[item.key as keyof AddOns]} + +
+
+ ))} +
+ {addOnsTotal > 0 && ( +
+

Add-ons Total: ₱{addOnsTotal.toLocaleString()}

+
+ )} +
+
+ )} + + {/* Step 4: Payment & Review */} + {currentStep === 4 && ( +
+
+

Booking Summary

+
+
Guest: {formData.firstName} {formData.lastName}
+
Email: {formData.email}
+
Phone: {formData.phone}
+
Room: {formData.roomName}
+
Check-in: {formData.checkInDate} at {formData.checkInTime}
+
Check-out: {formData.checkOutDate} at {formData.checkOutTime}
+
+ +
+

Price Breakdown

+
Room Rate₱{roomRate.toLocaleString()}
+
Security Deposit₱{securityDeposit.toLocaleString()}
+ {addOnsTotal > 0 &&
Add-ons₱{addOnsTotal.toLocaleString()}
} +
+ Total + ₱{totalAmount.toLocaleString()} +
+
+
Downpayment₱{downPayment.toLocaleString()}
+
Remaining Balance₱{(totalAmount - downPayment).toLocaleString()}
+
+
+
+ +
+

Payment Method

+
+ + +
+ +
+ + handleFileChange(e, 'payment')} className="hidden" id="payment-proof" /> + + {formData.paymentProofPreview && ( +
+ Payment proof +
+ )} +
+
+ +
+ +
+
+ )} +
+
+
+ + {/* Footer Actions */} +
+ {currentStep > 1 && ( + + )} + + {currentStep < 4 ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +export default NewReservationModal; \ No newline at end of file diff --git a/Components/admin/Owners/ReservationsPage.tsx b/Components/admin/Owners/ReservationsPage.tsx index 5ca1c704..4629b238 100644 --- a/Components/admin/Owners/ReservationsPage.tsx +++ b/Components/admin/Owners/ReservationsPage.tsx @@ -1,230 +1,363 @@ -"use client"; - -import { - Calendar, - User, - MapPin, - Check, - X, - AlertCircle, - Eye, - XCircle, - Package, - ChevronLeft, - ChevronRight, - ChevronsLeft, - ChevronsRight, - CreditCard, // added (was used but not imported) -} from "lucide-react"; -import Image from "next/image"; // added (used in modal) -import { useState } from "react"; -import { - useGetBookingsQuery, - useUpdateBookingStatusMutation, -} from "@/redux/api/bookingsApi"; -import { Booking, AdditionalGuest } from "@/types/booking"; +'use client'; + +import { Calendar, User, MapPin, Phone, Mail, Check, X, AlertCircle, Eye, XCircle, CreditCard, Package, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Wallet, Users } from "lucide-react"; +import { useState, useRef } from "react"; +import { useGetBookingsQuery, useUpdateBookingStatusMutation, useCreateBookingMutation } from "@/redux/api/bookingsApi"; +import NewReservationModal from "@/Components/admin/Owners/NewReservationModal"; +import toast from "react-hot-toast"; + +interface AdditionalGuest { + name: string; + age?: number; + [key: string]: unknown; +} + +interface Booking { + id: string; + status: string; + additional_guests?: AdditionalGuest[]; + booking_id?: string; + guest_first_name?: string; + guest_last_name?: string; + guest_email?: string; + guest_phone?: string; + room_name?: string; + check_in_date: string; + check_in_time?: string; + check_out_date: string; + check_out_time?: string; + adults?: number; + children?: number; + infants?: number; + total_amount?: string | number; + remaining_balance?: string | number; + main_guest?: any; + payment?: any; + add_ons?: any[]; + security_deposit?: any; + [key: string]: unknown; +} const ReservationsPage = () => { const [filter, setFilter] = useState("all"); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(5); const [searchQuery, setSearchQuery] = useState(""); - - const { data, isLoading, refetch } = useGetBookingsQuery( - {}, - { - pollingInterval: 5000, - skipPollingIfUnfocused: true, - refetchOnFocus: true, - refetchOnReconnect: true, - } - ); - const [updateBookingStatus] = useUpdateBookingStatusMutation(); + + const { data, isLoading, refetch } = useGetBookingsQuery({}); + const [updateBookingStatus, { isLoading: isUpdating }] = useUpdateBookingStatusMutation(); + const [createBooking] = useCreateBookingMutation(); const [selectedBooking, setSelectedBooking] = useState(null); + const [isNewReservationModalOpen, setIsNewReservationModalOpen] = useState(false); - const reservations: Booking[] = data ?? []; - - const formatShortDate = (date?: string | null) => - date - ? new Date(date).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }) - : "N/A"; - - const formatDateSafe = (date?: string | null) => - date ? new Date(date).toLocaleDateString() : ""; - - const formatStatus = (status?: string | null) => - status - ? status.charAt(0).toUpperCase() + status.slice(1).replace("-", " ") - : ""; + const reservations: Booking[] = (data as Booking[]) || []; const handleApprove = async (bookingId: string) => { try { - await updateBookingStatus({ + console.log('🔄 Approving booking:', bookingId); + + const result = await updateBookingStatus({ id: bookingId, - status: "approved", + status: 'approved' }).unwrap(); - alert("Booking approved! Confirmation email will be sent to the guest."); - refetch(); - } catch (error) { - console.error("Error approving booking:", error); - alert("Failed to approve booking. Please try again."); + console.log('✅ Approval result:', result); + + toast.success('Booking approved! Confirmation email will be sent to the guest.'); + await refetch(); + } catch (error: any) { + console.error('❌ Error approving booking:', error); + + // More detailed error message + const errorMessage = error?.data?.error || error?.message || 'Failed to approve booking'; + toast.error(`Failed to approve: ${errorMessage}`); } }; const handleReject = async (bookingId: string) => { - const reason = prompt("Please enter rejection reason:"); + const reason = prompt('Please enter rejection reason:'); if (!reason) return; + try { - await updateBookingStatus({ + console.log('🔄 Rejecting booking:', bookingId, 'Reason:', reason); + + const result = await updateBookingStatus({ id: bookingId, - status: "rejected", - rejection_reason: reason, + status: 'rejected', + rejection_reason: reason }).unwrap(); - alert("Booking rejected. Guest will be notified."); - refetch(); - } catch (error) { - console.error("Error rejecting booking:", error); - alert("Failed to reject booking. Please try again."); + console.log('✅ Rejection result:', result); + + toast.success('Booking rejected. Guest will be notified.'); + await refetch(); + } catch (error: any) { + console.error('❌ Error rejecting booking:', error); + + const errorMessage = error?.data?.error || error?.message || 'Failed to reject booking'; + toast.error(`Failed to reject: ${errorMessage}`); } }; const handleCheckIn = async (bookingId: string) => { try { - const booking = reservations.find((r) => r.id === bookingId); - if (!booking) { - alert("Booking not found"); + console.log('🔄 Checking in booking:', bookingId); + + // First, fetch the complete booking data with guest info + const response = await fetch(`/api/bookings/${bookingId}`); + const result = await response.json(); + + console.log('📥 Fetched booking data:', result); + + if (!result.success || !result.data) { + toast.error('Booking not found'); return; } - await updateBookingStatus({ + const booking = result.data; + const mainGuest = booking.main_guest || booking.guests?.[0]; + + // Update booking status + const updateResult = await updateBookingStatus({ id: bookingId, - status: "checked-in", + status: 'checked-in' }).unwrap(); + console.log('✅ Check-in status updated:', updateResult); + + // Send check-in email try { const emailData = { - firstName: booking.guest_first_name, - lastName: booking.guest_last_name, - email: booking.guest_email, + firstName: mainGuest?.first_name || 'Guest', + lastName: mainGuest?.last_name || '', + email: mainGuest?.email || '', bookingId: booking.booking_id, roomName: booking.room_name, - checkInDate: formatDateSafe(booking.check_in_date), + checkInDate: new Date(booking.check_in_date).toLocaleDateString(), checkInTime: booking.check_in_time, - checkOutDate: formatDateSafe(booking.check_out_date), + checkOutDate: new Date(booking.check_out_date).toLocaleDateString(), checkOutTime: booking.check_out_time, - guests: `${booking.adults || 0} Adults, ${booking.children || 0} Children, ${booking.infants || 0} Infants`, + guests: `${booking.adults} Adults, ${booking.children} Children, ${booking.infants} Infants`, }; - const emailResponse = await fetch("/api/send-checkin-email", { - method: "POST", - headers: { "Content-Type": "application/json" }, + const emailResponse = await fetch('/api/send-checkin-email', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(emailData), }); if (!emailResponse.ok) { - console.error("Failed to send check-in email"); + console.error('⚠️ Failed to send check-in email'); } } catch (emailError) { - console.error("Email sending error:", emailError); + console.error('⚠️ Email sending error:', emailError); } - alert("Guest checked in successfully! Confirmation email sent."); - refetch(); - } catch (error) { - console.error("Error checking in:", error); - alert("Failed to check in. Please try again."); + toast.success('Guest checked in successfully! Confirmation email sent.'); + await refetch(); + } catch (error: any) { + console.error('❌ Error checking in:', error); + + const errorMessage = error?.data?.error || error?.message || 'Failed to check in'; + toast.error(`Failed to check in: ${errorMessage}`); } }; const handleCheckOut = async (bookingId: string) => { try { - const booking = reservations.find((r) => r.id === bookingId); - if (!booking) { - alert("Booking not found"); + console.log('🔄 Checking out booking:', bookingId); + + // First, fetch the complete booking data + const response = await fetch(`/api/bookings/${bookingId}`); + const result = await response.json(); + + console.log('📥 Fetched booking data:', result); + + if (!result.success || !result.data) { + toast.error('Booking not found'); return; } - await updateBookingStatus({ + const booking = result.data; + const mainGuest = booking.main_guest || booking.guests?.[0]; + const payment = booking.payment; + + // Update booking status + const updateResult = await updateBookingStatus({ id: bookingId, - status: "completed", + status: 'completed' }).unwrap(); + console.log('✅ Check-out status updated:', updateResult); + + // Send check-out email try { const emailData = { - firstName: booking.guest_first_name, - lastName: booking.guest_last_name, - email: booking.guest_email, + firstName: mainGuest?.first_name || 'Guest', + lastName: mainGuest?.last_name || '', + email: mainGuest?.email || '', bookingId: booking.booking_id, roomName: booking.room_name, - checkInDate: formatDateSafe(booking.check_in_date), - checkOutDate: formatDateSafe(booking.check_out_date), - totalAmount: Number(booking.total_amount).toLocaleString(), - remainingBalance: Number(booking.remaining_balance), + checkInDate: new Date(booking.check_in_date).toLocaleDateString(), + checkOutDate: new Date(booking.check_out_date).toLocaleDateString(), + totalAmount: payment ? Number(payment.total_amount).toLocaleString() : '0', + remainingBalance: payment ? Number(payment.remaining_balance) : 0, }; - const emailResponse = await fetch("/api/send-checkout-email", { - method: "POST", - headers: { "Content-Type": "application/json" }, + const emailResponse = await fetch('/api/send-checkout-email', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(emailData), }); if (!emailResponse.ok) { - console.error("Failed to send check-out email"); + console.error('⚠️ Failed to send check-out email'); } } catch (emailError) { - console.error("Email sending error:", emailError); + console.error('⚠️ Email sending error:', emailError); } - alert("Guest checked out successfully! Thank you email sent."); - refetch(); - } catch (error) { - console.error("Error checking out:", error); - alert("Failed to check out. Please try again."); + toast.success('Guest checked out successfully! Thank you email sent.'); + await refetch(); + } catch (error: any) { + console.error('❌ Error checking out:', error); + + const errorMessage = error?.data?.error || error?.message || 'Failed to check out'; + toast.error(`Failed to check out: ${errorMessage}`); } }; - const getStatusColor = (status: string | null | undefined) => { + const handleNewReservation = async (bookingData: any) => { + try { + console.log("📤 Sending booking data:", bookingData); + + let paymentProofBase64 = ''; + if (bookingData.paymentProof) { + const reader = new FileReader(); + paymentProofBase64 = await new Promise((resolve, reject) => { + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(bookingData.paymentProof as File); + }); + } + + let validIdBase64 = ''; + if (bookingData.validId) { + const reader = new FileReader(); + validIdBase64 = await new Promise((resolve, reject) => { + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(bookingData.validId as File); + }); + } + + const additionalGuestsData = await Promise.all( + (bookingData.additionalGuests || []).map(async (guest: any) => { + let guestIdBase64 = ''; + if (guest.validId) { + const reader = new FileReader(); + guestIdBase64 = await new Promise((resolve, reject) => { + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(guest.validId as File); + }); + } + return { + firstName: guest.firstName, + lastName: guest.lastName, + age: guest.age, + gender: guest.gender, + validId: guestIdBase64, + }; + }) + ); + + const bookingId = `BK${Date.now()}`; + + const bookingRequestData = { + booking_id: bookingId, + user_id: null, + // Main guest info + guest_first_name: bookingData.firstName, + guest_last_name: bookingData.lastName, + guest_age: bookingData.age, + guest_gender: bookingData.gender, + guest_email: bookingData.email, + guest_phone: bookingData.phone, + facebook_link: bookingData.facebookLink || '', + valid_id: validIdBase64, + // Additional guests + additional_guests: additionalGuestsData, + // Booking details + room_name: bookingData.roomName, + check_in_date: bookingData.checkInDate, + check_out_date: bookingData.checkOutDate, + check_in_time: bookingData.checkInTime, + check_out_time: bookingData.checkOutTime, + adults: bookingData.adults, + children: bookingData.children, + infants: bookingData.infants, + // Payment info + payment_method: bookingData.paymentMethod, + payment_proof: paymentProofBase64, + room_rate: bookingData.roomRate, + security_deposit: bookingData.securityDeposit, + add_ons_total: bookingData.addOnsTotal, + total_amount: bookingData.totalAmount, + down_payment: bookingData.downPayment, + remaining_balance: bookingData.remainingBalance, + // Add-ons object + addOns: bookingData.addOns, + }; + + console.log("📤 Final request data:", bookingRequestData); + + const result = await createBooking(bookingRequestData).unwrap(); + + if (result.success) { + toast.success(`Reservation created successfully! Booking ID: ${bookingId}`); + setIsNewReservationModalOpen(false); + await refetch(); + } else { + toast.error('Failed to create reservation. Please try again.'); + } + } catch (error: any) { + console.error('❌ Error creating reservation:', error); + + const errorMessage = error?.data?.error || error?.message || 'An error occurred'; + toast.error(`Failed to create reservation: ${errorMessage}`); + } + }; + + const getStatusColor = (status: string) => { switch (status) { case "approved": - case "confirmed": - return "bg-green-100 text-green-700"; - case "pending": - return "bg-yellow-100 text-yellow-700"; - case "checked-in": - return "bg-blue-100 text-blue-700"; - case "completed": - return "bg-gray-100 text-gray-700"; + case "confirmed": return "bg-green-100 text-green-700"; + case "pending": return "bg-yellow-100 text-yellow-700"; + case "checked-in": return "bg-blue-100 text-blue-700"; + case "completed": return "bg-gray-100 text-gray-700"; case "rejected": - case "cancelled": - return "bg-red-100 text-red-700"; - default: - return "bg-gray-100 text-gray-700"; + case "cancelled": return "bg-red-100 text-red-700"; + default: return "bg-gray-100 text-gray-700"; } }; const filteredReservations = reservations.filter((r: Booking) => { const matchesStatus = filter === "all" || r.status === filter; - if (!searchQuery?.trim()) return matchesStatus; + if (!searchQuery || typeof searchQuery !== 'string' || searchQuery.trim() === '') { + return matchesStatus; + } const q = searchQuery.trim().toLowerCase(); - const bookingId = String(r.booking_id || r.id || "").toLowerCase(); - const guestFirst = String(r.guest_first_name || "").toLowerCase(); - const guestLast = String(r.guest_last_name || "").toLowerCase(); + const bookingId = String(r.booking_id || r.id || '').toLowerCase(); + const guestFirst = String((r as any).guest_first_name || '').toLowerCase(); + const guestLast = String((r as any).guest_last_name || '').toLowerCase(); const guestFull = `${guestFirst} ${guestLast}`.trim(); - return ( - matchesStatus && - (bookingId.includes(q) || - guestFirst.includes(q) || - guestLast.includes(q) || - guestFull.includes(q)) - ); + const matchesSearch = bookingId.includes(q) || guestFirst.includes(q) || guestLast.includes(q) || guestFull.includes(q); + + return matchesStatus && matchesSearch; }); const totalPages = Math.ceil(filteredReservations.length / itemsPerPage); @@ -232,8 +365,25 @@ const ReservationsPage = () => { const endIndex = startIndex + itemsPerPage; const currentReservations = filteredReservations.slice(startIndex, endIndex); - const handleViewDetails = (booking: Booking) => { - setSelectedBooking(booking); + const handleViewDetails = async (booking: Booking) => { + try { + console.log('🔍 Viewing details for booking:', booking.id); + + // Fetch complete booking data with all related tables + const response = await fetch(`/api/bookings/${booking.id}`); + const result = await response.json(); + + console.log('📥 Fetched complete booking:', result); + + if (result.success) { + setSelectedBooking(result.data); + } else { + toast.error('Failed to load booking details'); + } + } catch (error) { + console.error('❌ Error loading booking details:', error); + toast.error('Failed to load booking details'); + } }; const closeModal = () => { @@ -242,254 +392,326 @@ const ReservationsPage = () => { const goToFirstPage = () => setCurrentPage(1); const goToLastPage = () => setCurrentPage(totalPages); - const goToNextPage = () => - setCurrentPage((prev) => Math.min(prev + 1, totalPages)); - const goToPrevPage = () => setCurrentPage((prev) => Math.max(prev - 1, 1)); + const goToNextPage = () => setCurrentPage(prev => Math.min(prev + 1, totalPages)); + const goToPrevPage = () => setCurrentPage(prev => Math.max(prev - 1, 1)); + + const statusCardsData = [ + { key: 'pending', label: 'Pending', bg: 'bg-yellow-400 text-white', icon: }, + { key: 'approved', label: 'Approved', bg: 'bg-green-500 text-white', icon: }, + { key: 'confirmed', label: 'Confirmed', bg: 'bg-emerald-500 text-white', icon: }, + { key: 'checked-in', label: 'Checked In', bg: 'bg-blue-500 text-white', icon: }, + { key: 'completed', label: 'Completed', bg: 'bg-gray-500 text-white', icon: }, + { key: 'rejected', label: 'Rejected', bg: 'bg-red-500 text-white', icon: }, + { key: 'cancelled', label: 'Cancelled', bg: 'bg-red-600 text-white', icon: }, + ]; return ( <> {/* Details Modal */} {selectedBooking && (
-
-
+
+ {/* Header */} +

Booking Details

-

- ID: {selectedBooking.booking_id} -

+

ID: {selectedBooking.booking_id}

-
+ {/* Status Badge */}
- - {formatStatus(selectedBooking.status)} + + {selectedBooking.status.toUpperCase().replace("-", " ")}
-
-

- + {/* Main Guest Information */} +
+

+ Main Guest Information

-

Full Name

-

- {selectedBooking.guest_first_name}{" "} - {selectedBooking.guest_last_name} +

Full Name

+

+ {selectedBooking.guests?.[0]?.firstName || selectedBooking.guests?.[0]?.first_name || selectedBooking.guest_first_name || 'N/A'} {selectedBooking.guests?.[0]?.lastName || selectedBooking.guests?.[0]?.last_name || selectedBooking.guest_last_name || ''}

-

Email

-

- {selectedBooking.guest_email} -

+

Email

+

{selectedBooking.guests?.[0]?.email || selectedBooking.guest_email || 'N/A'}

-

Phone

-

- {selectedBooking.guest_phone} -

+

Phone

+

{selectedBooking.guests?.[0]?.phone || selectedBooking.guest_phone || 'N/A'}

- - {selectedBooking.guest_gender && ( + {(selectedBooking.guests?.[0]?.age || selectedBooking.guest_age) && (
-

Gender

-

- {selectedBooking.guest_gender} -

+

Age

+

{selectedBooking.guests?.[0]?.age || selectedBooking.guest_age}

)} - {selectedBooking.facebook_link && ( + {(selectedBooking.guests?.[0]?.gender || selectedBooking.guest_gender) && ( + )} + {selectedBooking.guests?.[0]?.facebook_link && ( + )}
- - {selectedBooking.valid_id_url && ( -
-

- - Valid ID -

-
- Main Guest Valid ID -
- - Open in new tab → - + {selectedBooking.guests?.[0]?.valid_id_url && ( +
+

Valid ID

+ Valid ID
)}
- {selectedBooking.additional_guests?.length ? ( -
-

- - Additional Guests ( - {selectedBooking.additional_guests.length}) + {/* Additional Guests - FIXED VERSION */} + {selectedBooking.guests && selectedBooking.guests.length > 1 && ( +
+

+ + Additional Guests ({selectedBooking.guests.length - 1})

-
- {Array.isArray(selectedBooking.additional_guests) && - selectedBooking.additional_guests.map( - (guest: AdditionalGuest, index: number) => { - const guestNumber = index + 2; - const isAdult = - index < (selectedBooking.adults || 0) - 1; - const guestType = isAdult - ? `Adult ${guestNumber}` - : `Child ${guestNumber - ((selectedBooking.adults || 0) - 1)}`; - - return ( -
-

- {guestType} -

-
-
-

- Full Name -

-

- {guest.firstName} {guest.lastName} -

-
- {guest.age && ( -
-

Age

-

- {guest.age} years old -

-
- )} - {guest.gender && ( -
-

- Gender -

-

- {guest.gender} -

-
- )} -
- - {guest.validIdUrl && ( -
-
- - Valid ID -
-
- {`${guest.firstName} -
- - Open in new tab → - -
- )} -
- ); - }, - )} +
+ {selectedBooking.guests.slice(1).map((guest: any, index: number) => ( +
+
+
+

+ {guest.firstName || guest.first_name} {guest.lastName || guest.last_name} +

+ {guest.age && ( +

Age: {guest.age}

+ )} + {guest.gender && ( +

Gender: {guest.gender}

+ )} +
+ {(guest.valid_id_url || guest.validIdUrl) && ( + ✓ ID Verified + )} +
+
+ ))} +
+
+ )} + + {/* Booking Details */} +
+

+ + Booking Details +

+
+
+

Room/Haven

+

{selectedBooking.room_name}

+
+
+

Guests

+

+ {selectedBooking.adults} Adults, {selectedBooking.children} Children, {selectedBooking.infants} Infants +

+
+
+

Check-in

+

+ {new Date(selectedBooking.check_in_date).toLocaleDateString()} at {selectedBooking.check_in_time} +

+
+
+

Check-out

+

+ {new Date(selectedBooking.check_out_date).toLocaleDateString()} at {selectedBooking.check_out_time} +

+
+
+
+ + {/* Payment Information */} + {selectedBooking.payment && ( +
+

+ + Payment Information +

+
+
+ Payment Method + {selectedBooking.payment.payment_method} +
+
+ Room Rate + ₱{Number(selectedBooking.payment.room_rate).toLocaleString()} +
+ {selectedBooking.payment.add_ons_total > 0 && ( +
+ Add-ons + ₱{Number(selectedBooking.payment.add_ons_total).toLocaleString()} +
+ )} +
+ Total Amount + ₱{Number(selectedBooking.payment.total_amount).toLocaleString()} +
+
+ Down Payment + ₱{Number(selectedBooking.payment.down_payment).toLocaleString()} +
+
+ Remaining Balance + ₱{Number(selectedBooking.payment.remaining_balance).toLocaleString()} +
+ {selectedBooking.payment.payment_proof_url && ( +
+

Payment Proof

+ Payment Proof +
+ )} +
+
+ )} + + {/* Add-ons */} + {selectedBooking.add_ons && selectedBooking.add_ons.length > 0 && ( +
+

+ + Add-ons ({selectedBooking.add_ons.length}) +

+
+ {selectedBooking.add_ons.map((addon: any) => ( +
+
+

{addon.name}

+

+ ₱{Number(addon.price).toLocaleString()} × {addon.quantity} +

+
+ + ₱{Number(addon.price * addon.quantity).toLocaleString()} + +
+ ))} +
+
+ )} + + {/* Security Deposit */} +
+

+ + Security Deposit +

+
+
+ Amount + + ₱{Number(selectedBooking.security_deposit || 0).toLocaleString()} +
+
+ Status + + {selectedBooking.deposit_status?.toUpperCase() || 'PENDING'} + +
+ {selectedBooking.security_deposit_payment_method && ( +
+ Payment Method + + {selectedBooking.security_deposit_payment_method} + +
+ )} + {selectedBooking.security_deposit_payment_proof_url && ( +
+

Payment Proof

+ Security Deposit Payment Proof +
+ )}
- ) : null} +
-
+ {/* Action Buttons */} +
{selectedBooking.status === "pending" && ( <> - - )} - - {(selectedBooking.status === "approved" || - selectedBooking.status === "confirmed") && ( - )} - {selectedBooking.status === "checked-in" && ( - )} - - @@ -500,93 +722,70 @@ const ReservationsPage = () => { )}
+ {/* Header */}
-

- Reservations -

-

- Manage all your bookings and reservations -

+

Reservations

+

Manage all your bookings and reservations

-
-
- {[ - { - key: "pending", - label: "Pending", - bg: "bg-yellow-400 text-white", - icon: , - }, - { - key: "approved", - label: "Approved", - bg: "bg-green-500 text-white", - icon: , - }, - { - key: "confirmed", - label: "Confirmed", - bg: "bg-emerald-500 text-white", - icon: , - }, - { - key: "checked-in", - label: "Checked In", - bg: "bg-blue-500 text-white", - icon: , - }, - { - key: "completed", - label: "Completed", - bg: "bg-gray-500 text-white", - icon: , - }, - { - key: "rejected", - label: "Rejected", - bg: "bg-red-500 text-white", - icon: , - }, - { - key: "cancelled", - label: "Cancelled", - bg: "bg-red-600 text-white", - icon: , - }, - ].map((s) => { - const count = reservations.filter((r) => r.status === s.key).length; - return ( -
-
- {s.label} + {/* Status summary cards */} +
+
+ {statusCardsData.slice(0, 4).map((s) => { + const count = reservations.filter((r: Booking) => r.status === s.key).length; + return ( +
+
{s.label}
+
{count}
+
{s.icon}
-
{count}
-
- {s.icon} + ); + })} +
+ +
+ {statusCardsData.slice(4).map((s) => { + const count = reservations.filter((r: Booking) => r.status === s.key).length; + return ( +
+
{s.label}
+
{count}
+
{s.icon}
+ ); + })} +
+
+ +
+ {statusCardsData.map((s) => { + const count = reservations.filter((r: Booking) => r.status === s.key).length; + return ( +
+
{s.label}
+
{count}
+
{s.icon}
); })}
-
+ {/* Filters */} +
{ - setSearchQuery(e.target.value); - setCurrentPage(1); - }} + onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }} placeholder="Search by booking ID or guest name..." - className="w-full border rounded-lg px-4 py-2 text-sm" + className="w-full border rounded-lg px-4 py-2 text-sm dark:bg-slate-800 dark:border-slate-700 dark:text-gray-200" />
-
+ {/* Table */} +
{isLoading ? (
-
+
) : filteredReservations.length === 0 ? ( -
+
-

- No Reservations Found -

-

- There are no {filter !== "all" ? filter : ""} reservations at - the moment. -

+

No Reservations Found

+

There are no {filter !== 'all' ? filter : ''} reservations at the moment.

) : ( -
- - +
+
+ - - - - - - - - - + + + + + + + + - - {currentReservations.map((reservation) => ( - - + {currentReservations.map((reservation: Booking) => ( + + - - - - - - - - @@ -826,76 +945,85 @@ const ReservationsPage = () => { )} - {!isLoading && filteredReservations.length > 0 && ( -
-
- Showing {startIndex + 1} to{" "} - {Math.min(endIndex, filteredReservations.length)} of{" "} - {filteredReservations.length} entries -
+ {/* Pagination */} + {(!isLoading && filteredReservations.length > 0) && ( +
+
+
+ Showing {startIndex + 1} to {Math.min(endIndex, filteredReservations.length)} of {filteredReservations.length} entries +
-
- - +
+ - {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { - let pageNum; - if (totalPages <= 5) { - pageNum = i + 1; - } else if (currentPage <= 3) { - pageNum = i + 1; - } else if (currentPage >= totalPages - 2) { - pageNum = totalPages - 4 + i; - } else { - pageNum = currentPage - 2 + i; - } - return ( - - ); - })} + - - +
+ {Array.from({ length: Math.min(3, totalPages) }, (_, i) => i + 1).map((pageNum) => ( + + ))} +
+ + + + +
)}
+ + {/* New Reservation Modal */} + setIsNewReservationModalOpen(false)} + onSubmit={handleNewReservation} + /> ); }; -export default ReservationsPage; +export default ReservationsPage; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8796d31d..b01e1fd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -894,6 +894,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1828,6 +1829,7 @@ "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", "license": "MIT", + "peer": true, "dependencies": { "preact": "~10.12.1" } @@ -3863,6 +3865,7 @@ "resolved": "https://registry.npmjs.org/@nextui-org/system/-/system-2.4.6.tgz", "integrity": "sha512-6ujAriBZMfQ16n6M6Ad9g32KJUa1CzqIVaHN/tymadr/3m8hrr7xDw6z50pVjpCRq2PaaA1hT8Hx7EFU3f2z3Q==", "license": "MIT", + "peer": true, "dependencies": { "@internationalized/date": "3.6.0", "@nextui-org/react-utils": "2.1.3", @@ -3957,6 +3960,7 @@ "resolved": "https://registry.npmjs.org/@nextui-org/theme/-/theme-2.4.5.tgz", "integrity": "sha512-c7Y17n+hBGiFedxMKfg7Qyv93iY5MteamLXV4Po4c1VF1qZJI6I+IKULFh3FxPWzAoz96r6NdYT7OLFjrAJdWg==", "license": "MIT", + "peer": true, "dependencies": { "@nextui-org/shared-utils": "2.1.2", "clsx": "^1.2.1", @@ -7068,6 +7072,7 @@ "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -7140,6 +7145,7 @@ "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", @@ -7639,6 +7645,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8269,6 +8276,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8793,7 +8801,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-array": { "version": "3.2.4", @@ -9559,6 +9568,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9744,6 +9754,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10317,6 +10328,7 @@ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.29.0.tgz", "integrity": "sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg==", "license": "MIT", + "peer": true, "dependencies": { "motion-dom": "^12.29.0", "motion-utils": "^12.27.2", @@ -11502,6 +11514,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -11596,6 +11609,7 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz", "integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", @@ -11790,7 +11804,8 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/levn": { "version": "0.4.1", @@ -12327,6 +12342,7 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", "license": "MIT-0", + "peer": true, "engines": { "node": ">=6.0.0" } @@ -12708,6 +12724,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", @@ -12866,6 +12883,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13053,6 +13071,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -13194,6 +13213,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13203,6 +13223,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13253,6 +13274,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -13406,7 +13428,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-persist": { "version": "6.0.0", @@ -14589,6 +14612,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14683,6 +14707,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -14794,6 +14819,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15423,6 +15449,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }
- ID - - Guest - - Haven - - Check-In - - Check-Out - - Guests - - Status - - Amount - - Actions - Booking IDHaven LocationGuestCheck-InCheck-OutAssigned ToStatusActions
- - {reservation.booking_id} - +
+ {reservation.booking_id} -
- -
-
- {reservation.guest_first_name}{" "} - {reservation.guest_last_name} -
-
- {reservation.guest_email} -
-
- {reservation.guest_phone} -
-
+
+
+ + {reservation.room_name || 'N/A'}
-
- - - {reservation.room_name || "N/A"} - +
+
+ + {reservation.guest_first_name} {reservation.guest_last_name}
-
- {formatShortDate(reservation.check_in_date)} -
-
- {reservation.check_in_time} -
+
+
{new Date(reservation.check_in_date).toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit' })} {reservation.check_in_time}
-
- {formatShortDate(reservation.check_out_date)} -
-
- {reservation.check_out_time} -
+
+
{new Date(reservation.check_out_date).toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit' })} {reservation.check_out_time}
-
- {(reservation.adults || 0) + - (reservation.children || 0) + - (reservation.infants || 0)} -
-
- A:{reservation.adults || 0} C: - {reservation.children || 0} I: - {reservation.infants || 0} -
+
+
Unassigned
- - {formatStatus(reservation.status)} + + + {reservation.status.charAt(0).toUpperCase() + reservation.status.slice(1).replace("-", " ")} -
- ₱{Number(reservation.total_amount).toLocaleString()} -
-
- Bal: ₱ - {Number( - reservation.remaining_balance, - ).toLocaleString()} -
-
-
+
+
{reservation.status === "pending" && ( <> - - )} - - {(reservation.status === "approved" || - reservation.status === "confirmed") && ( - )} - {reservation.status === "checked-in" && ( - )} - - +