"use client"; import { useState, useEffect, useMemo, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { useAppTheme } from "@/components/ThemeProvider"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Calendar, Clock, User, Mail, Phone, MessageSquare, ArrowLeft, Heart, CheckCircle2, CheckCircle, Loader2, LogOut, CalendarCheck, } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { LoginDialog } from "@/components/LoginDialog"; import { SignupDialog } from "@/components/SignupDialog"; import { useAuth } from "@/hooks/useAuth"; import { useAppointments } from "@/hooks/useAppointments"; import { toast } from "sonner"; import type { Appointment } from "@/lib/models/appointments"; interface User { ID: number; CreatedAt?: string; UpdatedAt?: string; DeletedAt?: string | null; first_name: string; last_name: string; email: string; phone: string; location: string; date_of_birth?: string; is_admin?: boolean; bookings?: null; } interface Booking { ID: number; CreatedAt: string; UpdatedAt: string; DeletedAt: string | null; user_id: number; user: User; scheduled_at: string; duration: number; status: string; jitsi_room_id: string; jitsi_room_url: string; payment_id: string; payment_status: string; amount: number; notes: string; } interface BookingsResponse { bookings: Booking[]; limit: number; offset: number; total: number; } export default function BookNowPage() { const router = useRouter(); const { theme } = useAppTheme(); const isDark = theme === "dark"; const { isAuthenticated, logout } = useAuth(); const { create, isCreating, weeklyAvailability, isLoadingWeeklyAvailability, availabilityOverview, isLoadingAvailabilityOverview, availabilityConfig, } = useAppointments(); const [formData, setFormData] = useState({ firstName: "", lastName: "", email: "", phone: "", selectedSlots: [] as Array<{ day: number; time_slot: string }>, // New format message: "", }); const [booking, setBooking] = useState(null); const [error, setError] = useState(null); const [showLoginDialog, setShowLoginDialog] = useState(false); const [showSignupDialog, setShowSignupDialog] = useState(false); const [loginPrefillEmail, setLoginPrefillEmail] = useState(undefined); // Helper function to convert day name to day number (0-6) const getDayNumber = (dayName: string): number => { const dayMap: Record = { 'monday': 0, 'tuesday': 1, 'wednesday': 2, 'thursday': 3, 'friday': 4, 'saturday': 5, 'sunday': 6, }; return dayMap[dayName.toLowerCase()] ?? -1; }; // Get available days from availability overview (primary) or weekly availability (fallback) const availableDaysOfWeek = useMemo(() => { // Try availability overview first (preferred) if (availabilityOverview && availabilityOverview.available && availabilityOverview.next_available_dates && availabilityOverview.next_available_dates.length > 0) { // Group by day name and get unique days with their slots from next_available_dates const dayMap = new Map }>(); availabilityOverview.next_available_dates.forEach((dateInfo: any) => { if (!dateInfo || !dateInfo.day_name) return; const dayName = String(dateInfo.day_name).trim(); const dayNum = getDayNumber(dayName); if (dayNum >= 0 && dayNum <= 6 && dateInfo.available_slots && Array.isArray(dateInfo.available_slots) && dateInfo.available_slots.length > 0) { const existingDay = dayMap.get(dayName); if (existingDay) { // Merge slots if day already exists dateInfo.available_slots.forEach((slot: string) => { existingDay.availableSlots.add(String(slot).toLowerCase().trim()); }); } else { // Create new day entry const slotsSet = new Set(); dateInfo.available_slots.forEach((slot: string) => { slotsSet.add(String(slot).toLowerCase().trim()); }); dayMap.set(dayName, { day: dayNum, dayName: dayName, availableSlots: slotsSet, }); } } }); // Time slot order: morning, afternoon (lunchtime), evening const timeSlotOrder: Record = { morning: 0, afternoon: 1, evening: 2, }; // Convert Map values to array, sort slots, and sort by day number return Array.from(dayMap.values()) .map(day => ({ day: day.day, dayName: day.dayName, availableSlots: Array.from(day.availableSlots).sort((a, b) => { const aOrder = timeSlotOrder[a.toLowerCase().trim()] ?? 999; const bOrder = timeSlotOrder[b.toLowerCase().trim()] ?? 999; return aOrder - bOrder; }), })) .sort((a, b) => a.day - b.day); } // Fallback to weekly availability if (weeklyAvailability) { // Handle both array format and object with 'week' property const weekArray = Array.isArray(weeklyAvailability) ? weeklyAvailability : (weeklyAvailability as any)?.week; if (weekArray && Array.isArray(weekArray)) { // Time slot order: morning, afternoon (lunchtime), evening const timeSlotOrder: Record = { morning: 0, afternoon: 1, evening: 2, }; return weekArray .filter(day => { const dayNum = Number(day.day); return day.is_available && day.available_slots && Array.isArray(day.available_slots) && day.available_slots.length > 0 && !isNaN(dayNum) && dayNum >= 0 && dayNum <= 6; }) .map(day => ({ day: Number(day.day), dayName: day.day_name || 'Unknown', availableSlots: (day.available_slots || []).sort((a: string, b: string) => { const aOrder = timeSlotOrder[String(a).toLowerCase().trim()] ?? 999; const bOrder = timeSlotOrder[String(b).toLowerCase().trim()] ?? 999; return aOrder - bOrder; }), })) .sort((a, b) => a.day - b.day); } } return []; }, [availabilityOverview, weeklyAvailability]); const handleLogout = () => { logout(); toast.success("Logged out successfully"); router.push("/"); }; // Handle submit button click const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); // Check if user is authenticated if (!isAuthenticated) { // Open login dialog if not authenticated setShowLoginDialog(true); return; } // If authenticated, proceed with booking await submitBooking(); }; const handleLoginSuccess = async () => { // Close login dialog setShowLoginDialog(false); // After successful login, proceed with booking submission await submitBooking(); }; const handleSignupSuccess = async () => { // Close signup dialog setShowSignupDialog(false); // After successful signup, proceed with booking submission await submitBooking(); }; const handleSwitchToSignup = () => { // Close login dialog and open signup dialog setShowLoginDialog(false); setTimeout(() => { setShowSignupDialog(true); }, 100); }; const handleSwitchToLogin = (email?: string) => { // Close signup dialog and open login dialog with email prefilled setShowSignupDialog(false); setLoginPrefillEmail(email); setTimeout(() => { setShowLoginDialog(true); }, 100); }; const submitBooking = async () => { setError(null); try { // Get current slots from formData const currentSlots = formData.selectedSlots || []; // Check if slots are selected if (!currentSlots || currentSlots.length === 0) { setError("Please select at least one day and time slot combination by clicking on the time slot buttons."); return; } // Prepare and validate slots - only send exactly what user selected // Filter out duplicates and ensure we only send the specific selected slots const uniqueSlots = new Map(); currentSlots.forEach(slot => { if (!slot) return; // Get day - handle any format let dayNum: number; if (typeof slot.day === 'number') { dayNum = slot.day; } else { dayNum = parseInt(String(slot.day || 0), 10); } // Validate day if (isNaN(dayNum) || dayNum < 0 || dayNum > 6) { return; } // Get time_slot - normalize const timeSlot = String(slot.time_slot || '').trim().toLowerCase(); // Validate time_slot - accept morning, afternoon, evening if (!timeSlot || !['morning', 'afternoon', 'evening'].includes(timeSlot)) { return; } // Create unique key to prevent duplicates const uniqueKey = `${dayNum}-${timeSlot}`; uniqueSlots.set(uniqueKey, { day: dayNum, time_slot: timeSlot as "morning" | "afternoon" | "evening", }); }); // Convert map to array const validSlots = Array.from(uniqueSlots.values()).map(slot => ({ day: slot.day, time_slot: slot.time_slot as "morning" | "afternoon" | "evening", })); // Final validation check if (!validSlots || validSlots.length === 0) { setError("Please select at least one day and time slot combination by clicking on the time slot buttons."); return; } // Validate and limit field lengths to prevent database errors const firstName = formData.firstName.trim().substring(0, 100); const lastName = formData.lastName.trim().substring(0, 100); const email = formData.email.trim().toLowerCase().substring(0, 100); const phone = formData.phone ? formData.phone.trim().substring(0, 100) : undefined; const reason = formData.message ? formData.message.trim().substring(0, 100) : undefined; // Validate required fields if (!firstName || firstName.length === 0) { setError("First name is required."); return; } if (!lastName || lastName.length === 0) { setError("Last name is required."); return; } if (!email || email.length === 0) { setError("Email address is required."); return; } if (!email.includes('@')) { setError("Please enter a valid email address."); return; } // Prepare payload with validated and limited fields // CRITICAL: Only send exactly what the user selected, nothing more const selectedSlotsPayload = validSlots.map(slot => ({ day: Number(slot.day), // Ensure it's a number (0-6) time_slot: String(slot.time_slot).toLowerCase().trim() as "morning" | "afternoon" | "evening", // Ensure lowercase and correct type })); // Build payload with ONLY the fields the API requires/accepts // API required: first_name, last_name, email, selected_slots // API optional: phone, reason // DO NOT include: preferred_dates, preferred_time_slots (not needed) const payload = { first_name: firstName, last_name: lastName, email: email, selected_slots: selectedSlotsPayload, // Only send what user explicitly selected (day + time_slot format) ...(phone && phone.length > 0 && { phone: phone }), ...(reason && reason.length > 0 && { reason: reason }), }; // Call the actual API using the hook const appointmentData = await create(payload); // Convert API response to Booking format for display // Use a stable ID - if appointmentData.id exists, use it, otherwise use 0 const appointmentId = appointmentData.id ? parseInt(appointmentData.id, 10) : 0; const now = new Date().toISOString(); const bookingData: Booking = { ID: appointmentId || 0, CreatedAt: appointmentData.created_at || now, UpdatedAt: appointmentData.updated_at || now, DeletedAt: null, user_id: 0, // API doesn't return user_id in this response user: { ID: 0, first_name: appointmentData.first_name, last_name: appointmentData.last_name, email: appointmentData.email, phone: appointmentData.phone || "", location: "", is_admin: false, bookings: null, }, scheduled_at: appointmentData.scheduled_datetime || "", duration: appointmentData.scheduled_duration || 60, status: appointmentData.status || "pending_review", jitsi_room_id: appointmentData.jitsi_room_id || "", jitsi_room_url: appointmentData.jitsi_meet_url || "", payment_id: "", payment_status: "pending", amount: 0, notes: appointmentData.reason || "", }; setBooking(bookingData); toast.success("Appointment request submitted successfully! We'll review and get back to you soon."); // Stay on the booking page to show the receipt - no redirect } catch (err) { const errorMessage = err instanceof Error ? err.message : "Failed to submit booking. Please try again."; setError(errorMessage); toast.error(errorMessage); } }; const handleChange = (field: string, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); }; // Handle slot selection (day + time slot combination) // CRITICAL: Only toggle the specific slot that was clicked, nothing else const handleSlotToggle = useCallback((day: number, timeSlot: string) => { const normalizedDay = Number(day); const normalizedTimeSlot = String(timeSlot).toLowerCase().trim(); // Validate inputs if (isNaN(normalizedDay) || normalizedDay < 0 || normalizedDay > 6) { return; // Invalid day, don't change anything } if (!['morning', 'afternoon', 'evening'].includes(normalizedTimeSlot)) { return; // Invalid time slot, don't change anything } setFormData((prev) => { const currentSlots = prev.selectedSlots || []; // Helper to check if two slots match EXACTLY (both day AND time_slot) const slotsMatch = (slot1: { day: number; time_slot: string }, slot2: { day: number; time_slot: string }) => { return Number(slot1.day) === Number(slot2.day) && String(slot1.time_slot).toLowerCase().trim() === String(slot2.time_slot).toLowerCase().trim(); }; const targetSlot = { day: normalizedDay, time_slot: normalizedTimeSlot }; // Check if this EXACT slot exists (check for duplicates too) const existingIndex = currentSlots.findIndex(slot => slotsMatch(slot, targetSlot)); if (existingIndex >= 0) { // Remove ONLY this specific slot (also removes duplicates) const newSlots = currentSlots.filter(slot => !slotsMatch(slot, targetSlot)); return { ...prev, selectedSlots: newSlots, }; } else { // Add ONLY this specific slot if it doesn't exist (prevent duplicates) const newSlots = [...currentSlots, targetSlot]; return { ...prev, selectedSlots: newSlots, }; } }); }, []); // Check if a slot is selected const isSlotSelected = (day: number, timeSlot: string): boolean => { const normalizedDay = Number(day); const normalizedTimeSlot = String(timeSlot).toLowerCase().trim(); return (formData.selectedSlots || []).some( slot => Number(slot.day) === normalizedDay && String(slot.time_slot).toLowerCase().trim() === normalizedTimeSlot ); }; const formatDateTime = (dateString: string) => { const date = new Date(dateString); return date.toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit", hour12: true, }); }; return (
{/* Main Content */}
{/* Left Side - Image (Fixed) */}
Therapy session with diverse clients
{/* Logo at Top */}
Attune Heart Therapy
{/* Overlay Content - Lower Position */}

Begin Your Journey to Wellness

Take the first step towards healing and growth. Our compassionate team is here to support you every step of the way.

{/* Features List */}
Safe and confidential environment
Experienced licensed therapists
Personalized treatment plans
Flexible scheduling options
{/* Right Side - Form (Scrollable) */}
{/* Page Header */}

Book Your Appointment

Fill out the form below and we'll get back to you to confirm your appointment

{/* Booking Form or Success Message */}
{booking ? (

Booking Request Submitted!

Your appointment request has been received.

Name

{booking.user.first_name} {booking.user.last_name}

Email

{booking.user.email}

{booking.user.phone && (

Phone

{booking.user.phone}

)}

You will be contacted shortly to confirm your appointment.

) : ( <>
{error && (

{error}

)}
{/* Personal Information Section */}

Personal Information

handleChange("firstName", e.target.value) } maxLength={100} required className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`} />
handleChange("lastName", e.target.value) } maxLength={100} required className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`} />
handleChange("email", e.target.value)} maxLength={100} required className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`} />
handleChange("phone", e.target.value)} maxLength={100} required className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`} />
{/* Appointment Details Section */}

Appointment Details

{(isLoadingWeeklyAvailability || isLoadingAvailabilityOverview) ? (
Loading availability...
) : availableDaysOfWeek.length === 0 ? (

No available days at the moment. Please check back later.

) : ( <>

Select one or more day-time combinations that work for you. You can select multiple time slots for the same day (e.g., Monday Morning and Monday Evening).

{availableDaysOfWeek.map((dayInfo, dayIndex) => { // Ensure day is always a valid number (already validated in useMemo) const currentDay = typeof dayInfo.day === 'number' && !isNaN(dayInfo.day) ? dayInfo.day : dayIndex; // Fallback to index if invalid // Skip if day is still invalid if (isNaN(currentDay) || currentDay < 0 || currentDay > 6) { return null; } return (

{dayInfo.dayName || `Day ${currentDay}`}

{dayInfo.availableSlots.map((timeSlot: string, slotIndex: number) => { if (!timeSlot) return null; const timeSlotLabels: Record = { morning: "Morning", afternoon: "Lunchtime", evening: "Evening", }; // Normalize time slot for consistent comparison const normalizedTimeSlot = String(timeSlot).toLowerCase().trim(); // Create unique key combining day, time slot, and index to ensure uniqueness const slotKey = `day-${currentDay}-slot-${normalizedTimeSlot}-${slotIndex}`; // Check if THIS specific day-time combination is selected const isSelected = isSlotSelected(currentDay, normalizedTimeSlot); return ( ); })}
); })}
)}
{/* Additional Message Section */}