"use client"; import React from "react"; import { useEffect, useState, useCallback, useMemo } from "react"; import { Permission } from "../auth-context"; import { api } from "../api"; import { Header } from "../components/Header"; import { useRequireAuth } from "../hooks/useRequireAuth"; import { components } from "../generated/api"; import constants from "../../../shared/constants.json"; import { formatDate, formatTime, getDateRange } from "../utils/date"; import { layoutStyles, typographyStyles, bannerStyles, formStyles, buttonStyles, } from "../styles/shared"; const { slotDurationMinutes, maxAdvanceDays, minAdvanceDays, noteMaxLength } = constants.booking; type BookableSlot = components["schemas"]["BookableSlot"]; type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"]; type AppointmentResponse = components["schemas"]["AppointmentResponse"]; export default function BookingPage() { const { user, isLoading, isAuthorized } = useRequireAuth({ requiredPermission: Permission.BOOK_APPOINTMENT, fallbackRedirect: "/", }); const [selectedDate, setSelectedDate] = useState(null); const [availableSlots, setAvailableSlots] = useState([]); const [selectedSlot, setSelectedSlot] = useState(null); const [note, setNote] = useState(""); const [isLoadingSlots, setIsLoadingSlots] = useState(false); const [isBooking, setIsBooking] = useState(false); const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const [datesWithAvailability, setDatesWithAvailability] = useState>(new Set()); const [isLoadingAvailability, setIsLoadingAvailability] = useState(true); // Memoize dates to prevent infinite re-renders const dates = useMemo( () => getDateRange(minAdvanceDays, maxAdvanceDays), [minAdvanceDays, maxAdvanceDays] ); const fetchSlots = useCallback(async (date: Date) => { setIsLoadingSlots(true); setError(null); setAvailableSlots([]); setSelectedSlot(null); try { const dateStr = formatDate(date); const data = await api.get(`/api/booking/slots?date=${dateStr}`); setAvailableSlots(data.slots); } catch (err) { console.error("Failed to fetch slots:", err); setError("Failed to load available slots"); } finally { setIsLoadingSlots(false); } }, []); // Fetch availability for all dates on mount useEffect(() => { if (!user || !isAuthorized) return; const fetchAllAvailability = async () => { setIsLoadingAvailability(true); const availabilitySet = new Set(); // Fetch availability for all dates in parallel const promises = dates.map(async (date) => { try { const dateStr = formatDate(date); const data = await api.get(`/api/booking/slots?date=${dateStr}`); if (data.slots.length > 0) { availabilitySet.add(dateStr); } } catch (err) { // Silently fail for individual dates - they'll just be marked as unavailable console.error(`Failed to fetch availability for ${formatDate(date)}:`, err); } }); await Promise.all(promises); setDatesWithAvailability(availabilitySet); setIsLoadingAvailability(false); }; fetchAllAvailability(); }, [user, isAuthorized]); // Removed dates from dependencies - dates is memoized and stable useEffect(() => { if (selectedDate && user && isAuthorized) { fetchSlots(selectedDate); } }, [selectedDate, user, isAuthorized, fetchSlots]); const handleDateSelect = (date: Date) => { const dateStr = formatDate(date); // Only allow selection if date has availability if (datesWithAvailability.has(dateStr)) { setSelectedDate(date); setSuccessMessage(null); } }; const handleSlotSelect = (slot: BookableSlot) => { setSelectedSlot(slot); setNote(""); setError(null); }; const handleBook = async () => { if (!selectedSlot) return; setIsBooking(true); setError(null); try { const appointment = await api.post("/api/booking", { slot_start: selectedSlot.start_time, note: note || null, }); setSuccessMessage( `Appointment booked for ${formatTime(appointment.slot_start)} - ${formatTime(appointment.slot_end)}` ); setSelectedSlot(null); setNote(""); // Refresh slots to show the booked one is gone if (selectedDate) { await fetchSlots(selectedDate); } } catch (err) { setError(err instanceof Error ? err.message : "Failed to book appointment"); } finally { setIsBooking(false); } }; const cancelSlotSelection = () => { setSelectedSlot(null); setNote(""); setError(null); }; if (isLoading) { return (
Loading...
); } if (!isAuthorized) { return null; } return (

Book an Appointment

Select a date to see available {slotDurationMinutes}-minute slots

{successMessage &&
{successMessage}
} {error &&
{error}
} {/* Date Selection */}

Select a Date

{dates.map((date) => { const dateStr = formatDate(date); const isSelected = selectedDate && formatDate(selectedDate) === dateStr; const hasAvailability = datesWithAvailability.has(dateStr); const isDisabled = !hasAvailability || isLoadingAvailability; return ( ); })}
{/* Available Slots */} {selectedDate && (

Available Slots for{" "} {selectedDate.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", })}

{isLoadingSlots ? (
Loading slots...
) : availableSlots.length === 0 ? (
No available slots for this date
) : (
{availableSlots.map((slot) => { const isSelected = selectedSlot?.start_time === slot.start_time; return ( ); })}
)}
)} {/* Booking Form */} {selectedSlot && (

Confirm Booking

Time: {formatTime(selectedSlot.start_time)} -{" "} {formatTime(selectedSlot.end_time)}